# Two modes:
# (1) interactive: Everything runs in one thread. Long-running event handlers block the update.
# (2) separated: non-main thread for all non-mainloop code.
"""
We match the Tk hierarchy of classes onto the ACM hierarchy as follows:
Each application (i.e. instance of TkBackend) has exactly one Tk root (i.e. one tkinter.Tk()).
This top widget takes care of everything.
After instantiation of the first instance of a TkBackend, the tkinter._default_root will be set.
Except in extreme circumstances, there should only ever be one instance of TkBackend.
Each GWindow is associated to a single Canvas. GWindows beyond the first will open a new Toplevel window.
## EXIT BEHAVIOR
(A) At program termination, Tk object is not destroyed, and "program" isn't done until Tk window is closed.
(B) At program termination, Tk object is destroyed and program is finished.
"""
# TODO(sredmond): For all methods that implicitly operate on the most recent
# GWindow, allow the client to pass an optional GWindow on which to operate.
# It is discouraged to instantiate multiple instances of Tk graphics
from campy.private.backends.backend_base import GraphicsBackendBase
from campy.private.backends.tk.menu import setup_menubar
import atexit
import functools
import logging
import pathlib
import tkinter as tk
import tkinter.font as tkfont
import tkinter.filedialog as tkfiledialog
import tkinter.messagebox as tkmessagebox
import tkinter.simpledialog as tksimpledialog
import threading
import sys
# TODO(sredmond): What magic is this?
try:
from _tkinter import DONT_WAIT
except ImportError:
DONT_WAIT = 2
# Load the PIL PhotoImage class if possible, otherwise use tkinter's.
try:
from PIL.ImageTk import PhotoImage
except ImportError:
from tkinter import PhotoImage
# Module-level logger.
logger = logging.getLogger(__name__)
[docs]class TkWindow:
"""The Tk equivalent to a :class:`GWindow`."""
def __init__(self, root, width, height, parent):
self._parent = parent
self._master = tk.Toplevel(root)
self._closed = False
self._master.protocol("WM_DELETE_WINDOW", self._close)
self._master.resizable(width=False, height=False) # Disable resizing by default.
# Raise the master to be the top window.
self._master.wm_attributes("-topmost", 1) # TODO(sredmond): Is this really necessary?
self._master.lift()
self._master.focus_force()
# TODO(sredmond): On macOS, multiple backends might race to set the process-level menu bar.
setup_menubar(self._master)
self._frame = tk.Frame(self._master) #, bd=2, bg='red'
self._canvas = tk.Canvas(self._frame, width=width, height=height, highlightthickness=0, bd=0)
self._canvas.pack(fill=tk.BOTH, expand=True)
self._frame.pack(fill=tk.BOTH, expand=True)
self._frame.update()
self._master.update()
# Empty side regions for interaction. Their ordering and layout depends
# on order of construction, so start them off empty.
self._top = None
self._bottom = None
self._left = None
self._right = None
@property
def canvas(self):
return self._canvas
@property
def top(self):
"""Get the top bar for interactors, creating it if needed."""
if not self._top:
self._top = tk.Frame(self._master)
self._top.pack(fill=tk.X, side=tk.TOP)
self._frame.pack_forget()
self._frame.pack(fill=tk.BOTH, expand=True)
return self._top
@property
def bottom(self):
"""Get the bottom bar for interactors, creating it if needed."""
if not self._bottom:
self._bottom = tk.Frame(self._master)
self._bottom.pack(fill=tk.X, side=tk.BOTTOM)
self._frame.pack_forget()
self._frame.pack(fill=tk.BOTH, expand=True)
return self._bottom
@property
def left(self):
"""Get the left bar for interactors, creating it if needed."""
if not self._left:
self._left = tk.Frame(self._master)
self._left.pack(fill=tk.Y, side=tk.LEFT)
self._frame.pack_forget()
self._frame.pack(fill=tk.BOTH, expand=True)
return self._left
@property
def right(self):
"""Get the right bar for interactors, creating it if needed."""
if not self._right:
self._right = tk.Frame(self._master)
self._right.pack(fill=tk.Y, side=tk.LEFT)
self._frame.pack_forget()
self._frame.pack(fill=tk.BOTH, expand=True)
return self._right
[docs] def clear(self):
# Delete all canvas elements and all interactors, but leave the canvas and interactor regions in place."""
self.clear_canvas()
if self._top:
for child in self._top.children:
child.destroy()
if self._bottom:
for child in self._bottom.children:
child.destroy()
if self._left:
for child in self._left.children:
child.destroy()
if self._right:
for child in self._right.children:
child.destroy()
[docs] def clear_canvas(self):
# Delete all canvas elements, but leave the canvas (and all interactor regions) in place.
self.canvas.delete('all')
def _close(self):
if self._closed: return
self._closed = True
self._master.destroy()
# TODO(sredmond): Consider autoflushing like Zelle.
self._parent._remove_tkwin(self) # Tell the parent that we have closed.
[docs]class TkBackend(GraphicsBackendBase):
def __init__(self):
self._root = tk.Tk() # A wrapper around a new Tcl interpreter.
self._root.withdraw() # Removes the window from the screen (without destroying it).
atexit.register(self._root.mainloop) # TODO(sredmond): For debugging only.
self._windows = [] # TODO(sredmond): Use winfo_children().
def _update_active_window(self, window):
# Optimization: Don't mess with the windows when there's only one.
if len(self._windows) == 1: return
window._master.lift()
# Move the window to top of the stack.
self._windows.remove(window)
self._windows.append(window)
def _remove_tkwin(self, window):
if not window._closed:
window._close()
try:
self._windows.remove(window)
except ValueError:
pass
if not self._windows:
self._shutdown()
def _shutdown(self):
self._root.destroy()
######################
# GWindow lifecycle. #
######################
[docs] def gwindow_constructor(self, gwindow, width, height, top_compound, visible=True):
gwindow._tkwin = TkWindow(self._root, width, height, parent=self)
gwindow._tkwin._gwindow = gwindow # Circular reference so a TkWindow knows its originating GWindow.
self._windows.append(gwindow._tkwin)
# HACK: Get nice titles built in.
self.gwindow_set_window_title(gwindow, gwindow.title)
[docs] def gwindow_close(self, gwindow):
self._remove_tkwin(gwindow._tkwin)
[docs] def gwindow_delete(self, gwindow):
self._remove_tkwin(gwindow._tkwin)
[docs] def gwindow_exit_graphics(self):
self._remove_tkwin(gwindow._tkwin)
[docs] def gwindow_set_exit_on_close(self, gwindow, exit_on_close): pass
####################
# GWindow drawing. #
####################
[docs] def gwindow_clear(self, gwindow):
self._update_active_window(gwindow._tkwin)
gwindow._tkwin.clear()
[docs] def gwindow_clear_canvas(self, gwindow):
self._update_active_window(gwindow._tkwin)
gwindow._tkwin.clear_canvas()
[docs] def gwindow_repaint(self, gwindow):
# Update any unresolved tasks.
gwindow._tkwin._master.update_idletasks()
[docs] def gwindow_draw(self, gwindow, gobject): pass
####################
# GWindow drawing. #
####################
[docs] def gwindow_request_focus(self, gwindow):
self._update_active_window(gwindow._tkwin)
gwindow._tkwin._master.focus_force()
[docs] def gwindow_set_visible(self, gwindow, flag):
self._update_active_window(gwindow._tkwin)
if flag: # Show the window.
gwindow._tkwin._master.deiconify()
else: # Show the window.
gwindow._tkwin._master.withdraw()
[docs] def gwindow_set_window_title(self, gwindow, title):
self._update_active_window(gwindow._tkwin)
gwindow._tkwin._master.title(title)
[docs] def gwindow_get_width(self):
self._update_active_window(gwindow._tkwin)
return gwindow._tkwin._master.geometry()[0]
[docs] def gwindow_get_height(self):
self._update_active_window(gwindow._tkwin)
return gwindow._tkwin._master.geometry()[1]
######################
# GWindow alignment. #
######################
[docs] def gwindow_add_to_region(self, gwindow, gobject, region):
from campy.graphics.gwindow import Region
if region == Region.NORTH:
self._ginteractor_add(gobject, gwindow._tkwin.top)
if region == Region.EAST:
self._ginteractor_add(gobject, gwindow._tkwin.right)
if region == Region.SOUTH:
self._ginteractor_add(gobject, gwindow._tkwin.bottom)
if region == Region.WEST:
self._ginteractor_add(gobject, gwindow._tkwin.left)
[docs] def gwindow_remove_from_region(self, gwindow, gobject, region): pass
[docs] def gwindow_set_region_alignment(self, gwindow, region, align): pass
##############################
# Shared GObject operations. #
##############################
[docs] def gobject_set_location(self, gobject, x, y):
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
coords = win.canvas.coords(tkid)
win.canvas.move(tkid, x - coords[0], y - coords[1])
win._master.update_idletasks()
[docs] def gobject_set_filled(self, gobject, flag):
from campy.graphics.gobjects import GArc
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
if flag:
win.canvas.itemconfig(tkid, fill=gobject.fill_color.hex)
if isinstance(object, GArc):
win.canvas.itemconfig(tkid, style=tk.PIESLICE)
else:
win.canvas.itemconfig(tkid, fill='')
if isinstance(object, GArc):
self.itemconfig(tkid, style=tkinter.ARC)
win._master.update_idletasks()
[docs] def gobject_remove(self, gobject):
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
win.canvas.delete(tkid)
delattr(gobject, '_tkid')
delattr(gobject, '_tkwin')
win._master.update_idletasks()
[docs] def gobject_set_color(self, gobject, color):
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
# Awkward import.
from campy.graphics.gobjects import GLabel, GLine
if not isinstance(gobject, GLabel) and not isinstance(gobject, GLine):
win.canvas.itemconfig(tkid, outline=color.hex)
else:
# GLabels and GLines are special because their "color" is actually a fill color.
win.canvas.itemconfig(tkid, fill=color.hex)
win._master.update_idletasks()
[docs] def gobject_set_fill_color(self, gobject, color):
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
win.canvas.itemconfig(tkid, fill=color.hex)
win._master.update_idletasks()
[docs] def gobject_send_forward(self, gobject): pass
[docs] def gobject_send_to_front(self, gobject):
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
win.canvas.tag_raise(tkid)
win._master.update_idletasks()
[docs] def gobject_send_backward(self, gobject): pass
[docs] def gobject_send_to_back(self, gobject):
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
win.canvas.tag_lower(tkid)
win._master.update_idletasks()
[docs] def gobject_set_size(self, gobject, width, height): pass
[docs] def gobject_get_size(self, gobject):
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
x0, y0, x1, y1 = win.canvas.bbox(tkid)
return x1 - x0, y1 - y0
[docs] def gobject_get_bounds(self, gobject): pass
[docs] def gobject_set_line_width(self, gobject, line_width): pass
[docs] def gobject_contains(self, gobject, x, y): pass
[docs] def gobject_set_visible(self, gobject, flag):
if not hasattr(gobject, '_tkid'): return
tkid = gobject._tkid
win = gobject._tkwin
if not flag:
win.canvas.itemconfig(tkid, state=tk.HIDDEN)
else:
win.canvas.itemconfig(tkid, state=tk.NORMAL)
[docs] def gobject_scale(self, gobject, sx, sy): pass
[docs] def gobject_rotate(self, gobject, theta): pass
########################
# Rectangular regions. #
########################
[docs] def grect_constructor(self, grect):
if hasattr(grect, '_tkwin'):
return
win = self._windows[-1]
grect._tkwin = win
grect._tkid = win.canvas.create_rectangle(
grect.x, grect.y, grect.x + grect.width, grect.y + grect.height,
outline=grect.color.hex, fill=grect.fill_color.hex if grect.filled else '',
state=tk.NORMAL if grect.visible else tk.HIDDEN)
win._master.update_idletasks()
[docs] def groundrect_constructor(self, gobject, width, height, corner): pass
[docs] def g3drect_constructor(self, gobject, width, height, raised): pass
[docs] def g3drect_set_raised(self, gobject, raised): pass
#######################
# Elliptical regions. #
#######################
[docs] def goval_constructor(self, goval):
if hasattr(goval, '_tkwin'):
return
win = self._windows[-1]
goval._tkwin = win
goval._tkid = win.canvas.create_oval(
goval.x, goval.y, goval.x + goval.width, goval.y + goval.height,
outline=goval.color.hex, fill=goval.fill_color.hex if goval.filled else '',
state=tk.NORMAL if goval.visible else tk.HIDDEN)
win._master.update_idletasks()
[docs] def garc_constructor(self, garc):
if hasattr(garc, '_tkwin'):
return
win = self._windows[-1]
garc._tkwin = win
garc._tkid = win.canvas.create_arc(
garc.x, garc.y, garc.x + garc.width, garc.y + garc.height,
start=garc.start, extent=garc.sweep,
outline=garc.color.hex, fill=garc.fill_color.hex if garc.filled else '',
style=tk.PIESLICE if garc.filled else tk.ARC,
state=tk.NORMAL if garc.visible else tk.HIDDEN)
win._master.update_idletasks()
[docs] def garc_set_start_angle(self, garc, angle):
if not hasattr(garc, '_tkid'): return
tkid = garc._tkid
win = garc._tkwin
win.canvas.itemconfig(tkid, start=angle)
win._master.update_idletasks()
[docs] def garc_set_sweep_angle(self, garc, angle):
if not hasattr(garc, '_tkid'): return
tkid = garc._tkid
win = garc._tkwin
win.canvas.itemconfig(tkid, extent=angle)
win._master.update_idletasks()
[docs] def garc_set_frame_rectangle(self, garc, x, y, width, height): pass
##########
# GLines #
##########
[docs] def gline_constructor(self, gline):
if hasattr(gline, '_tkwin'):
return
win = self._windows[-1]
gline._tkwin = win
gline._tkid = win.canvas.create_line(
gline.start.x, gline.start.y, gline.end.x, gline.end.y,
fill=gline.color.hex,
state=tk.NORMAL if gline.visible else tk.HIDDEN)
win._master.update_idletasks()
[docs] def gline_set_start_point(self, gline, x, y):
if not hasattr(gline, '_tkid'): return
tkid = gline._tkid
win = gline._tkwin
win.canvas.coords(tkid, x, y, gline.end.x, gline.end.y,)
win._master.update_idletasks()
[docs] def gline_set_end_point(self, gline, x, y):
if not hasattr(gline, '_tkid'): return
tkid = gline._tkid
win = gline._tkwin
win.canvas.coords(tkid, gline.start.x, gline.start.y, x, y)
win._master.update_idletasks()
##############
# GCompounds #
##############
[docs] def gcompound_constructor(self, gobject): pass
[docs] def gcompound_add(self, compound, gobject): pass
#########
# Fonts #
#########
# See: https://www.astro.princeton.edu/~rhl/Tcl-Tk_docs/tk/font.n.html
[docs] def gfont_default_attributes(self):
# Resolves to the platform-specific default.
font = tkfont.nametofont('TkDefaultFont')
return font.config()
[docs] def gfont_attributes_from_system_name(self, font_name):
# Attempt to load the font with the given name.
font = tkfont.nametofont(font_name)
return font.config()
[docs] def gfont_get_font_metrics(self, gfont):
font = tkfont.Font(family=gfont.family, size=gfont.size,
weight=tkfont.BOLD if gfont.weight else tkfont.NORMAL,
slant=tkfont.ITALIC if gfont.slant else tkfont.ROMAN)
if not hasattr(gfont, '_tkfont'):
gfont._tkfont = font
return font.metrics()
[docs] def gfont_measure_text_width(self, gfont, text):
if not hasattr(gfont, '_tkfont'):
gfont._tkfont = tkfont.Font(family=gfont.family, size=gfont.size,
weight=tkfont.BOLD if gfont.weight else tkfont.NORMAL,
slant=tkfont.ITALIC if gfont.slant else tkfont.ROMAN)
font = gfont._tkfont
return font.measure(text)
##########
# Labels #
##########
[docs] def glabel_constructor(self, glabel):
if hasattr(glabel, '_tkwin'):
return
win = self._windows[-1]
glabel._tkwin = win
# TODO(sredmond): Document that we're putting the anchor at the NW corner.
# TODO(sredmond): Respect the font that's been set.
glabel._tkid = win.canvas.create_text(
glabel.x, glabel.y,
text=glabel.text,
fill=glabel.color.hex, anchor=tk.SW,
state=tk.NORMAL if glabel.visible else tk.HIDDEN)
self.glabel_set_font(glabel, glabel.font)
win._master.update_idletasks()
[docs] def glabel_set_font(self, glabel, gfont):
if not hasattr(glabel, '_tkid'): return
if not hasattr(gfont, '_tkfont'):
gfont._tkfont = tkfont.Font(family=gfont.family, size=gfont.size,
weight=tkfont.BOLD if gfont.weight else tkfont.NORMAL,
slant=tkfont.ITALIC if gfont.slant else tkfont.ROMAN)
font = gfont._tkfont
tkid = glabel._tkid
win = glabel._tkwin
win.canvas.itemconfig(tkid, font=font)
[docs] def glabel_set_label(self, glabel, text):
if not hasattr(glabel, '_tkid'): return
tkid = glabel._tkid
win = glabel._tkwin
win.canvas.itemconfig(tkid, text=text)
[docs] def glabel_get_font_ascent(self, glabel): pass
[docs] def glabel_get_font_descent(self, glabel): pass
[docs] def glabel_get_size(self, glabel):
# TODO(sredmond): This is currently broken.
if not hasattr(glabel, '_tkid'): return 0, 0
tkid = glabel._tkid
win = glabel._tkwin
x0, y0, x1, y1 = win.canvas.bbox(tkid)
return x1 - x0, y1 - y0
# Polygons
[docs] def gpolygon_constructor(self, gpolygon):
if hasattr(gpolygon, '_tkwin'):
return
win = self._windows[-1]
gpolygon._tkwin = win
coords = sum(((v.x + gpolygon.x, v.y + gpolygon.y) for v in gpolygon.vertices), ())
gpolygon._tkid = win.canvas.create_polygon(coords, # Not the fastest, but it'll do
outline=gpolygon.color.hex, fill=gpolygon.fill_color.hex if gpolygon.filled else '',
state=tk.NORMAL if gpolygon.visible else tk.HIDDEN)
win._master.update_idletasks()
[docs] def gpolygon_add_vertex(self, gpolygon, x, y):
if not hasattr(gpolygon, '_tkid'): return
tkid = gpolygon._tkid
win = gpolygon._tkwin
win.canvas.coords(tkid, x, y, gpolygon.end.x, gpolygon.end.y,)
win._master.update_idletasks()
##########
# Images #
##########
[docs] def image_find(self, filename):
# TODO(sredmond): Couple image file searching and image file loading.
path = pathlib.Path(filename)
if path.is_absolute():
if path.is_file():
return path
return None
# For relative paths, search for images in the following places.
# (1) The actual relative path to the scripts current directory.
# (1) An `images/` subfolder in the scripts current directory.
# TODO(sredmond): Read in a path environmental variable for searching.
if path.is_file():
return path.resolve() # We found it, even though it's relative!
if (path.parent / 'images' / path.name).is_file():
return (path.parent / 'images' / path.name).resolve()
# TODO(sredmond): Also search through library-specific images.
return None
[docs] def image_load(self, filename):
try:
from PIL import Image
logger.info('Loading image using PIL.')
im = Image.open(filename)
im = im.convert('RGB') # This is an unfortunate conversion, in that it kills transparent images.
return im, im.width, im.height
except ImportError:
im = tk.PhotoImage(file=filename)
return im, im.width(), im.height()
[docs] def gimage_constructor(self, gimage):
"""Try to create some sort of Tk Photo Image."""
if hasattr(gimage, '_tkwin'):
return
win = self._windows[-1]
gimage._tkwin = win
image = gimage._data # Either a tk.PhotoImage or a PIL.Image
# This is an awkward state, since ImageTk.PhotoImage isn't a subclass.
if not isinstance(image, tk.PhotoImage):
image = PhotoImage(image=image)
gimage._tkid = win.canvas.create_image(
gimage.x, gimage.y, anchor=tk.NW, image=image)
# Keep a reference to the PhotoImage object so that the Python GC
# doesn't destroy the data.
gimage._tkim = image
win._master.update_idletasks()
[docs] def gimage_blank(self, gimage, width, height): pass
[docs] def gimage_get_pixel(self, gimage, row, col):
from campy.graphics.gcolor import Pixel
try:
# Using Tk.PhotoImage.
value = gimage._data.get(col, row)
return Pixel(*map(int, value.split(' '))) # TODO(sredmond): Make sure Tk always returns 'r g b' and not 'a' or a single channel.
except AttributeError: # No get method on ImageTk.PhotoImage.
value = gimage._data.getpixel((col, row)) # Should be an (r, g, b) tuple
return Pixel(*value)
[docs] def gimage_set_pixel(self, gimage, row, col, rgb):
try: # Default to using PIL
gimage._data.putpixel((col, row), rgb)
# Oh no... Look at this abuse of Python. This is the type of thing they warn you about in school.
# TODO(sredmond): Move this into the hex method of colors.
r, g, b = rgb
hexcolor = '#{:02x}{:02x}{:02x}'.format(r, g, b)
gimage._tkim._PhotoImage__photo.put(hexcolor, (col, row))
except AttributeError: # No putpixel in Tk, so try to fall back.
r, g, b = rgb
hexcolor = '#{:02x}{:02x}{:02x}'.format(r, g, b)
gimage._tkim.put(hexcolor, (col, row))
[docs] def gimage_preview(self, gimage): pass
##########
# Events #
##########
[docs] def set_action_command(self, gobject, cmd): pass
[docs] def get_next_event(self, mask): pass
[docs] def wait_for_event(self, mask): pass
[docs] def event_add_keypress_handler(self, event, handler): pass
[docs] def event_generate_keypress(self, event): pass
@staticmethod
def _wrap_mouse_event(event, window, event_type):
from campy.gui.events.mouse import GMouseEvent
# TODO(sredmond): As written, this joins the TkWindow, not the GWindow, to this event.
return GMouseEvent(event_type=event_type, gwindow=window._gwindow, x=event.x, y=event.y)
[docs] def event_add_mouse_handler(self, event, handler):
from campy.gui.events.mouse import MouseEventType
if not self._windows:
logger.warning('Refusing to add a mouse listener before any windows are created.')
return
win = self._windows[-1]
if event == MouseEventType.MOUSE_CLICKED:
win._master.bind('<Button-1>', lambda cb_event: handler(self._wrap_mouse_event(cb_event, win, event)))
elif event == MouseEventType.MOUSE_RELEASED:
win._master.bind('<ButtonRelease-1>', lambda cb_event: handler(self._wrap_mouse_event(cb_event, win, event)))
elif event == MouseEventType.MOUSE_MOVED:
win._master.bind('<Motion>', lambda cb_event: handler(self._wrap_mouse_event(cb_event, win, event)))
elif event == MouseEventType.MOUSE_DRAGGED:
win._master.bind('<B1-Motion>', lambda cb_event: handler(self._wrap_mouse_event(cb_event, win, event)))
else:
logger.warning('Unrecognized event type: {}. Quietly passing.'.format(event))
[docs] def event_generate_mouse(self, event): pass
@staticmethod
def _wrap_window_event(event, window):
from campy.gui.events.window import GWindowEvent
return GWindowEvent(window._gwindow, x=event.x, y=event.y, width=event.width, height=event.height)
[docs] def event_add_window_changed_handler(self, handler):
if not self._windows:
logger.warning('Refusing to add a window listener before any windows are created.')
return
win = self._windows[-1]
win._master.bind('<Configure>', lambda cb_event: handler(self._wrap_window_event(cb_event, win)))
[docs] def event_set_window_closed_handler(self, handler):
# TODO(sredmond): Don't allow this method to set a handler multiple times, or warn about replacing the old one.
if not self._windows:
logger.warning('Refusing to add a window listener before any windows are created.')
return
win = self._windows[-1]
@functools.wraps(handler)
def wrapper():
result = handler()
# Perform the default action when the handler returns a False value.
if not result:
win._close()
# Unlike some of the other event methods, this callback is bound via protocol.
win._master.protocol("WM_DELETE_WINDOW", wrapper)
[docs] def event_pump_one(self):
# Forcibly process queued tasks, but don't process newly queued ones.
self._root.update_idletasks()
self._root.dooneevent(DONT_WAIT)
# TODO(sredmond): Rename these backend events for consistency.
[docs] def timer_pause(self, event): pass
[docs] def timer_schedule(self, function, delay_ms):
self._root.after(delay_ms, function)
###############
# Interactors #
###############
def _ginteractor_add(self, gint, frame):
from campy.gui.ginteractors import GButton
if isinstance(gint, GButton):
# TODO(sredmond): Wrap up a GActionEvent on the Tk side to supply.
gint._tkobj = tk.Button(frame, text=gint.label, command=gint.click,
state=tk.NORMAL if not gint.disabled else tk.DISABLED)
gint._tkobj.pack()
frame.update_idletasks()
[docs] def gcheckbox_constructor(self, gcheckbox):
if hasattr(gcheckbox, '_tkwin'):
return
win = self._windows[-1]
gcheckbox._tkwin = win
# TODO(sredmond): Wrap up a GActionEvent on the Tk side to supply.
var = tkinter.IntVar()
gcheckbox._tkobj = tk.Checkbutton(win._master, text=gcheckbox.label, command=gcheckbutton.select,
variable=var,
state=tk.NORMAL if not gcheckbox.disabled else tk.DISABLED)
gcheckbox._tkobj.var = var
gcheckbox._tkobj.pack()
win._master.update_idletasks()
[docs] def gcheckbox_is_selected(self, gcheckbox):
return bool(gcheckbox._tkobj.var.get())
[docs] def gcheckbox_set_selected(self, gcheckbox, state):
return gcheckbox._tkobj.var.set(1 if state else 0)
[docs] def gslider_constructor(self, gslider, min, max, value): pass
[docs] def gslider_get_value(self, gslider): pass
[docs] def gslider_set_value(self, gslider, value): pass
[docs] def gtextfield_constructor(self, gtextfield, num_chars): pass
[docs] def gtextfield_get_text(self, gtextfield): pass
[docs] def gtextfield_set_text(self, gtextfield, str): pass
[docs] def gchooser_constructor(self, gchooser): pass
[docs] def gchooser_add_item(self, gchooser, item): pass
[docs] def gchooser_get_selected_item(self, gchooser): pass
[docs] def gchooser_set_selected_item(self, gchooser, item): pass
###########
# Dialogs #
###########
# TODO(sredmond): Make these dialogs steal focus.
[docs] def gfilechooser_show_open_dialog(self, current_dir, file_filter):
logger.debug('Ignoring file_filter argument to gfilechooser_show_open_dialog.')
parent = None
if self._windows:
parent=self._windows[-1]
return tkfiledialog.askopenfilename(initialdir=current_dir, title='Select File to Open', parent=parent) or None
[docs] def gfilechooser_show_save_dialog(self, current_dir, file_filter):
logger.debug('Ignoring file_filter argument to gfilechooser_show_save_dialog.')
parent = None
if self._windows:
parent=self._windows[-1]
return tkfiledialog.asksaveasfilename(initialdir=current_dir, title='Select File to Save', parent=parent) or None
[docs] def goptionpane_show_confirm_dialog(self, message, title, confirm_type):
from campy.graphics.goptionpane import ConfirmType
if confirm_type == ConfirmType.YES_NO:
return tkmessagebox.askyesno(title, message)
elif confirm_type == ConfirmType.YES_NO_CANCEL:
return tkmessagebox.askyesnocancel(title, message)
elif confirm_type == ConfirmType.OK_CANCEL:
return tkmessagebox.askokcancel(title, message)
else:
logger.debug('Unrecognized confirm_type {!r}'.format(confirm_type))
[docs] def goptionpane_show_message_dialog(self, message, title, message_type):
from campy.graphics.goptionpane import MessageType
# TODO(sredmond): The icons aren't appearing correctly.
if message_type == MessageType.ERROR:
tkmessagebox.showerror(title, message, icon=tkmessagebox.ERROR)
elif message_type == MessageType.INFORMATION:
tkmessagebox.showinfo(title, message, icon=tkmessagebox.INFO)
elif message_type == MessageType.WARNING:
tkmessagebox.showwarning(title, message, icon=tkmessagebox.WARNING)
elif message_type == MessageType.QUESTION:
tkmessagebox.showinfo(title, message, icon=tkmessagebox.QUESTION)
elif message_type == MessageType.PLAIN:
tkmessagebox.showinfo(title, message)
else:
logger.debug('Unrecognized message_type {!r}'.format(message_type))
[docs] def goptionpane_show_option_dialog(self, message, title, options, initially_selected): pass
[docs] def goptionpane_show_text_file_dialog(self, message, title, rows, cols): pass
if __name__ == '__main__':
# Quick tests.
from campy.graphics.gwindow import GWindow
from campy.graphics.gobjects import GRect, GPolygon
from campy.graphics.gfilechooser import show_open_dialog, show_save_dialog
from campy.graphics.goptionpane import *
from campy.graphics.gtypes import *
from campy.gui.interactors import *
import math
print('{!r}'.format(show_open_dialog()))
print('{!r}'.format(show_save_dialog()))
window = GWindow()
rect = GRect(100, 200, x=50, y=60)
window.add(rect)
rect.location = 300, 300
button = GButton('Button')
window.add(button)
# Add a polygon.
edge_length = 75
stop_sign = GPolygon()
start = GPoint(-edge_length / 2, edge_length / 2 + edge_length / math.sqrt(2.0))
stop_sign.add_vertex(start)
for edge in range(8):
stop_sign.add_polar_edge(edge_length, 45*edge)
stop_sign.filled = True
stop_sign.color = "BLACK"
stop_sign.fill_color = "RED"
window.add(stop_sign, window.width / 2, window.height / 2)