import logging
from typing import Any, Callable
from ascii_designer.i18n import Translations
from ascii_designer.toolkit_tk import ToolkitTk
from .ascii_slice import slice_grids, merged_cells
from .toolkit import ToolkitBase, get_toolkit
__all__ = ["AutoFrame"]
def L():
return logging.getLogger(__name__)
def _convert_title(classname):
# insert space before each capital letter
title = "".join(map(lambda x: x if x.islower() else " " + x, classname))
title = title.strip()
return title
[docs]
class AutoFrame:
"""
Automatic frame.
class name is converted to title. Override with ``f_title``.
Set window icon by giving an icon file's path in ``f_icon``. Supported
formats are OS-specific; recommended are ``.ico`` on Windows and ``.png`` on
Unix.
Body definition with ``f_body``, menu definition with ``f_menu``.
To create own widgets or customize the autocreated ones, override :any:`f_on_build`.
To add initialization code, override :any:`f_on_show`.
Get at the created controls using AutoFrame[key].
close(), exit(), quit() provided for convenience.
Functions with same name as a control are autobound to the default handler (click or changed).
Attributes are autobound to the control value (get/set), except if they are explicitly overwritten.
"""
f_option_tk_autovalidate: bool = False
"""
If True, tk/ttk Entry and Combobox are set up for automatic update of widget state when validated.
I.e. when value is retrieved or handler is called, the widgets foreground color (tk) or ``invalid`` state (ttk) is updated.
Opt-in, because it might interfere with user code if not expected.
"""
f_translations: Translations = Translations()
"""Translation dictionary.
This can be set per-form or globally on the AutoFrame class. We only
actually need the ``.get(key, default)`` method.
Translation keys are formed as ``<Class name>.<Widget ID>``.
Translations are used in ``f_build`` and ``f_build_menu`` functions.
Currently there is no facility to retranslate after building the form.
"""
f_controls: dict[str, Any]
"""Dictionary of controls created by the form.
You can also access the controls by indexing the AutoFrame instance.
"""
f_toolkit: ToolkitBase
"""The toolkit used to create widgets, e.g ToolkitTk or ToolkitQt instance."""
@property
def f_translations_get_prefixed(self) -> Callable[[str, str | None], str]:
"""Returns a getter for translations with own form name as prefix.
I.e. identical to
``self.f_translations.get_prefix(self.__class__.__name__)``
Supports the usual case that you want additional translations keyed with
the form name as prefix.
Example::
# Somewhere in class MyForm(AutoFrame)
tr = self.f_translations_get_prefixed
# Retrieves translation key MyForm.msg_wait
self.label1 = tr(".msg_wait", "Please wait")
"""
return self.f_translations.get_prefix(self.__class__.__qualname__)
def __init__(self):
self.__dict__["f_controls"] = {}
self.__dict__["f_toolkit"] = get_toolkit()
try:
title = self.f_title
except AttributeError:
self.f_title = _convert_title(self.__class__.__name__)
try:
menu = self.f_menu
except AttributeError:
self.f_menu = []
try:
icon = self.f_icon
except AttributeError:
self.f_icon = ""
[docs]
def f_show(self):
"""Bring the frame on the screen."""
if not self.f_controls:
prefix = self.__class__.__qualname__ + "."
root = self.f_toolkit.root(
title=self.f_translations.get(prefix + "f_title", self.f_title),
icon=self.f_icon,
on_close=self.close,
)
self.f_build(root, self.f_body)
self.f_build_menu(root, self.f_menu)
else:
root = self.f_controls[""]
self.f_on_show()
self.f_toolkit.show(root)
[docs]
def f_build(self, parent, body: str | None = None):
if parent is not None:
self.f_controls[""] = parent
body = body or self.f_body
sliced_grid = slice_grids(body)
def set_stretch(container, grid):
# init rows / columns
for col, head in enumerate(grid.column_heads):
self.f_toolkit.col_stretch(container, col, head.count("-"))
for row, cells in enumerate(grid.body_lines):
# first cell
head = cells[0:1]
# first char of first cell
if head:
head = head[0][0:1]
self.f_toolkit.row_stretch(container, row, 1 if head == "I" else 0)
set_stretch(parent, sliced_grid)
self.f_add_widgets(parent, sliced_grid, autoframe=self)
for widget_id, subgrid in sliced_grid.subgrids.items():
parent = self.f_controls[widget_id]
set_stretch(parent, subgrid)
self.f_add_widgets(parent, subgrid, autoframe=self)
self.f_on_build()
[docs]
def f_on_build(self):
"""Hook that is called after form has been built.
Override this to add custom initialization of widgets.
"""
[docs]
def f_on_show(self):
"""Hook that is called when form is about to be shown on screen.
In contrast to f_on_build, this is called again if the form is closed
and reopened.
"""
def __setattr__(self, name, val):
if name in self:
self.f_toolkit.setval(self[name], val)
else:
super().__setattr__(name, val)
def __getattr__(self, name):
if "f_controls" not in self.__dict__:
raise RuntimeError("You forgot to call super().__init__!")
if name in self.f_controls:
# use toolkit to extract value from the widget
return self.f_toolkit.getval(self[name])
else:
raise AttributeError("Attribute %s is not defined" % (name,))
def __getitem__(self, key: str):
return self.f_controls[key]
def __contains__(self, key: str):
return key in self.f_controls
[docs]
def close(self):
"""Close the window.
This is also called when the window is closed using the x button. Be
sure to call ``super().close()`` or your window won't close.
"""
self.f_toolkit.close(self[""])
[docs]
def quit(self):
return self.close()
[docs]
def exit(self):
return self.close()