def __init__(self, *args, **kwargs): self._options = kwargs.copy() if not args: self.type = 'separator' elif len(args) == 2: self._options['label'], second_object = args if isinstance(second_object, teek.BooleanVar): self.type = 'checkbutton' self._options['variable'] = second_object elif callable(second_object): self.type = 'command' self._command_callback = second_object # see _adding_finalizer elif isinstance(second_object, Menu): self.type = 'cascade' self._options['menu'] = second_object else: # assume an iterable self.type = 'cascade' self._options['menu'] = Menu(second_object) elif len(args) == 3: self.type = 'radiobutton' (self._options['label'], self._options['variable'], self._options['value']) = args else: raise TypeError( "expected 0, 2 or 3 arguments to MenuItem, got %d" % len(args)) self._args = args self._kwargs = kwargs self._menu = None self._index = None self.config = CgetConfigureConfigDict(self._config_entrycommand_caller) self.config._types.update({ 'activebackground': teek.Color, 'activeforeground': teek.Color, 'accelerator': str, 'background': teek.Color, #'bitmap': ???, 'columnbreak': bool, 'compound': str, 'font': teek.Font, 'foreground': teek.Color, 'hidemargin': bool, 'image': teek.Image, 'indicatoron': bool, 'label': str, 'menu': Menu, 'offvalue': bool, 'onvalue': bool, 'selectcolor': teek.Color, 'selectimage': teek.Image, 'state': str, 'underline': bool, 'value': str, 'variable': (teek.BooleanVar if self.type == 'checkbutton' else teek.StringVar), }) self.config._special['command'] = self._create_command
def _setup(self, type_string, id_): self.type_string = type_string self._id = id_ self.tags = Tags(self) self.config = CgetConfigureConfigDict(self._config_caller) prefixed = { #'stipple': ???, #'outlinestipple': ???, 'fill': teek.Color, 'outline': teek.Color, 'dash': str, # TODO: support non-float coordinates? see COORDINATES in man page 'width': float, } for prefix in ['', 'active', 'disabled']: self.config._types.update( {prefix + key: value for key, value in prefixed.items()}) self.config._types.update({ 'offset': str, 'outlineoffset': str, 'joinstyle': str, 'splinesteps': int, 'smooth': str, 'state': str, 'tags': [str], 'capstyle': str, 'arrow': str, # see comment about floats in prefixed 'dashoffset': float, 'arrowshape': (float, float, float), })
def __init__(self, parent, **kwargs): if type(self)._widget_name is None: raise TypeError("cannot create instances of %s directly, " "use one of its subclasses instead" % type(self).__name__) if parent is None: parentpath = '' else: parentpath = parent.to_tcl() self.parent = parent # yes, it must be lowercase safe_class_name = re.sub(r'\W', '_', type(self).__name__).lower() # use some_widget.to_tcl() to access the _widget_path self._widget_path = '%s.%s%d' % (parentpath, safe_class_name, next(counts[safe_class_name])) # TODO: some config options can only be given when the widget is # created, add support for them self._call(None, type(self)._widget_name, self.to_tcl()) _widgets[self.to_tcl()] = self self.config = CgetConfigureConfigDict( lambda returntype, *args: self._call(returntype, self, *args)) self._init_config() # subclasses should override this and use super # support kwargs like from_=1, because from=1 is invalid syntax for invalid_syntax in keyword.kwlist: if invalid_syntax + '_' in kwargs: kwargs[invalid_syntax] = kwargs.pop(invalid_syntax + '_') self.config.update(kwargs) # command strings that are deleted when the widget is destroyed self.command_list = [] self.bindings = BindingDict( # BindingDict is defined below lambda returntype, *args: self._call(returntype, 'bind', self, * args), self.command_list) self.bind = self.bindings._convenience_bind if type(self)._widget_name.startswith('ttk::'): self.state = StateSet(self) else: self.state = None
class MenuItem: """ Represents an item of a menu. See :ref:`creating-menu-items` for details about the arguments. Tk's manual pages call these things "menu entries" instead of "menu items", but I called them items to avoid confusing these with :class:`.Entry`. There are two kinds of :class:`.MenuItem` objects: * Menu items that are not in any :class:`.Menu` widget because they haven't been added to a menu yet, or they have been removed from a menu. Trying to do something with these menu items will likely raise a :class:`.RuntimeError`. * Menu items that are currently in a :class:`.Menu`. Here's an example: >>> item = teek.MenuItem("Click me", print) >>> item.config['label'] = "New text" Traceback (most recent call last): ... RuntimeError: the MenuItem hasn't been added to a Menu yet >>> menu = teek.Menu() >>> menu.append(item) >>> item.config['label'] = "New text" >>> item.config['label'] 'New text' .. attribute:: config This attribute is similar to :attr:`.Widget.config`. See ``MENU ENTRY OPTIONS`` in :man:`menu(3tk)`. The types of the values are the same as for similar widgets. For example, the ``'command'`` of a :class:`.Button` widget is a :class:`.Callback` object connected to a function passed to :class:`.Button`, and so is the ``'command'`` of ``teek.MenuItem("Click me", some_function)``. .. attribute:: type This is a string. Currently the possible values are ``'separator'``, ``'checkbutton'``, ``'command'``, ``'cascade'`` and ``'radiobutton'`` as documented :ref:`above <creating-menu-items>`. Don't set this attribute yourself. """ def __init__(self, *args, **kwargs): self._options = kwargs.copy() if not args: self.type = 'separator' elif len(args) == 2: self._options['label'], second_object = args if isinstance(second_object, teek.BooleanVar): self.type = 'checkbutton' self._options['variable'] = second_object elif callable(second_object): self.type = 'command' self._command_callback = second_object # see _adding_finalizer elif isinstance(second_object, Menu): self.type = 'cascade' self._options['menu'] = second_object else: # assume an iterable self.type = 'cascade' self._options['menu'] = Menu(second_object) elif len(args) == 3: self.type = 'radiobutton' (self._options['label'], self._options['variable'], self._options['value']) = args else: raise TypeError( "expected 0, 2 or 3 arguments to MenuItem, got %d" % len(args)) self._args = args self._kwargs = kwargs self._menu = None self._index = None self.config = CgetConfigureConfigDict(self._config_entrycommand_caller) self.config._types.update({ 'activebackground': teek.Color, 'activeforeground': teek.Color, 'accelerator': str, 'background': teek.Color, #'bitmap': ???, 'columnbreak': bool, 'compound': str, 'font': teek.Font, 'foreground': teek.Color, 'hidemargin': bool, 'image': teek.Image, 'indicatoron': bool, 'label': str, 'menu': Menu, 'offvalue': bool, 'onvalue': bool, 'selectcolor': teek.Color, 'selectimage': teek.Image, 'state': str, 'underline': bool, 'value': str, 'variable': (teek.BooleanVar if self.type == 'checkbutton' else teek.StringVar), }) self.config._special['command'] = self._create_command def __repr__(self): parts = ['type=%r' % self.type] if self._menu is None: parts.append("not added to a menu yet") else: parts.append("added to a menu") return '<%s%r: %s>' % (type(self).__name__, self._args, ', '.join(parts)) def _prepare_adding(self): if self._menu is not None: raise RuntimeError("cannot add a MenuItem to two different menus " "or twice to the same menu") def _after_adding(self, menu, index): self._menu = menu self._index = index self.config.update(self._options) if self.type == 'command': self.config['command'].connect(self._command_callback) def _after_removing(self): self._menu = None self._index = None def _check_in_menu(self): assert (self._menu is None) == (self._index is None) if self._menu is None: raise RuntimeError("the MenuItem hasn't been added to a Menu yet") @make_thread_safe def _config_entrycommand_caller(self, returntype, subcommand, *args): assert subcommand in {'cget', 'configure'} self._check_in_menu() return teek.tcl_call(returntype, self._menu, 'entry' + subcommand, self._index, *args) def _create_command(self): self._check_in_menu() result = teek.Callback() command_string = teek.create_command(result.run) teek.tcl_call(None, self._menu, 'entryconfigure', self._index, '-command', command_string) self._menu.command_list.append(command_string) return result
class Widget: """This is a base class for all widgets. All widgets inherit from this class, and they have all the attributes and methods documented here. Don't create instances of ``Widget`` yourself like ``Widget(...)``; use one of the classes documented below instead. However, you can use ``Widget`` with :func:`isinstance`; e.g. ``isinstance(thingy, teek.Widget)`` returns ``True`` if ``thingy`` is a teek widget. .. attribute:: config A dict-like object that represents the widget's options. >>> window = teek.Window() >>> label = teek.Label(window, text='Hello World') >>> label.config <a config object, behaves like a dict> >>> label.config['text'] 'Hello World' >>> label.config['text'] = 'New Text' >>> label.config['text'] 'New Text' >>> label.config.update({'text': 'Even newer text'}) >>> label.config['text'] 'Even newer text' >>> import pprint >>> pprint.pprint(dict(label.config)) # prints everything nicely \ # doctest: +ELLIPSIS {..., 'text': 'Even newer text', ...} .. attribute:: state Represents the Ttk state of the widget. The state object behaves like a :class:`set` of strings. For example, ``widget.state.add('disabled')`` makes a widget look like it's grayed out, and ``widget.state.remove('disabled')`` undoes that. See ``STATES`` in :man:`ttk_intro(3tk)` for more details about states. .. note:: Only Ttk widgets have states, and this attribute is set to None for non-Ttk widgets. If you don't know what Ttk is, you should read about it in :ref:`the teek tutorial <tcl-tk-tkinter-teek>`. Most teek widgets are ttk widgets, but some aren't, and that's mentioned in the documentation of those widgets. .. attribute:: tk_class_name Tk's class name of the widget class, as a string. This is a class attribute, but it can be accessed from instances as well: >>> text = teek.Text(teek.Window()) >>> text.tk_class_name 'Text' >>> teek.Text.tk_class_name 'Text' Note that Tk's class names are sometimes different from the names of Python classes, and this attribute can also be None in some special cases. >>> teek.Label.tk_class_name 'TLabel' >>> class AsdLabel(teek.Label): ... pass ... >>> AsdLabel.tk_class_name 'TLabel' >>> print(teek.Window.tk_class_name) None >>> print(teek.Widget.tk_class_name) None .. attribute:: command_list A list of command strings from :func:`.create_command`. Append a command to this if you want the command to be deleted with :func:`.delete_command` when the widget is destroyed (with e.g. :meth:`.destroy`). """ _widget_name = None tk_class_name = None @make_thread_safe def __init__(self, parent, **kwargs): if type(self)._widget_name is None: raise TypeError("cannot create instances of %s directly, " "use one of its subclasses instead" % type(self).__name__) if parent is None: parentpath = '' else: parentpath = parent.to_tcl() self.parent = parent # yes, it must be lowercase safe_class_name = re.sub(r'\W', '_', type(self).__name__).lower() # use some_widget.to_tcl() to access the _widget_path self._widget_path = '%s.%s%d' % (parentpath, safe_class_name, next(counts[safe_class_name])) # TODO: some config options can only be given when the widget is # created, add support for them self._call(None, type(self)._widget_name, self.to_tcl()) _widgets[self.to_tcl()] = self self.config = CgetConfigureConfigDict( lambda returntype, *args: self._call(returntype, self, *args)) self._init_config() # subclasses should override this and use super # support kwargs like from_=1, because from=1 is invalid syntax for invalid_syntax in keyword.kwlist: if invalid_syntax + '_' in kwargs: kwargs[invalid_syntax] = kwargs.pop(invalid_syntax + '_') self.config.update(kwargs) # command strings that are deleted when the widget is destroyed self.command_list = [] self.bindings = BindingDict( # BindingDict is defined below lambda returntype, *args: self._call(returntype, 'bind', self, * args), self.command_list) self.bind = self.bindings._convenience_bind if type(self)._widget_name.startswith('ttk::'): self.state = StateSet(self) else: self.state = None def _init_config(self): # width and height aren't here because they are integers for some # widgets and ScreenDistances for others... and sometimes the manual # pages don't say which, so i have checked them by hand self.config._types.update({ # ttk_widget(3tk) 'class': str, 'cursor': str, 'style': str, # options(3tk) 'activebackground': teek.Color, 'activeborderwidth': teek.ScreenDistance, 'activeforeground': teek.Color, 'anchor': str, 'background': teek.Color, 'bg': teek.Color, #'bitmap': ???, 'borderwidth': teek.ScreenDistance, 'bd': teek.ScreenDistance, 'cursor': str, 'compound': str, 'disabledforeground': teek.Color, 'exportselection': bool, 'font': teek.Font, 'foreground': teek.Color, 'fg': teek.Color, 'highlightbackground': teek.Color, 'highlightcolor': teek.Color, 'highlightthickness': str, 'insertbackground': teek.Color, 'insertborderwidth': teek.ScreenDistance, 'insertofftime': int, 'insertontime': int, 'insertwidth': teek.ScreenDistance, 'jump': bool, 'justify': str, 'orient': str, 'padx': teek.ScreenDistance, 'pady': teek.ScreenDistance, 'relief': str, 'repeatdelay': int, 'repeatinterval': int, 'selectbackground': teek.Color, 'selectborderwidth': teek.ScreenDistance, 'selectforeground': teek.Color, 'setgrid': bool, 'text': str, 'troughcolor': teek.Color, 'wraplength': teek.ScreenDistance, # these options are in both man pages 'textvariable': teek.StringVar, 'underline': int, 'image': teek.Image, # 'xscrollcommand' and 'yscrollcommand' are done below 'takefocus': str, # this one is harder to do right than you think # other stuff that many things seem to have 'padding': teek.ScreenDistance, 'state': str, }) for option_name in ('xscrollcommand', 'yscrollcommand'): self.config._special[option_name] = functools.partial( self._create_scroll_callback, option_name) @classmethod @make_thread_safe def from_tcl(cls, path_string): """Creates a widget from a Tcl path name. In Tcl, widgets are represented as commands, and doing something to the widget invokes the command. Use this method if you know the Tcl command and you would like to have a widget object instead. This method raises :exc:`TypeError` if it's called from a different ``Widget`` subclass than what the type of the ``path_string`` widget is: >>> window = teek.Window() >>> teek.Button.from_tcl(teek.Label(window).to_tcl()) \ # doctest: +ELLIPSIS Traceback (most recent call last): ... TypeError: '...' is a Label, not a Button """ if path_string == '.': # this kind of sucks, i might make a _RootWindow class later return None result = _widgets[path_string] if not isinstance(result, cls): raise TypeError("%r is a %s, not a %s" % (path_string, type(result).__name__, cls.__name__)) return result def to_tcl(self): """Returns the widget's Tcl command name. See :meth:`from_tcl`.""" return self._widget_path def __repr__(self): class_name = type(self).__name__ if getattr(teek, class_name, None) is type(self): result = 'teek.%s widget' % class_name else: result = '{0.__module__}.{0.__name__} widget'.format(type(self)) if not self.winfo_exists(): # _repr_parts() doesn't need to work with destroyed widgets return '<destroyed %s>' % result parts = self._repr_parts() if parts: result += ': ' + ', '.join(parts) return '<' + result + '>' def _repr_parts(self): # overrided in subclasses return [] def _create_scroll_callback(self, option_name): result = teek.Callback() command_string = teek.create_command(result.run, [float, float]) self.command_list.append(command_string) self._call(None, self, 'configure', '-' + option_name, command_string) return result __getitem__ = _tkinter_hint("widget.config['option']", "widget['option']") __setitem__ = _tkinter_hint("widget.config['option']", "widget['option']") cget = _tkinter_hint("widget.config['option']", "widget.cget('option')") configure = _tkinter_hint("widget.config['option'] = value", "widget.configure(option=value)") # like _tcl_calls.tcl_call, but with better error handling @make_thread_safe def _call(self, *args, **kwargs): try: return teek.tcl_call(*args, **kwargs) except teek.TclError as err: if not self.winfo_exists(): raise RuntimeError("the widget has been destroyed") from None raise err @make_thread_safe def destroy(self): """Delete this widget and all child widgets. Manual page: :man:`destroy(3tk)` .. note:: Don't override this in a subclass. In some cases, the widget is destroyed without a call to this method. >>> class BrokenFunnyLabel(teek.Label): ... def destroy(self): ... print("destroying") ... super().destroy() ... >>> BrokenFunnyLabel(teek.Window()).pack() >>> teek.quit() >>> # nothing was printed! Use the ``<Destroy>`` event instead: >>> class WorkingFunnyLabel(teek.Label): ... def __init__(self, *args, **kwargs): ... super().__init__(*args, **kwargs) ... self.bind('<Destroy>', self._destroy_callback) ... def _destroy_callback(self): ... print("destroying") ... >>> WorkingFunnyLabel(teek.Window()).pack() >>> teek.quit() destroying """ for name in self._call([str], 'winfo', 'children', self): # allow overriding the destroy() method if the widget was # created by teek if name in _widgets: _widgets[name]._destroy_recurser() else: self._call(None, 'destroy', name) # this must be BEFORE deleting command_list commands because <Destroy> # bindings may need command_list stuff self._call(None, 'destroy', self) # this is here because now the widget is basically useless del _widgets[self.to_tcl()] for command in self.command_list: teek.delete_command(command) self.command_list.clear() # why not # can be overrided when .destroy() in .destroy() would cause infinite # recursion, see Window in windows.py def _destroy_recurser(self): self.destroy() @_ClassProperty @make_thread_safe def class_bindings(cls): if cls is Widget: assert cls.tk_class_name is None bindtag = 'all' elif cls.tk_class_name is not None: bindtag = cls.tk_class_name else: raise AttributeError( "%s cannot be used with class_bindings and bind_class()" % cls.__name__) try: return _class_bindings[bindtag] except KeyError: def call_bind(returntype, *args): return teek.tcl_call(returntype, 'bind', bindtag, *args) # all commands are deleted when the interpreter shuts down, and the # binding dict created here should be alive until then, so it's # fine to pass a new empty list for command list bindings = BindingDict(call_bind, []) _class_bindings[bindtag] = bindings return bindings @classmethod @make_thread_safe def bind_class(cls, *args, **kwargs): return cls.class_bindings._convenience_bind(*args, **kwargs) def winfo_children(self): """Returns a list of child widgets that this widget has. Manual page: :man:`winfo(3tk)` """ return self._call([Widget], 'winfo', 'children', self) def winfo_exists(self): """Returns False if the widget has been destroyed. See :meth:`destroy`. Manual page: :man:`winfo(3tk)` """ # self._call uses this, so this must not use that return teek.tcl_call(bool, 'winfo', 'exists', self) def winfo_ismapped(self): """ Returns True if the widget is showing on the screen, or False otherwise. Manual page: :man:`winfo(3tk)` """ return self._call(bool, 'winfo', 'ismapped', self) def winfo_toplevel(self): """Returns the :class:`Toplevel` widget that this widget is in. Manual page: :man:`winfo(3tk)` """ return self._call(Widget, 'winfo', 'toplevel', self) def winfo_width(self): """Calls ``winfo width``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'width', self) def winfo_height(self): """Calls ``winfo height``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'height', self) def winfo_reqwidth(self): """Calls ``winfo reqwidth``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'reqwidth', self) def winfo_reqheight(self): """Calls ``winfo reqheight``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'reqheight', self) def winfo_x(self): """Calls ``winfo x``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'x', self) def winfo_y(self): """Calls ``winfo y``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'y', self) def winfo_rootx(self): """Calls ``winfo rootx``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'rootx', self) def winfo_rooty(self): """Calls ``winfo rooty``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'rooty', self) def winfo_id(self): """Calls ``winfo id``. Returns an integer. Manual page: :man:`winfo(3tk)` """ return self._call(int, 'winfo', 'id', self) def focus(self, *, force=False): """Focuses the widget with :man:`focus(3tk)`. If ``force=True`` is given, the ``-force`` option is used. """ if force: self._call(None, 'focus', '-force', self) else: self._call(None, 'focus', self) def _geometry_manager_slaves(self, geometry_manager): return self._call([Widget], geometry_manager, 'slaves', self) pack_slaves = functools.partialmethod(_geometry_manager_slaves, 'pack') grid_slaves = functools.partialmethod(_geometry_manager_slaves, 'grid') place_slaves = functools.partialmethod(_geometry_manager_slaves, 'place') @property def grid_rows(self): width, height = self._call([int], 'grid', 'size', self) return [ GridRowOrColumn(self, 'row', number) for number in range(height) ] @property def grid_columns(self): width, height = self._call([int], 'grid', 'size', self) return [ GridRowOrColumn(self, 'column', number) for number in range(width) ] grid_rowconfigure = _tkinter_hint( "widget.grid_rows[index].config['option'] = value", "widget.grid_rowconfigure(index, option=value)") grid_columnconfigure = _tkinter_hint( "widget.grid_columns[index].config['option'] = value", "widget.grid_columnconfigure(index, option=value)") def busy_hold(self): """See ``tk busy hold`` in :man:`busy(3tk)`. *New in Tk 8.6.* """ self._call(None, 'tk', 'busy', 'hold', self) def busy_forget(self): """See ``tk busy forget`` in :man:`busy(3tk)`. *New in Tk 8.6.* """ self._call(None, 'tk', 'busy', 'forget', self) def busy_status(self): """See ``tk busy status`` in :man:`busy(3tk)`. This Returns True or False. *New in Tk 8.6.* """ return self._call(bool, 'tk', 'busy', 'status', self) @contextlib.contextmanager def busy(self): """A context manager that calls :func:`busy_hold` and :func:`busy_forg\ et`. Example:: with window.busy(): # window.busy_hold() has been called, do something ... # now window.busy_forget() has been called """ self.busy_hold() try: yield finally: self.busy_forget() def event_generate(self, event, **kwargs): """Calls ``event generate`` documented in :man:`event(3tk)`. As usual, options are given without dashes as keyword arguments, so Tcl code like ``event generate $widget <SomeEvent> -data $theData`` looks like ``widget.event_generate('<SomeEvent>', data=the_data)`` in teek. """ option_args = [] for name, value in kwargs.items(): option_args.extend(['-' + name, value]) self._call(None, 'event', 'generate', self, event, *option_args)