Source code for teek._widgets.windows

import 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


[docs]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 <eventloop>`; 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()))
[docs]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)
[docs] 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