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.
- pyqttoolkit/__init__.py +20 -0
- pyqttoolkit/_version.py +21 -0
- pyqttoolkit/application/__init__.py +18 -0
- pyqttoolkit/application/application.py +86 -0
- pyqttoolkit/application/garbage_collector.py +65 -0
- pyqttoolkit/colors/__init__.py +84 -0
- pyqttoolkit/config/__init__.py +20 -0
- pyqttoolkit/config/base.py +22 -0
- pyqttoolkit/config/composite.py +31 -0
- pyqttoolkit/config/default.py +27 -0
- pyqttoolkit/config/environment.py +35 -0
- pyqttoolkit/config/multi_json_file.py +93 -0
- pyqttoolkit/config/with_signal.py +30 -0
- pyqttoolkit/config/with_value_changed.py +31 -0
- pyqttoolkit/data/__init__.py +21 -0
- pyqttoolkit/data/bits.py +23 -0
- pyqttoolkit/data/cache.py +64 -0
- pyqttoolkit/data/interval.py +26 -0
- pyqttoolkit/data/message.py +29 -0
- pyqttoolkit/data/names.py +24 -0
- pyqttoolkit/datetime/__init__.py +16 -0
- pyqttoolkit/datetime/datetime.py +51 -0
- pyqttoolkit/datetime/qdatetime.py +53 -0
- pyqttoolkit/dependencies/__init__.py +56 -0
- pyqttoolkit/errors/__init__.py +19 -0
- pyqttoolkit/exceptions/__init__.py +18 -0
- pyqttoolkit/filesystem/__init__.py +24 -0
- pyqttoolkit/logs/__init__.py +140 -0
- pyqttoolkit/metadata/__init__.py +23 -0
- pyqttoolkit/metadata/dataframe_metadata_manager.py +105 -0
- pyqttoolkit/metadata/dict_metadata_manager.py +63 -0
- pyqttoolkit/metadata/id_generator.py +20 -0
- pyqttoolkit/metadata/metadata_manager_base.py +54 -0
- pyqttoolkit/models/__init__.py +26 -0
- pyqttoolkit/models/code_text.py +43 -0
- pyqttoolkit/models/dataframetable.py +135 -0
- pyqttoolkit/models/dictpropertytree.py +155 -0
- pyqttoolkit/models/dicttable.py +140 -0
- pyqttoolkit/models/json_text.py +52 -0
- pyqttoolkit/models/list_model.py +52 -0
- pyqttoolkit/models/module.py +26 -0
- pyqttoolkit/models/roles.py +23 -0
- pyqttoolkit/models/series_data.py +68 -0
- pyqttoolkit/models/span.py +36 -0
- pyqttoolkit/models/validation.py +27 -0
- pyqttoolkit/modules/__init__.py +18 -0
- pyqttoolkit/modules/events/__init__.py +19 -0
- pyqttoolkit/modules/events/add_signal.py +31 -0
- pyqttoolkit/modules/events/module_opening.py +21 -0
- pyqttoolkit/modules/events/rejectable.py +29 -0
- pyqttoolkit/modules/launcher_config.py +55 -0
- pyqttoolkit/modules/module.py +111 -0
- pyqttoolkit/properties/__init__.py +18 -0
- pyqttoolkit/properties/auto.py +221 -0
- pyqttoolkit/properties/throttled.py +77 -0
- pyqttoolkit/scripting/__init__.py +19 -0
- pyqttoolkit/scripting/script_context.py +36 -0
- pyqttoolkit/scripting/script_runner.py +132 -0
- pyqttoolkit/scripting/script_template.py +42 -0
- pyqttoolkit/services/__init__.py +32 -0
- pyqttoolkit/services/autosave.py +41 -0
- pyqttoolkit/services/email.py +25 -0
- pyqttoolkit/services/event_registry.py +52 -0
- pyqttoolkit/services/file_dialog.py +71 -0
- pyqttoolkit/services/hotkey_manager.py +137 -0
- pyqttoolkit/services/link_manager.py +135 -0
- pyqttoolkit/services/message_board.py +90 -0
- pyqttoolkit/services/module_service.py +154 -0
- pyqttoolkit/services/object_converter.py +45 -0
- pyqttoolkit/services/project_updater.py +149 -0
- pyqttoolkit/services/task_runner.py +281 -0
- pyqttoolkit/services/theme_manager.py +46 -0
- pyqttoolkit/services/tool_window_service.py +46 -0
- pyqttoolkit/viewmanagers/__init__.py +17 -0
- pyqttoolkit/viewmanagers/tool.py +23 -0
- pyqttoolkit/views/__init__.py +50 -0
- pyqttoolkit/views/bulk_value_selector.py +112 -0
- pyqttoolkit/views/check_box.py +36 -0
- pyqttoolkit/views/code/__init__.py +18 -0
- pyqttoolkit/views/code/code_edit.py +268 -0
- pyqttoolkit/views/code/python_syntax.py +226 -0
- pyqttoolkit/views/combo_box.py +101 -0
- pyqttoolkit/views/count_selector.py +59 -0
- pyqttoolkit/views/datetime.py +111 -0
- pyqttoolkit/views/datetime_range_selector.py +231 -0
- pyqttoolkit/views/default_text_line_edit.py +50 -0
- pyqttoolkit/views/delegates.py +117 -0
- pyqttoolkit/views/dtype_edit.py +117 -0
- pyqttoolkit/views/editable_combo_box.py +45 -0
- pyqttoolkit/views/file_selector.py +51 -0
- pyqttoolkit/views/icon.py +67 -0
- pyqttoolkit/views/icon_button.py +129 -0
- pyqttoolkit/views/json_edit.py +21 -0
- pyqttoolkit/views/labelled.py +29 -0
- pyqttoolkit/views/layout/__init__.py +21 -0
- pyqttoolkit/views/layout/collabsible.py +59 -0
- pyqttoolkit/views/layout/group_box.py +48 -0
- pyqttoolkit/views/layout/group_panel.py +188 -0
- pyqttoolkit/views/layout/lines.py +30 -0
- pyqttoolkit/views/layout/link.py +38 -0
- pyqttoolkit/views/line_edit.py +61 -0
- pyqttoolkit/views/linkable.py +49 -0
- pyqttoolkit/views/list_view.py +43 -0
- pyqttoolkit/views/main_window.py +91 -0
- pyqttoolkit/views/metadata_selector.py +84 -0
- pyqttoolkit/views/modulewindow.py +81 -0
- pyqttoolkit/views/plot/__init__.py +26 -0
- pyqttoolkit/views/plot/legend_control.py +121 -0
- pyqttoolkit/views/plot/matplotlib/__init__.py +23 -0
- pyqttoolkit/views/plot/matplotlib/base.py +1056 -0
- pyqttoolkit/views/plot/matplotlib/colorbar.py +75 -0
- pyqttoolkit/views/plot/matplotlib/colormap.py +48 -0
- pyqttoolkit/views/plot/matplotlib/font.py +55 -0
- pyqttoolkit/views/plot/plot_options.py +211 -0
- pyqttoolkit/views/plot/plot_toolbar_options.py +133 -0
- pyqttoolkit/views/plot/size.py +41 -0
- pyqttoolkit/views/plot/tool_type.py +26 -0
- pyqttoolkit/views/plot/toolbar.py +100 -0
- pyqttoolkit/views/popout.py +41 -0
- pyqttoolkit/views/popup.py +41 -0
- pyqttoolkit/views/property_tree.py +26 -0
- pyqttoolkit/views/shrunk_push_button.py +34 -0
- pyqttoolkit/views/steppable_combo_box.py +67 -0
- pyqttoolkit/views/styleable.py +37 -0
- pyqttoolkit/views/table.py +146 -0
- pyqttoolkit/views/text_edit.py +31 -0
- pyqttoolkit/views/tool_window.py +35 -0
- pyqttoolkit-0.9.1.dist-info/METADATA +69 -0
- pyqttoolkit-0.9.1.dist-info/RECORD +133 -0
- pyqttoolkit-0.9.1.dist-info/WHEEL +5 -0
- pyqttoolkit-0.9.1.dist-info/licenses/LICENSE.txt +674 -0
- pyqttoolkit-0.9.1.dist-info/licenses/LICENSE_SHORT +16 -0
- 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')
|