py10x-universe 0.1.3__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.
- core_10x/__init__.py +42 -0
- core_10x/backbone/__init__.py +0 -0
- core_10x/backbone/backbone_store.py +59 -0
- core_10x/backbone/backbone_traitable.py +30 -0
- core_10x/backbone/backbone_user.py +66 -0
- core_10x/backbone/bound_data_domain.py +49 -0
- core_10x/backbone/namespace.py +101 -0
- core_10x/backbone/vault.py +38 -0
- core_10x/code_samples/__init__.py +0 -0
- core_10x/code_samples/_package_manifest.py +3 -0
- core_10x/code_samples/directories.py +181 -0
- core_10x/code_samples/person.py +76 -0
- core_10x/concrete_traits.py +356 -0
- core_10x/conftest.py +12 -0
- core_10x/curve.py +321 -0
- core_10x/data_domain.py +48 -0
- core_10x/data_domain_binder.py +45 -0
- core_10x/directory.py +250 -0
- core_10x/entity.py +8 -0
- core_10x/entity_filter.py +5 -0
- core_10x/environment_variables.py +147 -0
- core_10x/exec_control.py +84 -0
- core_10x/experimental/__init__.py +0 -0
- core_10x/experimental/data_protocol_ex.py +34 -0
- core_10x/global_cache.py +121 -0
- core_10x/manual_tests/__init__.py +0 -0
- core_10x/manual_tests/calendar_test.py +35 -0
- core_10x/manual_tests/ctor_update_bug.py +58 -0
- core_10x/manual_tests/debug_graph_on.py +17 -0
- core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
- core_10x/manual_tests/enum_bits_test.py +17 -0
- core_10x/manual_tests/env_vars_trivial_test.py +12 -0
- core_10x/manual_tests/existing_traitable.py +33 -0
- core_10x/manual_tests/k10x_test1.py +13 -0
- core_10x/manual_tests/named_constant_test.py +121 -0
- core_10x/manual_tests/nucleus_trivial_test.py +42 -0
- core_10x/manual_tests/polars_test.py +14 -0
- core_10x/manual_tests/py_class_test.py +4 -0
- core_10x/manual_tests/rc_test.py +42 -0
- core_10x/manual_tests/rdate_test.py +12 -0
- core_10x/manual_tests/reference_serialization_bug.py +19 -0
- core_10x/manual_tests/resource_trivial_test.py +10 -0
- core_10x/manual_tests/store_uri_test.py +6 -0
- core_10x/manual_tests/trait_definition_test.py +19 -0
- core_10x/manual_tests/trait_filter_test.py +15 -0
- core_10x/manual_tests/trait_flag_modification_test.py +42 -0
- core_10x/manual_tests/trait_modification_bug.py +26 -0
- core_10x/manual_tests/traitable_as_of_test.py +82 -0
- core_10x/manual_tests/traitable_heir_test.py +39 -0
- core_10x/manual_tests/traitable_history_test.py +41 -0
- core_10x/manual_tests/traitable_serialization_test.py +54 -0
- core_10x/manual_tests/traitable_trivial_test.py +71 -0
- core_10x/manual_tests/trivial_graph_test.py +16 -0
- core_10x/manual_tests/ts_class_association_test.py +64 -0
- core_10x/manual_tests/ts_trivial_test.py +35 -0
- core_10x/named_constant.py +425 -0
- core_10x/nucleus.py +81 -0
- core_10x/package_manifest.py +85 -0
- core_10x/package_refactoring.py +153 -0
- core_10x/py_class.py +431 -0
- core_10x/rc.py +155 -0
- core_10x/rdate.py +339 -0
- core_10x/resource.py +189 -0
- core_10x/roman_number.py +67 -0
- core_10x/testlib/__init__.py +0 -0
- core_10x/testlib/test_store.py +240 -0
- core_10x/testlib/traitable_history_tests.py +787 -0
- core_10x/testlib/ts_tests.py +280 -0
- core_10x/trait.py +377 -0
- core_10x/trait_definition.py +176 -0
- core_10x/trait_filter.py +205 -0
- core_10x/trait_method_error.py +36 -0
- core_10x/traitable.py +1082 -0
- core_10x/traitable_cli.py +153 -0
- core_10x/traitable_heir.py +33 -0
- core_10x/traitable_id.py +31 -0
- core_10x/ts_store.py +172 -0
- core_10x/ts_store_type.py +26 -0
- core_10x/ts_union.py +147 -0
- core_10x/ui_hint.py +153 -0
- core_10x/unit_tests/test_concrete_traits.py +156 -0
- core_10x/unit_tests/test_converters.py +51 -0
- core_10x/unit_tests/test_curve.py +157 -0
- core_10x/unit_tests/test_directory.py +54 -0
- core_10x/unit_tests/test_documentation.py +172 -0
- core_10x/unit_tests/test_environment_variables.py +15 -0
- core_10x/unit_tests/test_filters.py +239 -0
- core_10x/unit_tests/test_graph.py +348 -0
- core_10x/unit_tests/test_named_constant.py +98 -0
- core_10x/unit_tests/test_rc.py +11 -0
- core_10x/unit_tests/test_rdate.py +484 -0
- core_10x/unit_tests/test_trait_method_error.py +80 -0
- core_10x/unit_tests/test_trait_modification.py +19 -0
- core_10x/unit_tests/test_traitable.py +959 -0
- core_10x/unit_tests/test_traitable_history.py +1 -0
- core_10x/unit_tests/test_ts_store.py +1 -0
- core_10x/unit_tests/test_ts_union.py +369 -0
- core_10x/unit_tests/test_ui_nodes.py +81 -0
- core_10x/unit_tests/test_xxcalendar.py +471 -0
- core_10x/vault/__init__.py +0 -0
- core_10x/vault/sec_keys.py +133 -0
- core_10x/vault/security_keys_old.py +168 -0
- core_10x/vault/vault.py +56 -0
- core_10x/vault/vault_traitable.py +56 -0
- core_10x/vault/vault_user.py +70 -0
- core_10x/xdate_time.py +136 -0
- core_10x/xnone.py +71 -0
- core_10x/xxcalendar.py +228 -0
- infra_10x/__init__.py +0 -0
- infra_10x/manual_tests/__init__.py +0 -0
- infra_10x/manual_tests/test_misc.py +16 -0
- infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
- infra_10x/mongodb_admin.py +111 -0
- infra_10x/mongodb_store.py +346 -0
- infra_10x/mongodb_utils.py +129 -0
- infra_10x/unit_tests/conftest.py +13 -0
- infra_10x/unit_tests/test_mongo_db.py +36 -0
- infra_10x/unit_tests/test_mongo_history.py +1 -0
- py10x_universe-0.1.3.dist-info/METADATA +406 -0
- py10x_universe-0.1.3.dist-info/RECORD +214 -0
- py10x_universe-0.1.3.dist-info/WHEEL +4 -0
- py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
- ui_10x/__init__.py +0 -0
- ui_10x/apps/__init__.py +0 -0
- ui_10x/apps/collection_editor_app.py +100 -0
- ui_10x/choice.py +212 -0
- ui_10x/collection_editor.py +135 -0
- ui_10x/concrete_trait_widgets.py +220 -0
- ui_10x/conftest.py +8 -0
- ui_10x/entity_stocker.py +173 -0
- ui_10x/examples/__init__.py +0 -0
- ui_10x/examples/_guess_word_data.py +14076 -0
- ui_10x/examples/collection_editor.py +17 -0
- ui_10x/examples/date_selector.py +14 -0
- ui_10x/examples/entity_stocker.py +18 -0
- ui_10x/examples/guess_word.py +392 -0
- ui_10x/examples/message_box.py +20 -0
- ui_10x/examples/multi_choice.py +17 -0
- ui_10x/examples/py_data_browser.py +66 -0
- ui_10x/examples/radiobox.py +29 -0
- ui_10x/examples/single_choice.py +31 -0
- ui_10x/examples/style_sheet.py +47 -0
- ui_10x/examples/trivial_entity_editor.py +18 -0
- ui_10x/platform.py +20 -0
- ui_10x/platform_interface.py +517 -0
- ui_10x/py_data_browser.py +249 -0
- ui_10x/qt6/__init__.py +0 -0
- ui_10x/qt6/conftest.py +8 -0
- ui_10x/qt6/manual_tests/__init__.py +0 -0
- ui_10x/qt6/manual_tests/basic_test.py +35 -0
- ui_10x/qt6/platform_implementation.py +275 -0
- ui_10x/qt6/utils.py +665 -0
- ui_10x/rio/__init__.py +0 -0
- ui_10x/rio/apps/examples/examples/__init__.py +22 -0
- ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
- ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
- ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
- ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
- ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
- ui_10x/rio/apps/examples/rio.toml +14 -0
- ui_10x/rio/component_builder.py +497 -0
- ui_10x/rio/components/__init__.py +9 -0
- ui_10x/rio/components/group_box.py +31 -0
- ui_10x/rio/components/labeled_checkbox.py +18 -0
- ui_10x/rio/components/line_edit.py +37 -0
- ui_10x/rio/components/radio_button.py +32 -0
- ui_10x/rio/components/separator.py +24 -0
- ui_10x/rio/components/splitter.py +121 -0
- ui_10x/rio/components/tree_view.py +75 -0
- ui_10x/rio/conftest.py +35 -0
- ui_10x/rio/internals/__init__.py +0 -0
- ui_10x/rio/internals/app.py +192 -0
- ui_10x/rio/manual_tests/__init__.py +0 -0
- ui_10x/rio/manual_tests/basic_test.py +24 -0
- ui_10x/rio/manual_tests/splitter.py +27 -0
- ui_10x/rio/platform_implementation.py +91 -0
- ui_10x/rio/style_sheet.py +53 -0
- ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
- ui_10x/rio/unit_tests/test_internals.py +630 -0
- ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
- ui_10x/rio/widgets/__init__.py +46 -0
- ui_10x/rio/widgets/application.py +109 -0
- ui_10x/rio/widgets/button.py +48 -0
- ui_10x/rio/widgets/button_group.py +60 -0
- ui_10x/rio/widgets/calendar.py +23 -0
- ui_10x/rio/widgets/checkbox.py +24 -0
- ui_10x/rio/widgets/dialog.py +137 -0
- ui_10x/rio/widgets/group_box.py +27 -0
- ui_10x/rio/widgets/layout.py +34 -0
- ui_10x/rio/widgets/line_edit.py +37 -0
- ui_10x/rio/widgets/list.py +105 -0
- ui_10x/rio/widgets/message_box.py +70 -0
- ui_10x/rio/widgets/scroll_area.py +31 -0
- ui_10x/rio/widgets/spacer.py +6 -0
- ui_10x/rio/widgets/splitter.py +45 -0
- ui_10x/rio/widgets/text_edit.py +28 -0
- ui_10x/rio/widgets/tree.py +89 -0
- ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
- ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
- ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
- ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
- ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
- ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
- ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
- ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
- ui_10x/table_header_view.py +305 -0
- ui_10x/table_view.py +174 -0
- ui_10x/trait_editor.py +189 -0
- ui_10x/trait_widget.py +131 -0
- ui_10x/traitable_editor.py +200 -0
- ui_10x/traitable_view.py +131 -0
- ui_10x/unit_tests/conftest.py +8 -0
- ui_10x/unit_tests/test_platform.py +9 -0
- ui_10x/utils.py +661 -0
ui_10x/utils.py
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections import deque
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from core_10x.global_cache import singleton
|
|
8
|
+
from core_10x.named_constant import NamedConstant
|
|
9
|
+
from core_10x.rc import RC
|
|
10
|
+
|
|
11
|
+
from ui_10x.platform import ux
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from datetime import date
|
|
16
|
+
|
|
17
|
+
from core_10x.directory import Directory
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UxAsync(ux.Object):
|
|
21
|
+
SIGNAL = ux.signal_decl()
|
|
22
|
+
s_instances = {}
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def init(cls, bound_method):
|
|
26
|
+
instance = cls.s_instances.get(bound_method)
|
|
27
|
+
if not instance:
|
|
28
|
+
if not ux.is_ui_thread():
|
|
29
|
+
raise RuntimeError(f'You must have called UxAsync.init({bound_method.__name__}) in the UI thread')
|
|
30
|
+
|
|
31
|
+
instance = cls()
|
|
32
|
+
cls.s_instances[bound_method] = instance
|
|
33
|
+
|
|
34
|
+
instance.SIGNAL.connect(bound_method)
|
|
35
|
+
|
|
36
|
+
return instance
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def call(cls, bound_method):
|
|
40
|
+
instance = cls.init(bound_method)
|
|
41
|
+
instance.SIGNAL.emit()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class UxRadioBox(ux.GroupBox):
|
|
45
|
+
__slots__ = ('group', 'nm_class', 'values',)
|
|
46
|
+
def __init__(self, named_constant_class, title = '', horizontal = False, default_value: NamedConstant = None):
|
|
47
|
+
assert inspect.isclass(named_constant_class) and issubclass(named_constant_class, NamedConstant), 'subclass of NamedConstant is expected'
|
|
48
|
+
|
|
49
|
+
self.nm_class = named_constant_class
|
|
50
|
+
self.values = values = list(named_constant_class.s_dir.values())
|
|
51
|
+
|
|
52
|
+
if default_value is None:
|
|
53
|
+
default = 0
|
|
54
|
+
else:
|
|
55
|
+
assert isinstance(default_value, named_constant_class), f'{named_constant_class} - unknown default_values = {default_value}'
|
|
56
|
+
for i, c_def in enumerate(values):
|
|
57
|
+
if c_def is default_value:
|
|
58
|
+
default = i
|
|
59
|
+
break
|
|
60
|
+
else:
|
|
61
|
+
raise RuntimeError(f'{default_value} is not found')
|
|
62
|
+
|
|
63
|
+
if title:
|
|
64
|
+
super().__init__(title)
|
|
65
|
+
else:
|
|
66
|
+
super().__init__()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
lay = ux.HBoxLayout() if horizontal else ux.VBoxLayout()
|
|
70
|
+
self.set_layout(lay)
|
|
71
|
+
|
|
72
|
+
self.group = group = ux.ButtonGroup()
|
|
73
|
+
for i, c_def in enumerate(named_constant_class.s_dir.values()):
|
|
74
|
+
rb = ux.RadioButton(c_def.label)
|
|
75
|
+
lay.add_widget(rb)
|
|
76
|
+
group.add_button(rb, id = i)
|
|
77
|
+
group.button(default).set_checked(True)
|
|
78
|
+
|
|
79
|
+
def choice(self):
|
|
80
|
+
i = self.group.checked_id()
|
|
81
|
+
return self.values[i]
|
|
82
|
+
|
|
83
|
+
def ux_success(text: str, parent = None, title = '', on_close: Callable[[bool],None] = None):
|
|
84
|
+
if not title:
|
|
85
|
+
title = 'Success'
|
|
86
|
+
if not parent:
|
|
87
|
+
parent = None
|
|
88
|
+
ux.MessageBox.information(parent, title, text, on_close=on_close)
|
|
89
|
+
|
|
90
|
+
def ux_warning(text: str, parent = None, title = '', on_close: Callable[[bool],None] = None):
|
|
91
|
+
if not title:
|
|
92
|
+
title = 'Warning'
|
|
93
|
+
if not parent:
|
|
94
|
+
parent = None
|
|
95
|
+
ux.MessageBox.warning(parent, title, text, on_close=on_close)
|
|
96
|
+
|
|
97
|
+
def ux_answer(question: str, parent = None, title = '', on_close: Callable[[bool],None] = None) -> bool|None:
|
|
98
|
+
if not title:
|
|
99
|
+
title = 'Waiting for your answer...'
|
|
100
|
+
if not parent:
|
|
101
|
+
parent = None
|
|
102
|
+
sb = ux.MessageBox.question(parent, title, question, on_close=(lambda result: on_close(ux.MessageBox.is_yes_button(result))) if on_close else None)
|
|
103
|
+
if not on_close:
|
|
104
|
+
print(f'ux_answer: question = {question}, title = {title}, parent = {parent}', sb)
|
|
105
|
+
return ux.MessageBox.is_yes_button(sb)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def ux_multi_choice_answer(named_constant_class, parent = None, title = '', default_value: NamedConstant = None, on_close: Callable[[NamedConstant],None] = None):
|
|
110
|
+
if not title:
|
|
111
|
+
title = 'Pick one of the choices below:'
|
|
112
|
+
|
|
113
|
+
if not parent:
|
|
114
|
+
parent = None
|
|
115
|
+
|
|
116
|
+
box = UxRadioBox(named_constant_class, default_value = default_value)
|
|
117
|
+
accept_callback = (lambda: on_close(box.choice())) if on_close else None
|
|
118
|
+
d = UxDialog(box, parent = parent, title = title, cancel = '', accept_callback=accept_callback)
|
|
119
|
+
if not on_close:
|
|
120
|
+
d.exec()
|
|
121
|
+
return box.choice()
|
|
122
|
+
d.show()
|
|
123
|
+
|
|
124
|
+
def ux_push_button(label: str, callback = None, style_icon = None, flat = False):
|
|
125
|
+
if style_icon:
|
|
126
|
+
if isinstance(style_icon, str):
|
|
127
|
+
try:
|
|
128
|
+
style_icon = getattr(ux.Style.StandardPixmap, f'SP_{style_icon}')
|
|
129
|
+
except AttributeError as e:
|
|
130
|
+
raise RuntimeError(f"Unknown style_icon = '{style_icon}'") from e
|
|
131
|
+
|
|
132
|
+
assert isinstance(style_icon, int), 'Currently only str or int are supported for style_icon'
|
|
133
|
+
|
|
134
|
+
style = ux.Application.style()
|
|
135
|
+
icon = style.standard_icon(style_icon)
|
|
136
|
+
button = ux.PushButton(icon, label)
|
|
137
|
+
else:
|
|
138
|
+
button = ux.PushButton(label)
|
|
139
|
+
|
|
140
|
+
if callback:
|
|
141
|
+
button.clicked_connect(callback)
|
|
142
|
+
|
|
143
|
+
if flat:
|
|
144
|
+
button.set_flat(True)
|
|
145
|
+
|
|
146
|
+
return button
|
|
147
|
+
|
|
148
|
+
class UxDialog(ux.Dialog):
|
|
149
|
+
__slots__ = ('accept_callback','cancel_callback','w_message',)
|
|
150
|
+
def _create_button(self, ok: bool, button_spec) -> ux.PushButton|None:
|
|
151
|
+
if not button_spec:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
if isinstance(button_spec, str):
|
|
155
|
+
label = button_spec
|
|
156
|
+
icon = ''
|
|
157
|
+
|
|
158
|
+
elif isinstance(button_spec, tuple):
|
|
159
|
+
try:
|
|
160
|
+
label, icon = button_spec
|
|
161
|
+
except Exception:
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
cb = self.on_ok if ok else self.on_cancel
|
|
168
|
+
return ux_push_button(label, callback = cb, style_icon = icon)
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
w: ux.Widget,
|
|
173
|
+
parent: ux.Widget = None,
|
|
174
|
+
title: str = None,
|
|
175
|
+
ok = 'Ok',
|
|
176
|
+
cancel = 'Cancel',
|
|
177
|
+
accept_callback = None,
|
|
178
|
+
cancel_callback = None,
|
|
179
|
+
min_width = 0,
|
|
180
|
+
min_height = 0,
|
|
181
|
+
#window_flags = None
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
:param w: a Widget to show
|
|
185
|
+
:param parent: parent widget, if any
|
|
186
|
+
:param title: dialog title
|
|
187
|
+
:param accept_callback: context.method, where: method( context ) -> RC
|
|
188
|
+
:param ok, cancel: labels and corresponding roles for Ok and Cancel buttons. No button if empty
|
|
189
|
+
"""
|
|
190
|
+
super().__init__(parent)
|
|
191
|
+
if title:
|
|
192
|
+
self.set_window_title( title )
|
|
193
|
+
|
|
194
|
+
# if window_flags is None:
|
|
195
|
+
# window_flags = Qt.WindowType.CustomizeWindowHint | Qt.WindowType.Window | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint
|
|
196
|
+
|
|
197
|
+
##self.setWindowFlags(window_flags)
|
|
198
|
+
self.accept_callback = accept_callback
|
|
199
|
+
self.cancel_callback = cancel_callback if cancel_callback else self.reject
|
|
200
|
+
|
|
201
|
+
if ok:
|
|
202
|
+
ok = self._create_button(True, ok)
|
|
203
|
+
if cancel:
|
|
204
|
+
cancel = self._create_button(False, cancel)
|
|
205
|
+
|
|
206
|
+
lay = ux.VBoxLayout()
|
|
207
|
+
self.set_layout(lay)
|
|
208
|
+
|
|
209
|
+
lay.add_widget(w)
|
|
210
|
+
|
|
211
|
+
self.w_message = ux.Label()
|
|
212
|
+
self.w_message.set_style_sheet('color: red;')
|
|
213
|
+
lay.add_widget(self.w_message)
|
|
214
|
+
|
|
215
|
+
if ok or cancel:
|
|
216
|
+
lay.add_widget(ux.separator())
|
|
217
|
+
|
|
218
|
+
bar = ux.HBoxLayout()
|
|
219
|
+
lay.add_layout(bar)
|
|
220
|
+
|
|
221
|
+
bar.add_widget(ux.Label(), stretch = 1)
|
|
222
|
+
if ok:
|
|
223
|
+
bar.add_widget(ok)
|
|
224
|
+
if cancel:
|
|
225
|
+
bar.add_widget(cancel)
|
|
226
|
+
|
|
227
|
+
if min_width > 0:
|
|
228
|
+
self.set_minimum_width(min_width)
|
|
229
|
+
if min_height > 0:
|
|
230
|
+
self.set_minimum_height(min_height)
|
|
231
|
+
|
|
232
|
+
def on_ok(self):
|
|
233
|
+
if self.accept_callback:
|
|
234
|
+
rc: RC = self.accept_callback()
|
|
235
|
+
if not rc:
|
|
236
|
+
self.message(rc.error())
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
self.done(1)
|
|
240
|
+
|
|
241
|
+
def on_cancel(self):
|
|
242
|
+
self.reject()
|
|
243
|
+
self.done(0)
|
|
244
|
+
|
|
245
|
+
def message(self, text: str):
|
|
246
|
+
self.w_message.set_text(text)
|
|
247
|
+
|
|
248
|
+
def ux_pick_date(title = 'Pick a Date', show_date: date = None, grid = True, default = None, on_accept = None) -> date|None:
|
|
249
|
+
cal = ux.CalendarWidget()
|
|
250
|
+
cal.set_grid_visible(bool(grid))
|
|
251
|
+
if show_date:
|
|
252
|
+
cal.set_selected_date(show_date)
|
|
253
|
+
|
|
254
|
+
accept_callback = (lambda: on_accept(cal.selected_date())) if on_accept else None
|
|
255
|
+
dlg = UxDialog(cal, title = title, accept_callback = accept_callback)
|
|
256
|
+
if on_accept:
|
|
257
|
+
dlg.show()
|
|
258
|
+
return None
|
|
259
|
+
return cal.selected_date() if dlg.exec() else default
|
|
260
|
+
|
|
261
|
+
class UxStyleSheet:
|
|
262
|
+
def __init__(self, widget):
|
|
263
|
+
self.widget = widget
|
|
264
|
+
sh = widget.style_sheet()
|
|
265
|
+
self.data = self.loads(sh)
|
|
266
|
+
self.replacement_stack = deque()
|
|
267
|
+
|
|
268
|
+
def set(self):
|
|
269
|
+
self.widget.set_style_sheet(self.dumps(self.data))
|
|
270
|
+
|
|
271
|
+
def update(self, named_sheet_attrs: dict, _system = False):
|
|
272
|
+
if not named_sheet_attrs:
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
data = self.data
|
|
276
|
+
if not any(value != data.get(name) for name, value in named_sheet_attrs.items()): #-- no attribute values changed, nothing to do!
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
if _system:
|
|
280
|
+
old_data = { name: data.get(name) for name in named_sheet_attrs.keys() }
|
|
281
|
+
self.replacement_stack.append(old_data)
|
|
282
|
+
|
|
283
|
+
data.update(named_sheet_attrs)
|
|
284
|
+
self.set()
|
|
285
|
+
|
|
286
|
+
def restore(self):
|
|
287
|
+
if self.replacement_stack:
|
|
288
|
+
old_data = self.replacement_stack.pop()
|
|
289
|
+
data = self.data
|
|
290
|
+
for name, value in old_data.items():
|
|
291
|
+
if value is None:
|
|
292
|
+
data.pop(name, None)
|
|
293
|
+
else:
|
|
294
|
+
data[name] = value
|
|
295
|
+
|
|
296
|
+
self.set()
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
def dumps(cls, data: dict) -> str:
|
|
300
|
+
return '\n'.join(f'{name}: {value};' for name, value in data.items())
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def loads(cls, sheet: str) -> dict:
|
|
304
|
+
res = {}
|
|
305
|
+
pairs = sheet.split(';')
|
|
306
|
+
for pair in pairs:
|
|
307
|
+
pair = pair.strip()
|
|
308
|
+
if pair:
|
|
309
|
+
name_value = pair.split(':')
|
|
310
|
+
if len(name_value) == 2:
|
|
311
|
+
name = name_value[0].strip()
|
|
312
|
+
value = name_value[1].strip()
|
|
313
|
+
res[name] = value
|
|
314
|
+
|
|
315
|
+
return res
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def modify(cls, widget, named_sheet_attrs: dict):
|
|
319
|
+
if not named_sheet_attrs:
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
sh = widget.style_sheet()
|
|
323
|
+
data = cls.loads(sh)
|
|
324
|
+
data.update(named_sheet_attrs)
|
|
325
|
+
widget.set_style_sheet(cls.dumps(data))
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
s_verticalAlignmentMap = { # noqa: N816
|
|
329
|
+
-1: ux.TEXT_ALIGN.TOP,
|
|
330
|
+
0: ux.TEXT_ALIGN.V_CENTER,
|
|
331
|
+
1: ux.TEXT_ALIGN.BOTTOM,
|
|
332
|
+
}
|
|
333
|
+
s_horizontalAlignmentMap = { # noqa: N816
|
|
334
|
+
-1: ux.TEXT_ALIGN.LEFT,
|
|
335
|
+
0: ux.TEXT_ALIGN.CENTER,
|
|
336
|
+
1: ux.TEXT_ALIGN.RIGHT,
|
|
337
|
+
}
|
|
338
|
+
def ux_text_alignment(align_value: int, horizontal = True) -> int:
|
|
339
|
+
if horizontal:
|
|
340
|
+
return s_horizontalAlignmentMap.get(align_value, ux.TEXT_ALIGN.RIGHT) | ux.TEXT_ALIGN.V_CENTER
|
|
341
|
+
|
|
342
|
+
raise NotImplementedError
|
|
343
|
+
|
|
344
|
+
def ux_make_scrollable(w: ux.Widget, h = ux.SCROLL.AS_NEEDED, v = ux.SCROLL.AS_NEEDED) -> ux.Widget:
|
|
345
|
+
if h == ux.SCROLL.OFF and v == ux.SCROLL.OFF:
|
|
346
|
+
return w
|
|
347
|
+
|
|
348
|
+
sa = ux.ScrollArea()
|
|
349
|
+
sa.set_widget(w)
|
|
350
|
+
sa.set_horizontal_scroll_bar_policy(h)
|
|
351
|
+
sa.set_vertical_scroll_bar_policy(v)
|
|
352
|
+
return sa
|
|
353
|
+
|
|
354
|
+
class UxSearchableList(ux.GroupBox):
|
|
355
|
+
__slots__ = ('case_sensitive', 'current_choices', 'hook', 'initial_choices', 'reset_selection', 'selection', 'sort', 'w_field', 'w_list')
|
|
356
|
+
def __init__(
|
|
357
|
+
self,
|
|
358
|
+
text_widget: ux.LineEdit = None,
|
|
359
|
+
reset_selection = True,
|
|
360
|
+
choices: list = None,
|
|
361
|
+
select_hook = None,
|
|
362
|
+
sort = False,
|
|
363
|
+
case_sensitive = False,
|
|
364
|
+
title = ''
|
|
365
|
+
):
|
|
366
|
+
"""
|
|
367
|
+
:param text_widget: if an external text_widget given, it is used, otherwise its own is created on top of the list
|
|
368
|
+
:param reset_selection: if True, text_widget will be reset on new selection
|
|
369
|
+
:param choices: all choices
|
|
370
|
+
:param select_hook: if given, will be called after a new selection is made: select_hook( selected_choice: str )
|
|
371
|
+
:param sort: sort choices if True
|
|
372
|
+
:param case_sensitive: whether to search with case sensitivity
|
|
373
|
+
:param title: an optional title
|
|
374
|
+
"""
|
|
375
|
+
super().__init__()
|
|
376
|
+
self.sort = sort
|
|
377
|
+
self.initial_choices = choices if not sort else sorted(choices)
|
|
378
|
+
self.current_choices = self.initial_choices
|
|
379
|
+
self.hook = select_hook
|
|
380
|
+
self.reset_selection = reset_selection
|
|
381
|
+
self.case_sensitive = case_sensitive
|
|
382
|
+
self.selection = None
|
|
383
|
+
|
|
384
|
+
if title:
|
|
385
|
+
self.set_title(title)
|
|
386
|
+
lay = ux.VBoxLayout()
|
|
387
|
+
|
|
388
|
+
if text_widget:
|
|
389
|
+
self.w_field = text_widget
|
|
390
|
+
else:
|
|
391
|
+
self.w_field = ux.LineEdit()
|
|
392
|
+
lay.add_widget(self.w_field)
|
|
393
|
+
self.w_field.text_edited_connect(self.process_input)
|
|
394
|
+
|
|
395
|
+
self.w_list = ux.ListWidget()
|
|
396
|
+
|
|
397
|
+
self.w_list.add_items(self.initial_choices)
|
|
398
|
+
self.w_list.clicked_connect(self.item_selected)
|
|
399
|
+
lay.add_widget(self.w_list)
|
|
400
|
+
|
|
401
|
+
self.set_layout(lay)
|
|
402
|
+
|
|
403
|
+
def add_choice(self, choice: str):
|
|
404
|
+
if choice not in self.initial_choices:
|
|
405
|
+
self.initial_choices.append(choice)
|
|
406
|
+
if self.sort:
|
|
407
|
+
self.initial_choices = sorted(self.initial_choices)
|
|
408
|
+
|
|
409
|
+
self.w_list.clear()
|
|
410
|
+
self.w_list.add_items(self.initial_choices)
|
|
411
|
+
|
|
412
|
+
def remove_choice(self, choice: str):
|
|
413
|
+
if choice in self.initial_choices:
|
|
414
|
+
self.initial_choices.remove(choice)
|
|
415
|
+
self.w_list.clear()
|
|
416
|
+
self.w_list.add_items(self.initial_choices)
|
|
417
|
+
|
|
418
|
+
def process_input(self, input):
|
|
419
|
+
self.w_list.clear()
|
|
420
|
+
if not input:
|
|
421
|
+
self.current_choices = self.initial_choices
|
|
422
|
+
else:
|
|
423
|
+
if not self.current_choices:
|
|
424
|
+
self.current_choices = self.initial_choices
|
|
425
|
+
|
|
426
|
+
if not self.case_sensitive:
|
|
427
|
+
input = input.lower()
|
|
428
|
+
self.current_choices = [s for s in self.current_choices if not s.lower().find(input) == -1]
|
|
429
|
+
else:
|
|
430
|
+
self.current_choices = [s for s in self.current_choices if not s.find(input) == -1]
|
|
431
|
+
|
|
432
|
+
self.w_list.add_items(self.current_choices)
|
|
433
|
+
|
|
434
|
+
def item_selected(self, item: ux.ListItem):
|
|
435
|
+
i = item.row()
|
|
436
|
+
text = self.current_choices[i]
|
|
437
|
+
self.reset()
|
|
438
|
+
|
|
439
|
+
items = self.w_list.find_items(text, ux.MatchExactly)
|
|
440
|
+
if len(items):
|
|
441
|
+
items[0].set_selected(True)
|
|
442
|
+
text_s = '' if self.reset_selection else text
|
|
443
|
+
self.w_field.set_text(text_s)
|
|
444
|
+
|
|
445
|
+
self.selection = text
|
|
446
|
+
if self.hook:
|
|
447
|
+
self.hook(text)
|
|
448
|
+
|
|
449
|
+
def choice(self) -> str:
|
|
450
|
+
return self.selection
|
|
451
|
+
|
|
452
|
+
def reset(self):
|
|
453
|
+
if not self.current_choices == self.initial_choices:
|
|
454
|
+
self.current_choices = self.initial_choices
|
|
455
|
+
self.w_list.clear()
|
|
456
|
+
self.w_list.add_items( self.initial_choices)
|
|
457
|
+
|
|
458
|
+
class UxTreeViewer(ux.TreeWidget):
|
|
459
|
+
s_label_max_length = 40
|
|
460
|
+
|
|
461
|
+
def __init__(self, dir: Directory, select_hook = None, label_max_length = -1, expand = False, **kwargs):
|
|
462
|
+
super().__init__()
|
|
463
|
+
if label_max_length < 0:
|
|
464
|
+
label_max_length = self.s_label_max_length
|
|
465
|
+
|
|
466
|
+
self.dir = dir
|
|
467
|
+
self.select_hook = select_hook
|
|
468
|
+
|
|
469
|
+
dir_value = dir.value
|
|
470
|
+
dir_name = dir.name
|
|
471
|
+
if dir_value and dir_name and dir_name != dir_value:
|
|
472
|
+
num_cols = 2
|
|
473
|
+
header_labels = [dir_name, 'Description']
|
|
474
|
+
else:
|
|
475
|
+
num_cols = 1
|
|
476
|
+
header_labels = [dir.show_value()]
|
|
477
|
+
|
|
478
|
+
self.num_cols = num_cols
|
|
479
|
+
self.set_column_count(num_cols)
|
|
480
|
+
self.set_header_labels(header_labels)
|
|
481
|
+
self.item_clicked_connect(self.tree_item_clicked)
|
|
482
|
+
|
|
483
|
+
for subdir in dir.members.values(): #-- without the root!
|
|
484
|
+
self.create_tree(subdir, self, label_max_length, num_cols)
|
|
485
|
+
|
|
486
|
+
if expand:
|
|
487
|
+
for i in range(self.top_level_item_count()):
|
|
488
|
+
top = self.top_level_item(i)
|
|
489
|
+
top.set_expanded(True)
|
|
490
|
+
|
|
491
|
+
self.resize_column_to_contents(0)
|
|
492
|
+
if num_cols == 2:
|
|
493
|
+
self.resize_column_to_contents(1)
|
|
494
|
+
self.set_size_policy(ux.SizePolicy.MINIMUM_EXPANDING, ux.SizePolicy.MINIMUM_EXPANDING)
|
|
495
|
+
|
|
496
|
+
class TreeItem(ux.TreeItem):
|
|
497
|
+
def __init__(self, parent_node, dir: Directory):
|
|
498
|
+
super().__init__(parent_node)
|
|
499
|
+
self.dir = dir
|
|
500
|
+
|
|
501
|
+
@classmethod
|
|
502
|
+
def create_tree(cls, dir: Directory, parent_node, label_max_length: int, num_cols: int):
|
|
503
|
+
node = cls.TreeItem(parent_node, dir)
|
|
504
|
+
show_value = dir.show_value()
|
|
505
|
+
node.set_text(0, show_value)
|
|
506
|
+
if num_cols == 2:
|
|
507
|
+
label = dir.name
|
|
508
|
+
if label and label != show_value:
|
|
509
|
+
if len(label) > label_max_length:
|
|
510
|
+
label = label[ :label_max_length ] + '...'
|
|
511
|
+
|
|
512
|
+
node.set_text(1, label)
|
|
513
|
+
|
|
514
|
+
node.set_tool_tip(num_cols-1, dir.name)
|
|
515
|
+
|
|
516
|
+
for subdir in dir.members.values():
|
|
517
|
+
cls.create_tree(subdir, node, label_max_length, num_cols)
|
|
518
|
+
|
|
519
|
+
def tree_item_clicked(self, item: TreeItem):
|
|
520
|
+
self.on_tag_selected(item.dir)
|
|
521
|
+
|
|
522
|
+
def on_tag_selected(self, dir: Directory):
|
|
523
|
+
if self.select_hook:
|
|
524
|
+
self.select_hook(dir)
|
|
525
|
+
|
|
526
|
+
# class UxPixmap:
|
|
527
|
+
# COLOR_AUTO = Qt.ColorScheme.AutoColor
|
|
528
|
+
# COLOR_DITHER = Qt.ColorScheme.ColorOnly
|
|
529
|
+
# COLOR_MONO = Qt.ColorScheme.MonoOnly
|
|
530
|
+
# RATIO_IGNORE = Qt.ColorScheme.IgnoreAspectRatio
|
|
531
|
+
# RATIO_KEEP = Qt.ColorScheme.KeepAspectRatio
|
|
532
|
+
# RATIO_KEEP_EXP = Qt.ColorScheme.KeepAspectRatioByExpanding
|
|
533
|
+
#
|
|
534
|
+
# s_sourceTypeMap = {
|
|
535
|
+
# str: QPixmap.load,
|
|
536
|
+
# bytes: QPixmap.loadFromData,
|
|
537
|
+
# bytearray: QPixmap.loadFromData,
|
|
538
|
+
# }
|
|
539
|
+
# def __init__( self, source = None, image_format = '', color_flags = COLOR_AUTO ):
|
|
540
|
+
# self.m_pixmap = QPixmap()
|
|
541
|
+
# rc = self.load( source, image_format = image_format, color_flags = color_flags )
|
|
542
|
+
# if not rc:
|
|
543
|
+
# raise RuntimeError( rc.err() )
|
|
544
|
+
#
|
|
545
|
+
# def load( self, source, image_format = '', color_flags = COLOR_AUTO ) -> RC:
|
|
546
|
+
# if not source:
|
|
547
|
+
# return RC( True )
|
|
548
|
+
#
|
|
549
|
+
# method = self.s_sourceTypeMap.get( type( source ) )
|
|
550
|
+
# if not method:
|
|
551
|
+
# return RC( False, f'UxPixmap - unknown type of source = {type( source )}' )
|
|
552
|
+
#
|
|
553
|
+
# rc = method( self.m_pixmap, source, format = image_format, flags = color_flags )
|
|
554
|
+
# if not rc:
|
|
555
|
+
# return RC( False, f'UxPixmap - failed to initialize from source = {source}' )
|
|
556
|
+
#
|
|
557
|
+
# return RC( True )
|
|
558
|
+
#
|
|
559
|
+
# def _toData( self, array: bytearray, format = '', quality = -1 ) -> bool:
|
|
560
|
+
# buffer = QBuffer( array )
|
|
561
|
+
# buffer.open( QIODevice.WriteOnly )
|
|
562
|
+
# return self.m_pixmap.save( buffer, format = format, quality = quality )
|
|
563
|
+
#
|
|
564
|
+
# s_destinationTypeMap = {
|
|
565
|
+
# str: QPixmap.save,
|
|
566
|
+
# bytearray: _toData,
|
|
567
|
+
# }
|
|
568
|
+
# def save( self, destination, color_format = '', quality = -1 ) -> RC:
|
|
569
|
+
# method = self.s_destinationTypeMap.get( type( destination ) )
|
|
570
|
+
# if not method:
|
|
571
|
+
# return RC( False, f'UxPixmap - unknown destination type = {type( destination )}' )
|
|
572
|
+
#
|
|
573
|
+
# rc = method( self, destination, format = color_format, quality = quality )
|
|
574
|
+
# if not rc:
|
|
575
|
+
# return RC( False, f'UxPixmap - failed to save to destination = {destination}' )
|
|
576
|
+
#
|
|
577
|
+
# return RC( True )
|
|
578
|
+
#
|
|
579
|
+
# def render( self, label: QLabel, w = 0, h = 0, scaled = RATIO_IGNORE, smooth = True ):
|
|
580
|
+
# transform_mode = Qt.SmoothTransformation if smooth else Qt.FastTransformation
|
|
581
|
+
# pixmap = self.m_pixmap
|
|
582
|
+
# if w > 0 and h > 0:
|
|
583
|
+
# pixmap = pixmap.scaled( w, h, aspectRatioMode = scaled, transformMode = transform_mode )
|
|
584
|
+
# label.setPixmap( pixmap )
|
|
585
|
+
|
|
586
|
+
@singleton
|
|
587
|
+
class UxClipBoard:
|
|
588
|
+
def __init__(self):
|
|
589
|
+
self.dir = {}
|
|
590
|
+
self.entity = None
|
|
591
|
+
self.trait_name = ''
|
|
592
|
+
|
|
593
|
+
# def copy(self, tag: str, value):
|
|
594
|
+
# self.dir[tag] = value
|
|
595
|
+
#
|
|
596
|
+
# def paste(self, tag: str):
|
|
597
|
+
# return self.dir.get(tag)
|
|
598
|
+
#
|
|
599
|
+
# def clear(self):
|
|
600
|
+
# self.dir = {}
|
|
601
|
+
#
|
|
602
|
+
# def default_tag( self ) -> str:
|
|
603
|
+
# assert self.entity and self.trait_name, 'entity and trait name are not defined'
|
|
604
|
+
#
|
|
605
|
+
# cls = self.entity.__class__
|
|
606
|
+
# trait = cls.trait(self.trait_name)
|
|
607
|
+
# assert trait, f"{cls}: unknown trait '{self.trait_name}'"
|
|
608
|
+
# #tag_label = trait.label if trait.label else self.trait_name
|
|
609
|
+
# tag_label = self.trait_name
|
|
610
|
+
# return f"{self.entity}: '{tag_label}'"
|
|
611
|
+
#
|
|
612
|
+
# def layout(self) -> QWidget:
|
|
613
|
+
# w = QWidget()
|
|
614
|
+
# lay = QVBoxLayout()
|
|
615
|
+
# w.setLayout(lay)
|
|
616
|
+
#
|
|
617
|
+
# add_row = QHBoxLayout()
|
|
618
|
+
# add_b = uxPushButton('Add', callback = self.onCopy)
|
|
619
|
+
# self.m_addNameWidget = add_name = QLineEdit()
|
|
620
|
+
# add_name.setText(self.default_tag())
|
|
621
|
+
# add_row.addWidget(add_b)
|
|
622
|
+
# add_row.addWidget(add_name)
|
|
623
|
+
# lay.addLayout(add_row)
|
|
624
|
+
#
|
|
625
|
+
# lay.addWidget(uxSeparator())
|
|
626
|
+
#
|
|
627
|
+
# self.m_sl = sl = UxSearchableList(choices = list( self.dir.keys() ), select_hook = self.onPaste, title = 'Use Data')
|
|
628
|
+
# lay.addWidget(sl)
|
|
629
|
+
#
|
|
630
|
+
# return w
|
|
631
|
+
#
|
|
632
|
+
# def popup(self, parent_widget: QWidget, entity, trait_name: str):
|
|
633
|
+
# self.m_parent = parent_widget
|
|
634
|
+
# self.entity = entity
|
|
635
|
+
# self.trait_name = trait_name
|
|
636
|
+
# self.m_d = d = UxDialog(self.layout(), parent = parent_widget, cancel = '', title = 'Copy or Paste Data')
|
|
637
|
+
# d.exec()
|
|
638
|
+
#
|
|
639
|
+
# def onCopy(self):
|
|
640
|
+
# name = self.m_addNameWidget.text()
|
|
641
|
+
# if name:
|
|
642
|
+
# value = self.entity.get_value( self.trait_name )
|
|
643
|
+
# if value is not None:
|
|
644
|
+
# self.copy( name, value )
|
|
645
|
+
#
|
|
646
|
+
# self.m_d.close()
|
|
647
|
+
#
|
|
648
|
+
# def onPaste( self, name: str ):
|
|
649
|
+
# value = self.paste( name )
|
|
650
|
+
# if value is not None:
|
|
651
|
+
# rc = self.m_entity.setValue( self.m_traitName, value )
|
|
652
|
+
# if not rc:
|
|
653
|
+
# uxWarning(
|
|
654
|
+
# f"Data '{name}' is not suitable for '{self.m_entity.trait( self.m_traitName ).m_label}'",
|
|
655
|
+
# parent = self.m_parent,
|
|
656
|
+
# title = 'Invalid Data'
|
|
657
|
+
# )
|
|
658
|
+
#
|
|
659
|
+
# self.m_d.close()
|
|
660
|
+
|
|
661
|
+
|