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.
- composeui/__init__.py +11 -0
- composeui/core/__init__.py +0 -0
- composeui/core/application.py +368 -0
- composeui/core/chartcolors.py +23 -0
- composeui/core/chartimage.py +336 -0
- composeui/core/dataframesource.py +307 -0
- composeui/core/jobs.py +426 -0
- composeui/core/model.py +66 -0
- composeui/core/registry.py +66 -0
- composeui/core/reload.py +476 -0
- composeui/core/renderer.py +193 -0
- composeui/core/signal.py +358 -0
- composeui/core/sqlitemodel.py +1183 -0
- composeui/core/study.py +121 -0
- composeui/core/translation.py +221 -0
- composeui/core/translationcheck.py +254 -0
- composeui/core/view.py +5508 -0
- composeui/images/composeui.svg +15 -0
- composeui/locales/fr.json +73 -0
- composeui/mypy.py +46 -0
- composeui/py.typed +0 -0
- composeui/qtcore/__init__.py +0 -0
- composeui/qtcore/maprenderer.py +115 -0
- composeui/qtcore/qtapplication.py +222 -0
- composeui/qtcore/qtrenderer.py +2862 -0
- composeui/qtcore/widgets/__init__.py +0 -0
- composeui/qtcore/widgets/map.qml +123 -0
- composeui/qtcore/widgets/qchart.py +393 -0
- composeui/qtcore/widgets/qdatefield.py +101 -0
- composeui/qtcore/widgets/qexpander.py +60 -0
- composeui/qtcore/widgets/qfilepathfield.py +34 -0
- composeui/qtcore/widgets/qfiltertable.py +394 -0
- composeui/qtcore/widgets/qformdialog.py +64 -0
- composeui/qtcore/widgets/qformlabel.py +53 -0
- composeui/qtcore/widgets/qmap.py +68 -0
- composeui/qtcore/widgets/qmarkicon.py +37 -0
- composeui/qtcore/widgets/qmetric.py +44 -0
- composeui/qtcore/widgets/qplaintext.py +29 -0
- composeui/qtcore/widgets/qradiogroup.py +57 -0
- composeui/qtcore/widgets/qsidebar.py +116 -0
- composeui/qtcore/widgets/qsourcetable.py +216 -0
- composeui/qtcore/widgets/qsplitter.py +69 -0
- composeui/qtcore/widgets/qtexttable.py +703 -0
- composeui/qtcore/widgets/qvaluecombobox.py +41 -0
- composeui/qtcore/widgets/qvalueslider.py +64 -0
- composeui/qtcore/widgets/qwindowrelease.py +32 -0
- composeui/textualcore/__init__.py +0 -0
- composeui/textualcore/textualapplication.py +121 -0
- composeui/textualcore/textualrenderer.py +2893 -0
- composeui/textualcore/widgets/__init__.py +0 -0
- composeui/textualcore/widgets/composebutton.py +17 -0
- composeui/textualcore/widgets/composechart.py +212 -0
- composeui/textualcore/widgets/composecheckbox.py +35 -0
- composeui/textualcore/widgets/composecollapsible.py +41 -0
- composeui/textualcore/widgets/composecombobox.py +54 -0
- composeui/textualcore/widgets/composedateinput.py +325 -0
- composeui/textualcore/widgets/composedialog.py +90 -0
- composeui/textualcore/widgets/composefilepathinput.py +85 -0
- composeui/textualcore/widgets/composefiltertable.py +367 -0
- composeui/textualcore/widgets/composeformdialog.py +86 -0
- composeui/textualcore/widgets/composeinput.py +50 -0
- composeui/textualcore/widgets/composelist.py +20 -0
- composeui/textualcore/widgets/composemarkdown.py +17 -0
- composeui/textualcore/widgets/composemetric.py +60 -0
- composeui/textualcore/widgets/composeradioset.py +54 -0
- composeui/textualcore/widgets/composesidebar.py +111 -0
- composeui/textualcore/widgets/composeslider.py +144 -0
- composeui/textualcore/widgets/composesourcetable.py +288 -0
- composeui/textualcore/widgets/composesplitter.py +211 -0
- composeui/textualcore/widgets/composetabbedcontent.py +35 -0
- composeui/textualcore/widgets/composetable.py +476 -0
- composeui/textualcore/widgets/composetoolbar.py +62 -0
- composeui/textualcore/widgets/composetree.py +139 -0
- composeui/webcore/__init__.py +0 -0
- composeui/webcore/headers.py +57 -0
- composeui/webcore/icons.py +58 -0
- composeui/webcore/limits.py +55 -0
- composeui/webcore/origin.py +63 -0
- composeui/webcore/page.py +146 -0
- composeui/webcore/routes.py +303 -0
- composeui/webcore/session.py +436 -0
- composeui/webcore/static/app.css +2 -0
- composeui/webcore/static/cally.js +1312 -0
- composeui/webcore/static/daisyui.LICENSE +21 -0
- composeui/webcore/static/deckgl.LICENSE +19 -0
- composeui/webcore/static/deckgl.min.js +6423 -0
- composeui/webcore/static/htmx.min.js +1 -0
- composeui/webcore/static/page.js +1175 -0
- composeui/webcore/static/uPlot.LICENSE +21 -0
- composeui/webcore/static/uPlot.iife.min.js +2 -0
- composeui/webcore/webapplication.py +495 -0
- composeui/webcore/webrenderer.py +5113 -0
- composeui-0.1.0.dist-info/METADATA +222 -0
- composeui-0.1.0.dist-info/RECORD +97 -0
- composeui-0.1.0.dist-info/WHEEL +5 -0
- composeui-0.1.0.dist-info/licenses/LICENSE +9 -0
- 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)
|