Source code for tkgui.controls

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint:disable=unused-wildcard-import,wildcard-import
"""Controls used by the TKinter GUI."""

import sys
import tkinter.font as tkFont
import types
from tkinter import *  # noqa: F403
from tkinter import simpledialog
from tkinter.ttk import *  # noqa: F403

from core.lnp import lnp

from . import binding

# Monkeypatch simpledialog to use themed dialogs from ttk
if sys.platform != 'darwin':  # OS X looks better without patch
    simpledialog.Toplevel = Toplevel
    simpledialog.Entry = Entry
    simpledialog.Frame = Frame
    simpledialog.Button = Button

# Make Enter on button with focus activate it
TtkButton = Button


[docs]class Button(TtkButton): # pylint:disable=function-redefined,missing-class-docstring def __init__(self, master=None, **kw): TtkButton.__init__(self, master, **kw) if 'command' in kw: self.bind('<Return>', lambda e: kw['command']())
# http://effbot.org/zone/tkinter-autoscrollbar.htm class _AutoScrollbar(Scrollbar): """A scrollbar that hides itself if it's not needed.""" def set(self, first, last): """Only show scrollbar when there's more content than will fit.""" # pylint:disable=no-member if not lnp.userconfig.get_bool('tkgui_show_scroll'): if (float(first) <= 0.0 and float(last) >= 1.0) or ( hasattr(self, 'hidden') and self.hidden): self.grid_remove() else: self.grid() Scrollbar.set(self, first, last) # http://www.voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml#e387 class _ToolTip(object): """Tooltip widget.""" def __init__(self, widget, text): self.widget = widget self.tipwindow = None self.id = None self.event = None self.text = text self.active = False def showtip(self): """Displays the tooltip.""" self.active = True if self.tipwindow or not self.text: return x = self.widget.winfo_pointerx() + 16 y = self.widget.winfo_pointery() + 16 self.tipwindow = tw = Toplevel(self.widget) tw.wm_overrideredirect(1) tw.wm_geometry("+%d+%d" % (x, y)) try: # pylint:disable=protected-access # For OS X tw.tk.call( "::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "noActivates") except TclError: pass label = Label( tw, text=self.text, justify=LEFT, background="#ffffe0", relief=SOLID, borderwidth=1) label.pack(ipadx=1) def hidetip(self): """Hides the tooltip.""" tw = self.tipwindow self.tipwindow = None self.active = False if tw: tw.destroy() def settext(self, text): """ Sets the tooltip text and redraws it if necessary. Args: text: the new tooltip text. """ if text == self.text: return self.text = text if self.active: self.hidetip() self.showtip() _TOOLTIP_DELAY = 500 __ui = None # pylint:disable=too-few-public-methods class _FakeControl(object): """Fake control returned if an option doesn't exist.""" # pylint:disable=unused-argument @staticmethod def grid(*args, **kwargs): """Prevents breaking for code that tries to lay out the control.""" return pack = grid fake_control = _FakeControl()
[docs]def init(ui): """Connect to a TkGui instance.""" # pylint:disable=global-statement global __ui __ui = ui
[docs]def create_tooltip(widget, text): """ Creates and returns a tooltip for a widget. Args: widget: the widget to associate the tooltip to. text: the tooltip text. """ tooltip = _ToolTip(widget, text) # pylint:disable=unused-argument def enter(event): """ Event handler on mouse enter. Args: event: the event data.""" if tooltip.event: widget.after_cancel(tooltip.event) tooltip.event = widget.after(_TOOLTIP_DELAY, tooltip.showtip) def leave(event): """ Event handler on mouse exit. Args: event: the event data. """ if tooltip.event is not None: widget.after_cancel(tooltip.event) tooltip.event = None tooltip.hidetip() widget.bind('<Enter>', enter) widget.bind('<Leave>', leave) return tooltip
[docs]def create_control_group(parent, text, dual_column=False): """ Creates and returns a Frame or Labelframe to group controls. Args: text: the caption for the Labelframe. If None, returns a Frame. dual_column: configure the frame for a dual-column grid layout if True. """ f = None if text is not None: f = Labelframe(parent, text=text) else: f = Frame(parent) f.configure(pad=(2, 0, 2, 2)) if dual_column: f.columnconfigure((0, 1), weight=1, uniform=1) return f
[docs]def create_option_button( parent, text, tooltip, option, update_func=None): """ Creates and returns a button bound to an option. Args: parent: the parent control for the button. text: the button text. tooltip: the tooltip for the button. option: the keyword used for the option. update_func: if given, a reference to a function that pre-processes the given option for display. """ return create_trigger_option_button( parent, text, tooltip, lambda: __ui.cycle_option(option), option, update_func)
[docs]def create_trigger_button(parent, text, tooltip, command): """ Creates and returns a button that triggers an action when clicked. Args: parent The parent control for the button. text The button text. tooltip The tooltip for the button. command Reference to the function called when the button is clicked. """ b = Button(parent, text=text, command=command) create_tooltip(b, tooltip) return b
# pylint: disable=too-many-arguments
[docs]def create_trigger_option_button( parent, text, tooltip, command, option, update_func=None): """ Creates and returns a button bound to an option, with a special action triggered on click. Args: parent: the parent control for the button. text: the button text. tooltip: the tooltip for the button. command: Reference to the function called when the button is clicked. option: the keyword used for the option. update_func: f given, a reference to a function that pre-processes the given option for display. """ if not binding.version_has_option(option): return fake_control b = create_trigger_button(parent, text, tooltip, command) binding.bind(b, option, update_func) return b
[docs]def create_scrollbar(parent, control, **gridargs): """ Creates and layouts a vertical scrollbar associated to <control>. Args: parent: the parent control for the scrollbar. control: the control to attach the scrollbar to. gridargs: Keyword arguments used to apply grid layout to the scrollbar. """ s = _AutoScrollbar(parent, orient=VERTICAL, command=control.yview) control['yscrollcommand'] = s.set s.grid(sticky="ns", **gridargs) if not lnp.userconfig.get_bool('tkgui_show_scroll'): s.grid_remove() return s
[docs]def listbox_identify(listbox, y): """Returns the index of the listbox item at the supplied (relative) y coordinate""" item = listbox.nearest(y) if item != -1 and listbox.bbox(item)[1] + listbox.bbox(item)[3] > y: return item return None
[docs]def listbox_dyn_tooltip(listbox, item_get, tooltip_get): """Attaches a dynamic tooltip to a listbox. Args: listbox: The listbox to attach to. item_get: A function taking the index of the item and returning a reference to the item. tooltip_get: A function taking a reference to an item and returning its tooltip (or the empty string for no tooltip). """ tooltip = create_tooltip(listbox, '') def motion_handler(event): """ Event handler for mouse motion over items in a listbox. If the mouse has moved out of the last list element, hides the tooltip. Then, if the mouse is over a list item, wait controls._TOOLTIP_DELAY milliseconds (without mouse movement) before showing the tooltip""" item = listbox.identify(event.y) if item is not None: item = item_get(item) def show(): """Sets and shows a tooltip""" tooltip.settext(tooltip_get(item)) tooltip.showtip() if tooltip.event: listbox.after_cancel(tooltip.event) tooltip.event = None if not item or tooltip_get(item) != tooltip.text: tooltip.hidetip() if item: tooltip.event = listbox.after(_TOOLTIP_DELAY, show) listbox.bind('<Motion>', motion_handler)
[docs]def treeview_tag_set(tree, tag, item, state=True, toggle=False): """ Adds or removes a tag from the Treeview item's tags. Returns True if tag is now set or False if it is not. Args: item: Treeview item id state: True to set the tag; False to remove the tag. toggle: If set to True, will toggle the tag. Overrides on. """ # This is necessary because tag_add and tag_remove are not in the Python # bindings for Tk, and this is more readable (and likely not any slower) # than using arcane tk.call() syntax tags = list(tree.item(item, 'tags')) is_set = tag in tags if toggle: state = not is_set if state and (not is_set): tags.append(tag) elif (not state) and is_set: tags.remove(tag) tree.item(item, tags=tags) return state
[docs]def create_file_list(parent, title, listvar, **args): """ Creates a file list with a scrollbar. Returns a tuple (frame, listbox). Args: parent: The parent control for the list. title: The title for the frame. listvar: The variable containing the list items. args: Additions keyword arguments for the file list itself. Returns: (tuple, listbox) """ if 'height' not in args: args['height'] = 4 lf = create_control_group(parent, title) lf.pack(fill=BOTH, expand=Y) Grid.columnconfigure(lf, 0, weight=2) Grid.rowconfigure(lf, 1, weight=1) lb = Listbox( lf, listvariable=listvar, activestyle='dotbox', exportselection=0, **args) lb.identify = types.MethodType(listbox_identify, lb) lb.grid(column=0, row=0, rowspan=2, sticky="nsew") create_scrollbar(lf, lb, column=1, row=0, rowspan=2) return (lf, lb)
[docs]def create_readonly_file_list_buttons( parent, title, listvar, load_fn, refresh_fn, **args): """ Creates a file list with load and refresh buttons. Returns a tuple (frame, listbox, buttons). Args: parent: The parent control for the list. title: The title for the frame. listvar: The variable containing the list items. load_fn: Reference to a function to be called when the Load button is clicked. refresh_fn: Reference to a function to be called when the Refresh button is clicked. args: Additions keyword arguments for the file list itself. Returns: (frame, listbox, buttons) """ (lf, lb) = create_file_list(parent, title, listvar, **args) buttons = Frame(lf) load = create_trigger_button(buttons, 'Load', 'Load selected', load_fn) load.pack(side=TOP) refresh = create_trigger_button( buttons, 'Refresh', 'Refresh list', refresh_fn) refresh.pack(side=TOP) buttons.grid(column=2, row=0, sticky="n") return (lf, lb, buttons)
# pylint: disable=too-many-arguments
[docs]def create_file_list_buttons( parent, title, listvar, load_fn, refresh_fn, save_fn, delete_fn, **args): """ Creates a file list with load, refresh, save and delete buttons. Returns a tuple (frame, listbox, buttons). Args: parent: The parent control for the list. title: The title for the frame. listvar: The variable containing the list items. load_fn: Reference to a function to be called when the Load button is clicked. refresh_fn: Reference to a function to be called when the Refresh button is clicked. save_fn: Reference to a function to be called when the Save button is clicked. delete_fn: Reference to a function to be called when the Delete button is clicked. args: Additions keyword arguments for the file list itself. Returns: (frame, listbox, buttons) """ (lf, lb, buttons) = create_readonly_file_list_buttons( parent, title, listvar, load_fn, refresh_fn, **args) save = create_trigger_button(buttons, 'Save', 'Save current', save_fn) save.pack(side=TOP) delete = create_trigger_button( buttons, 'Delete', 'Delete selected', delete_fn) delete.pack(side=TOP) return (lf, lb, buttons)
[docs]def add_default_to_entry(entry, default_text): """Adds bindings to entry such that when there is no user text in the entry, the entry will display default_text in grey and italics.""" normal_font = tkFont.Font(font='TkDefaultFont') default_font = tkFont.Font(font='TkDefaultFont') default_font.config(slant=tkFont.ITALIC) entry.default_showing = True def focus_out(_): """Insert text and focus""" if len(entry.get()) == 0: entry.insert(0, default_text) entry.configure(font=default_font, foreground='grey') entry.default_showing = True def focus_in(_): """Insert text but don't focus""" if entry.default_showing: entry.delete(0, END) entry.configure(font=normal_font, foreground='black') entry.default_showing = False entry.bind('<FocusIn>', focus_in) entry.bind('<FocusOut>', focus_out) focus_out(0)
[docs]def create_list_with_entry(parent, title, listvar, buttonspec, **kwargs): """ Creates a control group with a listbox, a text entry, and any number of buttons specified with buttonspec. Does not lay out the control group in its parent. Args: parent: The parent control for the list. title: The title for the frame. listvar: The variable containing the list items. buttonspec: A list of tuples (title, tooltip, function) specifying the buttons Returns: a tuple (frame, entry, lsitbox) """ entry_default = kwargs.pop('entry_default', None) if 'height' not in kwargs: kwargs['height'] = 4 kf = create_control_group(parent, title) kf.columnconfigure(0, weight=1) kf.rowconfigure(2, weight=1) ke = Entry(kf) # text box ke.grid(row=1, column=0, sticky='ewn', pady=(1, 4)) lf = Frame(kf) # Listbox and scrollbar kb = Listbox(lf, listvariable=listvar, activestyle='dotbox', exportselection=0, **kwargs) lf.configure(borderwidth=kb['borderwidth'], relief=kb['relief']) kb.configure(borderwidth=0, relief='flat') kb.grid(row=0, column=0, sticky='nsew') create_scrollbar(lf, kb, row=0, column=1) lf.rowconfigure(0, weight=1) lf.columnconfigure(0, weight=1) lf.grid(row=2, column=0, rowspan=1, sticky='nsew') bf = Frame(kf) # buttons for i, bn in enumerate(buttonspec): pad = 0 if i == 0 else (5, 0) create_trigger_button(bf, *bn).grid(row=i, pady=pad) bf.grid(column=1, row=1, rowspan=2, sticky='ns', padx=(4, 0)) if entry_default: add_default_to_entry(ke, entry_default) return (kf, ke, kb)
[docs]def create_toggle_list(parent, columns, framegridopts, listopts=None): """ Creates and returns a two-column Treeview in a frame to show toggleable items in a list. Args: parent: The parent control for the Treeview. columns: Column data for the Treeview. framegridopts: Additional options for grid layout of the frame. listopts: Additional options for the Treeview. """ if listopts is None: listopts = {} lf = Frame(parent) lf.grid(**framegridopts) Grid.rowconfigure(lf, 0, weight=1) Grid.columnconfigure(lf, 0, weight=1) lst = Treeview(lf, columns=columns, show=['headings'], **listopts) lst.tag_set = types.MethodType(treeview_tag_set, lst) lst.grid(column=0, row=0, sticky="nsew") create_scrollbar(lf, lst, column=1, row=0) return lst
[docs]def create_numeric_entry(parent, variable, option, tooltip): """ Creates and returns an Entry suitable for input of small, numeric values and hooks up notification of changes. Args: parent: The parent control for the Entry. variable: The StringVar used to store the value internally. option: The keyword used for the option. tooltip: The tooltip for the Entry. """ if not binding.version_has_option(option): return fake_control e = Entry( parent, width=4, validate='key', justify='center', validatecommand=__ui.vcmd, textvariable=variable) variable.trace( "w", lambda name, index, mode: __ui.change_entry(option, variable)) create_tooltip(e, tooltip) binding.bind(e, option) return e
# vim:expandtab