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.
Files changed (214) hide show
  1. core_10x/__init__.py +42 -0
  2. core_10x/backbone/__init__.py +0 -0
  3. core_10x/backbone/backbone_store.py +59 -0
  4. core_10x/backbone/backbone_traitable.py +30 -0
  5. core_10x/backbone/backbone_user.py +66 -0
  6. core_10x/backbone/bound_data_domain.py +49 -0
  7. core_10x/backbone/namespace.py +101 -0
  8. core_10x/backbone/vault.py +38 -0
  9. core_10x/code_samples/__init__.py +0 -0
  10. core_10x/code_samples/_package_manifest.py +3 -0
  11. core_10x/code_samples/directories.py +181 -0
  12. core_10x/code_samples/person.py +76 -0
  13. core_10x/concrete_traits.py +356 -0
  14. core_10x/conftest.py +12 -0
  15. core_10x/curve.py +321 -0
  16. core_10x/data_domain.py +48 -0
  17. core_10x/data_domain_binder.py +45 -0
  18. core_10x/directory.py +250 -0
  19. core_10x/entity.py +8 -0
  20. core_10x/entity_filter.py +5 -0
  21. core_10x/environment_variables.py +147 -0
  22. core_10x/exec_control.py +84 -0
  23. core_10x/experimental/__init__.py +0 -0
  24. core_10x/experimental/data_protocol_ex.py +34 -0
  25. core_10x/global_cache.py +121 -0
  26. core_10x/manual_tests/__init__.py +0 -0
  27. core_10x/manual_tests/calendar_test.py +35 -0
  28. core_10x/manual_tests/ctor_update_bug.py +58 -0
  29. core_10x/manual_tests/debug_graph_on.py +17 -0
  30. core_10x/manual_tests/debug_graphoff_inside_graph_on.py +28 -0
  31. core_10x/manual_tests/enum_bits_test.py +17 -0
  32. core_10x/manual_tests/env_vars_trivial_test.py +12 -0
  33. core_10x/manual_tests/existing_traitable.py +33 -0
  34. core_10x/manual_tests/k10x_test1.py +13 -0
  35. core_10x/manual_tests/named_constant_test.py +121 -0
  36. core_10x/manual_tests/nucleus_trivial_test.py +42 -0
  37. core_10x/manual_tests/polars_test.py +14 -0
  38. core_10x/manual_tests/py_class_test.py +4 -0
  39. core_10x/manual_tests/rc_test.py +42 -0
  40. core_10x/manual_tests/rdate_test.py +12 -0
  41. core_10x/manual_tests/reference_serialization_bug.py +19 -0
  42. core_10x/manual_tests/resource_trivial_test.py +10 -0
  43. core_10x/manual_tests/store_uri_test.py +6 -0
  44. core_10x/manual_tests/trait_definition_test.py +19 -0
  45. core_10x/manual_tests/trait_filter_test.py +15 -0
  46. core_10x/manual_tests/trait_flag_modification_test.py +42 -0
  47. core_10x/manual_tests/trait_modification_bug.py +26 -0
  48. core_10x/manual_tests/traitable_as_of_test.py +82 -0
  49. core_10x/manual_tests/traitable_heir_test.py +39 -0
  50. core_10x/manual_tests/traitable_history_test.py +41 -0
  51. core_10x/manual_tests/traitable_serialization_test.py +54 -0
  52. core_10x/manual_tests/traitable_trivial_test.py +71 -0
  53. core_10x/manual_tests/trivial_graph_test.py +16 -0
  54. core_10x/manual_tests/ts_class_association_test.py +64 -0
  55. core_10x/manual_tests/ts_trivial_test.py +35 -0
  56. core_10x/named_constant.py +425 -0
  57. core_10x/nucleus.py +81 -0
  58. core_10x/package_manifest.py +85 -0
  59. core_10x/package_refactoring.py +153 -0
  60. core_10x/py_class.py +431 -0
  61. core_10x/rc.py +155 -0
  62. core_10x/rdate.py +339 -0
  63. core_10x/resource.py +189 -0
  64. core_10x/roman_number.py +67 -0
  65. core_10x/testlib/__init__.py +0 -0
  66. core_10x/testlib/test_store.py +240 -0
  67. core_10x/testlib/traitable_history_tests.py +787 -0
  68. core_10x/testlib/ts_tests.py +280 -0
  69. core_10x/trait.py +377 -0
  70. core_10x/trait_definition.py +176 -0
  71. core_10x/trait_filter.py +205 -0
  72. core_10x/trait_method_error.py +36 -0
  73. core_10x/traitable.py +1082 -0
  74. core_10x/traitable_cli.py +153 -0
  75. core_10x/traitable_heir.py +33 -0
  76. core_10x/traitable_id.py +31 -0
  77. core_10x/ts_store.py +172 -0
  78. core_10x/ts_store_type.py +26 -0
  79. core_10x/ts_union.py +147 -0
  80. core_10x/ui_hint.py +153 -0
  81. core_10x/unit_tests/test_concrete_traits.py +156 -0
  82. core_10x/unit_tests/test_converters.py +51 -0
  83. core_10x/unit_tests/test_curve.py +157 -0
  84. core_10x/unit_tests/test_directory.py +54 -0
  85. core_10x/unit_tests/test_documentation.py +172 -0
  86. core_10x/unit_tests/test_environment_variables.py +15 -0
  87. core_10x/unit_tests/test_filters.py +239 -0
  88. core_10x/unit_tests/test_graph.py +348 -0
  89. core_10x/unit_tests/test_named_constant.py +98 -0
  90. core_10x/unit_tests/test_rc.py +11 -0
  91. core_10x/unit_tests/test_rdate.py +484 -0
  92. core_10x/unit_tests/test_trait_method_error.py +80 -0
  93. core_10x/unit_tests/test_trait_modification.py +19 -0
  94. core_10x/unit_tests/test_traitable.py +959 -0
  95. core_10x/unit_tests/test_traitable_history.py +1 -0
  96. core_10x/unit_tests/test_ts_store.py +1 -0
  97. core_10x/unit_tests/test_ts_union.py +369 -0
  98. core_10x/unit_tests/test_ui_nodes.py +81 -0
  99. core_10x/unit_tests/test_xxcalendar.py +471 -0
  100. core_10x/vault/__init__.py +0 -0
  101. core_10x/vault/sec_keys.py +133 -0
  102. core_10x/vault/security_keys_old.py +168 -0
  103. core_10x/vault/vault.py +56 -0
  104. core_10x/vault/vault_traitable.py +56 -0
  105. core_10x/vault/vault_user.py +70 -0
  106. core_10x/xdate_time.py +136 -0
  107. core_10x/xnone.py +71 -0
  108. core_10x/xxcalendar.py +228 -0
  109. infra_10x/__init__.py +0 -0
  110. infra_10x/manual_tests/__init__.py +0 -0
  111. infra_10x/manual_tests/test_misc.py +16 -0
  112. infra_10x/manual_tests/test_prepare_filter_and_pipeline.py +25 -0
  113. infra_10x/mongodb_admin.py +111 -0
  114. infra_10x/mongodb_store.py +346 -0
  115. infra_10x/mongodb_utils.py +129 -0
  116. infra_10x/unit_tests/conftest.py +13 -0
  117. infra_10x/unit_tests/test_mongo_db.py +36 -0
  118. infra_10x/unit_tests/test_mongo_history.py +1 -0
  119. py10x_universe-0.1.3.dist-info/METADATA +406 -0
  120. py10x_universe-0.1.3.dist-info/RECORD +214 -0
  121. py10x_universe-0.1.3.dist-info/WHEEL +4 -0
  122. py10x_universe-0.1.3.dist-info/licenses/LICENSE +21 -0
  123. ui_10x/__init__.py +0 -0
  124. ui_10x/apps/__init__.py +0 -0
  125. ui_10x/apps/collection_editor_app.py +100 -0
  126. ui_10x/choice.py +212 -0
  127. ui_10x/collection_editor.py +135 -0
  128. ui_10x/concrete_trait_widgets.py +220 -0
  129. ui_10x/conftest.py +8 -0
  130. ui_10x/entity_stocker.py +173 -0
  131. ui_10x/examples/__init__.py +0 -0
  132. ui_10x/examples/_guess_word_data.py +14076 -0
  133. ui_10x/examples/collection_editor.py +17 -0
  134. ui_10x/examples/date_selector.py +14 -0
  135. ui_10x/examples/entity_stocker.py +18 -0
  136. ui_10x/examples/guess_word.py +392 -0
  137. ui_10x/examples/message_box.py +20 -0
  138. ui_10x/examples/multi_choice.py +17 -0
  139. ui_10x/examples/py_data_browser.py +66 -0
  140. ui_10x/examples/radiobox.py +29 -0
  141. ui_10x/examples/single_choice.py +31 -0
  142. ui_10x/examples/style_sheet.py +47 -0
  143. ui_10x/examples/trivial_entity_editor.py +18 -0
  144. ui_10x/platform.py +20 -0
  145. ui_10x/platform_interface.py +517 -0
  146. ui_10x/py_data_browser.py +249 -0
  147. ui_10x/qt6/__init__.py +0 -0
  148. ui_10x/qt6/conftest.py +8 -0
  149. ui_10x/qt6/manual_tests/__init__.py +0 -0
  150. ui_10x/qt6/manual_tests/basic_test.py +35 -0
  151. ui_10x/qt6/platform_implementation.py +275 -0
  152. ui_10x/qt6/utils.py +665 -0
  153. ui_10x/rio/__init__.py +0 -0
  154. ui_10x/rio/apps/examples/examples/__init__.py +22 -0
  155. ui_10x/rio/apps/examples/examples/components/__init__.py +3 -0
  156. ui_10x/rio/apps/examples/examples/components/collection_editor.py +15 -0
  157. ui_10x/rio/apps/examples/examples/pages/collection_editor.py +21 -0
  158. ui_10x/rio/apps/examples/examples/pages/login_page.py +88 -0
  159. ui_10x/rio/apps/examples/examples/pages/style_sheet.py +21 -0
  160. ui_10x/rio/apps/examples/rio.toml +14 -0
  161. ui_10x/rio/component_builder.py +497 -0
  162. ui_10x/rio/components/__init__.py +9 -0
  163. ui_10x/rio/components/group_box.py +31 -0
  164. ui_10x/rio/components/labeled_checkbox.py +18 -0
  165. ui_10x/rio/components/line_edit.py +37 -0
  166. ui_10x/rio/components/radio_button.py +32 -0
  167. ui_10x/rio/components/separator.py +24 -0
  168. ui_10x/rio/components/splitter.py +121 -0
  169. ui_10x/rio/components/tree_view.py +75 -0
  170. ui_10x/rio/conftest.py +35 -0
  171. ui_10x/rio/internals/__init__.py +0 -0
  172. ui_10x/rio/internals/app.py +192 -0
  173. ui_10x/rio/manual_tests/__init__.py +0 -0
  174. ui_10x/rio/manual_tests/basic_test.py +24 -0
  175. ui_10x/rio/manual_tests/splitter.py +27 -0
  176. ui_10x/rio/platform_implementation.py +91 -0
  177. ui_10x/rio/style_sheet.py +53 -0
  178. ui_10x/rio/unit_tests/test_collection_editor.py +68 -0
  179. ui_10x/rio/unit_tests/test_internals.py +630 -0
  180. ui_10x/rio/unit_tests/test_style_sheet.py +37 -0
  181. ui_10x/rio/widgets/__init__.py +46 -0
  182. ui_10x/rio/widgets/application.py +109 -0
  183. ui_10x/rio/widgets/button.py +48 -0
  184. ui_10x/rio/widgets/button_group.py +60 -0
  185. ui_10x/rio/widgets/calendar.py +23 -0
  186. ui_10x/rio/widgets/checkbox.py +24 -0
  187. ui_10x/rio/widgets/dialog.py +137 -0
  188. ui_10x/rio/widgets/group_box.py +27 -0
  189. ui_10x/rio/widgets/layout.py +34 -0
  190. ui_10x/rio/widgets/line_edit.py +37 -0
  191. ui_10x/rio/widgets/list.py +105 -0
  192. ui_10x/rio/widgets/message_box.py +70 -0
  193. ui_10x/rio/widgets/scroll_area.py +31 -0
  194. ui_10x/rio/widgets/spacer.py +6 -0
  195. ui_10x/rio/widgets/splitter.py +45 -0
  196. ui_10x/rio/widgets/text_edit.py +28 -0
  197. ui_10x/rio/widgets/tree.py +89 -0
  198. ui_10x/rio/widgets/unit_tests/test_button.py +101 -0
  199. ui_10x/rio/widgets/unit_tests/test_button_group.py +33 -0
  200. ui_10x/rio/widgets/unit_tests/test_calendar.py +114 -0
  201. ui_10x/rio/widgets/unit_tests/test_checkbox.py +109 -0
  202. ui_10x/rio/widgets/unit_tests/test_group_box.py +158 -0
  203. ui_10x/rio/widgets/unit_tests/test_label.py +43 -0
  204. ui_10x/rio/widgets/unit_tests/test_line_edit.py +140 -0
  205. ui_10x/rio/widgets/unit_tests/test_list.py +146 -0
  206. ui_10x/table_header_view.py +305 -0
  207. ui_10x/table_view.py +174 -0
  208. ui_10x/trait_editor.py +189 -0
  209. ui_10x/trait_widget.py +131 -0
  210. ui_10x/traitable_editor.py +200 -0
  211. ui_10x/traitable_view.py +131 -0
  212. ui_10x/unit_tests/conftest.py +8 -0
  213. ui_10x/unit_tests/test_platform.py +9 -0
  214. 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
+