From 18a82e2fed2c66caf84b70296d1fa5e9eb62b245 Mon Sep 17 00:00:00 2001 From: Edvard Rejthar Date: Thu, 9 Jan 2025 19:26:53 +0100 Subject: [PATCH] tui layout --- .github/workflows/python-publish.yml | 2 +- docs/Changelog.md | 5 ++- .../textual_interface/textual_adaptor.py | 5 +++ mininterface/textual_interface/textual_app.py | 9 +++++ .../textual_interface/textual_button_app.py | 3 ++ .../textual_interface/textual_facet.py | 35 ++++++++++++++++++- mininterface/tk_interface/external_fix.py | 5 +-- mininterface/tk_interface/tk_facet.py | 11 +++--- pyproject.toml | 4 ++- 9 files changed, 69 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index be33ad1..6a9773c 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -18,6 +18,6 @@ jobs: - name: Build the package run: python3 -m pip install --upgrade build && python3 -m build - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.8.10 + uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_GITHUB_MININTERFACE }} \ No newline at end of file diff --git a/docs/Changelog.md b/docs/Changelog.md index 5a37f96..ba32300 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,6 +1,9 @@ # Changelog -## 0.7.2 +## 0.7.3 (2025-01-09) +* fix: put GUI descriptions back to the bottom + +## 0.7.2 (2024-12-30) * GUI calendar ## 0.7.1 (2024-11-27) diff --git a/mininterface/textual_interface/textual_adaptor.py b/mininterface/textual_interface/textual_adaptor.py index c92f21a..d2fe91f 100644 --- a/mininterface/textual_interface/textual_adaptor.py +++ b/mininterface/textual_interface/textual_adaptor.py @@ -28,6 +28,8 @@ class TextualAdaptor(BackendAdaptor): def __init__(self, interface: "TextualInterface"): self.interface = interface self.facet = interface.facet = TextualFacet(self, interface.env) + self.app: TextualApp | None = None + self.layout_elements = [] @staticmethod def widgetize(tag: Tag) -> Widget | Changeable: @@ -73,6 +75,9 @@ def header(self, text: str): def run_dialog(self, form: TagDict, title: str = "", submit: bool | str = True) -> TagDict: super().run_dialog(form, title, submit) + # Unfortunately, there seems to be no way to reuse the app. + # Which blocks using multiple form external .form() calls from the web interface. + # Textual cannot run in a thread, it seems it cannot run in another process, self.suspend() is of no use. self.app = app = TextualApp(self, submit) if title: app.title = title diff --git a/mininterface/textual_interface/textual_app.py b/mininterface/textual_interface/textual_app.py index c9d21e4..239e9f7 100644 --- a/mininterface/textual_interface/textual_app.py +++ b/mininterface/textual_interface/textual_app.py @@ -26,6 +26,14 @@ class TextualApp(App[bool | None]): # ("down", "go_up", "Go down"), # ] + DEFAULT_CSS = """ + ImageViewer{ + + height: 20; + } + """ + """ Limit layout image size """ + def __init__(self, adaptor: "TextualAdaptor", submit: str | bool = True): super().__init__() self.title = adaptor.facet._title @@ -52,6 +60,7 @@ def compose(self) -> ComposeResult: yield Label(text, id="buffered_text") focus_set = False with VerticalScroll(): + yield from self.adaptor.layout_elements for i, fieldt in enumerate(self.widgets): if isinstance(fieldt, Input): yield Label(fieldt.placeholder) diff --git a/mininterface/textual_interface/textual_button_app.py b/mininterface/textual_interface/textual_button_app.py index 8c01700..6751dd8 100644 --- a/mininterface/textual_interface/textual_button_app.py +++ b/mininterface/textual_interface/textual_button_app.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any from textual.app import App, ComposeResult +from textual.containers import VerticalScroll from textual.widgets import Button, Footer, Label from ..exceptions import Cancelled @@ -60,6 +61,7 @@ def __init__(self, interface: "TextualInterface"): self.focused_i: int = 0 self.values = {} self.interface = interface + self.adaptor = self.interface.adaptor def yes_no(self, text: str, focus_no=True) -> DummyWrapper: return self.buttons(text, [("Yes", True), ("No", False)], int(focus_no)) @@ -78,6 +80,7 @@ def compose(self) -> ComposeResult: yield Footer() if text := self.interface._redirected.join(): yield Label(text, id="buffered_text") + yield from self.adaptor.layout_elements yield Label(self.text, id="question") self.values.clear() diff --git a/mininterface/textual_interface/textual_facet.py b/mininterface/textual_interface/textual_facet.py index 91e6959..098af04 100644 --- a/mininterface/textual_interface/textual_facet.py +++ b/mininterface/textual_interface/textual_facet.py @@ -1,6 +1,15 @@ +from datetime import datetime from typing import TYPE_CHECKING from warnings import warn -from ..facet import Facet +from pathlib import Path + +from textual.widgets import (Checkbox, Footer, Header, Input, Label, + RadioButton, Static) + +from humanize import naturalsize + +from ..exceptions import DependencyRequired +from ..facet import Facet, Image, LayoutElement if TYPE_CHECKING: from .textual_adaptor import TextualAdaptor @@ -22,6 +31,30 @@ def set_title(self, title: str): # NOTE: When you receive Facet in Command.init, the app does not exist yet warn("Setting textual title not implemented well.") + def _layout(self, elements: list[LayoutElement]): + append = self.adaptor.layout_elements.append + try: + from PIL import Image as ImagePIL + from textual_imageview.viewer import ImageViewer + PIL = True + except: + PIL = False + + for el in elements: + match el: + case Image(): + if not PIL: + raise DependencyRequired("img") + append(ImageViewer(ImagePIL.open(el.src))) + case Path(): + size = naturalsize(el.stat().st_size) + mtime = datetime.fromtimestamp(el.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S") + append(Label(f"{el} / {size} / {mtime}")) + case str(): + append(Label(el)) + case _: + append(Label("Error in the layout: Unknown {el}")) + def submit(self, *args, **kwargs): super().submit(*args, **kwargs) try: diff --git a/mininterface/tk_interface/external_fix.py b/mininterface/tk_interface/external_fix.py index 8f998e2..3753c62 100644 --- a/mininterface/tk_interface/external_fix.py +++ b/mininterface/tk_interface/external_fix.py @@ -1,4 +1,5 @@ -# The purpose of the file is to put the descriptions to the bottom of the widgets as it was in the former version of the tkinter_form. +# The purpose of the file is to put the descriptions to the bottom of the widgets +# as it was in the former version of the tkinter_form and to limit their width. from tkinter import ttk from tkinter_form import Form, Value, FieldForm @@ -48,7 +49,7 @@ def __create_widgets_monkeypatched( description_label = None if not description is None: index += 1 - description_label = ttk.Label(self, text=description) + description_label = ttk.Label(self, text=description, wraplength=1000) description_label.grid(row=index, column=1, columnspan=2, sticky="nesw", padx=2, pady=2) self.fields[name_key] = FieldForm( diff --git a/mininterface/tk_interface/tk_facet.py b/mininterface/tk_interface/tk_facet.py index 8035d73..939e83e 100644 --- a/mininterface/tk_interface/tk_facet.py +++ b/mininterface/tk_interface/tk_facet.py @@ -36,10 +36,13 @@ def _layout(self, elements: list[LayoutElement]): raise DependencyRequired("img") filename = el.src img = ImagePIL.open(filename) - img = img.resize((250, 250)) - img = ImageTk.PhotoImage(img) - panel = Label(self.adaptor.frame, image=img) - panel.image = img + max_width, max_height = 250, 250 + w_o, h_o = img.size + scale = min(max_width / w_o, max_height / h_o) + img = img.resize((int(w_o * scale), int(h_o * scale)), ImagePIL.LANCZOS) + img_p = ImageTk.PhotoImage(img) + panel = Label(self.adaptor.frame, image=img_p) + panel.image = img_p panel.pack() case Path(): size = naturalsize(el.stat().st_size) diff --git a/pyproject.toml b/pyproject.toml index 7a39442..96f1665 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,10 @@ tkscrollableframe = "*" [tool.poetry.extras] web = ["textual-serve"] +img = ["pillow", "textual_imageview"] +tui = ["textual_imageview"] gui = ["pillow", "tkcalendar"] -all = ["textual-serve", "pillow", "tkcalendar"] +all = ["textual-serve", "pillow", "tkcalendar", "textual_imageview"] [tool.poetry.scripts] mininterface = "mininterface.__main__:main"