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
@@ -0,0 +1,305 @@
1
+ from PyQt5.QtCore import QModelIndex, QPointF, QRect, QSize, Qt, QVariant
2
+ from PyQt5.QtGui import QFont, QFontMetrics, QPainter, QPaintEvent
3
+ from PyQt5.QtWidgets import QStyle, QStyleOptionHeader
4
+
5
+ from ui_10x.platform import ux
6
+ from ui_10x.traitable_view import TraitableView
7
+
8
+
9
+ # ruff: noqa: N802 # Function name should be lowercase
10
+ class HeaderModel(ux.StandardItemModel):
11
+ def _create_subtree(self, traits: dict, root: ux.StandardItem, tree: dict):
12
+ col = 0
13
+ for subtree_name, subtree in tree.items():
14
+ if isinstance(subtree, str):
15
+ trait = traits.get(subtree_name)
16
+ if not trait:
17
+ continue
18
+ label = subtree
19
+ subtree = None
20
+ elif isinstance(subtree, dict):
21
+ label = subtree_name
22
+ else:
23
+ continue
24
+
25
+ item = ux.StandardItem(label)
26
+ if subtree:
27
+ self._create_subtree(traits, item, subtree)
28
+
29
+ if root:
30
+ root.append_column((item, ))
31
+ else:
32
+ self.set_item(0, col, item)
33
+
34
+ col += 1
35
+
36
+ def __init__(self, traits: dict, header_structure: dict):
37
+ super().__init__()
38
+ self.traits = traits
39
+ self.create(header_structure)
40
+
41
+ def create(self, header_structure: dict):
42
+ self._create_subtree(self.traits, None, header_structure)
43
+
44
+ def leaf_index(self, section_index: int) -> ux.ModelIndex:
45
+ current_leaf_index = -1
46
+ for i in range(self.column_count()):
47
+ res, current_leaf_index = self.find_leaf(self.index( 0, i), section_index, current_leaf_index)
48
+ if res.is_valid():
49
+ return res
50
+
51
+ return ux.ModelIndex()
52
+
53
+ @staticmethod
54
+ def leftmost_leaf(index: ux.ModelIndex) -> ux.ModelIndex:
55
+ prev_index = index
56
+ while index.is_valid():
57
+ prev_index = index
58
+ index = index.child(0, 0)
59
+ return prev_index
60
+
61
+ @staticmethod
62
+ def find_leaf(current_index: ux.ModelIndex, section_index: int, current_leaf_index: int) -> tuple: #-- ( index, current_leaf_index )
63
+ if current_index.is_valid():
64
+ child_count = current_index.model().column_count(current_index)
65
+ if child_count:
66
+ for i in range(child_count):
67
+ res, current_leaf_index = HeaderModel.find_leaf(current_index.child(0, i), section_index, current_leaf_index)
68
+ if res.is_valid():
69
+ return (res, current_leaf_index)
70
+ else:
71
+ current_leaf_index += 1
72
+ if current_leaf_index == section_index:
73
+ return (current_index, current_leaf_index)
74
+
75
+ return (ux.ModelIndex(), current_leaf_index)
76
+
77
+ @staticmethod
78
+ def search_leaves(current_index: ux.ModelIndex) -> list:
79
+ res = []
80
+ if current_index.is_valid():
81
+ if not current_index.child(0, 0).is_valid():
82
+ res.append(current_index)
83
+ else:
84
+ c = 0
85
+ child = current_index.child(0, c)
86
+ while child.is_valid():
87
+ found = HeaderModel.search_leaves(child)
88
+ res.extend(found)
89
+ c += 1
90
+ child = current_index.child(0, c)
91
+
92
+ return res
93
+
94
+ @staticmethod
95
+ def leaves(search_index: ux.ModelIndex ) -> list:
96
+ leaves = []
97
+ if search_index.is_valid():
98
+ child_count = search_index.model().column_count(search_index)
99
+ for i in range(child_count):
100
+ found = HeaderModel.search_leaves(search_index.child(0, i))
101
+ leaves.extend(found)
102
+
103
+ return leaves
104
+
105
+ @staticmethod
106
+ def find_root_index(index: ux.ModelIndex) -> ux.ModelIndex:
107
+ while index.parent().is_valid():
108
+ index = index.parent()
109
+ return index
110
+
111
+ @staticmethod
112
+ def parent_indexes(index: ux.ModelIndex) -> list:
113
+ indexes = []
114
+ while index.is_valid():
115
+ indexes.insert(0, index)
116
+ index = index.parent()
117
+ return indexes
118
+
119
+
120
+ class HeaderView(ux.HeaderView):
121
+ def __init__(self, view: TraitableView, parent: ux.Widget = None):
122
+ super().__init__(ux.Horizontal, parent = parent)
123
+ self.model = HeaderModel(view.ui_hints, view.header)
124
+ self.set_model(self.model)
125
+
126
+ self.painted_cells = set()
127
+ self.section_resized_connect(self.on_section_resized)
128
+
129
+ def set_foreground_brush(self, opt: ux.StyleOptionHeader, index: ux.ModelIndex ):
130
+ fgb = index.data(ux.ForegroundRole)
131
+ if fgb:
132
+ opt.palette.set_brush(ux.Palette.ButtonText, fgb)
133
+
134
+ def set_background_brush(self, opt: ux.StyleOptionHeader, index: ux.ModelIndex ):
135
+ bgb = index.data(ux.BackgroundRole)
136
+ if bgb:
137
+ opt.palette.set_brush(ux.Palette.Button, bgb)
138
+ opt.palette.set_brush(ux.Palette.Window, bgb)
139
+
140
+
141
+ # s_selected_position = (
142
+ # ux.StyleOptionHeader.NotAdjacent, # prev - 0, next 0
143
+ # ux.StyleOptionHeader.NextIsSelected, # prev - 0, next 1
144
+ # ux.StyleOptionHeader.PreviousIsSelected, # prev - 1, next 0
145
+ # ux.StyleOptionHeader.NextAndPreviousAreSelected # prev - 1, next 1
146
+ # )
147
+ def style_option_for_cell(self, logical_index: int) -> ux.StyleOptionHeader:
148
+ opt = ux.StyleOptionHeader()
149
+ # self.init_style_option(opt)
150
+ # if self.window().is_active_window():
151
+ # opt.state |= ux.Style.State_Active
152
+ # opt.text_alignment = ux.AlignCenter
153
+ # opt.icon_alignment = ux.AlignVCenter
154
+ # opt.section = logical_index
155
+ #
156
+ # visual = self.visual_index(logical_index)
157
+ #
158
+ # if self.count() == 1:
159
+ # opt.position = ux.StyleOptionHeader.OnlyOneSection
160
+ # else:
161
+ # if visual == 0:
162
+ # opt.position = QStyleOptionHeader.Beginning
163
+ # else:
164
+ # opt.position = QStyleOptionHeader.End if visual == ( self.count() - 1 ) else QStyleOptionHeader.Middle
165
+ #
166
+ # if self.sectionsClickable():
167
+ # if self.highlightSections() and self.selectionMode():
168
+ # if self.selectionModel().columnIntersectsSelection( logical_index, self.rootIndex() ):
169
+ # opt.state |= QStyle.State_On
170
+ # if self.selectionModel().isColumnSelected( logical_index, self.rootIndex() ):
171
+ # opt.state |= QStyle.State_Sunken
172
+ #
173
+ # if self.selectionModel():
174
+ # prev_selected = self.selectionModel().isColumnSelected( self.logicalIndex( visual - 1 ), self.rootIndex() )
175
+ # next_selected = self.selectionModel().isColumnSelected( self.logicalIndex( visual + 1 ), self.rootIndex() )
176
+ # code = 2 * prev_selected + next_selected
177
+ # opt.selectedPosition = self.s_selectedPosition[ code ]
178
+
179
+ return opt
180
+
181
+ def cell_size(self, leaf_index: ux.ModelIndex, style_options: ux.StyleOptionHeader) -> ux.Size:
182
+ res = ux.Size()
183
+ variant = QVariant(leaf_index.data(Qt.SizeHintRole))
184
+ if variant.isValid():
185
+ res = variant # TODO: cast?
186
+ fnt = QFont(self.font())
187
+ var = QVariant(leaf_index.data(Qt.FontRole))
188
+ if var.isValid():
189
+ fnt = var
190
+ fnt.setBold(True)
191
+ fm = QFontMetrics(fnt)
192
+ size = QSize(fm.size(0, leaf_index.data(Qt.DisplayRole)))
193
+ if leaf_index.data(Qt.UserRole): # isValid()
194
+ size.transpose()
195
+ decoration_size = self.style().sizeFromContents(QStyle.CT_HeaderSection, style_options, QSize(), self)
196
+ empty_text_size = QSize(fm.size( 0, '' ))
197
+ return res.expandedTo(size + decoration_size - empty_text_size)
198
+
199
+ def current_cell_width(self, searched_index: QModelIndex, leaf_index: QModelIndex, section_index: int) -> int:
200
+ leaves = HeaderModel.leaves(searched_index)
201
+ if not leaves:
202
+ return self.section_size(section_index)
203
+
204
+ first_leaf_section_index = section_index - leaves.index(leaf_index)
205
+ return sum(self.section_size(first_leaf_section_index + i) for i in range(len(leaves)))
206
+
207
+ def current_cell_left(self, searched_index: QModelIndex, leaf_index: QModelIndex, section_index: int, left: int) -> int:
208
+ leaves = HeaderModel.leaves(searched_index)
209
+ if leaves:
210
+ n = leaves.index(leaf_index)
211
+ first_leaf_section_index = section_index - n
212
+ n -= 1
213
+ left -= sum(self.section_size(first_leaf_section_index + i) for i in range(n))
214
+ return left
215
+
216
+ def paint_horizontal_cell(self, painter: QPainter, cell_index: QModelIndex, leaf_index: QModelIndex, logical_index: int, style_options: QStyleOptionHeader, section_rect: QRect, top: int) -> int:
217
+ uniopt = QStyleOptionHeader(style_options)
218
+ self.set_foreground_brush(uniopt, cell_index)
219
+ self.set_background_brush(uniopt, cell_index) # TODO!
220
+
221
+ height = self.cell_size(cell_index, uniopt).height()
222
+
223
+ if cell_index == leaf_index:
224
+ height = section_rect.height() - top
225
+
226
+ left = self.current_cell_left(cell_index, leaf_index, logical_index, section_rect.left())
227
+ width = self.current_cell_width(cell_index, leaf_index, logical_index)
228
+
229
+ if cell_index not in self.painted_cells:
230
+ self.painted_cells.add(cell_index)
231
+ r = QRect(left, top, width, height)
232
+
233
+ uniopt.text = cell_index.data(Qt.DisplayRole)
234
+ painter.save()
235
+ uniopt.rect = r
236
+ self.style().drawControl(QStyle.CE_Header, uniopt, painter, self)
237
+
238
+ painter.restore()
239
+
240
+ return top + height
241
+
242
+ def paintEvent(self, event: QPaintEvent):
243
+ self.painted_cells = set()
244
+ super().paintEvent(event)
245
+
246
+ def paintSection(self, painter: QPainter, rect: QRect, logical_index: int):
247
+ if rect.isValid():
248
+ leaf_index = self.model.leaf_index(logical_index)
249
+ if leaf_index.is_valid():
250
+ old_bo = QPointF(painter.brushOrigin())
251
+ top = rect.y()
252
+ indexes = HeaderModel.parent_indexes(leaf_index)
253
+ style_options = self.style_option_for_cell(logical_index)
254
+ for i in range(len(indexes)):
255
+ real_style_options = QStyleOptionHeader(style_options)
256
+ state = int(real_style_options.state)
257
+ if i < (len(indexes) - 1) and (bool(state & QStyle.State_Sunken) or bool(state & QStyle.State_On)):
258
+ real_style_options.state &= ~(QStyle.State_Sunken | QStyle.State_On)
259
+
260
+ cell = indexes[i]
261
+ top = self.paint_horizontal_cell(painter, cell, leaf_index, logical_index, real_style_options, rect, top)
262
+
263
+ painter.setBrushOrigin(old_bo)
264
+ return
265
+
266
+ super().paintSection(painter, rect, logical_index)
267
+
268
+ def sectionSizeFromContents(self, logical_index: int ) -> QSize:
269
+ cur_leaf_index = QModelIndex(self.model.leaf_index(logical_index))
270
+ if not cur_leaf_index.is_valid():
271
+ return super().sectionSizeFromContents(logical_index)
272
+
273
+ style_option = QStyleOptionHeader(self.style_option_for_cell(logical_index ))
274
+ s = QSize(self.cell_size(cur_leaf_index, style_option))
275
+ cur_leaf_index = cur_leaf_index.parent()
276
+ while cur_leaf_index.is_valid():
277
+ s.setHeight(s.height() + self.cell_size(cur_leaf_index, style_option).height())
278
+ cur_leaf_index = cur_leaf_index.parent()
279
+
280
+ return s
281
+
282
+ def on_section_resized(self, logical_index: int, i2, i3 ):
283
+ if self.is_section_hidden(logical_index):
284
+ return
285
+
286
+ leaf_index = QModelIndex(self.model.leaf_index(logical_index))
287
+ if leaf_index.is_valid():
288
+ root_index = HeaderModel.find_root_index(leaf_index)
289
+ leaves = HeaderModel.leaves(root_index)
290
+ try:
291
+ n = leaves.index(leaf_index)
292
+ while n > 0:
293
+ logical_index -= 1
294
+
295
+ w = self.viewport().width()
296
+ h = self.viewport().height()
297
+ pos = self.sectionViewportPosition(logical_index)
298
+ r = QRect(pos, 0, w - pos, h)
299
+ if self.isRightToLeft():
300
+ r.setRect(0, 0, pos + self.sectionSize(logical_index), h)
301
+
302
+ self.viewport().update( r.normalized() )
303
+ n -= 1
304
+ except Exception:
305
+ pass
ui_10x/table_view.py ADDED
@@ -0,0 +1,174 @@
1
+ import inspect
2
+
3
+ from core_10x.traitable import Trait, Traitable
4
+ from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt, QVariant
5
+ from PyQt5.QtWidgets import QAbstractScrollArea, QTableView
6
+
7
+ from ui_10x.table_header_view import HeaderModel, HeaderView
8
+ from ui_10x.traitable_editor import TraitableEditor
9
+ from ui_10x.traitable_view import TraitableView
10
+ from ui_10x.utils import UxStyleSheet, ux_text_alignment
11
+
12
+
13
+ # ruff: noqa: N802 # Function name should be lowercase
14
+ class Model(QAbstractTableModel):
15
+ def __init__(self, model: HeaderModel, data, parent = None):
16
+ super().__init__(parent = parent)
17
+ self.model = model
18
+ self.data = data
19
+ self.col2name = { i: name for i, name in enumerate( model.traits ) }
20
+ self.name2col = { name: i for i, name in self.col2name.items() }
21
+
22
+ def trait_name(self, col: int) -> str:
23
+ return self.col2name.get(col)
24
+
25
+ def rowCount(self, parent = None, *args, **kwargs):
26
+ return len(self.data)
27
+
28
+ def columnCount( self, parent = None, *args, **kwargs ):
29
+ if not self.data:
30
+ return 0
31
+ return len(self.col2name)
32
+
33
+ def data(self, index: QModelIndex, role: int = None):
34
+ if role == Qt.UserRole:
35
+ return self.model
36
+
37
+ entity: Traitable = self.data[index.row()]
38
+ trait_name = self.col2name.get(index.column())
39
+
40
+ cls = entity.__class__
41
+ trait: Trait = cls.trait(trait_name)
42
+ if role == Qt.DisplayRole and index.isValid():
43
+ if trait:
44
+ value = entity.get_value(trait)
45
+ text = trait.to_str(value)
46
+ else:
47
+ text = ''
48
+ return text
49
+
50
+ if role == Qt.ForegroundRole:
51
+ sh = entity.get_style_sheet(trait)
52
+ return UxStyleSheet.fg_color(sh)
53
+
54
+ if role == Qt.BackgroundRole:
55
+ sh = entity.get_style_sheet(trait)
56
+ return UxStyleSheet.bg_color(sh)
57
+
58
+ if role == Qt.TextAlignmentRole:
59
+ return ux_text_alignment(trait.ui_hint.param('align_h', 0), horizontal = True)
60
+
61
+ return QVariant()
62
+
63
+ def set_data(self, i: QModelIndex, dummy, role = None) -> bool:
64
+ self.dataChanged.emit(i, i)
65
+ return True
66
+
67
+ def change_data(self, row: int, trait_name: str):
68
+ col = self.name2col.get(trait_name)
69
+ i = QModelIndex(row, col)
70
+ self.set_data(i, None, role = None)
71
+
72
+ def insertRows(self, row: int, num_rows: int, parent = None, *args, **kwargs) -> bool:
73
+ #self.beginInsertRows( QModelIndex(), row, row + num_rows )
74
+ return True
75
+
76
+ def extendData(self, data) -> bool:
77
+ """
78
+ :param data: an iterable of entities
79
+ :return: True if model reset happened as well
80
+ """
81
+ if data:
82
+ the_data = self.data
83
+ is_empty = not bool(the_data)
84
+ if is_empty:
85
+ self.beginResetModel()
86
+
87
+ first = len(the_data)
88
+ last = first + len(data) - 1
89
+ self.beginInsertRows(QModelIndex(), first, last)
90
+ the_data.extend(data)
91
+ self.insertRows(first, len( data))
92
+ self.endInsertRows()
93
+
94
+ if is_empty:
95
+ self.endResetModel()
96
+ return is_empty
97
+
98
+ def entity(self, row: int) -> Traitable:
99
+ return self.data[row]
100
+
101
+ class TableView(QTableView):
102
+ def __init__(self, entities_or_class, view: TraitableView = None):
103
+ assert entities_or_class, 'first arg must not be empty'
104
+
105
+ if inspect.isclass(entities_or_class):
106
+ assert issubclass(entities_or_class, Traitable), 'first arg is a class, but not a subclass of Traitable'
107
+ proto = entities_or_class
108
+ entities = []
109
+ else:
110
+ entities = entities_or_class
111
+ proto = entities[0]
112
+
113
+ if not view:
114
+ view = TraitableView.default(proto)
115
+
116
+ super().__init__()
117
+ self.wheel_enabled = True
118
+
119
+ hv = HeaderView(view, parent = self)
120
+ hv.setStretchLastSection(True)
121
+
122
+ self.setHorizontalHeader(hv)
123
+ model = Model(hv.model(), entities, self)
124
+ self.setModel(model)
125
+
126
+ self.setAlternatingRowColors(True)
127
+ self.resizeColumnsToContents()
128
+
129
+ self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
130
+
131
+ def wheelEvent(self, *args, **kwargs):
132
+ if self.wheel_enabled:
133
+ QTableView.wheelEvent(self, args[0])
134
+
135
+ def keyPressEvent(self, *args, **kwargs):
136
+ if self.wheel_enabled:
137
+ QTableView.keyPressEvent(self, args[0])
138
+
139
+ def mousePressEvent(self, *args, **kwargs):
140
+ mouse_event = args[0]
141
+ row = self.rowAt(mouse_event.y())
142
+ col = self.columnAt(mouse_event.x())
143
+ if row == -1 or col == -1:
144
+ return
145
+
146
+ entity = self.model().entity(row)
147
+ trait_name = self.model().trait_name(col)
148
+ if not trait_name:
149
+ return
150
+
151
+ editor_class = TraitableEditor.findEditorClass(entity.__class__) #-- no alternative packages, look under ui subdir only
152
+ mouse_btn = mouse_event.button()
153
+ if mouse_btn == Qt.LeftButton:
154
+ cb = editor_class.leftMouseCallback(trait_name)
155
+ elif mouse_btn == Qt.RightButton:
156
+ cb = editor_class.rightMouseCallback(trait_name)
157
+ else:
158
+ return
159
+
160
+ if cb:
161
+ cb(self, entity)
162
+ else:
163
+ super().mousePressEvent(*args, **kwargs)
164
+
165
+ def render_entity(self, row: int, entity: Traitable):
166
+ last_col = len(self.model().col2name) - 1
167
+ first_index = self.model().create_index(row, 0)
168
+ last_index = self.model().create_index(row, last_col)
169
+ self.model().dataChanged.emit(first_index, last_index)
170
+
171
+ def extendData( self, data ):
172
+ if self.model().extendData(data):
173
+ self.resizeColumnsToContents()
174
+ #self.resizeRowsToContents()
ui_10x/trait_editor.py ADDED
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import nullcontext
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING
6
+
7
+ from core_10x.concrete_traits import date_trait, dict_trait, flags_trait, list_trait
8
+ from core_10x.trait import T, Ui
9
+ from core_10x.traitable import traitable_trait
10
+
11
+ import ui_10x.concrete_trait_widgets # do not remove -- this registers trait widgets
12
+ from ui_10x.choice import MultiChoice
13
+ from ui_10x.py_data_browser import PyDataBrowser
14
+ from ui_10x.trait_widget import TraitWidget
15
+ from ui_10x.utils import UxDialog, UxStyleSheet, ux, ux_pick_date
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Callable
19
+
20
+ from core_10x.entity import Entity
21
+ from core_10x.exec_control import BTP
22
+ from core_10x.rc import RC
23
+ from core_10x.trait import Trait
24
+
25
+
26
+ @dataclass
27
+ class EntityWrapper:
28
+ entity: Entity
29
+ traitable_processor: Callable[[], BTP]
30
+
31
+ def set_value(self, trait: Trait, value) -> RC:
32
+ with self.traitable_processor() or nullcontext():
33
+ return self.entity.set_value(trait, value)
34
+
35
+ def get_value(self, trait: Trait):
36
+ with self.traitable_processor() or nullcontext():
37
+ value = self.entity.get_value(trait)
38
+ return value
39
+
40
+ def is_valid(self, trait: Trait) -> bool:
41
+ with self.traitable_processor() or nullcontext():
42
+ return self.entity.is_valid(trait)
43
+
44
+ def invalidate_value(self, trait: Trait) -> None:
45
+ with self.traitable_processor() or nullcontext():
46
+ return self.entity.invalidate_value(trait)
47
+
48
+ def get_style_sheet(self, trait: Trait):
49
+ with self.traitable_processor() or nullcontext():
50
+ return self.entity.get_style_sheet(trait)
51
+
52
+ def get_choices(self, trait: Trait):
53
+ with self.traitable_processor() or nullcontext():
54
+ return self.entity.get_choices(trait)
55
+
56
+ def create_ui_node(self, trait: Trait, callback):
57
+ with self.traitable_processor() or nullcontext():
58
+ return self.entity.bui_class().create_ui_node(self.entity, trait, callback)
59
+
60
+ def update_ui_node(self, trait: Trait):
61
+ with self.traitable_processor() or nullcontext():
62
+ return self.entity.bui_class().update_ui_node(self.entity, trait)
63
+
64
+
65
+ class TraitEditor:
66
+ def __init__(self, entity, trait: Trait, ui_hint: Ui, custom_callback: Callable[[], None] = None, traitable_processor: Callable[[], BTP] = None):
67
+ self.entity = EntityWrapper(entity, traitable_processor)
68
+ self.trait = trait
69
+ self.widget: TraitWidget | None = None
70
+ self.ui_hint = ui_hint
71
+ self.trait_callback = self._establish_callback(custom_callback)
72
+
73
+ def is_read_only(self) -> bool:
74
+ return self.ui_hint.flags_on(Ui.READ_ONLY)
75
+
76
+ def _establish_callback(self, custom_callback: Callable[[], None]):
77
+ if custom_callback:
78
+ return custom_callback
79
+
80
+ if self.is_read_only():
81
+ return None
82
+
83
+ return self.builtin_callback()
84
+
85
+ def new_label(self) -> ux.Widget:
86
+ if self.ui_hint.param('right_label', False):
87
+ return ux.Label(' ')
88
+
89
+ if self.trait_callback:
90
+ label = ux.PushButton()
91
+ label.set_flat(True)
92
+ label.set_style_sheet('text-align: left')
93
+ label.set_text(self.ui_hint.label + '...')
94
+ label.clicked_connect(lambda v: self.trait_callback(self))
95
+ if self.trait.flags_on(T.EXPENSIVE):
96
+ UxStyleSheet.modify(label, {Ui.FG_COLOR: 'purple', Ui.FONT_STYLE: 'italic'})
97
+ label.set_tool_tip('Reaction on clicking this button may take considerable time...')
98
+ else:
99
+ label = ux.Label(self.ui_hint.label)
100
+
101
+ return label
102
+
103
+ def new_widget(self, update_self=True) -> TraitWidget:
104
+ tw: TraitWidget = TraitWidget.instance(self)
105
+ assert tw, f'{self.entity.entity.__class__}.{self.trait.name} - unknown trait widget class'
106
+
107
+ avg_char_width = tw.font_metrics().average_char_width()
108
+ min_width = self.ui_hint.param('min_width', 0)
109
+ if min_width > 0:
110
+ tw.set_minimum_width(min_width * avg_char_width)
111
+
112
+ max_width = self.ui_hint.param('max_width', 0)
113
+ if max_width > 0:
114
+ tw.set_maximum_width(max_width * avg_char_width)
115
+
116
+ if update_self:
117
+ self.widget = tw
118
+
119
+ return tw
120
+
121
+ # ---- Built-in Callbacks
122
+
123
+ def date_cb(self):
124
+ ux_pick_date(
125
+ title=f'Pick a date for {self.ui_hint.label}',
126
+ show_date=self.entity.get_value(self.trait),
127
+ on_accept=lambda value: self.entity.set_value(self.trait, value),
128
+ )
129
+
130
+ def list_cb(self):
131
+ choices = self.entity.get_value(self.trait)
132
+ mc = MultiChoice(choices=choices)
133
+ w = mc.widget()
134
+ if not w:
135
+ return
136
+
137
+ UxDialog(
138
+ w,
139
+ title=f'Choose one or more values for {self.ui_hint.label}',
140
+ accept_callback=lambda ctx: self.entity.set_value(self.trait, mc.values_selected),
141
+ ).show()
142
+
143
+ def dict_cb(self):
144
+ data = dict(self.entity.get_value(self.trait))
145
+ rc = PyDataBrowser.edit(data, title=f'Edit {self.trait.ui_hint.label}') # TODO: callback
146
+ if rc:
147
+ self.entity.set_value(self.trait, data)
148
+
149
+ def flags_cb(self):
150
+ flags = self.entity.get_value(self.trait)
151
+ bits_class = self.trait.data_type
152
+ tags_selected = bits_class.data_type.names_from_value(flags)
153
+ mc = MultiChoice(*tags_selected, choices=bits_class.choose_from())
154
+ w = mc.widget()
155
+ if not w:
156
+ return
157
+
158
+ UxDialog(
159
+ w,
160
+ title=f'Choose one or more flags for {self.ui_hint.label}',
161
+ accept_callback=lambda ctx: self.entity.set_value(self.trait, mc.values_selected),
162
+ ).show()
163
+
164
+ def traitable_cb(self):
165
+ # -- EntityEditor - popup
166
+ ...
167
+
168
+ def expensive_cb(self):
169
+ value = self.entity.get_value(self.trait)
170
+ self.widget.set_widget_value(value)
171
+ self.widget.style_sheet.restore()
172
+
173
+ def builtin_callback(self):
174
+ cb = self.s_builtin_callbacks.get(self.trait.__class__)
175
+ if not cb:
176
+ if self.trait.flags_on(T.EXPENSIVE):
177
+ cb = self.__class__.expensive_cb
178
+
179
+ return cb
180
+
181
+ # fmt: off
182
+ s_builtin_callbacks = {
183
+ date_trait: date_cb,
184
+ list_trait: list_cb,
185
+ dict_trait: dict_cb,
186
+ flags_trait: flags_cb,
187
+ traitable_trait: traitable_cb,
188
+ }
189
+ # fmt: on