composeui 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. composeui/__init__.py +11 -0
  2. composeui/core/__init__.py +0 -0
  3. composeui/core/application.py +368 -0
  4. composeui/core/chartcolors.py +23 -0
  5. composeui/core/chartimage.py +336 -0
  6. composeui/core/dataframesource.py +307 -0
  7. composeui/core/jobs.py +426 -0
  8. composeui/core/model.py +66 -0
  9. composeui/core/registry.py +66 -0
  10. composeui/core/reload.py +476 -0
  11. composeui/core/renderer.py +193 -0
  12. composeui/core/signal.py +358 -0
  13. composeui/core/sqlitemodel.py +1183 -0
  14. composeui/core/study.py +121 -0
  15. composeui/core/translation.py +221 -0
  16. composeui/core/translationcheck.py +254 -0
  17. composeui/core/view.py +5508 -0
  18. composeui/images/composeui.svg +15 -0
  19. composeui/locales/fr.json +73 -0
  20. composeui/mypy.py +46 -0
  21. composeui/py.typed +0 -0
  22. composeui/qtcore/__init__.py +0 -0
  23. composeui/qtcore/maprenderer.py +115 -0
  24. composeui/qtcore/qtapplication.py +222 -0
  25. composeui/qtcore/qtrenderer.py +2862 -0
  26. composeui/qtcore/widgets/__init__.py +0 -0
  27. composeui/qtcore/widgets/map.qml +123 -0
  28. composeui/qtcore/widgets/qchart.py +393 -0
  29. composeui/qtcore/widgets/qdatefield.py +101 -0
  30. composeui/qtcore/widgets/qexpander.py +60 -0
  31. composeui/qtcore/widgets/qfilepathfield.py +34 -0
  32. composeui/qtcore/widgets/qfiltertable.py +394 -0
  33. composeui/qtcore/widgets/qformdialog.py +64 -0
  34. composeui/qtcore/widgets/qformlabel.py +53 -0
  35. composeui/qtcore/widgets/qmap.py +68 -0
  36. composeui/qtcore/widgets/qmarkicon.py +37 -0
  37. composeui/qtcore/widgets/qmetric.py +44 -0
  38. composeui/qtcore/widgets/qplaintext.py +29 -0
  39. composeui/qtcore/widgets/qradiogroup.py +57 -0
  40. composeui/qtcore/widgets/qsidebar.py +116 -0
  41. composeui/qtcore/widgets/qsourcetable.py +216 -0
  42. composeui/qtcore/widgets/qsplitter.py +69 -0
  43. composeui/qtcore/widgets/qtexttable.py +703 -0
  44. composeui/qtcore/widgets/qvaluecombobox.py +41 -0
  45. composeui/qtcore/widgets/qvalueslider.py +64 -0
  46. composeui/qtcore/widgets/qwindowrelease.py +32 -0
  47. composeui/textualcore/__init__.py +0 -0
  48. composeui/textualcore/textualapplication.py +121 -0
  49. composeui/textualcore/textualrenderer.py +2893 -0
  50. composeui/textualcore/widgets/__init__.py +0 -0
  51. composeui/textualcore/widgets/composebutton.py +17 -0
  52. composeui/textualcore/widgets/composechart.py +212 -0
  53. composeui/textualcore/widgets/composecheckbox.py +35 -0
  54. composeui/textualcore/widgets/composecollapsible.py +41 -0
  55. composeui/textualcore/widgets/composecombobox.py +54 -0
  56. composeui/textualcore/widgets/composedateinput.py +325 -0
  57. composeui/textualcore/widgets/composedialog.py +90 -0
  58. composeui/textualcore/widgets/composefilepathinput.py +85 -0
  59. composeui/textualcore/widgets/composefiltertable.py +367 -0
  60. composeui/textualcore/widgets/composeformdialog.py +86 -0
  61. composeui/textualcore/widgets/composeinput.py +50 -0
  62. composeui/textualcore/widgets/composelist.py +20 -0
  63. composeui/textualcore/widgets/composemarkdown.py +17 -0
  64. composeui/textualcore/widgets/composemetric.py +60 -0
  65. composeui/textualcore/widgets/composeradioset.py +54 -0
  66. composeui/textualcore/widgets/composesidebar.py +111 -0
  67. composeui/textualcore/widgets/composeslider.py +144 -0
  68. composeui/textualcore/widgets/composesourcetable.py +288 -0
  69. composeui/textualcore/widgets/composesplitter.py +211 -0
  70. composeui/textualcore/widgets/composetabbedcontent.py +35 -0
  71. composeui/textualcore/widgets/composetable.py +476 -0
  72. composeui/textualcore/widgets/composetoolbar.py +62 -0
  73. composeui/textualcore/widgets/composetree.py +139 -0
  74. composeui/webcore/__init__.py +0 -0
  75. composeui/webcore/headers.py +57 -0
  76. composeui/webcore/icons.py +58 -0
  77. composeui/webcore/limits.py +55 -0
  78. composeui/webcore/origin.py +63 -0
  79. composeui/webcore/page.py +146 -0
  80. composeui/webcore/routes.py +303 -0
  81. composeui/webcore/session.py +436 -0
  82. composeui/webcore/static/app.css +2 -0
  83. composeui/webcore/static/cally.js +1312 -0
  84. composeui/webcore/static/daisyui.LICENSE +21 -0
  85. composeui/webcore/static/deckgl.LICENSE +19 -0
  86. composeui/webcore/static/deckgl.min.js +6423 -0
  87. composeui/webcore/static/htmx.min.js +1 -0
  88. composeui/webcore/static/page.js +1175 -0
  89. composeui/webcore/static/uPlot.LICENSE +21 -0
  90. composeui/webcore/static/uPlot.iife.min.js +2 -0
  91. composeui/webcore/webapplication.py +495 -0
  92. composeui/webcore/webrenderer.py +5113 -0
  93. composeui-0.1.0.dist-info/METADATA +222 -0
  94. composeui-0.1.0.dist-info/RECORD +97 -0
  95. composeui-0.1.0.dist-info/WHEEL +5 -0
  96. composeui-0.1.0.dist-info/licenses/LICENSE +9 -0
  97. composeui-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <!-- the default icon of an app (`MainView.app_icon`), and the documentation's favicon:
3
+ ComposeUI's icon without its fan-out lines and view square, which blur when small -->
4
+ <defs>
5
+ <linearGradient id="composeui-bg" x1="0" y1="0" x2="1" y2="1">
6
+ <stop offset="0" stop-color="#3446a8"/>
7
+ <stop offset="1" stop-color="#1d1a4f"/>
8
+ </linearGradient>
9
+ </defs>
10
+ <rect width="64" height="64" rx="14" fill="url(#composeui-bg)"/>
11
+ <path d="M33.5 19.9 A14 14 0 1 0 33.5 44.1" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round"/>
12
+ <circle cx="44.2" cy="14.3" r="4.6" fill="#2fd4c4"/>
13
+ <circle cx="51.5" cy="32.0" r="4.6" fill="#ffc857"/>
14
+ <circle cx="44.2" cy="49.7" r="4.6" fill="#ff8fc7"/>
15
+ </svg>
@@ -0,0 +1,73 @@
1
+ {
2
+ "File": "Fichier",
3
+ "New": "Nouveau",
4
+ "Open": "Ouvrir",
5
+ "Save": "Enregistrer",
6
+ "Save As": "Enregistrer sous",
7
+ "Exit": "Quitter",
8
+ "OK": "OK",
9
+ "Cancel": "Annuler",
10
+ "Help": "Aide",
11
+ "Can't open {name}: {error}.": "Impossible d'ouvrir {name} : {error}.",
12
+ "Language": "Langue",
13
+ "Save Image": "Enregistrer l'image",
14
+ "{header}: \"{text}\" isn't a valid value": "{header} : « {text} » n'est pas une valeur valide",
15
+ "Search": "Rechercher",
16
+ "+ Filter": "+ Filtre",
17
+ "Column": "Colonne",
18
+ "Value": "Valeur",
19
+ "Add": "Ajouter",
20
+ "Clear": "Effacer",
21
+ "{shown} of {total}": "{shown} sur {total}",
22
+ "No rows match the filters": "Aucune ligne ne correspond aux filtres",
23
+ "Remove the filter on {header}": "Retirer le filtre sur {header}",
24
+ "Type a value to filter by": "Saisissez une valeur pour filtrer",
25
+ "\"{text}\" isn't a number": "« {text} » n'est pas un nombre",
26
+ "\"{text}\" isn't a date, as 2024-01-31": "« {text} » n'est pas une date, comme 2024-01-31",
27
+ "\"{text}\" isn't true or false": "« {text} » n'est ni true ni false",
28
+ "The range's start is after its end": "Le début de l'intervalle est après sa fin",
29
+ ">=20, 18..24": ">=20, 18..24",
30
+ ">=2024-01-31": ">=2024-01-31",
31
+ "true or false": "true ou false",
32
+ "\"{text}\" isn't a valid regular expression": "« {text} » n'est pas une expression régulière valide",
33
+ "Edit the filter on {header}": "Modifier le filtre sur {header}",
34
+ "{header}: {filter}": "{header} : {filter}",
35
+ "contains, =exact, start*, !not": "contient, =exact, début*, !sauf",
36
+ "a regular expression, as Room [1-3]": "une expression régulière, comme Pièce [1-3]",
37
+ "{header} matches {filter}": "{header} correspond à {filter}",
38
+ "Regular expression": "Expression régulière",
39
+ "Sidebar": "Panneau latéral",
40
+ "MM/dd/yyyy": "dd/MM/yyyy",
41
+ "January": "janvier",
42
+ "February": "février",
43
+ "March": "mars",
44
+ "April": "avril",
45
+ "May": "mai",
46
+ "June": "juin",
47
+ "July": "juillet",
48
+ "August": "août",
49
+ "September": "septembre",
50
+ "October": "octobre",
51
+ "November": "novembre",
52
+ "December": "décembre",
53
+ "Mo": "lu",
54
+ "Tu": "ma",
55
+ "We": "me",
56
+ "Th": "je",
57
+ "Fr": "ve",
58
+ "Sa": "sa",
59
+ "Su": "di",
60
+ "Pick a date": "Choisir une date",
61
+ "Previous month": "Mois précédent",
62
+ "Next month": "Mois suivant",
63
+ "Resize the panes": "Redimensionner les volets",
64
+ "Done": "Terminé",
65
+ "Warning": "Avertissement",
66
+ "Error": "Erreur",
67
+ "Choose a file": "Choisir un fichier",
68
+ "Reload": "Recharger",
69
+ "No rows": "Aucune ligne",
70
+ "Export CSV": "Exporter en CSV",
71
+ "Exported {count} rows to {name}.": "{count} lignes exportées dans {name}.",
72
+ "Can't export to {name}: {error}.": "Impossible d'exporter dans {name} : {error}."
73
+ }
composeui/mypy.py ADDED
@@ -0,0 +1,46 @@
1
+ """Mypy integration for CSV row classes and field factories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from mypy.nodes import AssignmentStmt, CallExpr, MemberExpr, RefExpr, TypeInfo
8
+ from mypy.plugin import Plugin
9
+ from mypy.plugins.attrs import attr_attrib_makers, attr_class_maker_callback, attr_define_makers
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Callable
13
+
14
+ from mypy.plugin import ClassDefContext
15
+
16
+
17
+ _DEFINE_ROW = "composeui.core.sqlitemodel.define_csv_row"
18
+
19
+
20
+ def _define_csv_row(ctx: ClassDefContext) -> bool:
21
+ # Mypy's attrs plugin expects converter references to have a resolved node.
22
+ # Built-in method descriptors (e.g. str.strip) need their owning type's symbol.
23
+ for statement in ctx.cls.defs.body:
24
+ if not isinstance(statement, AssignmentStmt) or not isinstance(statement.rvalue, CallExpr):
25
+ continue
26
+ for name, expression in zip(statement.rvalue.arg_names, statement.rvalue.args):
27
+ if name != "converter" or not isinstance(expression, MemberExpr):
28
+ continue
29
+ owner = expression.expr
30
+ if isinstance(owner, RefExpr) and isinstance(owner.node, TypeInfo):
31
+ symbol = owner.node.get(expression.name)
32
+ if symbol is not None:
33
+ expression.node = symbol.node
34
+ return attr_class_maker_callback(ctx, auto_attribs_default=None, slots_default=True)
35
+
36
+
37
+ class _CSVPlugin(Plugin):
38
+ def get_class_decorator_hook_2(self, fullname: str) -> Callable[[ClassDefContext], bool] | None:
39
+ return _define_csv_row if fullname == _DEFINE_ROW else None
40
+
41
+
42
+ def plugin(_version: str) -> type[Plugin]:
43
+ """Register the CSV field factory and row decorator with mypy's attrs plugin."""
44
+ attr_attrib_makers.add("composeui.core.sqlitemodel.csv_field")
45
+ attr_define_makers.add(_DEFINE_ROW)
46
+ return _CSVPlugin
composeui/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,115 @@
1
+ """The map renderer, loaded only when an application uses a map."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from typing_extensions import Protocol
8
+
9
+ from composeui.core.renderer import ViewLike
10
+ from composeui.core.view import MapPoint, MapView
11
+ from composeui.qtcore.qtrenderer import QtWidgetRenderer
12
+ from composeui.qtcore.widgets.qmap import QMap
13
+
14
+ if TYPE_CHECKING:
15
+ from composeui.core.signal import Signal, SignalNoArgs
16
+
17
+
18
+ class MapViewLike(ViewLike, Protocol):
19
+ title: str
20
+ center: MapPoint | None
21
+ zoom: float | None
22
+ points: list[MapPoint]
23
+ height: int
24
+ tiles: bool
25
+ user_agent: str
26
+ clicked: Signal[float, float]
27
+ viewport_changed: SignalNoArgs
28
+
29
+
30
+ class QtMapRenderer(QtWidgetRenderer[MapViewLike, QMap]):
31
+ def __init__(self, view: MapViewLike) -> None:
32
+ super().__init__(view)
33
+ self._refresh_dispatcher.update(
34
+ {
35
+ "title": self._refresh_title,
36
+ "center": self._refresh_center,
37
+ "zoom": self._refresh_zoom,
38
+ "points": self._refresh_points,
39
+ "height": self._refresh_height,
40
+ "tiles": self._refresh_tiles,
41
+ "user_agent": self._refresh_user_agent,
42
+ }
43
+ )
44
+
45
+ def render(self) -> None:
46
+ if self._rendered_object is None:
47
+ widget = self._rendered_object = QMap(self._view.user_agent, tiles=self._view.tiles)
48
+ widget.viewport_moved.connect(self._viewport_moved)
49
+ widget.point_clicked.connect(self._view.clicked.emit)
50
+ widget.map.resized.connect(self._resize)
51
+ self.refresh()
52
+
53
+ @staticmethod
54
+ def _refresh_title(_view: MapViewLike, widget: QMap, value: str) -> None:
55
+ widget.title.setText(value)
56
+ widget.title.setVisible(bool(value))
57
+ widget.map.setAccessibleName(value)
58
+
59
+ @staticmethod
60
+ def _refresh_center(view: MapViewLike, widget: QMap, value: MapPoint | None) -> None:
61
+ QtMapRenderer._fit(widget, view.points, value, view.zoom)
62
+
63
+ @staticmethod
64
+ def _refresh_zoom(view: MapViewLike, widget: QMap, value: float | None) -> None:
65
+ QtMapRenderer._fit(widget, view.points, view.center, value)
66
+
67
+ @staticmethod
68
+ def _refresh_points(view: MapViewLike, widget: QMap, value: list[MapPoint]) -> None:
69
+ widget.root.setProperty("points", [[p.latitude, p.longitude] for p in value])
70
+ QtMapRenderer._fit(widget, value, view.center, view.zoom)
71
+
72
+ @staticmethod
73
+ def _fit(
74
+ widget: QMap, points: list[MapPoint], center: MapPoint | None, zoom: float | None
75
+ ) -> None:
76
+ position, span_x, span_y = MapView.point_bounds(points, center)
77
+ widget.root.setProperty("latitude", position.latitude)
78
+ widget.root.setProperty("longitude", position.longitude)
79
+ widget.root.setProperty(
80
+ "zoom",
81
+ zoom
82
+ if zoom is not None
83
+ else MapView.fitted_zoom(span_x, span_y, widget.map.width(), widget.map.height()),
84
+ )
85
+
86
+ def _resize(self) -> None:
87
+ widget = self._rendered_object
88
+ assert widget is not None
89
+ view = self._view
90
+ self._fit(widget, view.points, view.center, view.zoom)
91
+
92
+ @staticmethod
93
+ def _refresh_height(_view: MapViewLike, widget: QMap, value: int) -> None:
94
+ widget.map.setFixedHeight(value)
95
+
96
+ def _configure(self, view: MapViewLike, widget: QMap, user_agent: str, *, tiles: bool) -> None:
97
+ widget.configure(user_agent, tiles=tiles)
98
+ self._refresh_center(view, widget, view.center)
99
+ self._refresh_zoom(view, widget, view.zoom)
100
+ self._refresh_points(view, widget, view.points)
101
+
102
+ def _refresh_tiles(self, view: MapViewLike, widget: QMap, value: bool) -> None:
103
+ self._configure(view, widget, view.user_agent, tiles=value)
104
+
105
+ def _refresh_user_agent(self, view: MapViewLike, widget: QMap, value: str) -> None:
106
+ self._configure(view, widget, value, tiles=view.tiles)
107
+
108
+ def _viewport_moved(self, latitude: float, longitude: float, zoom: float) -> None:
109
+ with self.stop_refresh():
110
+ self._view.center = MapPoint(
111
+ latitude=latitude,
112
+ longitude=longitude,
113
+ )
114
+ self._view.zoom = zoom
115
+ self._view.viewport_changed.emit()
@@ -0,0 +1,222 @@
1
+ """The application classes of the Qt backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import signal
7
+ import sys
8
+ from typing import TYPE_CHECKING, NamedTuple, TypeVar
9
+
10
+ from qtpy.QtCore import QThread, QTimer
11
+ from qtpy.QtWidgets import QApplication, QMainWindow, QWidget
12
+
13
+ from composeui.core.application import Application, ModelApplication
14
+ from composeui.core.reload import (
15
+ acknowledge_changes,
16
+ can_reload_in_place,
17
+ is_reloaded,
18
+ keep_rerun,
19
+ load_reloaded_state,
20
+ rerun_program,
21
+ run_reloading,
22
+ save_reloaded_state,
23
+ take_changes,
24
+ )
25
+ from composeui.core.view import RendererType
26
+ from composeui.qtcore.qtrenderer import QtMainRenderer
27
+
28
+ if TYPE_CHECKING:
29
+ import argparse
30
+ from collections.abc import Callable
31
+ from pathlib import Path
32
+
33
+ from composeui.core.model import Model
34
+ from composeui.core.view import MainView, View
35
+
36
+
37
+ LOGGER = logging.getLogger("COMPOSEUI")
38
+
39
+ #: milliseconds between two looks, in a reloaded app, at whether files changed
40
+ _CHANGES_CHECK_INTERVAL = 200
41
+
42
+ MainViewT = TypeVar("MainViewT", bound="MainView")
43
+ ModelT = TypeVar("ModelT", bound="Model")
44
+
45
+
46
+ class _Run(NamedTuple):
47
+ """An app ready to show its main view, its command line parsed."""
48
+
49
+ args: argparse.Namespace
50
+ main_view: MainView
51
+ model: Model | None
52
+ render: Callable[[RendererType], None]
53
+
54
+
55
+ def _add_reload_argument(parser: argparse.ArgumentParser) -> None:
56
+ parser.add_argument(
57
+ "--reload",
58
+ help="Run the app again when a Python file under the current directory changes.",
59
+ action="store_true",
60
+ )
61
+
62
+
63
+ def _exec(run: _Run) -> int:
64
+ """Show the main view until its window is closed, or run it again on every change."""
65
+ if run.args.reload and not is_reloaded():
66
+ # never shown here: closed first, as the child processes build and open their own
67
+ if run.model is not None:
68
+ run.model.close()
69
+ # the app runs in a child process, which a new one replaces when it can't run the
70
+ # changed code itself
71
+ return run_reloading()
72
+ app = QApplication(sys.argv)
73
+ signal.signal(signal.SIGINT, signal.SIG_DFL) # kill the program when Ctr+c
74
+ if not is_reloaded():
75
+ run.render(RendererType.QT)
76
+ try:
77
+ return app.exec()
78
+ finally:
79
+ if run.model is not None:
80
+ run.model.close()
81
+ reloads = _Reloads(run)
82
+ try:
83
+ return app.exec()
84
+ finally:
85
+ reloads.close()
86
+
87
+
88
+ class _Reloads:
89
+ """The window of a reloaded app, which shows the app the changed code builds."""
90
+
91
+ def __init__(self, run: _Run) -> None:
92
+ self._run = run
93
+ #: the files changed, taken, whose code the app runs once it can
94
+ self._changed: list[Path] | None = None
95
+ load_reloaded_state(run.main_view, run.model)
96
+ run.render(RendererType.QT)
97
+ self._timer = QTimer()
98
+ self._timer.timeout.connect(self._follow_changes)
99
+ self._timer.start(_CHANGES_CHECK_INTERVAL)
100
+
101
+ def close(self) -> None:
102
+ """Close the model of the app shown last."""
103
+ if self._run.model is not None:
104
+ self._run.model.close()
105
+
106
+ def _follow_changes(self) -> None:
107
+ if self._changed is None:
108
+ self._changed = take_changes()
109
+ # The timer also fires in a nested event loop, e.g. a file dialog's that a callback
110
+ # opened: the callback goes on with the app once it returns. Not a check of the
111
+ # modal dialog, which its loop outlives for a last event once it's closed.
112
+ if self._changed is None or QThread.currentThread().loopLevel() > 1:
113
+ return
114
+ changed, self._changed = self._changed, None
115
+ save_reloaded_state(self._run.main_view, self._run.model)
116
+ if not can_reload_in_place(changed):
117
+ # the supervisor runs the program in a new process, which opens the state saved
118
+ self._timer.stop()
119
+ QApplication.quit()
120
+ return
121
+ try:
122
+ run = rerun_program()
123
+ except Exception:
124
+ LOGGER.exception("The changed code fails: the window keeps the previous code")
125
+ acknowledge_changes()
126
+ return
127
+ try:
128
+ self._show(run)
129
+ except Exception:
130
+ # the window may be half replaced: a new process shows the state saved
131
+ LOGGER.exception("The changed app can't show: it runs again in a new process")
132
+ if run.model is not None and run.model is not self._run.model:
133
+ run.model.close()
134
+ self._timer.stop()
135
+ QApplication.quit()
136
+ return
137
+ acknowledge_changes()
138
+
139
+ def _show(self, run: _Run) -> None:
140
+ """Show `run`'s main view in the window, in place of the previous one."""
141
+ previous = self._run
142
+ renderer = previous.main_view.renderer
143
+ window = renderer.qt_object if isinstance(renderer, QtMainRenderer) else None
144
+ assert window is not None
145
+ load_reloaded_state(run.main_view, run.model)
146
+ previous.main_view.jobs.drop()
147
+ _forget_widgets(previous.main_view, window)
148
+ run.main_view.renderer = QtMainRenderer(run.main_view, window)
149
+ run.render(RendererType.QT)
150
+ self._run = run
151
+ if previous.model is not None and previous.model is not run.model:
152
+ previous.model.close()
153
+
154
+
155
+ def _forget_widgets(view: View, window: QMainWindow) -> None:
156
+ """Detach `view` and its children from their widgets, which the window drops.
157
+
158
+ So a late change of a view, e.g. a status bar clearing its message, reaches no widget;
159
+ the windows of its own, e.g. a dialog's, close.
160
+ """
161
+ for child in view.children_views:
162
+ _forget_widgets(child, window)
163
+ widget = getattr(view.renderer, "qt_object", None)
164
+ if isinstance(widget, QWidget) and widget.isWindow() and widget is not window:
165
+ widget.close()
166
+ widget.deleteLater()
167
+ # not through the view's `on_setattr`: it would show the change on the renderer
168
+ object.__setattr__(view, "renderer", None)
169
+
170
+
171
+ class QtApplication(Application[MainViewT]):
172
+ """The Qt app: it shows the main view in a window."""
173
+
174
+ def __init__(self, *, build_main_view: Callable[[], MainViewT]) -> None:
175
+ super().__init__(build_main_view=build_main_view)
176
+ _add_reload_argument(self._parser)
177
+
178
+ def run(self) -> None:
179
+ super().run()
180
+ assert self._args is not None
181
+ run = _Run(self._args, self.main_view, None, self.render)
182
+ keep_rerun(run)
183
+ sys.exit(_exec(run))
184
+
185
+
186
+ class QtModelApplication(ModelApplication[MainViewT, ModelT]):
187
+ """The Qt app with a model, which it closes once the window is closed."""
188
+
189
+ def __init__(
190
+ self,
191
+ *,
192
+ build_main_view: Callable[[], MainViewT],
193
+ build_model: Callable[[], ModelT],
194
+ setup_model: Callable[[MainViewT, ModelT], None] | None = None,
195
+ ) -> None:
196
+ try:
197
+ super().__init__(
198
+ build_main_view=build_main_view, build_model=build_model, setup_model=setup_model
199
+ )
200
+ except BaseException:
201
+ # built first: e.g. a reload of a program whose `setup_model` now fails
202
+ model: Model | None = getattr(self, "_model", None)
203
+ if model is not None:
204
+ model.close()
205
+ raise
206
+ _add_reload_argument(self._parser)
207
+
208
+ def run(self) -> None:
209
+ try:
210
+ super().run()
211
+ except BaseException:
212
+ self.model.close()
213
+ raise
214
+ assert self._args is not None
215
+ run = _Run(self._args, self.main_view, self.model, self.render)
216
+ keep_rerun(run)
217
+ sys.exit(_exec(run))
218
+
219
+
220
+ #: the backend's applications, for `composeui.core.application.run`
221
+ APPLICATION = QtApplication
222
+ MODEL_APPLICATION = QtModelApplication