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,211 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, ClassVar
4
+
5
+ from textual.binding import Binding, BindingType
6
+ from textual.containers import Container
7
+ from textual.widget import Widget
8
+
9
+ from composeui.core.signal import Signal
10
+
11
+ if TYPE_CHECKING:
12
+ from textual import events
13
+ from textual.geometry import Size
14
+ from textual.layout import DockArrangeResult
15
+ from textual.visual import VisualType
16
+
17
+ #: the narrowest (or shortest) a pane gets, in cells
18
+ MINIMUM_PANE_LENGTH = 3
19
+
20
+
21
+ class SplitterHandle(Widget, can_focus=True):
22
+ """The border between two panes: dragged, or moved with the arrow keys once focused."""
23
+
24
+ DEFAULT_CSS = """
25
+ SplitterHandle {
26
+ width: 1;
27
+ /* as tall as the tallest pane: 1fr would take the screen's spare height */
28
+ height: 100%;
29
+ min-height: 1;
30
+ color: $foreground-darken-3;
31
+ }
32
+ SplitterHandle:hover, SplitterHandle.-dragging {
33
+ color: $accent;
34
+ }
35
+ SplitterHandle:focus {
36
+ background: $accent 30%;
37
+ color: $accent;
38
+ }
39
+ """
40
+ BINDINGS: ClassVar[list[BindingType]] = [
41
+ Binding("left", "move(-1, False)", "Narrow the pane before", show=False),
42
+ Binding("right", "move(1, False)", "Widen the pane before", show=False),
43
+ Binding("up", "move(-1, True)", "Shorten the pane above", show=False),
44
+ Binding("down", "move(1, True)", "Lengthen the pane above", show=False),
45
+ ]
46
+
47
+ def __init__(self) -> None:
48
+ super().__init__()
49
+ #: where the drag started, on the screen, and the panes' lengths then
50
+ self._drag_start: tuple[int, list[int]] | None = None
51
+ #: the panes' lengths as the drag left them, before they're laid out
52
+ self._dragged_lengths: list[int] = []
53
+
54
+ def render(self) -> VisualType:
55
+ if self.splitter.is_vertical:
56
+ return "─" * max(self.size.width, 1)
57
+ return "\n".join("│" * max(self.size.height, 1))
58
+
59
+ @property
60
+ def splitter(self) -> ComposeSplitter:
61
+ parent = self.parent
62
+ assert isinstance(parent, ComposeSplitter)
63
+ return parent
64
+
65
+ def _position(self, event: events.MouseEvent) -> int:
66
+ """Get where `event` is on the screen, along the splitter."""
67
+ return event.screen_y if self.splitter.is_vertical else event.screen_x
68
+
69
+ def on_mouse_down(self, event: events.MouseDown) -> None:
70
+ self.capture_mouse()
71
+ self.add_class("-dragging")
72
+ lengths = self.splitter.pane_lengths()
73
+ self._drag_start, self._dragged_lengths = (self._position(event), lengths), lengths
74
+ event.stop()
75
+
76
+ def on_mouse_move(self, event: events.MouseMove) -> None:
77
+ if self._drag_start is not None:
78
+ start, lengths = self._drag_start
79
+ self._dragged_lengths = self.splitter.move_border(
80
+ self, lengths, self._position(event) - start
81
+ )
82
+
83
+ def on_mouse_up(self, event: events.MouseUp) -> None:
84
+ if self._drag_start is not None:
85
+ self._drag_start = None
86
+ self.release_mouse()
87
+ self.remove_class("-dragging")
88
+ self.splitter.resized.emit(self._dragged_lengths)
89
+ event.stop()
90
+
91
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
92
+ # the keys along the splitter only: the others still scroll the screen
93
+ if action == "move":
94
+ return parameters[1] == self.splitter.is_vertical
95
+ return True # pragma: no cover - Textual asks a widget about its own bindings only
96
+
97
+ def action_move(self, cells: int, _is_vertical: Any) -> None:
98
+ splitter = self.splitter
99
+ splitter.resized.emit(splitter.move_border(self, None, cells))
100
+
101
+
102
+ class ComposeSplitter(Container):
103
+ """Panes in a row (or a column), each its share of it, a handle between two to drag.
104
+
105
+ Only the panes shown have a handle before them, past the first, so a hidden pane
106
+ leaves no border behind. `resized` reports the panes' widths (heights, vertical), in
107
+ cells (0 for a hidden one), once the user moved a border, not when `set_sizes`
108
+ shares them in code. Vertical, it is 24 rows tall, each pane scrolling within its
109
+ share.
110
+ """
111
+
112
+ DEFAULT_CSS = """
113
+ ComposeSplitter {
114
+ layout: horizontal;
115
+ height: auto;
116
+ }
117
+ ComposeSplitter.-vertical {
118
+ layout: vertical;
119
+ height: 24;
120
+ }
121
+ /* here: a handle's own CSS is scoped to the handle, so can't select its splitter */
122
+ ComposeSplitter.-vertical > SplitterHandle {
123
+ width: 100%;
124
+ height: 1;
125
+ }
126
+ """
127
+
128
+ def __init__(self, *panes: Widget, is_vertical: bool = False) -> None:
129
+ self.panes = list(panes)
130
+ self.is_vertical = is_vertical
131
+ self._pending_lengths: list[int] | None = None
132
+ children: list[Widget] = []
133
+ for index, pane in enumerate(self.panes):
134
+ if index > 0:
135
+ children.append(SplitterHandle())
136
+ if is_vertical:
137
+ pane.styles.overflow_y = "auto"
138
+ children.append(pane)
139
+ super().__init__(*children, classes="-vertical" if is_vertical else "")
140
+ self.resized: Signal[list[int]] = Signal()
141
+
142
+ def set_sizes(self, sizes: list[int]) -> None:
143
+ """Share the splitter between the panes in proportion to `sizes`."""
144
+ self._pending_lengths = None
145
+ for pane, size in zip(self.panes, sizes):
146
+ self._set_length(pane, size)
147
+
148
+ def pane_lengths(self) -> list[int]:
149
+ """Get the panes' widths (heights, vertical), in cells; 0 for a hidden one."""
150
+ return [
151
+ (pane.outer_size.height if self.is_vertical else pane.outer_size.width)
152
+ if pane.display
153
+ else 0
154
+ for pane in self.panes
155
+ ]
156
+
157
+ def move_border(
158
+ self,
159
+ handle: SplitterHandle,
160
+ lengths: list[int] | None,
161
+ cells: int,
162
+ ) -> list[int]:
163
+ """Move `handle` by `cells` from where it was with the panes `lengths` long.
164
+
165
+ Returns the panes' lengths once moved, which they take once laid out again.
166
+ If ``lengths`` is None, use the last move pending layout, or the current lengths.
167
+ """
168
+ if lengths is None:
169
+ # Rapid keys must build on the last move, not the layout before it.
170
+ lengths = self._pending_lengths or self.pane_lengths()
171
+ after = self._pane_after(handle)
172
+ shown_before = [index for index in range(after) if self.panes[index].display]
173
+ if len(shown_before) == 0:
174
+ return lengths
175
+ before = shown_before[-1]
176
+ # each keeps a few cells: a pane dragged to nothing would be lost to the user
177
+ cells = max(cells, MINIMUM_PANE_LENGTH - lengths[before])
178
+ cells = min(cells, lengths[after] - MINIMUM_PANE_LENGTH)
179
+ moved = list(lengths)
180
+ moved[before] += cells
181
+ moved[after] -= cells
182
+ self._pending_lengths = moved
183
+ # in proportion to their cells: the screen's size, as it changes, shared alike
184
+ for pane, length in zip(self.panes, moved):
185
+ if pane.display:
186
+ self._set_length(pane, max(length, 1))
187
+ return moved
188
+
189
+ def _set_length(self, pane: Widget, share: int) -> None:
190
+ if self.is_vertical:
191
+ pane.styles.height = f"{share}fr"
192
+ else:
193
+ pane.styles.width = f"{share}fr"
194
+
195
+ def _pane_after(self, handle: SplitterHandle) -> int:
196
+ return self.children.index(handle) // 2 + 1
197
+
198
+ def arrange(self, size: Size, optimal: bool = False) -> DockArrangeResult: # noqa: FBT002
199
+ # a handle shows before each pane shown, past the first: as a pane is shown or
200
+ # hidden, which this lays out again
201
+ handles = [child for child in self.children if isinstance(child, SplitterHandle)]
202
+ has_shown = False
203
+ for index, pane in enumerate(self.panes):
204
+ if index > 0:
205
+ shows = has_shown and bool(pane.display)
206
+ if handles[index - 1].display != shows:
207
+ handles[index - 1].display = shows
208
+ has_shown = has_shown or bool(pane.display)
209
+ arranged = super().arrange(size, optimal)
210
+ self._pending_lengths = None
211
+ return arranged
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from textual.widgets import TabbedContent, TabPane
4
+
5
+ from composeui.core.signal import Signal
6
+
7
+
8
+ class ComposeTabbedContent(TabbedContent):
9
+ """A `TabbedContent` re-exposing the user's picks as a plain composeui `Signal`.
10
+
11
+ Textual posts `TabbedContent.TabActivated` for any change of `active`, set in code
12
+ too; `set_active` keeps those quiet, as Qt's renderers block their widget's signals
13
+ while refreshing it, so `picked` only reports the tab the user picks, by its pane.
14
+ The message bubbles: one from tabs nested in a pane isn't this one's.
15
+ """
16
+
17
+ def __init__(self, *panes: TabPane, initial: str = "") -> None:
18
+ # `initial`: the pane shown first, else the first; Textual reports it as activated
19
+ super().__init__(initial=initial)
20
+ # as the compose syntax does: its positional arguments are titles, not panes
21
+ for pane in panes:
22
+ self.compose_add_child(pane)
23
+ self.picked: Signal[TabPane] = Signal()
24
+
25
+ def set_active(self, pane_id: str) -> None:
26
+ if not self.is_attached:
27
+ # not composed yet: shown first once mounted, as the tabs are built from it
28
+ self._initial = pane_id
29
+ return
30
+ with self.prevent(TabbedContent.TabActivated):
31
+ self.active = pane_id
32
+
33
+ def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
34
+ if event.tabbed_content is self:
35
+ self.picked.emit(event.pane)