pyqttoolkit 0.9.1__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 (133) hide show
  1. pyqttoolkit/__init__.py +20 -0
  2. pyqttoolkit/_version.py +21 -0
  3. pyqttoolkit/application/__init__.py +18 -0
  4. pyqttoolkit/application/application.py +86 -0
  5. pyqttoolkit/application/garbage_collector.py +65 -0
  6. pyqttoolkit/colors/__init__.py +84 -0
  7. pyqttoolkit/config/__init__.py +20 -0
  8. pyqttoolkit/config/base.py +22 -0
  9. pyqttoolkit/config/composite.py +31 -0
  10. pyqttoolkit/config/default.py +27 -0
  11. pyqttoolkit/config/environment.py +35 -0
  12. pyqttoolkit/config/multi_json_file.py +93 -0
  13. pyqttoolkit/config/with_signal.py +30 -0
  14. pyqttoolkit/config/with_value_changed.py +31 -0
  15. pyqttoolkit/data/__init__.py +21 -0
  16. pyqttoolkit/data/bits.py +23 -0
  17. pyqttoolkit/data/cache.py +64 -0
  18. pyqttoolkit/data/interval.py +26 -0
  19. pyqttoolkit/data/message.py +29 -0
  20. pyqttoolkit/data/names.py +24 -0
  21. pyqttoolkit/datetime/__init__.py +16 -0
  22. pyqttoolkit/datetime/datetime.py +51 -0
  23. pyqttoolkit/datetime/qdatetime.py +53 -0
  24. pyqttoolkit/dependencies/__init__.py +56 -0
  25. pyqttoolkit/errors/__init__.py +19 -0
  26. pyqttoolkit/exceptions/__init__.py +18 -0
  27. pyqttoolkit/filesystem/__init__.py +24 -0
  28. pyqttoolkit/logs/__init__.py +140 -0
  29. pyqttoolkit/metadata/__init__.py +23 -0
  30. pyqttoolkit/metadata/dataframe_metadata_manager.py +105 -0
  31. pyqttoolkit/metadata/dict_metadata_manager.py +63 -0
  32. pyqttoolkit/metadata/id_generator.py +20 -0
  33. pyqttoolkit/metadata/metadata_manager_base.py +54 -0
  34. pyqttoolkit/models/__init__.py +26 -0
  35. pyqttoolkit/models/code_text.py +43 -0
  36. pyqttoolkit/models/dataframetable.py +135 -0
  37. pyqttoolkit/models/dictpropertytree.py +155 -0
  38. pyqttoolkit/models/dicttable.py +140 -0
  39. pyqttoolkit/models/json_text.py +52 -0
  40. pyqttoolkit/models/list_model.py +52 -0
  41. pyqttoolkit/models/module.py +26 -0
  42. pyqttoolkit/models/roles.py +23 -0
  43. pyqttoolkit/models/series_data.py +68 -0
  44. pyqttoolkit/models/span.py +36 -0
  45. pyqttoolkit/models/validation.py +27 -0
  46. pyqttoolkit/modules/__init__.py +18 -0
  47. pyqttoolkit/modules/events/__init__.py +19 -0
  48. pyqttoolkit/modules/events/add_signal.py +31 -0
  49. pyqttoolkit/modules/events/module_opening.py +21 -0
  50. pyqttoolkit/modules/events/rejectable.py +29 -0
  51. pyqttoolkit/modules/launcher_config.py +55 -0
  52. pyqttoolkit/modules/module.py +111 -0
  53. pyqttoolkit/properties/__init__.py +18 -0
  54. pyqttoolkit/properties/auto.py +221 -0
  55. pyqttoolkit/properties/throttled.py +77 -0
  56. pyqttoolkit/scripting/__init__.py +19 -0
  57. pyqttoolkit/scripting/script_context.py +36 -0
  58. pyqttoolkit/scripting/script_runner.py +132 -0
  59. pyqttoolkit/scripting/script_template.py +42 -0
  60. pyqttoolkit/services/__init__.py +32 -0
  61. pyqttoolkit/services/autosave.py +41 -0
  62. pyqttoolkit/services/email.py +25 -0
  63. pyqttoolkit/services/event_registry.py +52 -0
  64. pyqttoolkit/services/file_dialog.py +71 -0
  65. pyqttoolkit/services/hotkey_manager.py +137 -0
  66. pyqttoolkit/services/link_manager.py +135 -0
  67. pyqttoolkit/services/message_board.py +90 -0
  68. pyqttoolkit/services/module_service.py +154 -0
  69. pyqttoolkit/services/object_converter.py +45 -0
  70. pyqttoolkit/services/project_updater.py +149 -0
  71. pyqttoolkit/services/task_runner.py +281 -0
  72. pyqttoolkit/services/theme_manager.py +46 -0
  73. pyqttoolkit/services/tool_window_service.py +46 -0
  74. pyqttoolkit/viewmanagers/__init__.py +17 -0
  75. pyqttoolkit/viewmanagers/tool.py +23 -0
  76. pyqttoolkit/views/__init__.py +50 -0
  77. pyqttoolkit/views/bulk_value_selector.py +112 -0
  78. pyqttoolkit/views/check_box.py +36 -0
  79. pyqttoolkit/views/code/__init__.py +18 -0
  80. pyqttoolkit/views/code/code_edit.py +268 -0
  81. pyqttoolkit/views/code/python_syntax.py +226 -0
  82. pyqttoolkit/views/combo_box.py +101 -0
  83. pyqttoolkit/views/count_selector.py +59 -0
  84. pyqttoolkit/views/datetime.py +111 -0
  85. pyqttoolkit/views/datetime_range_selector.py +231 -0
  86. pyqttoolkit/views/default_text_line_edit.py +50 -0
  87. pyqttoolkit/views/delegates.py +117 -0
  88. pyqttoolkit/views/dtype_edit.py +117 -0
  89. pyqttoolkit/views/editable_combo_box.py +45 -0
  90. pyqttoolkit/views/file_selector.py +51 -0
  91. pyqttoolkit/views/icon.py +67 -0
  92. pyqttoolkit/views/icon_button.py +129 -0
  93. pyqttoolkit/views/json_edit.py +21 -0
  94. pyqttoolkit/views/labelled.py +29 -0
  95. pyqttoolkit/views/layout/__init__.py +21 -0
  96. pyqttoolkit/views/layout/collabsible.py +59 -0
  97. pyqttoolkit/views/layout/group_box.py +48 -0
  98. pyqttoolkit/views/layout/group_panel.py +188 -0
  99. pyqttoolkit/views/layout/lines.py +30 -0
  100. pyqttoolkit/views/layout/link.py +38 -0
  101. pyqttoolkit/views/line_edit.py +61 -0
  102. pyqttoolkit/views/linkable.py +49 -0
  103. pyqttoolkit/views/list_view.py +43 -0
  104. pyqttoolkit/views/main_window.py +91 -0
  105. pyqttoolkit/views/metadata_selector.py +84 -0
  106. pyqttoolkit/views/modulewindow.py +81 -0
  107. pyqttoolkit/views/plot/__init__.py +26 -0
  108. pyqttoolkit/views/plot/legend_control.py +121 -0
  109. pyqttoolkit/views/plot/matplotlib/__init__.py +23 -0
  110. pyqttoolkit/views/plot/matplotlib/base.py +1056 -0
  111. pyqttoolkit/views/plot/matplotlib/colorbar.py +75 -0
  112. pyqttoolkit/views/plot/matplotlib/colormap.py +48 -0
  113. pyqttoolkit/views/plot/matplotlib/font.py +55 -0
  114. pyqttoolkit/views/plot/plot_options.py +211 -0
  115. pyqttoolkit/views/plot/plot_toolbar_options.py +133 -0
  116. pyqttoolkit/views/plot/size.py +41 -0
  117. pyqttoolkit/views/plot/tool_type.py +26 -0
  118. pyqttoolkit/views/plot/toolbar.py +100 -0
  119. pyqttoolkit/views/popout.py +41 -0
  120. pyqttoolkit/views/popup.py +41 -0
  121. pyqttoolkit/views/property_tree.py +26 -0
  122. pyqttoolkit/views/shrunk_push_button.py +34 -0
  123. pyqttoolkit/views/steppable_combo_box.py +67 -0
  124. pyqttoolkit/views/styleable.py +37 -0
  125. pyqttoolkit/views/table.py +146 -0
  126. pyqttoolkit/views/text_edit.py +31 -0
  127. pyqttoolkit/views/tool_window.py +35 -0
  128. pyqttoolkit-0.9.1.dist-info/METADATA +69 -0
  129. pyqttoolkit-0.9.1.dist-info/RECORD +133 -0
  130. pyqttoolkit-0.9.1.dist-info/WHEEL +5 -0
  131. pyqttoolkit-0.9.1.dist-info/licenses/LICENSE.txt +674 -0
  132. pyqttoolkit-0.9.1.dist-info/licenses/LICENSE_SHORT +16 -0
  133. pyqttoolkit-0.9.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,63 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from enum import Enum
18
+ from copy import deepcopy
19
+
20
+ from .metadata_manager_base import MetadataManagerBase
21
+
22
+ class DictMetadataManager(MetadataManagerBase):
23
+ def __init__(self, application):
24
+ MetadataManagerBase.__init__(self, application)
25
+
26
+ def get_property(self, id_, property_):
27
+ if not isinstance(property_, Enum):
28
+ raise TypeError('Property must be enum type')
29
+ return deepcopy(self.metadata.get(self._get_property_type_key(type(property_)), {}).get(id_, {}).get(property_.name, None))
30
+
31
+ def set_property(self, id_, property_, value):
32
+ if not isinstance(property_, Enum):
33
+ raise TypeError('Property must be enum type')
34
+ if id_ is None:
35
+ raise ValueError('Must specify an ID')
36
+ property_type = type(property_)
37
+ property_type_key = self._get_property_type_key(property_type)
38
+ self.metadata.setdefault(property_type_key, {}).setdefault(id_, self.default_properties(property_type))[property_.name] = value
39
+
40
+ def get_properties(self, id_, property_type):
41
+ return deepcopy(self.metadata.get(self._types.get(property_type), {}).get(id_, None))
42
+
43
+ def add_properties(self, id_, property_type, properties=None):
44
+ if id_ is None:
45
+ raise ValueError('Must specify an ID')
46
+ self.metadata.setdefault(self._get_property_type_key(property_type), {}).setdefault(id_, properties or self.default_properties(property_type))
47
+
48
+ def delete_properties(self, id_, property_type):
49
+ if id_ is None:
50
+ raise ValueError('Must specify an ID')
51
+ metadata = self.metadata.get(self._get_property_type_key(property_type), {})
52
+ if id_ in metadata:
53
+ del metadata[id_]
54
+
55
+ def get_metadata(self, property_type):
56
+ return deepcopy(self.metadata.get(self._get_property_type_key(property_type)))
57
+
58
+ def set_metadata(self, property_type, value):
59
+ self.metadata[self._get_property_type_key(property_type)] = deepcopy(value)
60
+
61
+ @staticmethod
62
+ def default_properties(property_type):
63
+ return {p.name: None for p in property_type}
@@ -0,0 +1,20 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ def next_id(root, existing):
18
+ current_ids = sorted(int(i.replace(root, '')) for i in existing if root in i)
19
+ last_id = current_ids[-1] if current_ids else 0
20
+ return f'{root}{last_id + 1}'
@@ -0,0 +1,54 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ class MetadataManagerBase:
18
+ def __init__(self, project_manager):
19
+ self._project_manager = project_manager
20
+ self._types = {}
21
+
22
+ def get_property(self, id_, property_):
23
+ raise NotImplementedError()
24
+
25
+ def set_property(self, id_, property_, value):
26
+ raise NotImplementedError()
27
+
28
+ def get_properties(self, id_, property_type):
29
+ raise NotImplementedError()
30
+
31
+ def add_properties(self, id_, property_type, properties=None):
32
+ raise NotImplementedError()
33
+
34
+ def delete_properties(self, id_, property_type):
35
+ raise NotImplementedError()
36
+
37
+ def get_metadata(self, property_type):
38
+ raise NotImplementedError()
39
+
40
+ def set_metadata(self, property_type, value):
41
+ raise NotImplementedError()
42
+
43
+ def _get_property_type_key(self, property_type):
44
+ property_type_key = self._types.get(property_type)
45
+ if not property_type_key:
46
+ raise ValueError(f'Property type {property_type} not found')
47
+ return property_type_key
48
+
49
+ @property
50
+ def metadata(self):
51
+ return self._project_manager.project.metadata
52
+
53
+ def register_type(self, type_, key):
54
+ self._types[type_] = key
@@ -0,0 +1,26 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from .validation import ValidationModel
18
+ from .json_text import JsonTextModel
19
+ from .code_text import CodeTextModel
20
+ from .list_model import ListModel, StringListModel, ToolTipStringListModel
21
+ from .dataframetable import DataFrameTableModel
22
+ from .dictpropertytree import DictPropertyTreeModel
23
+ from .dicttable import DictTableModel
24
+ from .series_data import SeriesDataModel
25
+ from .span import SpanModel
26
+ from .module import ModuleModel
@@ -0,0 +1,43 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from PyQt5.QtCore import QObject, pyqtSignal
18
+
19
+ from pyqttoolkit.properties import AutoProperty
20
+ from pyqttoolkit.logs import LogStream
21
+
22
+ class CodeTextModel(QObject):
23
+ def __init__(self, parent, validator):
24
+ QObject.__init__(self, parent)
25
+ self.textChanged.connect(self._validate)
26
+ self._validator = validator
27
+ self.text = ''
28
+ self.output = LogStream(self._handle_output_buffer_changed)
29
+
30
+ textChanged = pyqtSignal(str)
31
+ outputChanged = pyqtSignal(LogStream)
32
+ validationMessageChanged = pyqtSignal(str)
33
+
34
+ text = AutoProperty(str)
35
+ output = AutoProperty(LogStream)
36
+ validationMessage = AutoProperty(str)
37
+
38
+ def _validate(self, value):
39
+ validation_model = self._validator(value)
40
+ self.validationMessage = validation_model.error if validation_model else ''
41
+
42
+ def _handle_output_buffer_changed(self):
43
+ self.outputChanged.emit(self.output)
@@ -0,0 +1,135 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from PyQt5.QtCore import pyqtSignal, QAbstractTableModel, Qt, QVariant, QModelIndex, QItemSelectionModel
18
+
19
+ class DataFrameTableModel(QAbstractTableModel):
20
+ def __init__(self, parent, data, display_column=None, editable=False, row_headers=False):
21
+ QAbstractTableModel.__init__(self, parent)
22
+ self._data = data.copy()
23
+ self._display_column_index = (
24
+ 0 if display_column is None
25
+ else data.columns.get_loc(display_column)
26
+ )
27
+ self._selection_model = QItemSelectionModel(self)
28
+ self._editable = editable
29
+ self._row_headers = row_headers
30
+
31
+ dataUpdated = pyqtSignal()
32
+
33
+ def data(self, index, role=Qt.DisplayRole):
34
+ if role in [Qt.DisplayRole, Qt.EditRole, Qt.ToolTipRole]:
35
+ column = self._data.columns[index.column()]
36
+ return str(self._data[column].iloc[index.row()])
37
+ return QVariant()
38
+
39
+ def headerData(self, section, orientation, role=None):
40
+ role = role or Qt.DisplayRole
41
+ if role == Qt.DisplayRole:
42
+ if orientation == Qt.Horizontal:
43
+ return str(self._data.columns[section])
44
+ elif self._row_headers:
45
+ return str(self._data.index[section])
46
+ return None
47
+
48
+ def flags(self, _index):
49
+ flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled
50
+ if self._editable:
51
+ flags |= Qt.ItemIsEditable
52
+ return flags
53
+
54
+ def setData(self, index, value, role=Qt.EditRole):
55
+ if role == Qt.EditRole:
56
+ column = self._data.columns[index.column()]
57
+ self._data[column][index.row()] = value
58
+ self.dataUpdated.emit()
59
+ return True
60
+ return False
61
+
62
+ def columnCount(self, _parent=None):
63
+ return len(self._data.columns)
64
+
65
+ def rowCount(self, _parent=None):
66
+ return len(self._data)
67
+
68
+ def insertRows(self, row, count, parent=None, gen_row=None):
69
+ parent = parent or QModelIndex()
70
+ if row != self.rowCount(parent):
71
+ return False
72
+ if count != 1:
73
+ raise ValueError('Cannot insert more than 1 row')
74
+
75
+ gen_row = gen_row or (lambda: {n: '' for n in self._data.columns})
76
+
77
+ self.beginInsertRows(parent, row, row)
78
+ self._data.loc[row] = gen_row()
79
+ self.dataUpdated.emit()
80
+ self.endInsertRows()
81
+ return True
82
+
83
+ def removeRows(self, row, count, parent=None):
84
+ parent = parent or QModelIndex()
85
+ if count != 1:
86
+ raise ValueError('Cannot remove more than 1 row')
87
+
88
+ self.beginRemoveRows(parent, row, row)
89
+ self._data.drop(row, inplace=True)
90
+ self._data.reset_index(drop=True, inplace=True)
91
+ self.dataUpdated.emit()
92
+ self.endRemoveRows()
93
+ return True
94
+
95
+ def moveRows(self, source_parent, source_row, count, dest_parent, dest_row):
96
+ if count != 1:
97
+ raise ValueError('Cannot move more than 1 row')
98
+ dest_row = min(dest_row, len(self._data) - 1)
99
+ dest_row = max(dest_row, 0)
100
+ if dest_row == source_row:
101
+ return False
102
+ self.beginMoveRows(source_parent, source_row, source_row + count, dest_parent, dest_row)
103
+ temp = self._data.iloc[dest_row].copy()
104
+ self._data.iloc[dest_row] = self._data.iloc[source_row]
105
+ self._data.iloc[source_row] = temp
106
+ self.endMoveRows()
107
+ self.dataUpdated.emit()
108
+ self.selectionModel.setCurrentIndex(self.createIndex(dest_row, 0), QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
109
+ return True
110
+
111
+ def removeSelected(self):
112
+ selected_rows = {i.row() for i in self._selection_model.selectedIndexes()}
113
+ if len(selected_rows) > 1:
114
+ raise ValueError('Removal of more than one row not currently supported')
115
+ for row in selected_rows:
116
+ self.removeRows(row, 1)
117
+
118
+ @property
119
+ def dataFrame(self):
120
+ return self._data
121
+
122
+ @dataFrame.setter
123
+ def dataFrame(self, value):
124
+ if self._data.size != value.size or not (self._data == value).all().all():
125
+ self.beginResetModel()
126
+ self._data = value.copy()
127
+ self.endResetModel()
128
+
129
+ @property
130
+ def displayColumnIndex(self):
131
+ return self._display_column_index
132
+
133
+ @property
134
+ def selectionModel(self):
135
+ return self._selection_model
@@ -0,0 +1,155 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from enum import Enum
18
+
19
+ from PyQt5.QtCore import Qt, QAbstractItemModel, pyqtSignal, QModelIndex, QVariant
20
+
21
+ class DictPropertyTreeModel(QAbstractItemModel):
22
+ def __init__(self, parent, data):
23
+ QAbstractItemModel.__init__(self, parent)
24
+ self._data = data
25
+ self._nodes = []
26
+ self._indexes = {}
27
+
28
+ propertyChanged = pyqtSignal(object, object)
29
+
30
+ @property
31
+ def value(self):
32
+ return self._data
33
+
34
+ @value.setter
35
+ def value(self, value):
36
+ self._data = value
37
+ start_index = self.createIndex(0, 0)
38
+ end_index = self.createIndex(self.rowCount(), self.columnCount())
39
+
40
+ self.dataChanged.emit(start_index, end_index)
41
+
42
+ def rowCount(self, parent=None):
43
+ keys = self._keys(parent)
44
+ return len(keys) if keys else 0
45
+
46
+ def columnCount(self, _parent=None):
47
+ return 2
48
+
49
+ def parent(self, index):
50
+ return self._nodes[index.internalId()] if index.isValid() else QModelIndex()
51
+
52
+ def hasChildren(self, index):
53
+ return isinstance(self._property_type(index), dict)
54
+
55
+ def index(self, row, column, parent):
56
+ if (row, column, parent) not in self._indexes:
57
+ internal_id = len(self._nodes)
58
+ self._nodes.append(parent)
59
+ self._indexes[(row, column, parent)] = self.createIndex(row, column, internal_id)
60
+ return self._indexes[(row, column, parent)]
61
+
62
+ def flags(self, index):
63
+ if index.column() == 0 or isinstance(self._property_type(index), dict):
64
+ return Qt.ItemIsEnabled | Qt.ItemIsSelectable
65
+ return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
66
+
67
+ def data(self, index, role=None):
68
+ role = role or Qt.DisplayRole
69
+ if role in [Qt.DisplayRole, Qt.EditRole]:
70
+ if index.column() == 0:
71
+ return self._property_name(index)
72
+ data = self._properties(index)
73
+ if isinstance(data, dict):
74
+ return None
75
+ return data and self._property_type(index)(data)
76
+ return None
77
+
78
+ def setData(self, index, value, role=None):
79
+ role = role or Qt.EditRole
80
+ if role == Qt.EditRole:
81
+ if index.column() == 1:
82
+ self._set_property(index, value)
83
+ return True
84
+ return False
85
+
86
+ def headerData(self, section, orientation, role=None):
87
+ role = role or Qt.DisplayRole
88
+ if role in [Qt.DisplayRole, Qt.EditRole] and orientation in [Qt.Horizontal]:
89
+ return self.tr('Property Name') if section == 0 else self.tr('Property Value')
90
+ return QVariant()
91
+
92
+ def _keys(self, parent=None):
93
+ types = self._property_type(parent)
94
+ return self._sort_keys(types) if isinstance(types, dict) else None
95
+
96
+ def _properties(self, index):
97
+ properties = self._data
98
+ for property_ in self._property_path(index):
99
+ if properties is None:
100
+ break
101
+ properties = properties.get(property_.name, None)
102
+ return properties
103
+
104
+ def _set_property(self, index, value):
105
+ property_path = self._property_path(index)
106
+ properties = self._data
107
+ for property_ in property_path[:-1]:
108
+ properties = properties.setdefault(property_.name, {})
109
+ type_ = self._property_type(index)
110
+ try:
111
+ value = type_(value)
112
+ except ValueError:
113
+ value = properties.get(property_path[-1].name)
114
+ properties[property_path[-1].name] = value
115
+ self.propertyChanged.emit(property_path[-1], value)
116
+
117
+ def _property_name(self, index):
118
+ property_path = self._property_path(index)
119
+ property_enum = property_path[-1]
120
+ return self.propertyNames.get(property_enum, property_enum.name)
121
+
122
+ def _property_key(self, properties, index):
123
+ return self._sort_keys(properties)[index.row()]
124
+
125
+ def _property_type(self, index):
126
+ types = self.propertyTypes
127
+ for property_ in self._property_path(index):
128
+ types = types[property_]
129
+ return types
130
+
131
+ def _property_path(self, parent):
132
+ ancestors = []
133
+ while parent is not None and parent.isValid():
134
+ ancestors.insert(0, parent)
135
+ parent = self.parent(parent)
136
+ properties = self.propertyTypes
137
+ property_path = []
138
+ for ancestor in ancestors:
139
+ property_keys = self._sort_keys(properties)
140
+ current_property = property_keys[ancestor.row()]
141
+ properties = properties[current_property]
142
+ property_path.append(current_property)
143
+ return property_path
144
+
145
+ @property
146
+ def propertyNames(self):
147
+ raise NotImplementedError()
148
+
149
+ @property
150
+ def propertyTypes(self):
151
+ raise NotImplementedError()
152
+
153
+ @staticmethod
154
+ def _sort_keys(dict_):
155
+ return list(sorted(dict_.keys(), key=lambda v: v.value if isinstance(v, Enum) else v))
@@ -0,0 +1,140 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from PyQt5.QtCore import Qt, QAbstractTableModel, pyqtSignal, QModelIndex
18
+
19
+ from pyqttoolkit.models.roles import DataRole
20
+
21
+ class DictTableModel(QAbstractTableModel):
22
+ def __init__(self, parent, value):
23
+ if not isinstance(value, dict):
24
+ raise TypeError('value must be dict')
25
+ QAbstractTableModel.__init__(self, parent)
26
+ self._value = value
27
+ self._pasting = False
28
+ self._pasted_properties = {}
29
+
30
+ propertyChanged = pyqtSignal(str, object, object)
31
+ propertiesChanged = pyqtSignal(object)
32
+
33
+ @property
34
+ def value(self):
35
+ return self._value
36
+
37
+ def setValue(self, value):
38
+ if not isinstance(value, dict):
39
+ raise TypeError('value must be dict')
40
+ if self._value != value:
41
+ new_rows = len(value.keys()) - len(self._value.keys())
42
+ if new_rows > 0:
43
+ self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount() + new_rows - 1)
44
+ elif new_rows < 0:
45
+ self.beginRemoveRows(QModelIndex(), self.rowCount() + new_rows, self.rowCount() - 1)
46
+
47
+ self._value = value
48
+ start_index = self.createIndex(0, 0)
49
+ end_index = self.createIndex(self.rowCount(), self.columnCount())
50
+
51
+ self.dataChanged.emit(start_index, end_index)
52
+
53
+ if new_rows > 0:
54
+ self.endInsertRows()
55
+ elif new_rows < 0:
56
+ self.endRemoveRows()
57
+
58
+ def data(self, index, role=None):
59
+ role = role or Qt.DisplayRole
60
+ if role in [Qt.DisplayRole, Qt.EditRole]:
61
+ value = self._value[self._ids[index.row()]].get(self.displayProperties[index.column()])
62
+ if value is None:
63
+ return None
64
+ return self._format_value(value, index)
65
+ return None
66
+
67
+ def _format_value(self, value, index):
68
+ if isinstance(value, float):
69
+ return '{:0.2f}'.format(value)
70
+ return value
71
+
72
+ def setData(self, index, value, role=None):
73
+ role = role or Qt.EditRole
74
+ if role == Qt.EditRole:
75
+ if index.row() >= self.rowCount() or index.column() >= self.columnCount():
76
+ return False
77
+ id_ = self._ids[index.row()]
78
+ property_ = self.displayProperties[index.column()]
79
+ type_ = self.valueTypes[self.propertyType[property_]]
80
+ try:
81
+ converted_value = type_(value)
82
+ self._value[id_][property_] = converted_value
83
+ except ValueError:
84
+ return False
85
+ if not self._pasting:
86
+ self.dataChanged.emit(index, index)
87
+ self.propertyChanged.emit(id_, self.propertyType[property_], converted_value)
88
+ else:
89
+ self._pasted_properties[(id_, self.propertyType[property_])] = converted_value
90
+ return True
91
+ return False
92
+
93
+ def headerData(self, section, orientation, role=None):
94
+ role = role or Qt.DisplayRole
95
+ if role in [Qt.DisplayRole, DataRole]:
96
+ if orientation == Qt.Vertical:
97
+ return self._ids[section]
98
+ else:
99
+ return self.headings[self.propertyType[self.displayProperties[section]]]
100
+ return None
101
+
102
+ def beginPaste(self):
103
+ self.blockSignals(True)
104
+ self._pasting = True
105
+
106
+ def endPaste(self, from_index, to_index):
107
+ self._pasting = False
108
+ self.blockSignals(False)
109
+ self.dataChanged.emit(from_index, to_index)
110
+ self.propertiesChanged.emit(self._pasted_properties)
111
+ self._pasted_properties = {}
112
+
113
+ def flags(self, _index):
114
+ return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
115
+
116
+ def columnCount(self, _parent=None):
117
+ return len(self.displayProperties)
118
+
119
+ def rowCount(self, _parent=None):
120
+ return len(self._ids)
121
+
122
+ @property
123
+ def _ids(self):
124
+ return list(sorted(self._value.keys()))
125
+
126
+ @property
127
+ def propertyType(self):
128
+ raise NotImplementedError()
129
+
130
+ @property
131
+ def displayProperties(self):
132
+ raise NotImplementedError()
133
+
134
+ @property
135
+ def valueTypes(self):
136
+ raise NotImplementedError()
137
+
138
+ @property
139
+ def headings(self):
140
+ raise NotImplementedError()
@@ -0,0 +1,52 @@
1
+ # pyqttoolkit
2
+ # Copyright (C) 2018-2019, Simmovation Ltd.
3
+ #
4
+ # This program is free software; you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation; either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software Foundation,
16
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
+ from json import loads, dumps, JSONDecodeError
18
+
19
+ from PyQt5.QtCore import QObject, pyqtSignal
20
+
21
+ from pyqttoolkit.properties import AutoProperty
22
+
23
+ class JsonTextModel(QObject):
24
+ def __init__(self, parent):
25
+ QObject.__init__(self, parent)
26
+ self.textChanged.connect(self._set_data)
27
+ self._data = {}
28
+ self.text = '{}'
29
+
30
+ textChanged = pyqtSignal(str)
31
+ validationMessageChanged = pyqtSignal(str)
32
+
33
+ text = AutoProperty(str)
34
+ validationMessage = AutoProperty(str)
35
+
36
+ @property
37
+ def data(self):
38
+ return self._data
39
+
40
+ @data.setter
41
+ def data(self, value):
42
+ self.text = dumps(
43
+ value, sort_keys=True,
44
+ indent=2, separators=(',', ': ')
45
+ )
46
+
47
+ def _set_data(self, value):
48
+ try:
49
+ self._data = loads(value)
50
+ self.validationMessage = ''
51
+ except JSONDecodeError:
52
+ self.validationMessage = self.tr('Error in JSON')