From 9e9bf161da55aeb1fc4ef393bda21df3f01c1672 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 24 Feb 2023 09:37:21 -0500 Subject: [PATCH 1/6] feat: wip on textual --- magicgui/_version.py | 4 - src/magicgui/backends/__init__.py | 1 + src/magicgui/backends/_textual/__init__.py | 4 + src/magicgui/backends/_textual/application.py | 79 ++++++++ src/magicgui/backends/_textual/widgets.py | 182 ++++++++++++++++++ src/magicgui/widgets/protocols.py | 2 +- x.py | 6 + 7 files changed, 273 insertions(+), 5 deletions(-) delete mode 100644 magicgui/_version.py create mode 100644 src/magicgui/backends/_textual/__init__.py create mode 100644 src/magicgui/backends/_textual/application.py create mode 100644 src/magicgui/backends/_textual/widgets.py create mode 100644 x.py diff --git a/magicgui/_version.py b/magicgui/_version.py deleted file mode 100644 index b921eea20..000000000 --- a/magicgui/_version.py +++ /dev/null @@ -1,4 +0,0 @@ -# file generated by setuptools_scm -# don't change, don't track in version control -__version__ = version = "0.5.2.dev30+ga5e272f.d20220824" -__version_tuple__ = version_tuple = (0, 5, 2, "dev30", "ga5e272f.d20220824") diff --git a/src/magicgui/backends/__init__.py b/src/magicgui/backends/__init__.py index 214876cad..054e099e5 100644 --- a/src/magicgui/backends/__init__.py +++ b/src/magicgui/backends/__init__.py @@ -4,6 +4,7 @@ BACKENDS: dict[str, tuple[str, str]] = { "Qt": ("_qtpy", "qtpy"), "ipynb": ("_ipynb", "ipynb"), + "textual": ("_textual", "textual"), } for key in list(BACKENDS): diff --git a/src/magicgui/backends/_textual/__init__.py b/src/magicgui/backends/_textual/__init__.py new file mode 100644 index 000000000..d0bccbd59 --- /dev/null +++ b/src/magicgui/backends/_textual/__init__.py @@ -0,0 +1,4 @@ +from .application import ApplicationBackend +from .widgets import Label, LineEdit + +__all__ = ["ApplicationBackend", "Label", "LineEdit"] diff --git a/src/magicgui/backends/_textual/application.py b/src/magicgui/backends/_textual/application.py new file mode 100644 index 000000000..e92ac1d45 --- /dev/null +++ b/src/magicgui/backends/_textual/application.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, ClassVar, Iterable + +from textual.app import App +from textual.timer import Timer +from textual.widget import Widget + +from magicgui.widgets.protocols import BaseApplicationBackend + +if TYPE_CHECKING: + from textual.message import Message + + +class MguiApp(App): + _widgets: ClassVar[list[Widget]] = [] + + def compose(self) -> Iterable[Widget]: + return self._widgets + + +class ApplicationBackend(BaseApplicationBackend): + _app: ClassVar[MguiApp | None] = None + + @classmethod + def _instance(cls) -> MguiApp: + """Return the current instance of the application backend.""" + if not hasattr(cls, "__instance"): + cls.__instance = MguiApp() + return cls.__instance + + def _mgui_get_backend_name(self) -> str: + return "textual" + + def _mgui_process_events(self) -> None: + ... + + def _mgui_run(self) -> None: + self._mgui_get_native_app().run() + + def _mgui_quit(self) -> None: + return self._mgui_get_native_app().exit() + + def _mgui_get_native_app(self) -> App: + # Get native app + return self._instance() + + def _mgui_start_timer( + self, + interval: int = 0, + on_timeout: Callable[[], None] | None = None, + single: bool = False, + ) -> None: + # TODO: not sure what to do with these yet... + event_target = MessageTarget() + sender = MessageTarget() + self._timer = Timer( + event_target=event_target, + interval=interval / 1000, + sender=sender, + callback=on_timeout, + repeat=1 if single else None, + ) + self._timer.start() + + def _mgui_stop_timer(self) -> None: + if getattr(self, "_timer", None): + self._timer.stop() + + +class MessageTarget: + async def post_message(self, message: Message) -> bool: + ... + + async def _post_priority_message(self, message: Message) -> bool: + ... + + def post_message_no_wait(self, message: Message) -> bool: + ... diff --git a/src/magicgui/backends/_textual/widgets.py b/src/magicgui/backends/_textual/widgets.py new file mode 100644 index 000000000..8163118b5 --- /dev/null +++ b/src/magicgui/backends/_textual/widgets.py @@ -0,0 +1,182 @@ +from typing import TYPE_CHECKING, Any, Callable + +from textual import widgets as txtwdgs +from textual.widget import Widget as TxWidget + +from magicgui.widgets import protocols + +from .application import MguiApp + +if TYPE_CHECKING: + import numpy as np + from textual.dom import DOMNode + + +class TxtBaseWidget(protocols.WidgetProtocol): + """Base Widget Protocol: specifies methods that all widgets must provide.""" + + _txwidget: TxWidget + + def __init__( + self, wdg_class: type[TxWidget] | None = None, parent: TxWidget | None = None + ): + if wdg_class is None: + wdg_class = type(self).__annotations__.get("_txwidget") + if wdg_class is None: + raise TypeError("Must provide a valid textual widget type") + self._txwidget = wdg_class() + MguiApp._widgets.append(self._txwidget) # TODO + + # TODO: assign parent + + def _mgui_close_widget(self) -> None: + """Close widget.""" + raise NotImplementedError() + + def _mgui_get_visible(self) -> bool: + """Get widget visibility.""" + return self._txwidget.visible + + def _mgui_set_visible(self, value: bool) -> None: + """Set widget visibility.""" + self._txwidget.visible = value + + def _mgui_get_enabled(self) -> bool: + """Get the enabled state of the widget.""" + raise NotImplementedError() + + def _mgui_set_enabled(self, enabled: bool) -> None: + """Set the enabled state of the widget to `enabled`.""" + print("set enabled", enabled) + + def _mgui_get_parent(self) -> "DOMNode | None": + """Return the parent widget of this widget.""" + return self._txwidget.parent + + def _mgui_set_parent(self, widget: TxWidget) -> None: + """Set the parent widget of this widget.""" + raise NotImplementedError() + + def _mgui_get_native_widget(self) -> Any: + """Return the native backend widget instance. + + This is generally the widget that has the layout. + """ + return self._txwidget + + def _mgui_get_root_native_widget(self) -> Any: + """Return the root native backend widget. + + In most cases, this is the same as ``_mgui_get_native_widget``. However, in + cases where the native widget is in a scroll layout, this might be different. + """ + return self._txwidget + + def _mgui_bind_parent_change_callback( + self, callback: Callable[[Any], None] + ) -> None: + """Bind callback to parent change event.""" + print("bind parent change callback", callback) + + def _mgui_render(self) -> "np.ndarray": + """Return an RGBA (MxNx4) numpy array bitmap of the rendered widget.""" + raise NotImplementedError() + + def _mgui_get_width(self) -> int: + """Get the width of the widget. + + The intention is to get the width of the widget after it is shown, for the + purpose of unifying widget width in a layout. Backends may do what they need to + accomplish this. For example, Qt can use ``sizeHint().width()``, since + ``width()`` may return something large if the widget has not yet been painted + on screen. + """ + raise NotImplementedError() + + def _mgui_set_width(self, value: int) -> None: + """Set the width of the widget.""" + raise NotImplementedError() + + def _mgui_get_min_width(self) -> int: + """Get the minimum width of the widget.""" + raise NotImplementedError() + + def _mgui_set_min_width(self, value: int) -> None: + """Set the minimum width of the widget.""" + raise NotImplementedError() + + def _mgui_get_max_width(self) -> int: + """Get the maximum width of the widget.""" + raise NotImplementedError() + + def _mgui_set_max_width(self, value: int) -> None: + """Set the maximum width of the widget.""" + raise NotImplementedError() + + def _mgui_get_height(self) -> int: + """Get the height of the widget. + + The intention is to get the height of the widget after it is shown, for the + purpose of unifying widget height in a layout. Backends may do what they need to + accomplish this. For example, Qt can use ``sizeHint().height()``, since + ``height()`` may return something large if the widget has not yet been painted + on screen. + """ + raise NotImplementedError() + + def _mgui_set_height(self, value: int) -> None: + """Set the height of the widget.""" + raise NotImplementedError() + + def _mgui_get_min_height(self) -> int: + """Get the minimum height of the widget.""" + raise NotImplementedError() + + def _mgui_set_min_height(self, value: int) -> None: + """Set the minimum height of the widget.""" + raise NotImplementedError() + + def _mgui_get_max_height(self) -> int: + """Get the maximum height of the widget.""" + raise NotImplementedError() + + def _mgui_set_max_height(self, value: int) -> None: + """Set the maximum height of the widget.""" + raise NotImplementedError() + + def _mgui_get_tooltip(self) -> str: + """Get the tooltip for this widget.""" + raise NotImplementedError() + + def _mgui_set_tooltip(self, value: str | None) -> None: + """Set a tooltip for this widget.""" + pass + + +class TxtValueWidget(TxtBaseWidget, protocols.ValueWidgetProtocol): + _txwidget: txtwdgs.Static + + def _mgui_get_value(self) -> Any: + """Get current value of the widget.""" + return self._txwidget.renderable + + def _mgui_set_value(self, value: Any) -> None: + """Set current value of the widget.""" + self._txwidget.renderable = value + + def _mgui_bind_change_callback(self, callback: Callable[[Any], Any]) -> None: + """Bind callback to value change event.""" + print("bind change callback", callback) + + +class TxtStringWidget(TxtValueWidget): + def _mgui_set_value(self, value) -> None: + super()._mgui_set_value(str(value)) + + +class Label(TxtStringWidget): + _txwidget: txtwdgs.Label + + +class LineEdit(TxtStringWidget): + _txwidget: txtwdgs.Input diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index 6380b4a87..4e17472b4 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -579,7 +579,7 @@ def _mgui_start_timer( Parameters ---------- interval : int, optional - Interval between timeouts, by default 0 + Interval (in msec) between timeouts, by default 0 on_timeout : Optional[Callable[[], None]], optional Function to call when timer finishes, by default None single : bool, optional diff --git a/x.py b/x.py new file mode 100644 index 000000000..b831316d4 --- /dev/null +++ b/x.py @@ -0,0 +1,6 @@ +from magicgui import use_app, widgets + +app = use_app("textual") +l1 = widgets.Label(value="Hello World") +l2 = widgets.LineEdit(value="Hello World") +app.run() From a1cfb956b1e2e5592ada46603ca16e6e2d128c67 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 24 Feb 2023 13:52:14 -0500 Subject: [PATCH 2/6] a start --- pyproject.toml | 1 + src/magicgui/backends/_textual/__init__.py | 4 +- src/magicgui/backends/_textual/application.py | 17 ++++- src/magicgui/backends/_textual/widgets.py | 66 ++++++++++++++++--- x.py | 24 ++++++- 5 files changed, 97 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 93837ecd2..8b1cdaf4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ pyside2 = [ pyside6 = ["pyside6"] tqdm = ["tqdm>=4.30.0"] jupyter = ["ipywidgets>=8.0.0"] +textual = ["textual"] # TODO: figure min version image = ["pillow>=4.0"] quantity = ["pint>=0.13.0"] testing = [ diff --git a/src/magicgui/backends/_textual/__init__.py b/src/magicgui/backends/_textual/__init__.py index d0bccbd59..9a87f7416 100644 --- a/src/magicgui/backends/_textual/__init__.py +++ b/src/magicgui/backends/_textual/__init__.py @@ -1,4 +1,4 @@ from .application import ApplicationBackend -from .widgets import Label, LineEdit +from .widgets import CheckBox, Label, LineEdit, PushButton -__all__ = ["ApplicationBackend", "Label", "LineEdit"] +__all__ = ["ApplicationBackend", "Label", "LineEdit", "PushButton", "CheckBox"] diff --git a/src/magicgui/backends/_textual/application.py b/src/magicgui/backends/_textual/application.py index e92ac1d45..03c623d23 100644 --- a/src/magicgui/backends/_textual/application.py +++ b/src/magicgui/backends/_textual/application.py @@ -3,8 +3,10 @@ from typing import TYPE_CHECKING, Callable, ClassVar, Iterable from textual.app import App +from textual.binding import Binding from textual.timer import Timer from textual.widget import Widget +from textual.widgets import Footer, Header from magicgui.widgets.protocols import BaseApplicationBackend @@ -13,10 +15,21 @@ class MguiApp(App): - _widgets: ClassVar[list[Widget]] = [] + BINDINGS = [ + ("ctrl+t", "app.toggle_dark", "Toggle Dark mode"), + ("ctrl+s", "app.screenshot()", "Screenshot"), + Binding("ctrl+c,ctrl+q", "app.quit", "Quit", show=True), + ] + + HEADER = Header() + FOOTER = Footer() + + _mgui_widgets: ClassVar[list[Widget]] = [] def compose(self) -> Iterable[Widget]: - return self._widgets + yield self.HEADER + yield from self._mgui_widgets + yield self.FOOTER class ApplicationBackend(BaseApplicationBackend): diff --git a/src/magicgui/backends/_textual/widgets.py b/src/magicgui/backends/_textual/widgets.py index 8163118b5..113ec193a 100644 --- a/src/magicgui/backends/_textual/widgets.py +++ b/src/magicgui/backends/_textual/widgets.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, cast from textual import widgets as txtwdgs from textual.widget import Widget as TxWidget @@ -12,6 +12,32 @@ from textual.dom import DOMNode +# Convert class events to instance events... +# FIXME: there must be a better pattern, also need weakrefs +class _Button(txtwdgs.Button): + _callbacks: list[Callable[[Any], Any]] = [] + + def on_button_pressed(self): + for callback in self._callbacks: + callback(True) + + +class _Input(txtwdgs.Input): + _callbacks: list[Callable[[Any], Any]] = [] + + def on_input_changed(self): + for callback in self._callbacks: + callback(self.value) + + +class _Switch(txtwdgs.Switch): + _callbacks: list[Callable[[Any], Any]] = [] + + def on_switch_changed(self): + for callback in self._callbacks: + callback(self.value) + + class TxtBaseWidget(protocols.WidgetProtocol): """Base Widget Protocol: specifies methods that all widgets must provide.""" @@ -25,13 +51,14 @@ def __init__( if wdg_class is None: raise TypeError("Must provide a valid textual widget type") self._txwidget = wdg_class() - MguiApp._widgets.append(self._txwidget) # TODO + MguiApp._mgui_widgets.append(self._txwidget) # TODO # TODO: assign parent def _mgui_close_widget(self) -> None: """Close widget.""" - raise NotImplementedError() + # not sure there is a textual equivalent of closing? + self._mgui_set_visible(False) def _mgui_get_visible(self) -> bool: """Get widget visibility.""" @@ -43,11 +70,11 @@ def _mgui_set_visible(self, value: bool) -> None: def _mgui_get_enabled(self) -> bool: """Get the enabled state of the widget.""" - raise NotImplementedError() + return not self._txwidget.disabled def _mgui_set_enabled(self, enabled: bool) -> None: """Set the enabled state of the widget to `enabled`.""" - print("set enabled", enabled) + self._txwidget.disabled = not enabled def _mgui_get_parent(self) -> "DOMNode | None": """Return the parent widget of this widget.""" @@ -76,7 +103,7 @@ def _mgui_bind_parent_change_callback( self, callback: Callable[[Any], None] ) -> None: """Bind callback to parent change event.""" - print("bind parent change callback", callback) + pass def _mgui_render(self) -> "np.ndarray": """Return an RGBA (MxNx4) numpy array bitmap of the rendered widget.""" @@ -146,7 +173,7 @@ def _mgui_set_max_height(self, value: int) -> None: def _mgui_get_tooltip(self) -> str: """Get the tooltip for this widget.""" - raise NotImplementedError() + return "" def _mgui_set_tooltip(self, value: str | None) -> None: """Set a tooltip for this widget.""" @@ -166,7 +193,8 @@ def _mgui_set_value(self, value: Any) -> None: def _mgui_bind_change_callback(self, callback: Callable[[Any], Any]) -> None: """Bind callback to value change event.""" - print("bind change callback", callback) + if hasattr(self._txwidget, "_callbacks"): + cast("list", self._txwidget._callbacks).append(callback) class TxtStringWidget(TxtValueWidget): @@ -179,4 +207,24 @@ class Label(TxtStringWidget): class LineEdit(TxtStringWidget): - _txwidget: txtwdgs.Input + _txwidget: _Input + + +class TxtBaseButtonWidget(TxtValueWidget, protocols.SupportsText): + _txwidget: _Button + + def _mgui_set_text(self, value: str) -> None: + """Set text.""" + self._txwidget.label = value + + def _mgui_get_text(self) -> str: + """Get text.""" + return self._txwidget.label + + +class PushButton(TxtBaseButtonWidget): + pass + + +class CheckBox(TxtBaseButtonWidget): + _txwidget: _Switch diff --git a/x.py b/x.py index b831316d4..7c044a27e 100644 --- a/x.py +++ b/x.py @@ -1,6 +1,26 @@ from magicgui import use_app, widgets app = use_app("textual") -l1 = widgets.Label(value="Hello World") -l2 = widgets.LineEdit(value="Hello World") +l1 = widgets.Label(value="I'm a label") +l2 = widgets.LineEdit(value="I'm a line edit") +btn = widgets.PushButton(text="I'm a button") +chx = widgets.CheckBox(text="I'm a checkbox") + + +@l2.changed.connect +def _on_click(newval) -> None: + print("line edit changed", newval) + + +@btn.clicked.connect +def _on_click() -> None: + print("Button clicked!") + + +@chx.changed.connect +def _on_click(newval) -> None: + print("Checkbox changed", newval) + btn.enabled = not newval + + app.run() From 5ab07461cc59288cd0de5d65c6e20c3cb1e48256 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 24 Feb 2023 13:57:44 -0500 Subject: [PATCH 3/6] fix line edit value --- src/magicgui/backends/_textual/widgets.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/magicgui/backends/_textual/widgets.py b/src/magicgui/backends/_textual/widgets.py index 113ec193a..128bdbb05 100644 --- a/src/magicgui/backends/_textual/widgets.py +++ b/src/magicgui/backends/_textual/widgets.py @@ -209,6 +209,14 @@ class Label(TxtStringWidget): class LineEdit(TxtStringWidget): _txwidget: _Input + def _mgui_get_value(self) -> Any: + """Get current value of the widget.""" + return self._txwidget.value + + def _mgui_set_value(self, value: Any) -> None: + """Set current value of the widget.""" + self._txwidget.value = value + class TxtBaseButtonWidget(TxtValueWidget, protocols.SupportsText): _txwidget: _Button From 9f76d58295bb3adf397b9cbef09e6c7d47756f9a Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 27 Feb 2023 11:46:23 -0500 Subject: [PATCH 4/6] feat: update --- src/magicgui/application.py | 4 +- src/magicgui/backends/_textual/application.py | 11 ++-- src/magicgui/backends/_textual/widgets.py | 55 ++++++++++++------- x.py | 26 --------- 4 files changed, 43 insertions(+), 53 deletions(-) delete mode 100644 x.py diff --git a/src/magicgui/application.py b/src/magicgui/application.py index 278a1bc3c..65aa22ced 100644 --- a/src/magicgui/application.py +++ b/src/magicgui/application.py @@ -68,9 +68,9 @@ def get_obj(self, name: str) -> Any: f"Could not import object {name!r} from backend {self.backend_module}" ) from e - def run(self) -> None: + def run(self, **kwargs: Any) -> None: """Enter the native GUI event loop.""" - return self._backend._mgui_run() + return self._backend._mgui_run(**kwargs) @property def native(self) -> Any: diff --git a/src/magicgui/backends/_textual/application.py b/src/magicgui/backends/_textual/application.py index 03c623d23..5dfffcfb7 100644 --- a/src/magicgui/backends/_textual/application.py +++ b/src/magicgui/backends/_textual/application.py @@ -6,7 +6,7 @@ from textual.binding import Binding from textual.timer import Timer from textual.widget import Widget -from textual.widgets import Footer, Header +from textual.widgets import Footer from magicgui.widgets.protocols import BaseApplicationBackend @@ -21,13 +21,14 @@ class MguiApp(App): Binding("ctrl+c,ctrl+q", "app.quit", "Quit", show=True), ] - HEADER = Header() + HEADER = None FOOTER = Footer() _mgui_widgets: ClassVar[list[Widget]] = [] def compose(self) -> Iterable[Widget]: - yield self.HEADER + if self.HEADER is not None: + yield self.HEADER yield from self._mgui_widgets yield self.FOOTER @@ -48,8 +49,8 @@ def _mgui_get_backend_name(self) -> str: def _mgui_process_events(self) -> None: ... - def _mgui_run(self) -> None: - self._mgui_get_native_app().run() + def _mgui_run(self, **kwargs) -> None: + self._mgui_get_native_app().run(**kwargs) def _mgui_quit(self) -> None: return self._mgui_get_native_app().exit() diff --git a/src/magicgui/backends/_textual/widgets.py b/src/magicgui/backends/_textual/widgets.py index 128bdbb05..973bd793b 100644 --- a/src/magicgui/backends/_textual/widgets.py +++ b/src/magicgui/backends/_textual/widgets.py @@ -7,6 +7,17 @@ from .application import MguiApp +try: + # useful, but not yet public... + from psygnal._weak_callback import WeakCallback, weak_callback + +except ImportError: + WeakCallback = Callable[[Any], Any] + + def weak_callback(callback: Callable[[Any], Any]) -> WeakCallback: + return callback + + if TYPE_CHECKING: import numpy as np from textual.dom import DOMNode @@ -15,7 +26,7 @@ # Convert class events to instance events... # FIXME: there must be a better pattern, also need weakrefs class _Button(txtwdgs.Button): - _callbacks: list[Callable[[Any], Any]] = [] + _callbacks: list[WeakCallback] = [] def on_button_pressed(self): for callback in self._callbacks: @@ -23,7 +34,7 @@ def on_button_pressed(self): class _Input(txtwdgs.Input): - _callbacks: list[Callable[[Any], Any]] = [] + _callbacks: list[WeakCallback] = [] def on_input_changed(self): for callback in self._callbacks: @@ -31,7 +42,7 @@ def on_input_changed(self): class _Switch(txtwdgs.Switch): - _callbacks: list[Callable[[Any], Any]] = [] + _callbacks: list[WeakCallback] = [] def on_switch_changed(self): for callback in self._callbacks: @@ -51,9 +62,12 @@ def __init__( if wdg_class is None: raise TypeError("Must provide a valid textual widget type") self._txwidget = wdg_class() - MguiApp._mgui_widgets.append(self._txwidget) # TODO - # TODO: assign parent + # TODO: here we add the widget to our global app instance... but perhaps + # we should be using `mount()`? + MguiApp._mgui_widgets.append(self._txwidget) + + # TODO: assign parent ? def _mgui_close_widget(self) -> None: """Close widget.""" @@ -82,7 +96,7 @@ def _mgui_get_parent(self) -> "DOMNode | None": def _mgui_set_parent(self, widget: TxWidget) -> None: """Set the parent widget of this widget.""" - raise NotImplementedError() + raise NotImplementedError("Setting parent of textual widget not supported") def _mgui_get_native_widget(self) -> Any: """Return the native backend widget instance. @@ -107,7 +121,7 @@ def _mgui_bind_parent_change_callback( def _mgui_render(self) -> "np.ndarray": """Return an RGBA (MxNx4) numpy array bitmap of the rendered widget.""" - raise NotImplementedError() + raise NotImplementedError("Textual widget screenshots not yet implemented") def _mgui_get_width(self) -> int: """Get the width of the widget. @@ -118,27 +132,27 @@ def _mgui_get_width(self) -> int: ``width()`` may return something large if the widget has not yet been painted on screen. """ - raise NotImplementedError() + return self._txwidget.styles.width def _mgui_set_width(self, value: int) -> None: """Set the width of the widget.""" - raise NotImplementedError() + self._txwidget.styles.width = value def _mgui_get_min_width(self) -> int: """Get the minimum width of the widget.""" - raise NotImplementedError() + return self._txwidget.styles.min_width def _mgui_set_min_width(self, value: int) -> None: """Set the minimum width of the widget.""" - raise NotImplementedError() + self._txwidget.styles.min_width = value def _mgui_get_max_width(self) -> int: """Get the maximum width of the widget.""" - raise NotImplementedError() + return self._txwidget.styles.max_width def _mgui_set_max_width(self, value: int) -> None: """Set the maximum width of the widget.""" - raise NotImplementedError() + self._txwidget.styles.max_width = value def _mgui_get_height(self) -> int: """Get the height of the widget. @@ -149,27 +163,27 @@ def _mgui_get_height(self) -> int: ``height()`` may return something large if the widget has not yet been painted on screen. """ - raise NotImplementedError() + return self._txwidget.styles.height def _mgui_set_height(self, value: int) -> None: """Set the height of the widget.""" - raise NotImplementedError() + self._txwidget.styles.height = value def _mgui_get_min_height(self) -> int: """Get the minimum height of the widget.""" - raise NotImplementedError() + return self._txwidget.styles.min_height def _mgui_set_min_height(self, value: int) -> None: """Set the minimum height of the widget.""" - raise NotImplementedError() + self._txwidget.styles.min_height = value def _mgui_get_max_height(self) -> int: """Get the maximum height of the widget.""" - raise NotImplementedError() + return self._txwidget.styles.max_height def _mgui_set_max_height(self, value: int) -> None: """Set the maximum height of the widget.""" - raise NotImplementedError() + self._txwidget.styles.max_height = value def _mgui_get_tooltip(self) -> str: """Get the tooltip for this widget.""" @@ -194,7 +208,8 @@ def _mgui_set_value(self, value: Any) -> None: def _mgui_bind_change_callback(self, callback: Callable[[Any], Any]) -> None: """Bind callback to value change event.""" if hasattr(self._txwidget, "_callbacks"): - cast("list", self._txwidget._callbacks).append(callback) + callbacks = cast("list", self._txwidget._callbacks) + callbacks.append(weak_callback(callback)) class TxtStringWidget(TxtValueWidget): diff --git a/x.py b/x.py deleted file mode 100644 index 7c044a27e..000000000 --- a/x.py +++ /dev/null @@ -1,26 +0,0 @@ -from magicgui import use_app, widgets - -app = use_app("textual") -l1 = widgets.Label(value="I'm a label") -l2 = widgets.LineEdit(value="I'm a line edit") -btn = widgets.PushButton(text="I'm a button") -chx = widgets.CheckBox(text="I'm a checkbox") - - -@l2.changed.connect -def _on_click(newval) -> None: - print("line edit changed", newval) - - -@btn.clicked.connect -def _on_click() -> None: - print("Button clicked!") - - -@chx.changed.connect -def _on_click(newval) -> None: - print("Checkbox changed", newval) - btn.enabled = not newval - - -app.run() From 690ba0aef3f267038c93c3d9ccc7e1530a712f78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:15:27 +0000 Subject: [PATCH 5/6] style(pre-commit.ci): auto fixes [...] --- src/magicgui/backends/_textual/application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/magicgui/backends/_textual/application.py b/src/magicgui/backends/_textual/application.py index 5dfffcfb7..2d87bca53 100644 --- a/src/magicgui/backends/_textual/application.py +++ b/src/magicgui/backends/_textual/application.py @@ -5,13 +5,13 @@ from textual.app import App from textual.binding import Binding from textual.timer import Timer -from textual.widget import Widget from textual.widgets import Footer from magicgui.widgets.protocols import BaseApplicationBackend if TYPE_CHECKING: from textual.message import Message + from textual.widget import Widget class MguiApp(App): From a42eb0713592f81dc578da8d75ecdd2e303573bb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:09:22 +0000 Subject: [PATCH 6/6] style(pre-commit.ci): auto fixes [...] --- src/magicgui/backends/_textual/application.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/magicgui/backends/_textual/application.py b/src/magicgui/backends/_textual/application.py index 2d87bca53..7e198de66 100644 --- a/src/magicgui/backends/_textual/application.py +++ b/src/magicgui/backends/_textual/application.py @@ -46,8 +46,7 @@ def _instance(cls) -> MguiApp: def _mgui_get_backend_name(self) -> str: return "textual" - def _mgui_process_events(self) -> None: - ... + def _mgui_process_events(self) -> None: ... def _mgui_run(self, **kwargs) -> None: self._mgui_get_native_app().run(**kwargs) @@ -83,11 +82,8 @@ def _mgui_stop_timer(self) -> None: class MessageTarget: - async def post_message(self, message: Message) -> bool: - ... + async def post_message(self, message: Message) -> bool: ... - async def _post_priority_message(self, message: Message) -> bool: - ... + async def _post_priority_message(self, message: Message) -> bool: ... - def post_message_no_wait(self, message: Message) -> bool: - ... + def post_message_no_wait(self, message: Message) -> bool: ...