PKkN Dteek/__init__.py"""Teek is a pythonic alternative to tkinter.""" # flake8: noqa import os as _os import sys as _sys __version__ = '0.4' if _os.environ.get('READTHEDOCS', None) == 'True': # pragma: no cover # readthedocs must be able to import everything without _tkinter import types _sys.modules['_tkinter'] = types.SimpleNamespace( TclError=None, TCL_VERSION='8.6', TK_VERSION='8.6', ) else: # pragma: no cover # python 3.4's tkinter does this BEFORE importing _tkinter if _sys.platform.startswith("win32") and _sys.version_info < (3, 5): import tkinter._fix # not to be confused with _tkinter's TclError, this is defined here because # this way error messages say teek.TclError instead of # teek._something.TclError, or worse yet, _tkinter.TclError class TclError(Exception): """This is raised when a Tcl command fails.""" # _platform_info does a version check, it must be first from teek._platform_info import TCL_VERSION, TK_VERSION, windowingsystem from teek._font import Font, NamedFont from teek._structures import ( Callback, Color, Image, ScreenDistance, TclVariable, StringVar, IntVar, FloatVar, BooleanVar, before_quit, after_quit) from teek._tcl_calls import ( tcl_call, tcl_eval, create_command, delete_command, run, quit, update, init_threads, make_thread_safe) from teek._timeouts import after, after_idle from teek._widgets.base import Widget from teek._widgets.menu import Menu, MenuItem from teek._widgets.misc import ( Button, Checkbutton, Combobox, Entry, Frame, Label, LabelFrame, Progressbar, Scrollbar, Separator, Spinbox) from teek._widgets.notebook import Notebook, NotebookTab from teek._widgets.text import Text from teek._widgets.windows import Window, Toplevel from teek import dialog, extras PKwWNmI teek/_font.pyimport itertools import teek flatten = itertools.chain.from_iterable def _font_property(type_spec, option): def getter(self): return teek.tcl_call(type_spec, "font", "actual", self, "-" + option) def setter(self, value): if not isinstance(self, NamedFont): raise AttributeError( "cannot change options of non-named fonts, but you can use " "the_font.to_named_font() to create a mutable font object") teek.tcl_call(None, "font", "configure", self, "-" + option, value) return property(getter, setter) def _anonymous_font_new_helper(font_description): # magic: Font(a_font_name) returns a NamedFont # is the font description a font name? configure works only with # font names, not other kinds of font descriptions try: teek.tcl_call(None, 'font', 'configure', font_description) except teek.TclError: return None # it is a font name return NamedFont(font_description) class Font: """Represents an anonymous font. Creating a :class:`.Font` object with a valid font name as an argument returns a :class:`.NamedFont` object. For example: >>> teek.Font('Helvetica 12') # not a font name Font('Helvetica 12') >>> teek.Font('TkFixedFont') # special font name for default monospace \ font NamedFont('TkFixedFont') >>> teek.NamedFont('TkFixedFont') # does the same thing NamedFont('TkFixedFont') .. attribute:: family size weight slant underline overstrike See :man:`font(3tk)` for a description of each attribute. ``size`` is an integer, ``underline`` and ``overstrike`` are bools, and other attributes are strings. You can set values to these attributes only with :class:`.NamedFont`. The values of these attributes are looked up with ``font actual`` in :man:`font(3tk)`, so they might differ from the values passed to ``Font()``. For example, the ``'Helvetica'`` family can meany any Helvetica-like font, so this line of code gives different values platform-specifically: >>> teek.Font('Helvetica 12').family # doctest: +SKIP 'Nimbus Sans L' """ def __new__(cls, *args, **kwargs): if not issubclass(cls, NamedFont): # Font, but not NamedFont named_font = _anonymous_font_new_helper(*args, **kwargs) if named_font is not None: return named_font return super(Font, cls).__new__(cls) def __init__(self, font_description): # the _font_description of NamedFont is the font name self._font_description = font_description family = _font_property(str, 'family') size = _font_property(int, 'size') weight = _font_property(str, 'weight') slant = _font_property(str, 'slant') underline = _font_property(bool, 'underline') overstrike = _font_property(bool, 'overstrike') def __repr__(self): return '%s(%r)' % (type(self).__name__, self._font_description) def __eq__(self, other): if not isinstance(other, Font): return False return self._font_description == other._font_description def __hash__(self): return hash(self._font_description) @classmethod def from_tcl(cls, font_description): """ ``Font.from_tcl(font_description)`` returns ``Font(font_description)``. This is just for compatibility with :ref:`type specifications `. """ return cls(font_description) def to_tcl(self): """ Returns the font description passed to ``Font(font_description)``. """ return self._font_description def measure(self, text): """ Calls ``font measure`` documented in :man:`font(3tk)`, and returns an integer. """ return teek.tcl_call(int, "font", "measure", self, text) def metrics(self): """ Calls ``font metrics`` documented in :man:`font(3tk)`, and returns a dictionary that has at least the following keys: * The values of ``'ascent'``, ``'descent'`` and ``'linespace'`` are integers. * The value of ``'fixed'`` is True or False. """ result = teek.tcl_call( {"-ascent": int, "-descent": int, "-linespace": int, "-fixed": bool}, "font", "metrics", self) return {name.lstrip('-'): value for name, value in result.items()} def to_named_font(self): """Returns a :class:`.NamedFont` object created from this font. If this font is already a :class:`.NamedFont`, a copy of it is created and returned. """ options = teek.tcl_call({}, 'font', 'actual', self) kwargs = {name.lstrip('-'): value for name, value in options.items()} return NamedFont(**kwargs) @classmethod def families(self, *, allow_at_prefix=False): """Returns a list of font families as strings. On Windows, some font families start with ``'@'``. I don't know what those families are and how they might be useful, but most of the time tkinter users (including me) ignore those, so this method ignores them by default. Pass ``allow_at_prefix=True`` to get a list that includes the ``'@'`` fonts. """ result = teek.tcl_call([str], "font", "families") if allow_at_prefix: return result return [family for family in result if not family.startswith('@')] class NamedFont(Font): """A font that has a name in Tcl. :class:`.NamedFont` is a subclass of :class:`.Font`; that is, all NamedFonts are Fonts, but not all Fonts are NamedFonts: >>> isinstance(teek.NamedFont('toot'), teek.Font) True >>> isinstance(teek.Font('Helvetica 12'), teek.NamedFont) False If ``name`` is not given, Tk will choose a font name that is not in use yet. If ``name`` is given, it can be a name of an existing font, but if a font with the given name doesn't exist, it'll be created instead. The ``kwargs`` are values for ``family``, ``size``, ``weight``, ``slant``, ``underline`` and ``overstrike`` attributes. For example, this... :: shouting_font = teek.NamedFont(size=30, weight='bold') ...does the same thing as this:: shouting_font = teek.NamedFont() shouting_font.size = 30 shouting_font.weight = 'bold' """ def __init__(self, name=None, **kwargs): options_with_dashes = [] for option_name, value in kwargs.items(): options_with_dashes.extend(['-' + option_name, value]) if name is None: # let tk choose a name that's not used yet name = teek.tcl_call(str, 'font', 'create', *options_with_dashes) else: # do we need to create a font with the given name? try: teek.tcl_call(None, 'font', 'create', name, *options_with_dashes) except teek.TclError: # no, it exists already, but we must do something with the # options teek.tcl_call(None, 'font', 'configure', name, *options_with_dashes) super().__init__(name) # __repr__, __eq__, __hash__, and event to_named_font, {from,to}_tcl are # fine, to_named_font creates a copy of this font # TODO: rename this less verbosely if it's possible while keeping the # meaning obvious @classmethod def get_all_named_fonts(cls): """Returns a list of all :class:`.NamedFont` objects.""" return list(map(cls, teek.tcl_call([str], 'font', 'names'))) def delete(self): """Calls ``font delete``. The font object is useless after this, and most things will raise :exc:`.TclError`. """ teek.tcl_call(None, "font", "delete", self) PKwWN-jteek/_platform_info.pyimport _tkinter import teek # i'm not sure if these can be different, but why not allow that i guess... lol TCL_VERSION = tuple(map(int, _tkinter.TCL_VERSION.split('.'))) TK_VERSION = tuple(map(int, _tkinter.TK_VERSION.split('.'))) # this is a function to make this testable def _version_check(): if TK_VERSION < (8, 5) or TCL_VERSION < (8, 5): raise RuntimeError( "sorry, your Tcl/Tk installation is too old " "(expected 8.5 or newer, found Tcl %d.%d and Tk %d.%d)" % (TCL_VERSION + TK_VERSION)) _version_check() def windowingsystem(): return teek.tcl_call(str, 'tk', 'windowingsystem') PK԰YN7HjHjteek/_structures.pyimport abc import base64 import collections.abc import functools import itertools import os import sys import tempfile import traceback import teek from teek._tcl_calls import make_thread_safe def _is_from_teek(traceback_frame_summary): try: filename = traceback_frame_summary.filename except AttributeError: # python 3.4 or older, the frame summary is a tuple filename = traceback_frame_summary[0] teek_prefix = os.path.normcase(teek.__path__[0]).rstrip(os.sep) + os.sep return filename.startswith(teek_prefix) class Callback: """An object that calls functions. Example:: >>> c = Callback() >>> c.connect(print, args=["hello", "world"]) >>> c.run() # runs print("hello", "world"), usually teek does this hello world >>> c.connect(print, args=["hello", "again"]) >>> c.run() hello world hello again """ def __init__(self): self._connections = [] def connect(self, function, args=(), kwargs=None): """Schedule ``callback(*args, **kwargs)`` to run. If some arguments are passed to :meth:`run`, they will appear before the *args* given here. For example: >>> c = Callback() >>> c.connect(print, args=['hello'], kwargs={'sep': '-'}) >>> c.run(1, 2) # print(1, 2, 'hello', sep='-') 1-2-hello The callback may return ``None`` or ``'break'``. In the above example, ``print`` returned ``None``. If the callback returns ``'break'``, two things are done differently: 1. No more connected callbacks will be ran. 2. :meth:`.run` returns ``'break'``, so that the code that called :meth:`.run` knows that one of the callbacks returned ``'break'``. This is used in :ref:`bindings `. """ stack = traceback.extract_stack() # skip some teek implementation details, they are too verbose while stack and _is_from_teek(stack[-1]): del stack[-1] stack_info = ''.join(traceback.format_list(stack)) if kwargs is None: kwargs = {} self._connections.append((function, args, kwargs, stack_info)) def disconnect(self, function): """Undo a :meth:`~connect` call. Note that this method doesn't do anything to the *args* and *kwargs* passed to :meth:`~connect`, so when disconnecting a function connected multiple times with different arguments, only the first connection is undone. >>> c = Callback() >>> c.connect(print, ["hello"]) >>> c.connect(print, ["hello", "again"]) >>> c.run() hello hello again >>> c.disconnect(print) >>> c.run() hello """ # enumerate objects aren't reversible :( for index in range(len(self._connections) - 1, -1, -1): # can't use is because cpython does this: # >>> class Thing: # ... def stuff(): pass # ... # >>> t = Thing() # >>> t.stuff == t.stuff # True # >>> t.stuff is t.stuff # False if self._connections[index][0] == function: del self._connections[index] return raise ValueError("not connected: %r" % (function,)) def run(self, *args): """Run the connected callbacks. If a callback returns ``'break'``, this returns ``'break'`` too without running more callbacks. If all callbacks run without returning ``'break'``, this returns ``None``. If a callback raises an exception, a traceback is printed and ``None`` is returned. """ for func, extra_args, kwargs, stack_info in self._connections: try: result = func(*(args + tuple(extra_args)), **kwargs) if result == 'break': return 'break' elif result is not None: raise ValueError( "expected None or 'break', got " + repr(result)) except Exception: # it's important that this does NOT call sys.stderr.write # directly because sys.stderr is None when running in windows # with pythonw.exe, and print('blah', file=None) does nothing # but None.write('blah\n') is an error traceback_blabla, rest = traceback.format_exc().split('\n', 1) print(traceback_blabla, file=sys.stderr) print(stack_info + rest, end='', file=sys.stderr) return None return None # these are here because they are used in many places, like most other things # in this file before_quit = Callback() after_quit = Callback() class ConfigDict(collections.abc.MutableMapping): def __init__(self): # {option: type spec} # use .get(option, str), this is not a defaultdict because it tests # search for options not in this self._types = {} # {option: function called with 0 args that returns the value} self._special = {} # {option: return value from a function in self._special} self._special_values = {} def __repr__(self): return '' def __call__(self, *args, **kwargs): raise TypeError("use widget.config['option'] = value, " "not widget.config(option=value)") @abc.abstractmethod def _set(self, option, value): """Sets an option to the given value. *option* is an option name without a leading dash. """ @abc.abstractmethod def _get(self, option): """Returns the value of an option. See _set. Should return a value of type self._types.get(option, str). """ @abc.abstractmethod def _list_options(self): """Returns an iterable of options that can be passed to _get.""" def _check_option(self, option): # by default, e.g. -tex would be equivalent to -text, but that's # disabled to make lookups in self._types and self._disabled # easier if option not in self._list_options(): raise KeyError(option) # for compatibility with dicts # the type of value is not checked with self._types because python is # dynamically typed @make_thread_safe def __setitem__(self, option, value): self._check_option(option) if option in self._special: message = "cannot set the value of %r" % option if isinstance(self[option], Callback): message += ( ", maybe use widget.config[%r].connect() instead?" % option) raise ValueError(message) self._set(option, value) @make_thread_safe def __getitem__(self, option): self._check_option(option) if option in self._special_values: return self._special_values[option] if option in self._special: value = self._special[option]() self._special_values[option] = value return value return self._get(option) # MutableMapping requires that there is a __delitem__ def __delitem__(self, option): raise TypeError("options cannot be deleted") def __iter__(self): return iter(self._list_options()) def __len__(self): options = self._list_options() try: return len(options) except TypeError: # why can't len() consume iterators like 'in' :(( return len(list(options)) class CgetConfigureConfigDict(ConfigDict): def __init__(self, caller_func): super().__init__() self._caller_func = caller_func def _set(self, option, value): self._caller_func(None, 'configure', '-' + option, value) def _get(self, option): return self._caller_func(self._types.get(option, str), 'cget', '-' + option) def _list_options(self): infos = self._caller_func([[str]], 'configure') return (info[0].lstrip('-') for info in infos) class Color: """Represents an RGB color. There are a few ways to create color objects: * ``Color(red, green, blue)`` creates a new color from an RGB value. The ``red``, ``green`` and ``blue`` should be integers between 0 and 255 (inclusive). * ``Color(hex_string)`` creates a color from a hexadecimal color string. For example, ``Color('#ff0000')`` is equivalent to ``Color(0xff, 0x00, 0x00)`` where ``0xff`` is hexadecimal notation for 255, and ``0x00`` is 0. * ``Color(color_name)`` creates a color object from a Tk color. There is a long list of color names in :man:`colors(3tk)`. Examples:: >>> Color(255, 255, 255) # r, g and b are all maximum, this is white >>> Color('white') # 'white' is a Tk color name The string argument things are implemented by letting Tk interpret the color, so all of the ways to define colors as strings shown in :man:`Tk_GetColor(3tk)` are supported. Color objects are hashable, and they can be compared with ``==``:: >>> Color(0, 0, 255) == Color(0, 0, 255) True >>> Color(0, 255, 0) == Color(0, 0, 255) False Color objects are immutable. If you want to change a color, create a new Color object. .. attribute:: red green blue These are the values passed to ``Color()``. >>> Color(0, 0, 255).red 0 >>> Color(0, 0, 255).green 0 >>> Color(0, 0, 255).blue 255 Assigning to these like ``some_color.red = 255`` raises an exception. """ def __init__(self, *args): if len(args) == 3: for name, value in zip(['red', 'green', 'blue'], args): if value not in range(256): raise ValueError("invalid %s value: %r" % (name, value)) self._color_string = '#%02x%02x%02x' % args elif len(args) == 1: self._color_string = args[0] else: # python raises TypeError for wrong number of arguments raise TypeError("use {0}(red, green, blue) or {0}(color_string)" .format(type(self).__name__)) # any widget will do, i'm using the '.' root window because it # always exists rgb = teek.tcl_call([int], 'winfo', 'rgb', '.', self._color_string) # tk uses 16-bit colors for some reason, but most people are more # familiar with 8-bit colors so we'll shift away the "useless" bits self._rgb = tuple(value >> 8 for value in rgb) assert len(self._rgb) == 3 def __repr__(self): return '<%s %r: red=%d, green=%d, blue=%d>' % ( type(self).__name__, self._color_string, self.red, self.green, self.blue) @classmethod def from_tcl(cls, color_string): """``Color.from_tcl(color_string)`` returns ``Color(color_string)``. This is just for compatibility with :ref:`type specifications `. """ return cls(color_string) red = property(lambda self: self._rgb[0]) green = property(lambda self: self._rgb[1]) blue = property(lambda self: self._rgb[2]) def to_tcl(self): """Return this color as a Tk-compatible string. The string is *often* a hexadecimal ``'#rrggbb'`` string, but not always; it can be also e.g. a color name like ``'white'``. Use :attr:`red`, :attr:`green` and :attr:`blue` if you want a consistent representation. >>> Color(255, 0, 0).to_tcl() '#ff0000' >>> Color('red').to_tcl() 'red' >>> Color('red') == Color(255, 0, 0) True """ return self._color_string # must not compare self._color_string because 'white' and '#ffffff' should # be equal def __eq__(self, other): if isinstance(other, Color): return self._rgb == other._rgb return NotImplemented # equal objects MUST have the same hash, so self._color_string can't be # used here def __hash__(self): return hash(self._rgb) class TclVariable: """Represents a global Tcl variable. In Tcl, it's possible to e.g. run code when the value of a variable changes, or wait until the variable is set. Python's variables can't do things like that, so Tcl variables are represented as :class:`.TclVariable` objects in Python. If you want to set the value of the variable object, ``variable_object = new_value`` doesn't work because that only sets a Python variable, and you need ``variable_object.set(new_value)`` instead. Similarly, ``variable_object.get()`` returns the value of the Tcl variable. The :class:`.TclVariable` class is useless by itself. Usable variable classes are subclasses of it that override :attr:`type_spec`. Use ``SomeUsableTclVarSubclass(name='asd')`` to create a variable object that represents a Tcl variable named ``asd``, or ``SomeUsableTclVarSubclass()`` to let teek choose a variable name for you. .. attribute:: type_spec This class attribute should be set to a :ref:`type specification ` of what :meth:`get` returns. """ _default_names = map('teek_var_{}'.format, itertools.count(1)) type_spec = None def __init__(self, *, name=None): if type(self).type_spec is None: raise TypeError(("cannot create instances of {0}, subclass {0} " "and set a 'type_spec' class attribute " "instead").format(type(self).__name__)) if name is None: name = next(type(self)._default_names) self._name = name self._write_trace = None def __repr__(self): try: value_repr = repr(teek.tcl_call(str, 'set', self)) except teek.TclError: value_repr = 'no value has been set' return '<%s %r: %s>' % (type(self).__name__, self.to_tcl(), value_repr) def __eq__(self, other): if not isinstance(other, TclVariable): return NotImplemented return (type(self).type_spec is type(other).type_spec and self._name == other._name) def __hash__(self): return hash((type(self).type_spec, self._name)) @classmethod def from_tcl(cls, varname): """Creates a variable object from a name string. See :ref:`type-spec` for details. """ return cls(name=varname) def to_tcl(self): """Returns the variable name as a string.""" return self._name def set(self, new_value): """Sets the value of the variable. The value does not need to be of the variable's type; it can be anything that can be :ref:`converted to tcl `. """ teek.tcl_call(None, 'set', self, new_value) def get(self): """Returns the value of the variable.""" return teek.tcl_call(type(self).type_spec, 'set', self._name) def wait(self): """Waits for this variable to be modified. The GUI remains responsive during the waiting. See ``tkwait variable`` in :man:`tkwait(3tk)` for details. """ teek.tcl_call(None, 'tkwait', 'variable', self) @property def write_trace(self): """ A :class:`.Callback` that runs when the value of the variable changes. The connected functions will be called with one argument, the variable object. This is implemented with ``trace add variable``, documented in :man:`trace(3tcl)`. """ if self._write_trace is None: self._write_trace = Callback() def runner(*junk): self._write_trace.run(self) command = teek.create_command(runner, [str, str, str]) teek.tcl_call(None, 'trace', 'add', 'variable', self, 'write', command) return self._write_trace class StringVar(TclVariable): type_spec = str # flake8: noqa class IntVar(TclVariable): type_spec = int # flake8: noqa class FloatVar(TclVariable): type_spec = float # flake8: noqa class BooleanVar(TclVariable): type_spec = bool # flake8: noqa @functools.total_ordering class ScreenDistance: """Represents a Tk screen distance. If you don't know or care what screen distances are, use the :attr:`pixels` attribute. The ``value`` can be an integer or float of pixels or a string that :man:`Tk_GetPixels(3tk)` accepts; for example, ``123`` or ``'2i'``. ``ScreenDistance`` objects are hashable, and they can be compared with each other: >>> funny_dict = {ScreenDistance(1): 'lol'} >>> funny_dict[ScreenDistance(1)] 'lol' >>> ScreenDistance('1c') == ScreenDistance('1i') False >>> ScreenDistance('1c') < ScreenDistance('1i') True .. attribute:: pixels The number of pixels that this screen distance represents as an int. This is implemented with ``winfo pixels``, documented in :man:`winfo(3tk)`. .. attribute:: fpixels The number of pixels that this screen distance represents as a float. This is implemented with ``winfo fpixels``, documented in :man:`winfo(3tk)`. """ def __init__(self, value): self._value = str(value) # creating a ScreenDistance object must fail if the screen distance # is invalid, that's why this is here self.pixels = teek.tcl_call(int, 'winfo', 'pixels', '.', self._value) self.fpixels = teek.tcl_call(float, 'winfo', 'fpixels', '.', self._value) def __repr__(self): return '%s(%r)' % (type(self).__name__, self._value) # comparing works with integer pixels because floating point errors are # not fun def __eq__(self, other): if not isinstance(other, ScreenDistance): return NotImplemented return self.pixels == other.pixels def __gt__(self, other): if not isinstance(other, ScreenDistance): return NotImplemented return self.pixels > other.pixels def __hash__(self): return hash(self.fpixels) @classmethod def from_tcl(cls, value_string): """Creates a screen distance object from a Tk screen distance string. See :ref:`type-spec` for details. """ return cls(value_string) def to_tcl(self): """Return the ``value`` as a string.""" return self._value def _options(kwargs): for name, value in kwargs.items(): yield ('-from' if name == 'from_' else '-' + name) yield value class Image: """Represents a Tk photo image. If you want to display an image to the user, use :class:`.Label` with its ``image`` option. See :source:`examples/image.py`. Image objects are wrappers for things documented in :man:`image(3tk)` and :man:`photo(3tk)`. They are mutable, so you can e.g. set a label's image to an image object and then later change that image object; the label will update automatically. .. note:: PNG support was added in Tk 8.6. Use GIF images if you want backwards compatibility with Tk 8.5. If you want to create a program that can read as many different kinds of images as possible, use :mod:`teek.extras.image_loader`. Creating a new ``Image`` object with ``Image(...)`` calls ``image create photo`` followed by the options in Tcl. See :man:`image(3tk)` for details. Keyword arguments are passed as options to :man:`photo(3tk)` as usual, except that if a ``data`` keyword argument is given, it should be a :class:`bytes` object of data that came from e.g. an image file opened with ``'rb'``; it will be automatically converted to base64. Image objects can be compared with ``==``, and they compare equal if they represent the same Tk image; that is, ``image1 == image2`` returns ``image1.to_tcl() == image2.to_tcl()``. Image objects are also hashable. .. attribute:: config Similar to :ref:`the widget config attribute `. """ @make_thread_safe def __init__(self, **kwargs): if 'data' in kwargs: kwargs['data'] = base64.b64encode(kwargs['data']).decode('ascii') if 'file' in kwargs: self._repr_info = 'from %r, ' % (kwargs['file'],) else: self._repr_info = '' name = teek.tcl_call(str, 'image', 'create', 'photo', *_options(kwargs)) self._init_from_name(name) @make_thread_safe def _init_from_name(self, name): self._name = name self.config = CgetConfigureConfigDict( lambda returntype, *args: teek.tcl_call(returntype, self, *args)) self.config._types.update({ 'data': str, 'format': str, 'file': str, 'gamma': float, 'width': int, 'height': int, 'palette': str, }) @classmethod def from_tcl(cls, name): """Create a new image object from the name of a Tk image. See :ref:`type-spec` for details. """ image = cls.__new__(cls) # create an instance without calling __init__ image._init_from_name(name) return image def to_tcl(self): """Returns the Tk name of the image as a string.""" return self._name def __repr__(self): try: size = '%dx%d' % (self.width, self.height) except teek.TclError: size = 'deleted' return '<%s: %s%s>' % (type(self).__name__, self._repr_info, size) def __eq__(self, other): if not isinstance(other, Image): return NotImplemented return self._name == other._name def __hash__(self): return hash(self._name) def delete(self): """Calls ``image delete`` documented in :man:`image(3tk)`. The image object is useless after this, and most things will raise :exc:`.TclError`. """ teek.tcl_call(None, 'image', 'delete', self) def in_use(self): """True if any widget uses this image, or False if not. This calls ``image inuse`` documented in :man:`image(3tk)`. """ return teek.tcl_call(bool, 'image', 'inuse', self) @classmethod def get_all_images(cls): """Return all existing images as a list of :class:`.Image` objects.""" return teek.tcl_call([cls], 'image', 'names') def blank(self): """See ``imageName blank`` in :man:`photo(3tk)`.""" teek.tcl_call(None, self, 'blank') def copy_from(self, source_image, **kwargs): """See ``imageName copy sourceImage`` documented in :man:`photo(3tk)`. Options are passed as usual, except that ``from=something`` is invalid syntax in Python, so this method supports ``from_=something`` instead. If you do ``image1.copy_from(image2)``, the ``imageName`` in :man:`photo(3tk)` means ``image1``, and ``sourceImage`` means ``image2``. """ teek.tcl_call(None, self, 'copy', source_image, *_options(kwargs)) def copy(self, **kwargs): """ Create a new image with the same content as this image so that changing the new image doesn't change this image. This creates a new image and then calls :meth:`copy_from`, so that this... :: image2 = image1.copy() ...does the same thing as this:: image2 = teek.Image() image2.copy_from(image1) Keyword arguments passed to ``image1.copy()`` are passed to ``image2.copy_from()``. This means that it is possible to do some things with both :meth:`copy` and :meth:`copy_from`, but :meth:`copy` is consistent with e.g. :meth:`list.copy` and :meth:`dict.copy`. """ result = Image() result.copy_from(self, **kwargs) return result @property def width(self): """The current width of the image as pixels. Note that ``image.width`` is different from ``image.config['width']``; ``image.width`` changes if the image's size changes, but ``image.config['width']`` often represents the width that the image had when it was first created. **tl;dr:** Usually it's best to use ``image.width`` instead of ``image.config['width']``. """ return teek.tcl_call(int, 'image', 'width', self) @property def height(self): """See :attr:`width`.""" return teek.tcl_call(int, 'image', 'height', self) # TODO: data and put methods, will be hard because passing around binary def get(self, x, y): """Returns the :class:`.Color` of the pixel at (x,y).""" r, g, b = teek.tcl_call([int], self, 'get', x, y) return Color(r, g, b) def read(self, filename, **kwargs): """See ``imageName read filename`` in :man:`photo(3tk)`.""" teek.tcl_call(None, self, 'read', filename, *_options(kwargs)) def redither(self): """See ``imageName redither`` in :man:`photo(3tk)`.""" teek.tcl_call(None, self, 'redither') def transparency_get(self, x, y): """Check if the pixel at (x,y) is transparent, and return a bool. The *x* and *y* are pixels, as integers. See ``imageName transparency get`` in :man:`photo(3tk)`. """ return teek.tcl_call(bool, self, 'transparency', 'get', x, y) def transparency_set(self, x, y, is_transparent): """Make the pixel at (x,y) transparent or not transparent. See ``imageName transparency set`` in :man:`photo(3tk)` and :meth:`transparency_get`. """ teek.tcl_call(None, self, 'transparency', 'set', x, y, is_transparent) def write(self, filename, **kwargs): """See ``imageName write`` in :man:`photo(3tk)`. .. seealso:: Use :meth:`get_bytes` if you don't want to create a file. """ teek.tcl_call(None, self, 'write', filename, *_options(kwargs)) def get_bytes(self, format_, **kwargs): """ Like :meth:`write`, but returns the data as a :class:`bytes` object instead of writing it to a file. The ``format_`` argument can be any string that is compatible with the ``-format`` option of ``imageName write`` documented in :man:`photo(3tk)`. All keyword arguments are same as for :meth:`write`. """ with tempfile.TemporaryDirectory() as temp_dir: self.write(os.path.join(temp_dir, 'picture'), format=format_, **kwargs) with open(os.path.join(temp_dir, 'picture'), 'rb') as file: return file.read() PKwWNS1ZJZJteek/_tcl_calls.pyimport collections import functools import itertools import numbers import queue import sys import threading import traceback import _tkinter import teek _flatten = itertools.chain.from_iterable def raise_teek_tclerror(func): @functools.wraps(func) def result(*args, **kwargs): try: return func(*args, **kwargs) except _tkinter.TclError as e: raise teek.TclError( str(e)).with_traceback(e.__traceback__) from None return result counts = collections.defaultdict(lambda: itertools.count(1)) # because readability is good # TODO: is there something like this in e.g. concurrent.futures? class _Future: def __init__(self): self._value = None self._error = None self._event = threading.Event() self._success = None def set_value(self, value): self._value = value self._success = True self._event.set() def set_error(self, exc): self._error = exc self._success = False self._event.set() def get_value(self): self._event.wait() assert self._success is not None if not self._success: raise self._error return self._value class _TclInterpreter: def __init__(self): assert threading.current_thread() is threading.main_thread() self._init_threads_called = False # tkinter does this :D i have no idea what each argument means self._app = _tkinter.create(None, sys.argv[0], 'Tk', 1, 1, 1, 0, None) self._app.call('wm', 'withdraw', '.') self._app.call('package', 'require', 'Ttk') # when a main-thread-needing function is called from another thread, a # tuple like this is added to this queue: # # (func, args, kwargs, future) # # func is a function that MUST be called from main thread # args and kwargs are arguments for func # future will be set when the function has been called # # the function is called from Tk's event loop self._call_queue = queue.Queue() @raise_teek_tclerror def init_threads(self, poll_interval_ms=50): if threading.current_thread() is not threading.main_thread(): raise RuntimeError( "init_threads() must be called from main thread") # there is a race condition here, but if it actually creates problems, # you are doing something very wrong if self._init_threads_called: raise RuntimeError("init_threads() was called twice") # hard-coded name is ok because there is only one of these in each # Tcl interpreter poller_tcl_command = 'teek_init_threads_queue_poller' after_id = None @raise_teek_tclerror def poller(): nonlocal after_id while True: try: item = self._call_queue.get(block=False) except queue.Empty: break func, args, kwargs, future = item try: value = func(*args, **kwargs) except Exception as e: future.set_error(e) else: future.set_value(value) after_id = self._app.call( 'after', poll_interval_ms, 'teek_init_threads_queue_poller') self._app.createcommand(poller_tcl_command, poller) def quit_disconnecter(): if after_id is not None: self._app.call('after', 'cancel', after_id) teek.before_quit.connect(quit_disconnecter) poller() self._init_threads_called = True def call_thread_safely(self, non_threadsafe_func, args=(), kwargs=None): if kwargs is None: kwargs = {} if threading.current_thread() is threading.main_thread(): return non_threadsafe_func(*args, **kwargs) if not self._init_threads_called: raise RuntimeError("init_threads() wasn't called") future = _Future() self._call_queue.put((non_threadsafe_func, args, kwargs, future)) return future.get_value() # self._app must be accessed from the main thread, and this class provides # methods for calling it thread-safely @raise_teek_tclerror def run(self): if threading.current_thread() is not threading.main_thread(): raise RuntimeError("run() must be called from main thread") # no idea what the 0 does, tkinter calls it like this self._app.mainloop(0) @raise_teek_tclerror def getboolean(self, arg): return self.call_thread_safely(self._app.getboolean, [arg]) # _tkinter returns tuples when tcl represents something as a # list internally, but this forces it to string @raise_teek_tclerror def get_string(self, from_underscore_tkinter): if isinstance(from_underscore_tkinter, str): return from_underscore_tkinter if isinstance(from_underscore_tkinter, _tkinter.Tcl_Obj): return from_underscore_tkinter.string # it's probably a tuple, i think because _tkinter returns tuples when # tcl represents something as a list internally, this forces tcl to # represent it as a string instead result = self.call_thread_safely( self._app.call, ['format', '%s', from_underscore_tkinter]) assert isinstance(result, str) return result @raise_teek_tclerror def splitlist(self, value): return self.call_thread_safely(self._app.splitlist, [value]) @raise_teek_tclerror def call(self, *args): return self.call_thread_safely(self._app.call, args) @raise_teek_tclerror def eval(self, code): return self.call_thread_safely(self._app.eval, [code]) @raise_teek_tclerror def createcommand(self, name, func): return self.call_thread_safely(self._app.createcommand, [name, func]) @raise_teek_tclerror def deletecommand(self, name): return self.call_thread_safely(self._app.deletecommand, [name]) # a global _TclInterpreter instance _interp = None # these are the only functions that access _interp directly def _get_interp(): global _interp if _interp is None: if threading.current_thread() is not threading.main_thread(): raise RuntimeError("init_threads() wasn't called") _interp = _TclInterpreter() return _interp def quit(): """Stop the event loop and destroy all widgets. This function calls ``destroy .`` in Tcl, and that's documented in :man:`destroy(3tk)`. Note that this function does not tell Python to quit; only teek quits, so you can do this:: import teek window = teek.Window() teek.Button(window, "Quit", teek.quit).pack() teek.run() print("Still alive") If you click the button, it interrupts ``teek.run()`` and the print runs. """ global _interp if threading.current_thread() is not threading.main_thread(): # TODO: allow quitting from other threads or document this raise RuntimeError("can only quit from main thread") if _interp is not None: teek.before_quit.run() _interp.call('destroy', '.') # to avoid a weird errors, see test_weird_error in test_tcl_calls.py for command in teek.tcl_call([str], 'info', 'commands'): if command.startswith('teek_command_'): delete_command(command) _interp = None teek.after_quit.run() def run(): """Runs the event loop until :func:`~teek.quit` is called.""" _get_interp().run() @raise_teek_tclerror def init_threads(poll_interval_ms=50): """Allow using teek from other threads than the main thread. This is implemented with a queue. This function starts an :ref:`after callback ` that checks for new messages in the queue every 50 milliseconds (that is, 20 times per second), and when another thread calls a teek function that does a :ref:`Tcl call `, the information required for making the Tcl call is put to the queue and the Tcl call is done by the after callback. .. note:: After callbacks don't work without the event loop, so make sure to run the event loop with :func:`.run` after calling :func:`.init_threads`. ``poll_interval_ms`` can be given to specify a different interval than 50 milliseconds. When a Tcl call is done from another thread, that thread blocks until the after callback has handled it, which is slow. If this is a problem, there are two things you can do: * Use a smaller ``poll_interval_ms``. Watch your CPU usage though; if you make ``poll_interval_ms`` too small, you might get 100% CPU usage when your program is doing nothing. * Try to rewrite the program so that it does less teek stuff in threads. """ _get_interp().init_threads() def make_thread_safe(func): """A decorator that makes a function safe to be called from any thread. Functions decorated with this always run in the event loop, and therefore in the main thread. Most of the time you don't need to use this yourself; teek uses this a lot internally, so most teek things are already thread safe. However, if you have code like this... :: def bad_func123(): func1() func2() func3() ...where ``func1``, ``func2`` and ``func3`` do teek things and you need to call ``func123`` from a thread, it's best to decorate ``func123``:: @teek.make_thread_safe def good_func123(): func1() func2() func3() This may make ``func123`` noticably faster. If a function decorated with ``make_thread_safe()`` is called from some other thread than the main thread, it needs to communicate between the main thread and teek's event loop, which is slow. However, with ``good_func123``, there isn't much communication to do: the other thread needs to tell the main thread to run the function, and later the main thread tells the other thread that the function has finished running. The ``bad_func123`` function does this 3 times, once in each line of code. .. note:: Functions decorated with ``make_thread_safe()`` must not block because they are ran in the event loop. In other words, this code is bad, because it will freeze the GUI for about 5 seconds:: @teek.make_thread_safe def do_stuff(): time.sleep(5) """ @functools.wraps(func) def safe(*args, **kwargs): return _get_interp().call_thread_safely(func, args, kwargs) return safe def to_tcl(value): if hasattr(value, 'to_tcl'): # duck-typing ftw return value.to_tcl() if value is None: return '' if isinstance(value, str): return value if isinstance(value, collections.abc.Mapping): return tuple(map(to_tcl, _flatten(value.items()))) if isinstance(value, bool): return '1' if value else '0' if isinstance(value, numbers.Real): # after bool check, bools are ints return str(value) # assume it's some kind of iterable, this must be after the Mapping # and str stuff above return tuple(map(to_tcl, value)) def _pairs(sequence): assert len(sequence) % 2 == 0, "cannot divide %r into pairs" % (sequence,) return zip(sequence[0::2], sequence[1::2]) def from_tcl(type_spec, value): if type_spec is None: return None if type_spec is str: return _get_interp().get_string(value) if type_spec is bool: if not from_tcl(str, value): # '' is not a valid bool, but this is usually what was intended return None try: return _get_interp().getboolean(value) except teek.TclError as e: raise ValueError(str(e)).with_traceback(e.__traceback__) from None # special case to allow bases other than 10 and empty strings if type_spec is int: stringed_value = from_tcl(str, value) if not stringed_value: return None return int(stringed_value, 0) if isinstance(type_spec, type): # it's a class if issubclass(type_spec, numbers.Real): # must be after bool check string = from_tcl(str, value) if not string: return None return type_spec(string) if hasattr(type_spec, 'from_tcl'): string = from_tcl(str, value) # the empty string is the None value in tcl if not string: return None return type_spec.from_tcl(string) elif isinstance(type_spec, (list, tuple, dict)): items = _get_interp().splitlist(from_tcl(str, value)) if isinstance(type_spec, list): # [int] -> [1, 2, 3] (item_spec,) = type_spec return [from_tcl(item_spec, item) for item in items] if isinstance(type_spec, tuple): # (int, str) -> (1, 'hello') if len(type_spec) != len(items): raise ValueError("expected a sequence of %d items, got %r" % (len(type_spec), list(items))) return tuple(map(from_tcl, type_spec, items)) if isinstance(type_spec, dict): # {'a': int, 'b': str} -> {'a': 1, 'b': 'lol', 'c': 'str assumed'} result = {} for key, value in _pairs(items): key = from_tcl(str, key) result[key] = from_tcl(type_spec.get(key, str), value) return result raise RuntimeError("this should never happen") # pragma: no cover raise TypeError("unknown type specification " + repr(type_spec)) @raise_teek_tclerror def tcl_call(returntype, command, *arguments): """Call a Tcl command. The arguments are passed correctly, even if they contain spaces: >>> teek.tcl_eval(None, 'puts "hello world thing"') # 1 arguments to puts\ # doctest: +SKIP hello world thing >>> message = 'hello world thing' >>> teek.tcl_eval(None, 'puts %s' % message) # 3 args to puts, tcl error Traceback (most recent call last): ... teek.TclError: wrong # args: should be "puts ?-nonewline? ?channelId? \ string" >>> teek.tcl_call(None, 'puts', message) # 1 arg to puts\ # doctest: +SKIP hello world thing """ result = _get_interp().call(tuple(map(to_tcl, (command,) + arguments))) return from_tcl(returntype, result) @raise_teek_tclerror def tcl_eval(returntype, code): """Run a string of Tcl code. >>> teek.tcl_eval(None, 'proc add {a b} { return [expr $a + $b] }') >>> teek.tcl_eval(int, 'add 1 2') 3 >>> teek.tcl_call(int, 'add', 1, 2) # usually this is better, see below 3 """ result = _get_interp().eval(code) return from_tcl(returntype, result) # because there's no better place for this def update(*, idletasks_only=False): """Handles all pending events, and returns when they are all handled. See :man:`update(3tcl)` for details. If ``idletasks_only=True`` is given, this calls ``update idletasks``; otherwise, this calls ``update`` with no arguments. """ if idletasks_only: tcl_call(None, 'update', 'idletasks') else: tcl_call(None, 'update') # TODO: maybe some magic that uses type hints for this? @make_thread_safe def create_command(func, arg_type_specs=(), *, extra_args_type=None): """Create a Tcl command that calls ``func``. Here is a simple example: >>> tcl_print = teek.create_command(print, [str]) # calls print(a_string) >>> tcl_print # doctest: +SKIP 'teek_command_1' >>> teek.tcl_call(None, tcl_print, 'hello world') hello world >>> teek.tcl_eval(None, '%s "hello world"' % tcl_print) hello world Created commands should be deleted with :func:`.delete_command` when they are no longer needed. The function will take ``len(arg_type_specs)`` arguments, and the arguments are converted to Python objects using ``arg_type_specs``. The ``arg_type_specs`` must be a sequence of :ref:`type specifications `. If ``extra_args_type`` is given, the function can also take more than ``len(arg_type_specs)`` arguments, and the type of each extra argument will be *extra_args_type*. For example: >>> def func(a, b, *args): ... print(a - b) ... for arg in args: ... print(arg) ... >>> command = teek.create_command(func, [int, int], extra_args_type=str) >>> teek.tcl_call(None, command, 123, 23, 'asd', 'toot', 'boom boom') 100 asd toot boom boom The return value from the Python function is :ref:`converted to a string for Tcl `. If the function raises an exception, a traceback will be printed. However, the Tcl command returns an empty string on errors and does *not* raise an error in Tcl. Be sure to return a non-empty value on success if you want to do error handling in Tcl code. """ # verbose is better than implicit stack_info = ''.join(traceback.format_stack()) def real_func(*args): try: # python raises TypeError for wrong number of args if extra_args_type is None: expected = "%d arguments" % len(arg_type_specs) ok = (len(args) == len(arg_type_specs)) else: expected = "at least %d arguments" % len(arg_type_specs) ok = (len(args) >= len(arg_type_specs)) if not ok: raise TypeError("expected %s, got %d arguments" % (expected, len(args))) # map(func, a, b) stops when the shortest of a and b ends basic_args = map(from_tcl, arg_type_specs, args[:len(arg_type_specs)]) extra_args = (from_tcl(extra_args_type, arg) for arg in args[len(arg_type_specs):]) # func(*basic_args, *extra_args) doesn't work in 3.4 # basic_args + extra_args doesn't work because they are iterators return to_tcl(func(*itertools.chain(basic_args, extra_args))) except Exception: traceback_blabla, rest = traceback.format_exc().split('\n', 1) print(traceback_blabla + '\n' + stack_info + rest, end='', file=sys.stderr) return '' name = 'teek_command_%d' % next(counts['commands']) _get_interp().createcommand(name, real_func) return name @make_thread_safe def delete_command(name): """Delete a Tcl command by name. You can delete commands returned from :func:`create_command` to avoid memory leaks. """ _get_interp().deletecommand(name) PKPYN(  teek/_timeouts.pyimport teek from teek._tcl_calls import make_thread_safe # there's no after_info because i don't see how it would be useful in # teek class _Timeout: def __init__(self, after_what, callback, args, kwargs): if kwargs is None: kwargs = {} self._callback = callback self._args = args self._kwargs = kwargs self._state = 'pending' # just for __repr__ and error messages self._tcl_command = teek.create_command(self._run) self._id = teek.tcl_call(str, 'after', after_what, self._tcl_command) def __repr__(self): name = getattr(self._callback, '__name__', self._callback) return '<%s %r timeout %r>' % (self._state, name, self._id) def _run(self): needs_cleanup = True # this is important, thread tests freeze without this special # case for some reason def quit_callback(): nonlocal needs_cleanup needs_cleanup = False teek.before_quit.connect(quit_callback) try: self._callback(*self._args, **self._kwargs) self._state = 'successfully completed' except Exception as e: self._state = 'failed' raise e finally: teek.before_quit.disconnect(quit_callback) if needs_cleanup: teek.delete_command(self._tcl_command) @make_thread_safe def cancel(self): """Prevent this timeout from running as scheduled. :exc:`RuntimeError` is raised if the timeout has already ran or it has been cancelled. There is example code in :source:`examples/timeout.py`. """ if self._state != 'pending': raise RuntimeError("cannot cancel a %s timeout" % self._state) teek.tcl_call(None, 'after', 'cancel', self._id) self._state = 'cancelled' teek.delete_command(self._tcl_command) @make_thread_safe def after(ms, callback, args=(), kwargs=None): """Run ``callback(*args, **kwargs)`` after waiting for the given time. The *ms* argument should be a waiting time in milliseconds, and *kwargs* defaults to ``{}``. This returns a timeout object with a ``cancel()`` method that takes no arguments; you can use that to cancel the timeout before it runs. """ return _Timeout(ms, callback, args, kwargs) @make_thread_safe def after_idle(callback, args=(), kwargs=None): """Like :func:`after`, but runs the timeout as soon as possible.""" return _Timeout('idle', callback, args, kwargs) PKwWN"teek/dialog.pyfrom functools import partial import os import teek def _options(kwargs): if 'parent' in kwargs and isinstance(kwargs['parent'], teek.Window): kwargs['parent'] = kwargs['parent'].toplevel for name, value in kwargs.items(): yield '-' + name yield value def color(**kwargs): """Calls :man:`tk_chooseColor(3tk)`. The color selected by the user is returned, or ``None`` if the user cancelled the dialog. """ return teek.tcl_call(teek.Color, 'tk_chooseColor', *_options(kwargs)) def _messagebox(type, title, message, detail=None, **kwargs): kwargs['type'] = type kwargs['title'] = title kwargs['message'] = message if detail is not None: kwargs['detail'] = detail if type == 'ok': teek.tcl_call(None, 'tk_messageBox', *_options(kwargs)) return None if type == 'okcancel': return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'ok' if type == 'retrycancel': return ( teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'retry') if type == 'yesno': return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) == 'yes' # for anything else, return a string return teek.tcl_call(str, 'tk_messageBox', *_options(kwargs)) info = partial(_messagebox, 'ok', icon='info') warning = partial(_messagebox, 'ok', icon='warning') error = partial(_messagebox, 'ok', icon='error') ok_cancel = partial(_messagebox, 'okcancel', icon='question') retry_cancel = partial(_messagebox, 'retrycancel', icon='warning') yes_no = partial(_messagebox, 'yesno', icon='question') yes_no_cancel = partial(_messagebox, 'yesnocancel', icon='question') abort_retry_ignore = partial(_messagebox, 'abortretryignore', icon='error') def _check_multiple(kwargs): if 'multiple' in kwargs: raise TypeError( "the 'multiple' option is not supported, use open_file() or " "open_multiple_files() depending on whether you want to support " "selecting multiple files at once") def open_file(**kwargs): """ Ask the user to choose an existing file. Returns the path. This calls :man:`tk_getOpenFile(3tk)` without ``-multiple``. ``None`` is returned if the user cancels. """ _check_multiple(kwargs) result = teek.tcl_call(str, 'tk_getOpenFile', *_options(kwargs)) if not result: return None return os.path.abspath(result) def open_multiple_files(**kwargs): """ Ask the user to choose one or more existing files. Returns a list of paths. This calls :man:`tk_getOpenFile(3tk)` with ``-multiple`` set to true. An empty list is returned if the user cancels. """ _check_multiple(kwargs) result = teek.tcl_call( [str], 'tk_getOpenFile', '-multiple', True, *_options(kwargs)) return list(map(os.path.abspath, result)) def save_file(**kwargs): """Ask the user to choose a path for a new file. Return the path. This calls :man:`tk_getSaveFile(3tk)`, and returns ``None`` if the user cancels. """ result = teek.tcl_call(str, 'tk_getSaveFile', *_options(kwargs)) if not result: return None return os.path.abspath(result) def directory(**kwargs): """Asks the user to choose a directory, and return a path to it. This calls :man:`tk_chooseDirectory(3tk)`, and returns ``None`` if the user cancels. .. note:: By default, the user can choose a directory that doesn't exist yet. This behaviour is documented in :man:`tk_chooseDirectory(3tk)`. If you want the user to choose an existing directory, use ``mustexist=True``. """ result = teek.tcl_call(str, 'tk_chooseDirectory', *_options(kwargs)) if not result: return None return os.path.abspath(result) PKë-Nteek/_widgets/__init__.pyPKkNJ7uyyteek/_widgets/base.pyimport collections.abc import contextlib import functools import keyword import operator import re import teek from teek._tcl_calls import counts, from_tcl, make_thread_safe from teek._structures import ConfigDict, CgetConfigureConfigDict, after_quit _widgets = {} _class_bindings = {} after_quit.connect(_widgets.clear) after_quit.connect(_class_bindings.clear) # like what you would expect to get for combining @classmethod and @property, # but doesn't do any magic with assigning, only getting class _ClassProperty: def __init__(self, getter): assert isinstance(getter.__name__, str) self._getter = getter def __get__(self, instance_or_none, claas): if instance_or_none is None: return self._getter(claas) attribute = self._getter.__name__ classname = claas.__name__ raise AttributeError( "the %s attribute must be used like %s.%s, " "not like some_%s_instance.%s" % (attribute, classname, attribute, classname.lower(), attribute)) class StateSet(collections.abc.MutableSet): def __init__(self, widget): self._widget = widget def __repr__(self): # yes, this uses [] even though it behaves like a set, that's the best # thing i thought of return '' % (list(self),) def __iter__(self): return iter(self._widget._call([str], self._widget, 'state')) def __len__(self): return len(self._widget._call([str], self._widget, 'state')) def __contains__(self, state): return self._widget._call(bool, self._widget, 'instate', state) def add(self, state): self._widget._call(None, self._widget, 'state', state) def discard(self, state): self._widget._call(None, self._widget, 'state', '!' + state) class GridRowOrColumnConfig(ConfigDict): def __init__(self, configure_method): super().__init__() self._types.update({ 'minsize': teek.ScreenDistance, 'weight': float, 'uniform': str, 'pad': teek.ScreenDistance, }) self._configure = configure_method def _set(self, option, value): self._configure(None, '-' + option, value) def _get(self, option): return self._configure(self._types.get(option, str), '-' + option) def _list_options(self): return (key.lstrip('-') for key in self._configure({}).keys()) class GridRowOrColumn: def __init__(self, widget, row_or_column, number): super().__init__() self._widget = widget self._row_or_column = row_or_column self._number = number self.config = GridRowOrColumnConfig(self._configure) def __repr__(self): return ( "" % (self._row_or_column, self._number)) def __eq__(self, other): if not isinstance(other, GridRowOrColumn): return NotImplemented return (self._widget == other._widget and self._row_or_column == other._row_or_column and self._number == other._number) def __hash__(self): return hash((self._widget, self._row_or_column, self._number)) def _configure(self, returntype, *args): return self._widget._call( returntype, 'grid', self._row_or_column + 'configure', self._widget, self._number, *args) def get_slaves(self): return self._widget._call( [Widget], 'grid', 'slaves', self._widget, '-' + self._row_or_column, self._number) # make things more tkinter-user-friendly def _tkinter_hint(good, bad): def dont_use_this(self, *args, **kwargs): raise TypeError("use %s, not %s" % (good, bad)) return dont_use_this 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 >>> 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 `. 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 '' % 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 ```` event instead: >>> class WorkingFunnyLabel(teek.Label): ... def __init__(self, *args, **kwargs): ... super().__init__(*args, **kwargs) ... self.bind('', 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 # 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 -data $theData`` looks like ``widget.event_generate('', 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) # these are from bind(3tk), Tk 8.5 and 8.6 support all of these # # event(3tk) says that width and height are screen distances, but bind seems to # convert them to ints, so they are ints here # # if you change this, also change docs/bind.rst _BIND_SUBS = [ ('%#', int, 'serial'), ('%a', int, 'above'), ('%b', int, 'button'), ('%c', int, 'count'), ('%d', str, '_data'), ('%f', bool, 'focus'), ('%h', int, 'height'), ('%i', int, 'i_window'), ('%k', int, 'keycode'), ('%m', str, 'mode'), ('%o', bool, 'override'), ('%p', str, 'place'), ('%s', str, 'state'), ('%t', int, 'time'), ('%w', int, 'width'), ('%x', int, 'x'), ('%y', int, 'y'), ('%A', str, 'char'), ('%B', int, 'borderwidth'), ('%D', int, 'delta'), ('%E', bool, 'sendevent'), ('%K', str, 'keysym'), ('%N', int, 'keysym_num'), ('%P', str, 'property_name'), ('%R', int, 'root'), ('%S', int, 'subwindow'), ('%T', int, 'type'), ('%W', Widget, 'widget'), ('%X', int, 'rootx'), ('%Y', int, 'rooty'), ] class Event: def __repr__(self): # try to avoid making the repr too verbose ignored_names = ['widget', 'sendevent', 'subwindow', 'time', 'i_window', 'root', 'state'] ignored_values = [None, '??', -1, 0] pairs = [] for name, value in sorted(self.__dict__.items(), key=operator.itemgetter(0)): if name not in ignored_names and value not in ignored_values: display_name = 'data' if name == '_data' else name pairs.append('%s=%r' % (display_name, value)) return '' % ', '.join(pairs) def data(self, type_spec): return from_tcl(type_spec, self._data) class BindingDict(collections.abc.Mapping): # bind(3tk) calls things like '' sequences, so this code is # consistent with that def __init__(self, bind_caller, command_list): self._call_bind = bind_caller self.command_list = command_list self._callback_objects = {} # {sequence: callback} def __repr__(self): return '' def __iter__(self): # loops over all existing bindings, not all possible bindings return iter(self._call_bind([str])) def __len__(self): return len(self._call_bind([str])) def _callback_runner(self, callback, *args): assert len(args) == len(_BIND_SUBS) event = Event() for (character, type_, attrib), string_value in zip(_BIND_SUBS, args): assert isinstance(string_value, str) try: value = from_tcl(type_, string_value) except (ValueError, teek.TclError) as e: if string_value == '??': value = None elif attrib == 'sendevent': # this seems to be a bug in Tk, here's a minimal example: # # label .lab -text "click this to do the bug" # pack .lab # bind .lab { puts "leave: %E" } # bind .lab { tk_messageBox } # # for me this prints "leave: 343089580", even though # bind(3tk) says that %E is 1 or 0 value = None else: # pragma: no cover raise e # if this runs, there's a bug in teek setattr(event, attrib, value) return callback.run(event) def __getitem__(self, sequence): if sequence in self._callback_objects: return self._callback_objects[sequence] # <1> and are equivalent, this handles that for equiv_sequence, equiv_callback in self._callback_objects.items(): # this equivalence check should handle corner cases imo because the # command names from create_command are unique if (self._call_bind(str, sequence) == self._call_bind(str, equiv_sequence)): # flake8: noqa # found an equivalent binding, tcl commands are the same self._callback_objects[sequence] = equiv_callback return equiv_callback callback = teek.Callback() runner = functools.partial(self._callback_runner, callback) command = teek.create_command(runner, [str] * len(_BIND_SUBS)) self.command_list.append(command) # avoid memory leaks subs_string = ' '.join(subs for subs, type_, name in _BIND_SUBS) self._call_bind( None, sequence, '+ if { [%s %s] eq {break} } { break }' % ( command, subs_string)) self._callback_objects[sequence] = callback return callback # any_widget.bind is set to this def _convenience_bind(self, sequence, func, *, event=False): self[sequence].connect(func if event else (lambda event: func())) # TODO: "RELATIVE PLACEMENT" in grid(3tk) class ChildMixin: def _geometry_manage(self, geometry_manager, **kwargs): args = [] for name, value in kwargs.items(): if name == 'in_': name = 'in' args.append('-' + name) args.append(value) # special case: tkinter does nothing (lol), teek would give a # noob-unfriendly TclError otherwise if geometry_manager == 'place' and not args: raise TypeError( "cannot call widget.place() without any arguments, " "do e.g. widget.place(relx=0, rely=0) instead") self._call(None, geometry_manager, self.to_tcl(), *args) def _geometry_manager_forget(self, geometry_manager): self._call(None, geometry_manager, 'forget', self.to_tcl()) def _geometry_manager_info(self, geometry_manager): types = { '-in': Widget, } if geometry_manager == 'pack' or geometry_manager == 'grid': types.update({ # padx and pady can be lists of 2 screen distances or just 1 # screen distance, which is fine because a Tcl screen distance # string # behaves like a list of 1 item '-padx': [teek.ScreenDistance], '-pady': [teek.ScreenDistance], '-ipadx': teek.ScreenDistance, '-ipady': teek.ScreenDistance, }) if geometry_manager == 'pack': types.update({ '-expand': bool, }) elif geometry_manager == 'grid': types.update({ '-column': int, '-columnspan': int, '-row': int, '-rowspan': int, '-sticky': str, }) elif geometry_manager == 'place': types.update({ '-anchor': str, '-bordermode': str, '-width': teek.ScreenDistance, '-height': teek.ScreenDistance, '-relheight': float, '-relwidth': float, '-relx': float, '-rely': float, '-x': teek.ScreenDistance, '-y': teek.ScreenDistance, }) else: raise RuntimeError("oh no") # pragma: no cover result = self._call(types, geometry_manager, 'info', self.to_tcl()) return {key.lstrip('-'): value for key, value in result.items()} pack = functools.partialmethod(_geometry_manage, 'pack') grid = functools.partialmethod(_geometry_manage, 'grid') place = functools.partialmethod(_geometry_manage, 'place') pack_forget = functools.partialmethod(_geometry_manager_forget, 'pack') grid_forget = functools.partialmethod(_geometry_manager_forget, 'grid') place_forget = functools.partialmethod(_geometry_manager_forget, 'place') pack_info = functools.partialmethod(_geometry_manager_info, 'pack') grid_info = functools.partialmethod(_geometry_manager_info, 'grid') place_info = functools.partialmethod(_geometry_manager_info, 'place') PKwWNj..teek/_widgets/menu.pyimport collections.abc import teek from teek._structures import CgetConfigureConfigDict from teek._tcl_calls import make_thread_safe from teek._widgets.base import Widget # all menu item things that do something run in the main thread to avoid any # kind of use of menu items that are in an inconsistent state, and the Menu # class also does this... think of it as poor man's locking or something 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 `. 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 # does not use ChildMixin because usually it's a bad idea to e.g. pack a menu # TODO: document that this class assumes that nothing else changes the # underlying Tcl widget class Menu(Widget, collections.abc.MutableSequence): """This is the menu widget. The ``items`` should be an iterable of :class:`.MenuItem` objects, and it's treated so that this... :: menu = teek.Menu([ teek.MenuItem("Click me", print), teek.MenuItem("No, click me instead", print), ]) ...does the same thing as this:: menu = teek.Menu() menu.append(teek.MenuItem("Click me", print)) menu.append(teek.MenuItem("No, click me instead", print)) Menu widgets behave like lists of menu items, so if you can do something to a list of :class:`.MenuItem` objects, you can probably do it directly to a :class:`.Menu` widget as well. However, menu widgets don't support slicing, like lists do: >>> menu = teek.Menu([ ... teek.MenuItem("Click me", print), ... ]) >>> menu.append(teek.MenuItem("No, click me instead", print)) >>> menu >>> menu[0] # this works ): type='command', added to\ a menu> >>> for item in menu: # this works ... print(item) ... ): type='command', added to\ a menu> ): type='comman\ d', added to a menu> >>> menu[:2] # but this doesn't work Traceback (most recent call last): ... TypeError: slicing a Menu widget is not supported >>> list(menu)[:2] # workaround # doctest: +ELLIPSIS [, ] :class:`.Menu` objects assume that nothing changes the underlying Tk menu widget without the :class:`.Menu` object. For example: >>> menu = teek.Menu() >>> command = menu.to_tcl() >>> command # doctest: +SKIP '.menu1' >>> # DON'T DO THIS, this is a bad idea >>> teek.tcl_eval(None, '%s add checkbutton -command {puts hello}' % comma\ nd) >>> len(menu) # the menu widget doesn't know that we added an item 0 If you don't know what :func:`.tcl_eval` does, you don't need to worry about doing this accidentally. Manual page: :man:`menu(3tk)` """ _widget_name = 'menu' tk_class_name = 'Menu' def __init__(self, items=(), **kwargs): kwargs.setdefault('tearoff', False) super().__init__(None, **kwargs) self._items = [] self.extend(items) def _init_config(self): super()._init_config() self.config._types.update({ 'selectcolor': teek.Color, 'tearoff': bool, 'title': str, 'type': str, }) def _repr_parts(self): return ['contains %d items' % len(self)] @make_thread_safe def __getitem__(self, index): if isinstance(index, slice): raise TypeError("slicing a Menu widget is not supported") return self._items[index] @make_thread_safe def __delitem__(self, index): if isinstance(index, slice): raise TypeError("slicing a Menu widget is not supported") index = range(len(self))[index] # handle indexes like python does it self._call(None, self, 'delete', index) item = self._items.pop(index) item._after_removing() # indexes after the deleted item are messed up for index in range(index, len(self._items)): self._items[index]._index = index @make_thread_safe def __setitem__(self, index, value): if isinstance(index, slice): raise TypeError("slicing a Menu widget is not supported") # this is needed because otherwise this breaks with a negative index, # and this code handles indexes like python does it index = range(len(self))[index] del self[index] self.insert(index, value) @make_thread_safe def __len__(self): return len(self._items) @make_thread_safe def insert(self, index, item: MenuItem): if not isinstance(item, MenuItem): # TODO: test that tuples are handled correctly here because that # might be a common mistake raise TypeError("expected a MenuItem, got %r" % (item,)) # handle the index line python does it item._prepare_adding() self._items.insert(index, item) index = self._items.index(item) self._call(None, self, 'insert', index, item.type) item._after_adding(self, index) # inserting to self._items messed up items after the index for index2 in range(index + 1, len(self._items)): self._items[index2]._index = index2 def popup(self, x, y, menu_item=None): """Displays the menu on the screen. x and y are coordinates in pixels, relative to the screen. See :man:`tk_popup(3tk)` for details. If *menu_item* is given, its index is passed to :man:`tk_popup(3tk)`. There are two ways to show popup menus in Tk. This is one of them, and ``post`` is another. I spent a while trying to find something that explains the difference, and the best thing I found is `this book `_. The book uses ``tk_popup``, and one of the authors is John Ousterhout, the creator of Tcl and Tk. """ if menu_item is None: teek.tcl_call(None, 'tk_popup', self, x, y) else: menu_item._check_in_menu() teek.tcl_call(None, 'tk_popup', self, x, y, menu_item._index) PKPYN^oOEOEteek/_widgets/misc.pyimport teek from teek._tcl_calls import from_tcl, make_thread_safe from teek._widgets.base import Widget, ChildMixin class Button(ChildMixin, Widget): """A widget that runs a callback when it's clicked. See :source:`examples/button.py` for example code. ``text`` can be given as with :class:`Label`. The ``'command'`` option is not settable, and its value is a :class:`.Callback` that runs with no arguments when the button is clicked. If the *command* argument is given, it will be treated so that this... :: button = teek.Button(some_widget, "Click me", do_something) ...does the same thing as this:: button = teek.Button(some_widget, "Click me") button.config['command'].connect(do_something) See :meth:`.Callback.connect` documentation if you need to pass arguments to the ``do_something`` function. Manual page: :man:`ttk_button(3tk)` """ _widget_name = 'ttk::button' tk_class_name = 'TButton' @make_thread_safe def __init__(self, parent, text='', command=None, **kwargs): super().__init__(parent, text=text, **kwargs) if command is not None: self.config['command'].connect(command) def _init_config(self): super()._init_config() self.config._types.update({ 'default': str, 'width': teek.ScreenDistance, }) self.config._special['command'] = self._create_click_command def _create_click_command(self): result = teek.Callback() command_string = teek.create_command(result.run) self.command_list.append(command_string) self._call(None, self, 'configure', '-command', command_string) return result def _repr_parts(self): return ['text=' + repr(self.config['text'])] def invoke(self): """Runs the command callback. See ``pathname invoke`` in :man:`ttk_button(3tk)` for details. """ self._call(None, self, 'invoke') class Checkbutton(ChildMixin, Widget): """A square-shaped, checkable box with text next to it. See :source:`examples/checkbutton.py` for example code. For convenience, ``text`` and ``command`` arguments work the same way as with :class:`.Button`. The ``'command'`` option is not settable, and its value is a :class:`.Callback`. By default, it runs with ``True`` as the only argument when the checkbutton is checked, and with ``False`` when the checkbutton is unchecked. You can pass ``onvalue=False, offvalue=True`` to reverse this if you find it useful for some reason. This also affects the values that end up in the ``'variable'`` option (see manual page), which is a :class:`.BooleanVar`. Manual page: :man:`ttk_checkbutton(3tk)` """ _widget_name = 'ttk::checkbutton' tk_class_name = 'TCheckbutton' @make_thread_safe def __init__(self, parent, text='', command=None, **kwargs): super().__init__(parent, text=text, **kwargs) if command is not None: self.config['command'].connect(command) def _init_config(self): super()._init_config() self.config._types.update({ 'onvalue': bool, 'offvalue': bool, 'variable': teek.BooleanVar, 'width': teek.ScreenDistance, }) self.config._special['command'] = self._create_check_command def _command_runner(self): self.config['command'].run(self.config['variable'].get()) def _create_check_command(self): result = teek.Callback() command_string = teek.create_command(self._command_runner) self.command_list.append(command_string) self._call(None, self, 'configure', '-command', command_string) return result def invoke(self): """ Checks or unchecks the checkbutton, updates the variable and runs the command callback. See ``pathname invoke`` in :man:`ttk_checkbutton(3tk)` for details. """ self._call(None, self, 'invoke') class Entry(ChildMixin, Widget): """A widget for asking the user to enter a one-line string. The ``text`` option works as with :class:`.Label`. .. seealso:: Use :class:`.Label` if you want to display text without letting the user edit it. Entries are also not suitable for text with more than one line; use :class:`.Text` instead if you want multiple lines. Manual page: :man:`ttk_entry(3tk)` """ _widget_name = 'ttk::entry' tk_class_name = 'TEntry' @make_thread_safe def __init__(self, parent, text='', **kwargs): super().__init__(parent, **kwargs) self._call(None, self, 'insert', 0, text) def _init_config(self): super()._init_config() self.config._types.update({ 'exportselection': bool, #invalidcommand: ???, 'show': str, 'validate': str, #'validatecommand': ???, 'width': int, # NOT a screen distance }) def _repr_parts(self): return ['text=' + repr(self.text)] @property def text(self): """The string of text in the entry widget. Setting and getting this attribute calls ``get``, ``insert`` and ``delete`` documented in :man:`ttk_entry(3tk)`. """ return self._call(str, self, 'get') @text.setter @make_thread_safe def text(self, new_text): self._call(None, self, 'delete', 0, 'end') self._call(None, self, 'insert', 0, new_text) @property def cursor_pos(self): """ The integer index of the cursor in the entry, so that ``entry.text[:entry.cursor_pos]`` and ``entry.text[entry.cursor_pos:]`` are the text before and after the cursor, respectively. You can set this attribute to move the cursor. """ return self._call(int, self, 'index', 'insert') @cursor_pos.setter def cursor_pos(self, new_pos): self._call(None, self, 'icursor', new_pos) class Spinbox(Entry): """An entry with up and down buttons. This class inherits from :class:`.Entry`, so it has all the attributes and methods of :class:`.Entry`, like :attr:`~.Entry.text` and :attr:`~.Entry.cursor_pos`. The value of the ``'command'`` option is a :class:`.Callback` that is ran with no arguments. If a ``command`` keyword argument is given, it will be connected to the callback automatically. Manual page: :man:`ttk_spinbox(3tk)` """ _widget_name = 'ttk::spinbox' tk_class_name = 'TSpinbox' @make_thread_safe def __init__(self, parent, *, command=None, **kwargs): super().__init__(parent, **kwargs) if command is not None: self.config['command'].connect(command) def _init_config(self): super()._init_config() self.config._types.update({ 'from': float, 'to': float, 'increment': float, 'values': [str], 'wrap': bool, 'format': str, }) self.config._special['command'] = self._create_spin_command def _create_spin_command(self): result = teek.Callback() command_string = teek.create_command(result.run) self.command_list.append(command_string) self._call(None, self, 'configure', '-command', command_string) return result class Combobox(Entry): """An entry that displays a list of valid values. This class inherits from :class:`.Entry`, so it has all the attributes and methods of :class:`.Entry`, like :attr:`~.Entry.text` and :attr:`~.Entry.cursor_pos`. Manual page: :man:`ttk_combobox(3tk)` """ _widget_name = 'ttk::combobox' tk_class_name = 'TCombobox' def _init_config(self): super()._init_config() self.config._types.update({ 'height': int, 'values': [str], }) class Frame(ChildMixin, Widget): """An empty widget. Frames are often used as containers for other widgets. Manual page: :man:`ttk_frame(3tk)` """ _widget_name = 'ttk::frame' tk_class_name = 'TFrame' def _init_config(self): super()._init_config() # if you change these, also change Window's types in windows.py self.config._types.update({ 'height': teek.ScreenDistance, 'padding': teek.ScreenDistance, 'width': teek.ScreenDistance, }) class Label(ChildMixin, Widget): """A widget that displays text. For convenience, the ``text`` option can be also given as a positional initialization argument, so ``teek.Label(parent, "hello")`` and ``teek.Label(parent, text="hello")`` do the same thing. Manual page: :man:`ttk_label(3tk)` """ _widget_name = 'ttk::label' tk_class_name = 'TLabel' def __init__(self, parent, text='', **kwargs): super().__init__(parent, text=text, **kwargs) def _init_config(self): super()._init_config() self.config._types.update({ 'width': teek.ScreenDistance, }) def _repr_parts(self): return ['text=' + repr(self.config['text'])] class LabelFrame(ChildMixin, Widget): """A frame with a visible border line and title text. For convenience, the ``text`` option can be given as with :class:`.Label`. Manual page: :man:`ttk_labelframe(3tk)` """ _widget_name = 'ttk::labelframe' tk_class_name = 'TLabelframe' def __init__(self, parent, text='', **kwargs): super().__init__(parent, text=text, **kwargs) def _init_config(self): super()._init_config() self.config._types.update({ 'height': teek.ScreenDistance, 'labelanchor': str, 'labelwidget': Widget, 'width': teek.ScreenDistance, }) def _repr_parts(self): return ['text=' + repr(self.config['text'])] class Progressbar(ChildMixin, Widget): """ Displays progress of a long-running operation. This is useful if you are :ref:`running something concurrently ` and you want to let the user know that something is happening. The progress bar can be used in two modes. Pass ``mode='indeterminate'`` and call :meth:`start` to make the progress bar bounce back and forth forever. If you want to create a progress bar that actually displays progress instead of just letting the user know that something is happening, don't pass ``mode='indeterminate'``; the default is ``mode='determinate'``, which does what you want. There's a ``'value'`` option that can be used to set the progress in determinate mode. A value of 0 means that nothing is done, and 100 means that we are ready. If you do math on a regular basis, that's all you need to know, but if you are not very good at math, keep reading: .. admonition:: Progress Math If your program does 5 things, and 2 of them are done, you should do this:: progress_bar.config['value'] = (2 / 5) * 100 It works like this: * The program has done 2 things out of 5; that is, 2/5. That is a division. Its value turns out to be 0.4. * We want percents. They are numbers between 0 and 100. The ``done / total`` calculation gives us a number between 0 and 1; if we have done nothing, we have ``0 / 5 == 0.0``, and if we have done everything, we have ``5 / 5 == 1.0``. If we add ``* 100``, we get ``0.0 * 100 = 0.0`` when we haven't done anything, and ``1.0 * 100 == 100.0`` when we have done everything. Awesome! :: progress_bar.config['value'] = (done / total) * 100 However, this fails if ``total == 0``: >>> 1/0 Traceback (most recent call last): ... ZeroDivisionError: division by zero If we have no work to do and we have done nothing (``0/0``), then how many percents of the work is done? It doesn't make sense. You can handle these cases e.g. like this:: if total == 0: # 0/0 things done, make the progress bar grayed out because # there is no progress to indicate progress_bar.state.add('disabled') else: progress_bar.config['value'] = (done / total) * 100 If multiplying by 100 is annoying, you can create the progress bar like this... :: progress_bar = teek.Progressbar(parent_widget, maximum=1) ...and then set numbers between 0 and 1 to ``progress_bar.config['value']``:: if total == 0: progress_bar.state.add('disabled') else: progress_bar.config['value'] = done / total Manual page: :man:`ttk_progressbar(3tk)` """ _widget_name = 'ttk::progressbar' tk_class_name = 'TProgressbar' def _init_config(self): super()._init_config() self.config._types.update({ 'orient': str, 'length': teek.ScreenDistance, # undocumented but true 'maximum': float, 'mode': str, #'phase': ???, 'value': float, 'variable': teek.FloatVar, }) def _repr_parts(self): result = ['mode=' + repr(self.config['mode'])] if self.config['mode'] == 'determinate': result.append('value=' + repr(self.config['value'])) result.append('maximum=' + repr(self.config['maximum'])) return result def start(self, interval=50): """Makes an indeterminate mode progress bar bounce back and forth. The progress bar will move by a tiny bit every *interval* milliseconds. A small interval makes the progress bar look smoother, but don't make it too small to avoid keeping CPU usage down. The default should be good enough for most things. """ self._call(None, self, 'start', interval) def stop(self): """Stops the bouncing started by :meth:`start`.""" self._call(None, self, 'stop') class Scrollbar(ChildMixin, Widget): """A widget for scrolling other widgets, like :class:`.Text`. In order to use a scrollbar, there are two things you need to do: 1. Tell a scrollable widget (e.g. :class:`.Text`) to use the scrollbar. 2. Tell the scrollbar to scroll the widget. For example:: import teek window = teek.Window() text = teek.Text(window) text.pack(side='left', fill='both', expand=True) scrollbar = teek.Scrollbar(window) scrollbar.pack(side='left', fill='y') text.config['yscrollcommand'].connect(scrollbar.set) # 1. scrollbar.config['command'].connect(text.yview) # 2. window.on_delete_window.connect(teek.quit) teek.run() The value of the scrollbar's ``'command'`` option is a :class:`.Callback` that runs when the scrollbar is scrolled. It runs with arguments suitable for :meth:`.Text.xview` or :meth:`.Text.yview`. See ``SCROLLING COMMANDS`` in :man:`ttk_scrollbar(3tk)` for details about the arguments. Manual page: :man:`ttk_scrollbar(3tk)` """ _widget_name = 'ttk::scrollbar' tk_class_name = 'TScrollbar' def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) self.config._special['command'] = self._create_scrolling_command # this runs when the user moves the scrollbar def _command_runner(self, *args): if args[0] == 'moveto': moveto, fraction = args fraction = from_tcl(float, fraction) self.config['command'].run('moveto', fraction) elif args[0] == 'scroll' and args[-1] in ('units', 'pages'): scroll, number, units_or_pages = args number = from_tcl(int, number) self.config['command'].run('scroll', number, units_or_pages) else: # pragma: no cover raise ValueError("ttk::scrollbar's command ran with unexpected " "arguments: " + repr(args)) def _create_scrolling_command(self): result = teek.Callback() command_string = teek.create_command( self._command_runner, extra_args_type=str) self.command_list.append(command_string) self._call(None, self, 'configure', '-command', command_string) return result def set(self, first, last): """Set the scrollbar's position. See ``pathName set`` in :man:`ttk_scrollbar(3tk)` for details. """ self._call(None, self, 'set', first, last) def get(self): """Return a two-tuple of floats that have been passed to :meth:`set`. See also ``pathName get`` in :man:`ttk_scrollbar(3tk)`. """ return self._call((float, float), self, 'get') class Separator(ChildMixin, Widget): """A horizontal or vertical line, depending on an ``orient`` option. Create a horizontal separator like this... :: separator = teek.Separator(some_widget, orient='horizontal') separator.pack(fill='x') # default is side='top' ...and create a vertical separator like this:: separator = teek.Separator(some_widget, orient='vertical') separator.pack(fill='y', side='left') # can also use side='right' See :source:`examples/separator.py` for more example code. Manual page: :man:`ttk_separator(3tk)` """ # TODO: link to pack docs _widget_name = 'ttk::separator' tk_class_name = 'TSeparator' def _repr_parts(self): return ['orient=' + repr(self.config['orient'])] PKwWNu{<,/,/teek/_widgets/notebook.pyimport collections.abc import weakref import teek from teek._structures import ConfigDict from teek._tcl_calls import make_thread_safe from teek._widgets.base import ChildMixin, Widget class TabConfigDict(ConfigDict): def __init__(self, tab): self._tab = tab super().__init__() self._types.update({ 'state': str, 'sticky': str, 'padding': [teek.ScreenDistance], 'text': str, 'image': teek.Image, 'compound': str, 'underline': int, }) # self._tab.widget.parent is the notebook, lol def _set(self, option, value): self._tab._check_in_notebook() teek.tcl_call(None, self._tab.widget.parent, 'tab', self._tab.widget, '-' + option, value) def _get(self, option): self._tab._check_in_notebook() return teek.tcl_call(self._types.get(option, str), self._tab.widget.parent, 'tab', self._tab.widget, '-' + option) def _list_options(self): self._tab._check_in_notebook() for option in teek.tcl_call({}, self._tab.widget.parent, 'tab', self._tab.widget): yield option.lstrip('-') class NotebookTab: """ Represents a tab that is in a notebook, or is ready to be added to a notebook. The *widget* must be a child widget of a :class:`.Notebook` widget. Each :class:`.NotebookTab` belongs to the widget's parent notebook; for example, if you create a tab like this... :: tab = teek.NotebookTab(teek.Label(asd_notebook, "hello")) ...then the tab cannot be added to any other notebook widget than ``asd_notebook``, because ``asd_notebook`` is the parent widget of the label. Most methods raise :exc:`RuntimeError` if the tab has not been added to the notebook yet. This includes doing pretty much anything with :attr:`config`. For convenience, options can be passed when creating a :class:`.NotebookTab`, so that this... :: notebook.append(teek.NotebookTab(some_widget, text="Tab Title")) ...does the same thing as this:: tab = teek.NotebookTab(some_widget, text="Tab Title") notebook.append(tab) tab.config['text'] = "Tab Title" There are never multiple :class:`NotebookTab` objects that represent the same tab. .. attribute:: config Similar to the ``config`` attribute that widgets have. The available options are documented as ``TAB OPTIONS`` in :man:`ttk_notebook(3tk)`. Attempting to use this raises :exc:`RuntimeError` if the tab hasn't been added to the notebook yet. .. attribute:: widget This attribute and initialization argument is the widget in the tab. It should be a child widget of the notebook. Use ``tab.widget.parent`` to access the :class:`.Notebook` that the tab belongs to. .. attribute:: initial_options A dict of keyword arguments passed to NotebookTab. When the tab is added to the notebook for the first time, :attr:`.config` is updated from this dict. """ @make_thread_safe def __init__(self, widget, **kwargs): if not isinstance(widget.parent, Notebook): raise ValueError("widgets of NotebookTabs must be child widgets " "of a Notebook, but %r is a child widget of %r" % (widget, widget.parent)) if widget in widget.parent._tab_objects: raise RuntimeError("there is already a NotebookTab of %r" % (widget,)) self.widget = widget self.config = TabConfigDict(self) self.initial_options = kwargs # if anything above failed for whatever reason, this tab object is # broken, and in that case this doesn't run, which is good widget.parent._tab_objects[widget] = self def __repr__(self): item_reprs = ['%s=%r' % pair for pair in self.initial_options.items()] return '%s(%s)' % ( type(self).__name__, ', '.join([repr(self.widget)] + item_reprs)) def _check_in_notebook(self): if self not in self.widget.parent: raise RuntimeError("the tab is not in the notebook yet") def hide(self): """Call ``pathName hide`` documented in :man:`ttk_notebook(3tk)`. Use :meth:`unhide` to make the tab visible again. :exc:`RuntimeError` is raised if the tab has not been added to a notebook. """ self._check_in_notebook() self.widget.parent._call(None, self.widget.parent, 'hide', self.widget) def unhide(self): """Undo a :meth:`hide` call.""" self._check_in_notebook() self.widget.parent._call(None, self.widget.parent, 'add', self.widget) class Notebook(ChildMixin, Widget, collections.abc.MutableSequence): """This is the notebook widget. If you try to add a tab that is already in the notebook, that tab will be moved. For example: >>> notebook = teek.Notebook(teek.Window()) >>> tab1 = teek.NotebookTab(teek.Label(notebook, text="1"), text="One") >>> tab2 = teek.NotebookTab(teek.Label(notebook, text="2"), text="Two") >>> notebook.extend([tab1, tab2]) >>> list(notebook) # doctest: +NORMALIZE_WHITESPACE [NotebookTab(, text='One'), NotebookTab(, text='Two')] >>> notebook.append(notebook[0]) >>> list(notebook) # doctest: +NORMALIZE_WHITESPACE [NotebookTab(, text='Two'), NotebookTab(, text='One')] For doing advanced magic, you can create a new class that inherits from :class:`.Notebook`. Here are some facts that can be useful when deciding which methods to override: * Override ``__delitem__()`` to customize removing tabs from the notebook. A deletion like ``del notebook[index]`` does ``notebook.__delitem__(index)``, which calls ``pathName forget`` documented in :man:`ttk_notebook(3tk)`. All other kinds of deletions call ``__delitem__`` as well. * Override ``insert()`` if you want to customize adding new tabs to the notebook. The ``insert`` method is called every time when a new tab is added with any method. Make sure that your override is compatible with the ``insert()`` method of :class:`collections.abc.MutableSequence`, and make sure that only the order of the tabs changes if the new tab is already in the notebook. * Bind to ``<>`` if you want to customize what happens when a different tab is selected. That runs when the user changes a tab or the tab is changed with the :attr:`.selected_tab` property. ``<>`` is documented in the ``VIRTUAL EVENTS`` section of :man:`ttk_notebook(3tk)`. As usual, use :func:`super` when overriding. Manual page: :man:`ttk_notebook(3tk)` """ _widget_name = 'ttk::notebook' tk_class_name = 'TNotebook' def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) # keys are widgets, and values are NotebookTabs that can be garbage # collected when the widgets are garbage collected self._tab_objects = weakref.WeakKeyDictionary() def _init_config(self): super()._init_config() self.config._types.update({ 'width': int, 'height': int, }) def get_tab_by_widget(self, widget): """ Finds a :class:`.NotebookTab` object by the :attr:`~.NotebookTab.widget` attribute. If there is no tab with the given widget, a new tab is created. >>> notebook = teek.Notebook(teek.Window()) >>> label = teek.Label(notebook, text='lol') >>> tab = teek.NotebookTab(label) >>> notebook.append(tab) >>> tab NotebookTab() >>> notebook.get_tab_by_widget(label) NotebookTab() """ try: return self._tab_objects[widget] except KeyError: if widget.parent is not self: raise ValueError( "expected a widget with the notebook as its parent, " "got " + repr(widget)) # this can happen if the tab was added with a Tcl call return NotebookTab(widget) # adds the new tab to self._tab_objects def _repr_parts(self): return ["contains %d tabs" % len(self)] def __len__(self): return self._call(int, self, 'index', 'end') @make_thread_safe def __getitem__(self, index): if isinstance(index, slice): raise TypeError("slicing a Notebook is not supported") # there seems to be no way to get a widget by index without getting a # list of all widgets widgets = self._call([Widget], self, 'tabs') return self.get_tab_by_widget(widgets[index]) @make_thread_safe def __setitem__(self, index, tab): del self[index] self.insert(index, tab) @make_thread_safe def __delitem__(self, index): self._call(None, self, 'forget', self[index].widget) # yes, an empty docstring does the right thing here, otherwise the # docstring seems to be copied from MutableSequence which is not what i # want because MutableSequence's docstring: # * doesn't explain the details # * is not good RST @make_thread_safe def insert(self, index, tab): """""" if not isinstance(tab, NotebookTab): raise TypeError("expected a NotebookTab object, got %r" % (tab,)) if tab.widget.parent is not self: raise ValueError("cannot add %r's tab to %r" % (tab.widget.parent, self)) # lists do this if index < 0: index += len(self) if index < 0: index = 0 if index > len(self): index = len(self) # because ttk is fun, isn't it if index == len(self): index = 'end' moving_only = (tab in self) self._call(None, self, 'insert', index, tab.widget) if not moving_only: tab.config.update(tab.initial_options) @make_thread_safe def move(self, tab, new_index): """ Move a tab so that after calling this, ``self[new_index]`` is ``tab``. The new index may be negative. :class:`IndexError` is raised if the index is not in the correct range. """ if tab not in self: raise ValueError("add the tab to the notebook first, " "then you can move it") # support negative indexes, and raise IndexErrors accordingly new_index = range(len(self))[new_index] self._call(None, self, 'insert', new_index, tab.widget) @property @make_thread_safe def selected_tab(self): """This is the tab that the user is currently looking at. This is ``None`` if there are no tabs in the notebook. You can set this to any other tab in the notebook to change the currently selected tab. """ widget = self._call(Widget, self, 'select') if widget is None: return None # return a tab object instead of a widget index = self._call([Widget], self, 'tabs').index(widget) return self[index] @selected_tab.setter def selected_tab(self, tab): self._call(None, self, 'select', tab.widget) @make_thread_safe def append_and_select(self, tab): """A convenient way to add a tab to the notebook and select it. ``notebook.append_and_select(tab)`` is same as:: notebook.append(tab) notebook.selected_tab = tab """ self.append(tab) self.selected_tab = tab PKwWN_߃{6{6teek/_widgets/text.pyimport collections.abc import functools import re import teek from teek._structures import CgetConfigureConfigDict from teek._tcl_calls import make_thread_safe from teek._widgets.base import BindingDict, ChildMixin, Widget # a new subclass of IndexBase is created for each text widget, and inheriting # from namedtuple makes comparing the text indexes work nicely class IndexBase(collections.namedtuple('TextIndex', 'line column')): _widget = None def to_tcl(self): return '%d.%d' % self # lol, magicz @classmethod @make_thread_safe def from_tcl(cls, string): # the returned index may be out of bounds ONLY IF it doesn't contain # anything more fancy than 'line.column' if re.fullmatch(r'(\d+)\.(\d+)', string) is None: string = cls._widget._call(str, cls._widget, 'index', string) # hide the invisible newline that tk wants to have at the end tk_fake_end = cls._widget._call(str, cls._widget, 'index', 'end') if string == tk_fake_end: return cls._widget.end return cls(*map(int, string.split('.'))) @make_thread_safe def between_start_end(self): if self < self._widget.start: return self._widget.start if self > self._widget.end: return self._widget.end return self def forward(self, *, chars=0, indices=0, lines=0): result = '%s %+d lines %+d chars %+d indices' % ( self.to_tcl(), lines, chars, indices) return type(self).from_tcl(result).between_start_end() def back(self, *, chars=0, indices=0, lines=0): return self.forward(chars=-chars, indices=-indices, lines=-lines) def _apply_suffix(self, suffix): return type(self).from_tcl( '%s %s' % (self.to_tcl(), suffix)).between_start_end() linestart = functools.partialmethod(_apply_suffix, 'linestart') lineend = functools.partialmethod(_apply_suffix, 'lineend') wordstart = functools.partialmethod(_apply_suffix, 'wordstart') wordend = functools.partialmethod(_apply_suffix, 'wordend') class Tag(CgetConfigureConfigDict): def __init__(self, widget, name): self._widget = widget self.name = name super().__init__(self._call_tag_subcommand) self._types.update({ 'background': teek.Color, #'bgstipple': ???, 'borderwidth': teek.ScreenDistance, 'elide': bool, #'fgstipple': ???, 'font': teek.Font, 'foreground': teek.Color, 'justify': str, 'lmargin1': teek.ScreenDistance, 'lmargin2': teek.ScreenDistance, 'lmargin3': teek.ScreenDistance, 'lmargincolor': teek.Color, 'offset': teek.ScreenDistance, 'overstrike': bool, 'overstrikefg': teek.Color, 'relief': str, 'rmargin': teek.ScreenDistance, 'rmargincolor': teek.Color, 'selectbackground': teek.Color, 'selectforeground': teek.Color, 'spacing1': teek.ScreenDistance, 'spacing2': teek.ScreenDistance, 'spacing3': teek.ScreenDistance, 'tabs': [str], 'tabstyle': str, 'underline': bool, 'underlinefg': teek.Color, 'wrap': str, }) self.bindings = BindingDict(self._call_bind, widget.command_list) self.bind = self.bindings._convenience_bind def __repr__(self): return '' % self.name def to_tcl(self): return self.name # this inserts self between the arguments, that's why it's needed def _call_tag_subcommand(self, returntype, subcommand, *args): return self._widget._call( returntype, self._widget, 'tag', subcommand, self, *args) def _call_bind(self, returntype, *args): return self._call_tag_subcommand(returntype, 'bind', *args) def __eq__(self, other): if not isinstance(other, Tag): return NotImplemented return self._widget is other._widget and self.name == other.name def __hash__(self): return hash(self.name) @make_thread_safe def add(self, index1, index2): index1 = self._widget._get_index_obj(index1) index2 = self._widget._get_index_obj(index2) return self._call_tag_subcommand(None, 'add', index1, index2) # TODO: bind def delete(self): self._call_tag_subcommand(None, 'delete') @make_thread_safe def _prevrange_or_nextrange(self, prev_or_next, index1, index2=None): index1 = self._widget._get_index_obj(index1) if index2 is None: index2 = {'prev': self._widget.start, 'next': self._widget.end}[prev_or_next] else: index2 = self._widget._get_index_obj(index2) results = self._call_tag_subcommand( [self._widget.TextIndex], prev_or_next + 'range', index1, index2) if not results: # the tcl command returned "", no ranges found return None index1, index2 = results return (index1.between_start_end(), index2.between_start_end()) prevrange = functools.partialmethod(_prevrange_or_nextrange, 'prev') nextrange = functools.partialmethod(_prevrange_or_nextrange, 'next') @make_thread_safe def ranges(self): flat_pairs = map(self._widget.TextIndex.from_tcl, self._call_tag_subcommand([str], 'ranges')) # magic to convert a flat iterator to pairs: a,b,c,d --> (a,b), (c,d) return list(zip(flat_pairs, flat_pairs)) @make_thread_safe def remove(self, index1=None, index2=None): if index1 is None: index1 = self._widget.start else: index1 = self._widget._get_index_obj(index1) if index2 is None: index2 = self._widget.end else: index2 = self._widget._get_index_obj(index2) self._call_tag_subcommand(None, 'remove', index1, index2) @make_thread_safe def _lower_or_raise(self, lower_or_raise, other_tag=None): if other_tag is None: self._call_tag_subcommand(None, lower_or_raise) else: self._call_tag_subcommand(None, lower_or_raise, other_tag) lower = functools.partialmethod(_lower_or_raise, 'lower') raise_ = functools.partialmethod(_lower_or_raise, 'raise') class MarksDict(collections.abc.MutableMapping): def __init__(self, widget): self._widget = widget def _get_name_list(self): return self._widget._call([str], self._widget, 'mark', 'names') def __iter__(self): return iter(self._get_name_list()) def __len__(self): return len(self._get_name_list()) @make_thread_safe def __setitem__(self, name, index): index = self._widget._get_index_obj(index) self._widget._call(None, self._widget, 'mark', 'set', name, index) @make_thread_safe def __getitem__(self, name): if name not in self._get_name_list(): raise KeyError(name) result = self._widget._call( self._widget.TextIndex, self._widget, 'index', name) return result.between_start_end() def __delitem__(self, name): self._widget._call(None, self._widget, 'mark', 'unset', name) class Text(ChildMixin, Widget): r"""This is the text widget. Manual page: :man:`text(3tk)` .. attribute:: start end :ref:`TextIndex objects ` that represents the start and end of the text. .. tip:: Use ``textwidget.end.line`` to count the number of lines of text in the text widget. Note that ``end`` changes when the text widget's content changes: >>> window = teek.Window() >>> text = teek.Text(window) >>> text.end TextIndex(line=1, column=0) >>> old_end = text.end >>> text.insert(text.end, 'hello') >>> text.end TextIndex(line=1, column=5) >>> old_end TextIndex(line=1, column=0) >>> text.get(old_end, text.end) 'hello' Tk has a concept of an invisible newline character at the end of the widget. In pure Tcl or in tkinter, getting the text from beginning to ``end`` returns the text in the widget plus a ``\n``, which is why you almost always need to do ``end - 1 char`` instead of just ``end``. **Teek doesn't do that** because 99% of the time it's not useful and 1% of the time it's confusing to people reading the code, so ``text.get(text.start, text.end)`` doesn't return anything that is not visible in the text widget. .. attribute:: marks A dictionary-like object with mark names as keys and :ref:`index objects ` as values. See :ref:`textwidget-marks`. .. method:: xview(*args) yview(*args) These call ``pathName xview`` and ``pathName yview`` as documented in :man:`text(3tk)`. Pass string arguments to these methods to invoke the subcommands. For example, ``text_widget.yview('moveto', 1)`` scrolls to end vertically. If no arguments are given, these methods return a two-tuple of floats (see the manual page); otherwise, None is returned. """ _widget_name = 'text' tk_class_name = 'Text' def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) self.TextIndex = type( # creates a new subclass of IndexBase 'TextIndex', (IndexBase,), {'_widget': self}) self._tag_objects = {} self.marks = MarksDict(self) def _init_config(self): super()._init_config() self.config._types.update({ 'autoseparators': bool, 'blockcursor': bool, 'endline': int, # undocumented: height can also be a screen distance?? # probably a bug 'height': int, 'inactiveselectbackground': teek.Color, 'insertunfocussed': str, 'maxundo': int, 'spacing1': teek.ScreenDistance, 'spacing2': teek.ScreenDistance, 'spacing3': teek.ScreenDistance, 'startline': int, 'state': str, 'tabs': [str], 'tabstyle': str, 'undo': bool, 'width': int, 'wrap': str, }) def _repr_parts(self): return ['contains %d lines of text' % self.end.line] def _get_index_obj(self, index): if isinstance(index, str): raise TypeError( "string indexes are not supported, use (line, column) int " "tuples or TextIndex objects instead") return self.TextIndex(*index).between_start_end() @make_thread_safe def get_tag(self, name): """Return a tag object by name, creating a new one if needed.""" try: return self._tag_objects[name] except KeyError: tag = Tag(self, name) # this actually creates the tag so that it shows up in # get_all_tags() self._call(None, self, 'tag', 'configure', name) self._tag_objects[name] = tag return tag @make_thread_safe def get_all_tags(self, index=None): """Return all tags as tag objects. See ``pathName tag names`` in :man:`text(3tk)` for more details. """ args = [self, 'tag', 'names'] if index is not None: args.append(self._get_index_obj(index)) return [self.get_tag(name) for name in self._call([str], *args)] @property def start(self): return self.TextIndex(1, 0) @property def end(self): index_string = self._call(str, self, 'index', 'end - 1 char') return self.TextIndex(*map(int, index_string.split('.'))) @make_thread_safe def get(self, index1=None, index2=None): """Return text in the widget. If the indexes are not given, they default to the beginning and end of the text widget, respectively. """ if index1 is None: index1 = self.start else: index1 = self._get_index_obj(index1) if index2 is None: index2 = self.end else: index2 = self._get_index_obj(index2) return self._call(str, self, 'get', index1, index2) @make_thread_safe def insert(self, index, text, tag_list=()): """Add text to the widget. The ``tag_list`` can be any iterable of tag name strings or tag objects. """ index = self._get_index_obj(index) self._call(None, self, 'insert', index, text, tag_list) @make_thread_safe def replace(self, index1, index2, new_text, tag_list=()): """See :man:`text(3tk)` and :meth:`insert`.""" self._call(None, self, 'replace', self._get_index_obj(index1), self._get_index_obj(index2), new_text, tag_list) def delete(self, index1, index2): """See :man:`text(3tk)` and :meth:`insert`.""" self._call(None, self, 'delete', self._get_index_obj(index1), self._get_index_obj(index2)) def see(self, index): """Scroll so that an index is visible. See :man:`text(3tk)` for details. """ self._call(None, self, 'see', self._get_index_obj(index)) def _xview_or_yview(self, xview_or_yview, *args): if not args: return self._call((float, float), self, xview_or_yview) self._call(None, self, xview_or_yview, *args) return None xview = functools.partialmethod(_xview_or_yview, 'xview') yview = functools.partialmethod(_xview_or_yview, 'yview') PKwWNb"l11teek/_widgets/windows.pyimport collections.abc import re import teek from teek._structures import ConfigDict from teek._tcl_calls import make_thread_safe from teek._widgets.base import ChildMixin, Widget Geometry = collections.namedtuple('Geometry', 'width height x y') class WmMixin: @make_thread_safe def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.on_delete_window = teek.Callback() self.on_take_focus = teek.Callback() # TODO: delete the commands when they are no longer needed, mem leak self._call( None, 'wm', 'protocol', self._get_wm_widget(), 'WM_DELETE_WINDOW', teek.create_command(self.on_delete_window.run)) self._call( None, 'wm', 'protocol', self._get_wm_widget(), 'WM_TAKE_FOCUS', teek.create_command(self.on_take_focus.run)) def _repr_parts(self): result = ['title=' + repr(self.title)] if self.wm_state != 'normal': result.append('wm_state=' + repr(self.wm_state)) return result # note that these are documented in Toplevel, this is a workaround to get # sphinx to show the docs in the correct place while keeping this class # as an implementation detail @property def title(self): return self._call(str, 'wm', 'title', self._get_wm_widget()) @title.setter def title(self, new_title): self._call(None, 'wm', 'title', self._get_wm_widget(), new_title) # a property named 'state' might be confusing, explicit > implicit @property def wm_state(self): return self._call(str, 'wm', 'state', self._get_wm_widget()) @wm_state.setter def wm_state(self, state): self._call(None, 'wm', 'state', self._get_wm_widget(), state) @property def transient(self): return self._call(teek.Widget, 'wm', 'transient', self._get_wm_widget()) @transient.setter def transient(self, widget): self._call(None, 'wm', 'transient', self._get_wm_widget(), widget._get_wm_widget()) @make_thread_safe def geometry(self, width=None, height=None, x=None, y=None): if isinstance(width, str): # for tkinter users raise TypeError("use widget.geometry(width, height) instead of " "widget.geometry(string)") if (width is None) ^ (height is None): raise TypeError("specify both width and height, or neither") if (x is None) ^ (y is None): raise TypeError("specify both x and y, or neither") if x is y is width is height is None: string = self._call(str, 'wm', 'geometry', self._get_wm_widget()) match = re.search(r'^(\d+)x(\d+)\+(\d+)\+(\d+)$', string) return Geometry(*map(int, match.groups())) if x is y is None: string = '%dx%d' % (width, height) elif width is height is None: string = '+%d+%d' % (x, y) else: string = '%dx%d+%d+%d' % (width, height, x, y) self._call(None, 'wm', 'geometry', self._get_wm_widget(), string) @property def minsize(self): return self._call((int, int), 'wm', 'minsize', self._get_wm_widget()) @minsize.setter def minsize(self, size): width, height = size self._call(None, 'wm', 'minsize', self._get_wm_widget(), width, height) @property def maxsize(self): return self._call((int, int), 'wm', 'maxsize', self._get_wm_widget()) @maxsize.setter def maxsize(self, size): width, height = size self._call(None, 'wm', 'maxsize', self._get_wm_widget(), width, height) def iconphoto(self, *images): self._call(None, 'wm', 'iconphoto', self._get_wm_widget(), *images) def withdraw(self): self._call(None, 'wm', 'withdraw', self._get_wm_widget()) def iconify(self): self._call(None, 'wm', 'iconify', self._get_wm_widget()) def deiconify(self): self._call(None, 'wm', 'deiconify', self._get_wm_widget()) def wait_window(self): self._call(None, 'tkwait', 'window', self) # to be overrided def _get_wm_widget(self): # pragma: no cover raise NotImplementedError class Toplevel(WmMixin, Widget): """This represents a *non-Ttk* ``toplevel`` widget. Usually it's easiest to use :class:`Window` instead. It behaves like a ``Toplevel`` widget, but it's actually a ``Toplevel`` with a ``Frame`` inside it. Manual page: :man:`toplevel(3tk)` .. method:: geometry(width=None, height=None, x=None, y=None) Set or get the size and place of the window in pixels. Tk's geometries are strings like ``'100x200+300+400'``, but that's not very pythonic, so this method works with integers and namedtuples instead. This method can be called in a few different ways: * If *width* and *height* are given, the window is resized. * If *x* and *y* are given, the window is moved. * If all arguments are given, the window is resized and moved. * If no arguments are given, the current geometry is returned as a namedtuple with *width*, *height*, *x* and *y* fields. * Calling this method otherwise raises an error. Examples:: >>> import teek >>> window = teek.Window() >>> window.geometry(300, 200) # resize to 300px wide, 200px high >>> window.geometry(x=0, y=0) # move to upper left corner >>> window.geometry() # doctest: +SKIP Geometry(width=300, height=200, x=0, y=0) >>> window.geometry().width # doctest: +SKIP 300 See also ``wm geometry`` in :man:`wm(3tk)`. .. method:: iconphoto(*images, default=False) Calls ``wm iconphoto`` documented in :man:`wm(3tk)`. Positional arguments should be :class:`.Image` objects. If ``default=True`` is given, the ``-default`` switch is used; otherwise it isn't. .. attribute:: title wm_state transient minsize maxsize .. method:: withdraw() iconify() deiconify() These attributes and methods correspond to similarly named things in :man:`wm(3tk)`. Note that ``wm_state`` is ``state`` in the manual page; the teek attribute is ``wm_state`` to make it explicit that it is the wm state, not some other state. All of the attributes are settable, so you can do e.g. ``my_toplevel.title = "lol"``. Here are the types of the attributes: * ``title`` and ``wm_state`` are strings. * ``transient`` is a widget. * ``minsize`` and ``maxsize`` are tuples of two integers. .. note:: If ``transient`` is set to a :class:`.Window`, looking it up won't give back that same window; instead, it gives the :attr:`~.Window.toplevel` of the window. .. method:: wait_window() Waits until the window is destroyed with :meth:`~Widget.destroy`. This method blocks until the window is destroyed, but it can still be called from the :ref:`event loop `; behind the covers, it runs another event loop that makes the GUI not freeze. See ``tkwait window`` in :man:`tkwait(3tk)` for more details. .. attribute:: on_delete_window on_take_focus :class:`Callback` objects that run with no arguments when a ``WM_DELETE_WINDOW`` or ``WM_TAKE_FOCUS`` event occurs. See :man:`wm(3tk)`. These are connected to nothing by default. """ _widget_name = 'toplevel' tk_class_name = 'Toplevel' # this allows passing title as a positional argument @make_thread_safe def __init__(self, title=None, **options): # toplevel(3tk): "[...] it must be the window identifier of a container # window, specified as a hexadecimal string [...]" if 'use' in options and isinstance(options['use'], int): options['use'] = hex(options['use']) super().__init__(None, **options) if title is not None: self.title = title def _init_config(self): super()._init_config() # "didn't bother" ones are more work than they are worth because nobody # will use them anyway self.config._types.update({ 'colormap': str, # 'new' or a widget name, didn't bother 'container': bool, 'height': teek.ScreenDistance, 'menu': teek.Menu, 'screen': str, 'use': int, 'visual': str, # didn't bother 'width': teek.ScreenDistance, }) def _get_wm_widget(self): return self # allow accessing Toplevel config things like 'menu' through the Window widget class FallbackConfigDict(ConfigDict): def __init__(self, main_config, fallback_config): super().__init__() # this doesn't need a ChainMap because self._types isn't changed # after this self._types.update(main_config._types) self._types.update(fallback_config._types) self._main_config = main_config self._fallback_config = fallback_config def _set(self, option, value): if option in self._main_config._list_options(): self._main_config._set(option, value) else: self._fallback_config._set(option, value) def _get(self, option): if option in self._main_config._list_options(): return self._main_config._get(option) else: return self._fallback_config._get(option) def _list_options(self): return (set(self._main_config._list_options()) | set(self._fallback_config._list_options())) class Window(WmMixin, Widget): """A convenient widget that represents a Ttk frame inside a toplevel. Tk's windows like :class:`Toplevel` are *not* Ttk widgets, and there are no Ttk window widgets. If you add Ttk widgets to Tk windows like :class:`Toplevel` so that the widgets don't fill the entire window, your GUI looks messy on some systems, like my linux system with MATE desktop. This is why you should always create a big Ttk frame that fills the window, and then add all widgets into that frame. That's kind of painful and most people don't bother with it, but this class does that for you, so you can just create a :class:`Window` and add widgets to that. All initialization arguments are passed to :class:`Toplevel`. The :attr:`~.Widget.config` attribute combines options from the :class:`.Frame` and the :class:`.Toplevel` so that it uses :class:`.Frame` options whenever they are available, and :class:`.Toplevel` options otherwise. For example, :class:`.Frame` has an option named ``'width'``, so ``some_window.config['width']`` uses that, but frames don't have a ``'menu'`` option, so ``some_window.config['menu']`` uses the toplevel's menu option. There is no manual page for this class because this is purely a teek feature; there is no ``window`` widget in Tk. .. seealso:: :class:`Toplevel`, :class:`Frame` .. attribute:: toplevel The :class:`Toplevel` widget that the frame is in. The :class:`Window` object itself has all the attributes and methods of the :class:`Frame` inside the window, and for convenience, also many :class:`Toplevel` things like :attr:`~Toplevel.title`, :meth:`~Toplevel.withdraw` and :attr:`~Toplevel.on_delete_window`. """ _widget_name = 'ttk::frame' tk_class_name = None @make_thread_safe def __init__(self, *args, **kwargs): self.toplevel = Toplevel(*args, **kwargs) super().__init__(self.toplevel) # calls self._init_config() self.config = FallbackConfigDict(self.config, self.toplevel.config) ChildMixin.pack(self, fill='both', expand=True) def destroy(self): """Destroys the :attr:`.toplevel` and the frame in it. This overrides :meth:`.Widget.destroy`. """ self.toplevel.destroy() def _destroy_recurser(self): super().destroy() def _init_config(self): # if you change these, also change Frame's types in misc.py self.config._types.update({ 'height': teek.ScreenDistance, 'padding': teek.ScreenDistance, 'width': teek.ScreenDistance, }) def _get_wm_widget(self): return self.toplevel PKë-Nteek/extras/__init__.pyPKwWNxteek/extras/cross_platform.pyimport functools import teek # this is not called bind_tab to avoid confusing with: # * \t characters # * web browser tabs as in teek.Notebook def bind_tab_key(widget, callback, **bind_kwargs): """A cross-platform way to bind Tab and Shift+Tab. Use this function like this:: from teek.extras import cross_platform def on_tab(shifted): if shifted: print("Shift+Tab was pressed") else: print("Tab was pressed") cross_platform.bind_tab_key(some_widget, on_tab) Binding ``''`` works on all systems I've tried it on, but if you also want to bind tab presses where the shift key is held down, use this function instead. This function can also take any of the keyword arguments that :meth:`teek.Widget.bind` takes. If you pass ``event=True``, the callback will be called like ``callback(shifted, event)``; that is, the ``shifted`` bool is the first argument, and the event object is the second. """ if teek.windowingsystem() == 'x11': # even though the event keysym says Left, holding down the right # shift and pressing tab also works :D shift_tab = '' else: shift_tab = '' # pragma: no cover widget.bind('', functools.partial(callback, False), **bind_kwargs) widget.bind(shift_tab, functools.partial(callback, True), **bind_kwargs) PKyYNZc c teek/extras/image_loader.py# if you change this file, consider also changing image_loader_dummy.py import io # see pyproject.toml try: import lxml.etree import PIL.Image import reportlab.graphics.renderPM from svglib import svglib except ImportError as e: raise ImportError(str(e) + ". Maybe try 'pip install teek[image_loader]' " "to fix this?").with_traceback(e.__traceback__) from None import teek # both of these files have the same from_pil implementation, because it doesn't # actually need to import PIL from teek.extras.image_loader_dummy import from_pil def from_file(file): """Creates a :class:`teek.Image` from a file object. The file object must be readable, and it must be in bytes mode. It must have ``read()``, ``seek()`` and ``tell()`` methods. For example, files from ``open(some_path, 'rb')`` and ``io.BytesIO(some_data)`` work. This supports all file formats that PIL supports and SVG. Example:: from teek.extras import image_loader with open(the_image_path, 'rb') as file: image = image_loader.from_file(file) """ first_3_bytes = file.read(3) file.seek(0) if first_3_bytes == b'GIF': return teek.Image(data=file.read()) # https://stackoverflow.com/a/15136684 try: event, element = next(lxml.etree.iterparse(file, ('start',))) is_svg = (element.tag == '{http://www.w3.org/2000/svg}svg') except (lxml.etree.ParseError, StopIteration): is_svg = False file.seek(0) if is_svg: # svglib takes open file objects, even though it doesn't look like it # https://github.com/deeplook/svglib/issues/173 rlg = svglib.svg2rlg(io.TextIOWrapper(file, encoding='utf-8')) with reportlab.graphics.renderPM.drawToPIL(rlg) as pil_image: return from_pil(pil_image) with PIL.Image.open(file) as pil_image: return from_pil(pil_image) def from_bytes(bytes_): """ Creates a :class:`teek.Image` from bytes that would normally be in an image file. Example:: # if you have a file, it's recommended to use # from_file() instead, but this example works anyway with open(the_image_path, 'rb') as file: data = file.read() image = image_loader.from_bytes(data) """ with io.BytesIO(bytes_) as fake_file: return from_file(fake_file) PKyYNjIu!teek/extras/image_loader_dummy.pyimport io import teek def from_pil(pil_image, **kwargs): """Converts a PIL_ ``Image`` object to a :class:`teek.Image`. All keyword arguments are passed to PIL's save_ method. .. _PIL: https://pillow.readthedocs.io/en/stable/ .. _save: https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL\ .Image.Image.save """ # TODO: borrow some magic code from PIL.ImageTk to make this faster? # or maybe optimize teek.Image? currently it base64 encodes the data gif = io.BytesIO() pil_image.save(gif, 'gif', **kwargs) return teek.Image(data=gif.getvalue()) def from_file(file): return teek.Image(data=file.read()) def from_bytes(bytes_): return teek.Image(data=bytes_) PKyXNgAteek/extras/links.pyimport functools import webbrowser _TAG_PREFIX = 'teek-extras-link-' def _init_links(widget): if _TAG_PREFIX + 'common' in (tag.name for tag in widget.get_all_tags()): return widget.get_tag(_TAG_PREFIX + 'common') old_cursor = widget.config['cursor'] def enter(): nonlocal old_cursor old_cursor = widget.config['cursor'] widget.config['cursor'] = 'hand2' def leave(): widget.config['cursor'] = old_cursor tag = widget.get_tag(_TAG_PREFIX + 'common') tag['foreground'] = 'blue' tag['underline'] = True tag.bind('', enter) tag.bind('', leave) return tag def add_function_link(textwidget, function, start, end): """ Like :func:`add_url_link`, but calls a function instead of opening a URL in a web browser. """ common_tag = _init_links(textwidget) names = {tag.name for tag in textwidget.get_all_tags()} i = 1 while _TAG_PREFIX + str(i) in names: i += 1 specific_tag = textwidget.get_tag(_TAG_PREFIX + str(i)) # bind callbacks must return None or 'break', but this ignores the # function's return value def none_return_function(): function() specific_tag.bind('', none_return_function) for tag in [common_tag, specific_tag]: tag.add(start, end) def add_url_link(textwidget, url, start, end): """ Make some of the text in the textwidget to be clickable so that clicking it will open ``url``. The text between the :ref:`text indexes ` ``start`` and ``end`` becomes clickable, and rest of the text is not touched. Do this if you want to insert some text and make it a link immediately:: from teek.extras import links ... old_end = textwidget.end # adding text to end changes textwidget.end textwidget.insert(textwidget.end, 'Click me') links.add_url_link(textwidget, 'https://example.com/', old_end, textwi\ dget.end) This function uses :func:`webbrowser.open` for opening ``url``. """ add_function_link(textwidget, functools.partial(webbrowser.open, url), start, end) PKwWNteek/extras/more_dialogs.pyimport teek class _EntryDialog: def __init__(self, title, text, entry_creator, validator, initial_value, parent): self.validator = validator self.window = teek.Window(title) self.window.on_delete_window.connect(self.on_cancel) if parent is not None: self.window.transient = parent self.var = teek.StringVar() teek.Label(self.window, text).grid(row=0, column=0, columnspan=2) entry = entry_creator(self.window) entry.config['textvariable'] = self.var entry.grid(row=1, column=0, columnspan=2) entry.bind('', self.on_ok) entry.bind('', self.on_cancel) self.ok_button = teek.Button(self.window, "OK", self.on_ok) self.ok_button.grid(row=3, column=0) teek.Button(self.window, "Cancel", self.on_cancel).grid( row=3, column=1) self.window.grid_rows[0].config['weight'] = 1 self.window.grid_rows[2].config['weight'] = 1 for column in self.window.grid_columns: column.config['weight'] = 1 self.result = None self.var.write_trace.connect(self.on_var_changed) self.var.set(initial_value) self.on_var_changed(self.var) # TODO: is this needed? # TODO: add a way to select stuff to teek self.window.geometry(300, 150) entry.focus() teek.tcl_call(None, entry, 'selection', 'range', '0', 'end') def on_var_changed(self, var): result = self.var.get() try: self.result = self.validator(result) self.ok_button.config['state'] = 'normal' except ValueError: self.result = None self.ok_button.config['state'] = 'disabled' def on_ok(self): # this state check is needed because is bound to this, and # that binding can run even if the button is disabled if self.ok_button.config['state'] == 'normal': self.window.destroy() def on_cancel(self): self.result = None self.window.destroy() def run(self): self.window.wait_window() return self.result def ask_string(title, text, *, validator=str, initial_value='', parent=None): """Displays a dialog that contains a :class:`teek.Entry` widget. The ``validator`` should be a function that takes a string as an argument, and returns something useful (see below). By default, it returns the string unchanged, which is useful for asking a string from the user. If the validator raises :exc:`ValueError`, the OK button of the dialog is disabled to tell the user that the value they entered is invalid. Then the user needs to enter a valid value or cancel the dialog. This returns whatever ``validator`` returned, or ``None`` if the dialog was canceled. """ return _EntryDialog(title, text, teek.Entry, validator, initial_value, parent).run() def ask_integer(title, text, allowed_values, *, initial_value=None, parent=None): """Displays a dialog that contains a :class:`teek.Spinbox` widget. ``allowed_values`` can be a sequence of acceptable integers or a :class:`range`. If ``initial_value`` is given, it must be in ``allowed_values``. If it's not, ``allowed_values[0]`` is used. This returns an integer in ``allowed_values``, or ``None`` if the user cancels. """ def creator(spinbox_parent): if isinstance(allowed_values, range): # range(blah, blah, 0) raises an error, so the step can't be zero if allowed_values.step < 0: raise ValueError( "ranges with negative steps are not supported") # allowed_values.stop is not same as allowed_values[-1], the -1 one # is inclusive return teek.Spinbox( spinbox_parent, from_=allowed_values[0], to=allowed_values[-1], increment=allowed_values.step) return teek.Spinbox(spinbox_parent, values=allowed_values) def validator(value): int_value = int(value) if int_value not in allowed_values: raise ValueError return int_value if initial_value is None: initial_value = allowed_values[0] elif initial_value not in allowed_values: raise ValueError("initial value %r not in %r" % (initial_value, allowed_values)) return _EntryDialog(title, text, creator, validator, initial_value, parent).run() PKyYN ..teek/extras/soup.pyimport contextlib import functools import itertools import re import threading import urllib.request import warnings import teek from teek.extras import links try: from teek.extras import image_loader except ImportError: from teek.extras import image_loader_dummy as image_loader class SoupViewer: """Displays BeautifulSoup_ HTML elements in a text widget. .. BeautifulSoup: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ .. note:: If the soup contains ```` tags, the images are read or downloaded automatically by default. Subclass :class:`SoupViewer` and override :meth:`download` if you don't want that. Images are loaded in threads, so make sure to use :func:`teek.init_threads`. Alternatively, you can pass ``threads=False``, and the images won't be loaded at all. .. attribute:: widget The :class:`teek.Text` widget that everything is added to. """ def __init__(self, textwidget, threads=True): self._use_threads = threads self.widget = textwidget self.widget.bind('', functools.partial(self.stop_loading, cleanup=True)) self._image_mark_names = ('soup-img-' + str(i) for i in itertools.count(1)) self._loading_id = 1 self._loaded_images = [] def download(self, url): """Downloads the content of the URL, and returns it as bytes. This method is called whenever the soup contains an ```` or something else that has to be read from a file or downloaded. If it raises an exception, the ``alt`` of the ```` will be displayed instead of the actual image, if there is an ``alt``. The ``alt`` is also displayed while this method is running. By default, this uses :func:`urllib.request.urlopen`. You can override this if ``urllib`` is doing something dumb or you want to control which things can be downloaded. Usually this is called from some other thread than the main thread. """ with urllib.request.urlopen(url) as response: return response.read() @teek.make_thread_safe def stop_loading(self, cleanup=True): """Tell currently running threads to do nothing to the :attr:`widget`. Things like ```` elements are loaded with threads, so they might add something to the text widget several seconds after the :meth:`add_soup` call. If ``cleanup`` is ``True``, this method also e.g. deletes already loaded images, because then it assumes that they are not needed anymore. This means that if you don't pass ``cleanup=False``, you should clear the text widget after calling this method. This is automatically called with ``cleanup=True`` when the :attr:`widget` is destroyed. """ self._loading_id += 1 if cleanup: while self._loaded_images: self._loaded_images.pop().delete() def create_tags(self): """ Adds :ref:`text tags ` to the :attr:`widget` for displaying the soup elements. This is not called automatically; you should call this before actually using the ``SoupViewer``. Each text tag is named with ``'soup-'`` followed by the name of the corresponding HTML tag, such as ``'soup-p'`` or ``'soup-pre'``. If you are not happy with what this method does, you can change the text tags after calling it. """ monospace_family = teek.NamedFont('TkFixedFont').family family = self.widget.config['font'].family basic_size = self.widget.config['font'].size # may be negative h_sizes = { 'h1': round(2.5 * basic_size), 'h2': round(2.0 * basic_size), 'h3': round(1.6 * basic_size), 'h4': round(1.45 * basic_size), 'h5': round(1.25 * basic_size), 'h6': round(1.1 * basic_size), } # because pep8 line length tag = self.widget.get_tag # these tags don't need any special settings, but they need to be # created to avoid warnings in soup2teek tag('soup-p') tag('soup-ol') tag('soup-ul') # there's no soup-a because teek.extras.links handles that tag('soup-code')['font'] = (monospace_family, basic_size, '') tag('soup-pre')['font'] = (monospace_family, basic_size, '') tag('soup-pre')['lmargin1'] = 30 tag('soup-pre')['lmargin2'] = 50 tag('soup-li')['lmargin1'] = 10 tag('soup-li')['lmargin2'] = 10 tag('soup-strong')['font'] = (family, basic_size, 'bold') tag('soup-b')['font'] = (family, basic_size, 'bold') tag('soup-em')['font'] = (family, basic_size, 'italic') tag('soup-i')['font'] = (family, basic_size, 'italic') for h, size in h_sizes.items(): tag('soup-' + h)['font'] = (family, size, 'bold') # make sure that html_pre's indenting stuff works inside list elements tag('soup-li').lower('soup-pre') def add_soup(self, element): """Render a BeautifulSoup4 HTML element. The text, images, or whatever the element represents are added to the end of the text widget. This method looks for methods whose names are ``handle_`` followed by the name of a HTML tag; for example, ``handle_h1()`` or ``handle_p()``. Those methods run when an element with the corresponding tag name is added. You can subclass :class:`SoupViewer` and create more of these methods to handle more different kinds of tags. There are two things that the methods can do: 1. The method can return None to indicate that :meth:`add_soup` shouldn't do anything with the content of the element. :: def handle_pre(self, pre): self.widget.insert(self.widget.end, pre.text.rstrip() + '\ \\n\\n') 2. The method can be decorated with :func:`contextlib.contextmanager`. When it yields, :meth:`add_soup` will loop over the element and call itself recursively with each subelement. :: @contextlib.contextmanager def handle_ul(self, ul): for li in ul: if li.name == 'li': # '\\N{bullet} ' creates a Unicode black circle ch\ aracter li.insert(0, '\\N{bullet} ') yield # the content of the ul is added here self.widget.insert(self.widget.end, '\\n') In either case, :meth:`add_soup` adds a :ref:`textwidget tag ` as explained in :meth:`create_tags`. """ # beautifulsoup is buggy, sometimes this recurses infinitely and # sometimes this raises AttributeError # # see handle_ul() for an example of how this would be nice, if this # worked #element = copy.deepcopy(element) if element.name is None: # plain text, handle it kind of like web browsers do # \xa0 is non-breaking space text = str(element) text = re.sub(r'[^\S\xa0]+', ' ', text) last_char = self.widget.get( self.widget.end.back(chars=1), self.widget.end) if last_char.isspace(): text = text.lstrip(' ') self.widget.insert(self.widget.end, text) return try: handler = getattr(self, 'handle_' + element.name) except AttributeError: omg = ("soup contains a <%s> tag, but %s has no handle_%s() method" % (element.name, type(self).__name__, element.name)) warnings.warn(omg, RuntimeWarning) handler = self._fallback_handler old_end = self.widget.end handler_result = handler(element) if handler_result is not None: with handler_result: for sub in element: self.add_soup(sub) self.widget.get_tag('soup-' + element.name).add( old_end, self.widget.end) @contextlib.contextmanager def _fallback_handler(self, element): yield def handle_pre(self, element): self.widget.insert(self.widget.end, element.text.rstrip() + '\n\n') def handle_br(self, element): self.widget.insert(self.widget.end, '\n') @contextlib.contextmanager def _do_nothing_handler(self, element): yield handle_i = handle_em = _do_nothing_handler handle_b = handle_strong = _do_nothing_handler handle_code = _do_nothing_handler @contextlib.contextmanager def _double_newline_handler(self, element): yield self.widget.insert(self.widget.end, '\n\n') handle_h1 = _double_newline_handler handle_h2 = _double_newline_handler handle_h3 = _double_newline_handler handle_h4 = _double_newline_handler handle_h5 = _double_newline_handler handle_h6 = _double_newline_handler handle_p = _double_newline_handler @contextlib.contextmanager def handle_ul(self, element): for li in element: if li.name == 'li': li.insert(0, '\N{bullet} ') yield self.widget.insert(self.widget.end, '\n') @contextlib.contextmanager def handle_ol(self, element): for num, li in enumerate((sub for sub in element if sub.name == 'li'), start=1): li.insert(0, str(num) + '. ') yield self.widget.insert(self.widget.end, '\n') @contextlib.contextmanager def handle_li(self, element): yield last_char = self.widget.get(self.widget.end.back(chars=1)) if last_char != '\n': self.widget.insert(self.widget.end, '\n') @contextlib.contextmanager def handle_a(self, element): start = self.widget.end yield end = self.widget.end links.add_url_link(self.widget, element.attrs['href'], start, end) @contextlib.contextmanager def handle_img(self, element): loading_id = self._loading_id mark_name = next(self._image_mark_names) # TODO: add 'mark gravity' to teek self.widget.marks[mark_name + '-start'] = self.widget.end teek.tcl_call(None, self.widget, 'mark', 'gravity', mark_name + '-start', 'left') self.widget.insert(self.widget.end, element.attrs.get('alt', '')) self.widget.marks[mark_name + '-end'] = self.widget.end teek.tcl_call(None, self.widget, 'mark', 'gravity', mark_name + '-end', 'left') if self._use_threads: # daemon=True because i don't care wtf happens to this thread threading.Thread( target=self._image_loader_thread, args=[mark_name, element.attrs['src'], loading_id], daemon=True).start() yield def _image_loader_thread(self, mark_name, src, loading_id): bytez = self.download(src) if loading_id == self._loading_id: self._add_image(mark_name, bytez) # only one of these will be running at a time, because the decoration @teek.make_thread_safe def _add_image(self, mark_name, bytez): image = image_loader.from_bytes(bytez) self._loaded_images.append(image) start_pos = self.widget.marks[mark_name + '-start'] end_pos = self.widget.marks[mark_name + '-end'] tags = self.widget.get_all_tags(start_pos) self.widget.delete(start_pos, end_pos) teek.tcl_call(None, self.widget, 'image', 'create', start_pos, '-image', image) for tag in tags: tag.add(start_pos, start_pos.forward(chars=1)) PKwWN1 teek/extras/tooltips.pyimport teek class _TooltipManager: # This needs to be shared by all instances because there's only one # mouse pointer. tipwindow = None def __init__(self, widget: teek.Widget): widget.bind('', self.enter, event=True) widget.bind('', self.leave, event=True) widget.bind('', self.motion, event=True) self.widget = widget self.got_mouse = False self.text = None @classmethod def destroy_tipwindow(cls): if cls.tipwindow is not None: cls.tipwindow.destroy() cls.tipwindow = None def enter(self, event): # For some reason, toplevels get also notified of their # childrens' events. if event.widget is self.widget: self.destroy_tipwindow() self.got_mouse = True teek.after(1000, self.show) # these are important, it's possible to enter without mouse move self.mousex = event.rootx self.mousey = event.rooty def leave(self, event): if event.widget is self.widget: self.destroy_tipwindow() self.got_mouse = False def motion(self, event): self.mousex = event.rootx self.mousey = event.rooty def show(self): if not self.got_mouse: return self.destroy_tipwindow() if self.text is not None: # the label and the tipwindow are not ttk widgets because that way # they can look different than other widgets, which is what # tooltips are usually like tipwindow = type(self).tipwindow = teek.Toplevel() tipwindow.geometry(x=(self.mousex + 10), y=(self.mousey - 10)) tipwindow.bind('', self.destroy_tipwindow) # TODO: add overrideredirect to teek teek.tcl_call(None, 'wm', 'overrideredirect', tipwindow, 1) # i don't think there's a need to add better support for things # like non-ttk labels because they are not needed very often # # refactoring note: if you change this, make sure that either BOTH # of fg and bg are set, or NEITHER is set, because e.g. bg='white' # with no fg gives white text on white background on systems with # white default foreground, but works fine on systems with black # default foreground label = teek.tcl_call(str, 'label', tipwindow.to_tcl() + '.label', '-text', self.text, '-border', 3, '-fg', 'black', '-bg', '#ffc') teek.tcl_call(None, 'pack', label) def set_tooltip(widget, text): """Create tooltips for a widget. After calling ``set_tooltip(some_widget, "hello")``, "hello" will be displayed in a small window when the user moves the mouse over the widget and waits for a small period of time. Do ``set_tooltip(some_widget, None)`` to get rid of a tooltip. """ if text is None: if hasattr(widget, '_tooltip_manager'): widget._tooltip_manager.text = None else: if not hasattr(widget, '_tooltip_manager'): widget._tooltip_manager = _TooltipManager(widget) widget._tooltip_manager.text = text PKWNx55teek-0.4.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2017-2019 Akuli Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HSmPOteek-0.4.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,rzd&Y)r$[)T&UrPK!H7/(teek-0.4.dist-info/METADATAZm۶_$핸Dq8/oizD$D ʚN}HQ촞9x΁,i[݈,ՍJmU$yӖl7vBz7ҹƫ^)[]AlK57r]-/o[/ӟt*e㫿*lS>7qnX*$[,jnfmMVkzH쓿Qn9܈o_@卮=}o}6y/amawa <k)sHP|a,Ttyjx#ݭ}i>yRUkm'm`qX};]`F r3Is%~jS7^ֽw!-wrf=&zldot7M %-k,F6wno*_.eVB̍!clHBKU]ux{dނl90)PLm4g qD'ےl :VVUaxEaF^ T; >{ۊJyH%ǍSf5 Uv~IrC 6 euT#I^X(6[vF cl+~#8XPDj&Mi }<"Vp⁲t/TIm䃕E[.Y?bٓa s¼-bFcQnPk&Bnt\oBIt-eVw7sE )TQXh $kQ IeذQn=yWdSQal7UFs'vͷkW_^ݓL>uΊP~a$v'1&XEL\ (.mD_i8j4,am*)ڨ)YJ%zm+=0?9|C7(' w73Ѳ[F6/%\(Iz_k?⟽][[]P>"Cľl2KyMjMG\Uo gnșr;Yg[ )ل"avì1}eQ CG@)L`r/ jnbwվCnd5`gMGifKfV E#+AUcLr".mXn..$˲g_`d?s1)cf2eɽliAEVB9A+#(%ijl/Q 6:' ^(vL֘!)ۦU!m8Jcz \*3)OJhMX@k|ٮSvz2H#:@~O$AέS[- eWer3%`)6mIKxGxM3xBT- iMf;ds`)ʇj)rTy@D7!9F  qh5>D7]&T q ꏟ^M/lH۱pxTAL?Q%y P)O8IA&$ǎOٸم4"z'~ჳ>R =u"(q`FL@ZV 756i4 GXh؇Tq3u^fO9k ݣu69XԾ;1†TQJVy4=;>N FPdiPs=ؘn|iB-Z.~>:# >!E8R;2_(5:@)ae[tON!w |2 je еvƫk?!ĸ1>)^; RN_[dBx-N4.a˜t"tL=J~\bT Dg `O*gK F?)gQKC$Ֆ"[qO#IRrkS7C%c ɦt"loj}drܻIȆ 1{`71|N? Yl^:]Zn8E6cIv~`O')15n~z@z==80">]_][UX6<!a~%"5MN}_:"}`I.5Wv'.l c(qH %Fuʔ(Zl89: Cr]aa޲hp# L4{,b\!W"]SNӍ1`AE/ ]`=~, ^N3C p@brGL;]cu*3S/O0_."~1o6dk y5ZSH!BC"eѷRtF2~{?>XeroRL0]9@v9x)4dA4ݎ ӝx5=6B Ĕ9CtJ#>fY\JQ뺿 X=]ҝW5:n7}Q })O&'^#x c/c5q!WcAFhl FxU*A ?Җ\ ?n('=.T+D˽uQqz j)Jmx %ix +]'q,_1 2gm8G0boq:Jѷ+;2JwZ2:Jh PD`$ˆaRQnF_.xU r|wQZ|6 &*ܬ1o`1 cgT|iW`"]sP:t;Բ88(=a0xBMaR!o R~oϟ] YP +$P[ApCkj2|㱥,hLCpMG[0sDv4TAMHt b (LsZti]{Z %Ë0ȷxe Cڔ PK!H=`%teek-0.4.dist-info/RECORDDzVཟ$2HP$D[bT?MX͟q\ݫ14 FR)Չ^΁2Rq&P@ A?+;)ɛ'4딺&\GَAb^8M+xvXW㬐NGD&zR7 d*n'a `a|N8=aG̈́o'R?N!L}桇GPuI9a 0C/l8Kvo9"2wov_uJK71!Ep۪L㞺\1f<=eJt%8{pH ic?JZp1uuV,+1VAc.Z1] TAҩw8.IM(S4i\dt!\ Ŏ1!:\# uE){pC+Ӿ F}6`Nډĥ曛wGY]5d{nuL4DM,:FqccI` BiLwcP :ҵu"HrgYA;$|pQ >~/M}c:9e<6悬>' se`XP%n9XS׭Ϻ^QɊ%Jx(BZLP~pv|I컄||z&״|a  `Gskd}4bE<%TݲLT ~Xuɭw\ɔ: ȼSyi4klB=1|m'MLFN)yչ*/!?j/ _ය7406Ei9/ykU⊟[WoIJW7ɣnMykPGvRgͷ!zl$k0; 5)e6+_UyDU7N1kӴLxhǪ߇Ikx\^\J‚t WA80Ws44TA\ eM/%Qp Bs}>@$