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,112 @@
|
|
|
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
|
+
import weakref
|
|
18
|
+
from PyQt5.QtCore import pyqtSignal, QItemSelectionModel, QAbstractItemModel, QStringListModel, Qt
|
|
19
|
+
from PyQt5.QtWidgets import QWidget, QGridLayout
|
|
20
|
+
|
|
21
|
+
from pyqttoolkit.properties import auto_property
|
|
22
|
+
from pyqttoolkit.models.roles import DataRole
|
|
23
|
+
from .list_view import ListView
|
|
24
|
+
|
|
25
|
+
class BulkValueSelectorWidget(QWidget):
|
|
26
|
+
def __init__(self, parent):
|
|
27
|
+
QWidget.__init__(self, parent)
|
|
28
|
+
self._value_selector = ListView(self)
|
|
29
|
+
self._value_selector.setSelectionMode(ListView.ExtendedSelection)
|
|
30
|
+
self._value_selector.selectedItemsChanged.connect(self._handle_selected_items_changed())
|
|
31
|
+
|
|
32
|
+
self._layout = QGridLayout(self)
|
|
33
|
+
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
34
|
+
self._layout.addWidget(self._value_selector)
|
|
35
|
+
self.setLayout(self._layout)
|
|
36
|
+
|
|
37
|
+
valuesChanged = pyqtSignal(QAbstractItemModel)
|
|
38
|
+
selectedValuesChanged = pyqtSignal(list)
|
|
39
|
+
dataCommitted = pyqtSignal(list)
|
|
40
|
+
|
|
41
|
+
def _handle_selected_items_changed(self):
|
|
42
|
+
widget_ref = weakref.ref(self)
|
|
43
|
+
def _():
|
|
44
|
+
widget = widget_ref()
|
|
45
|
+
if widget:
|
|
46
|
+
widget.selectedValuesChanged.emit(widget.selectedValues)
|
|
47
|
+
return _
|
|
48
|
+
|
|
49
|
+
@auto_property(QAbstractItemModel)
|
|
50
|
+
def values(self):
|
|
51
|
+
return self._value_selector.model()
|
|
52
|
+
|
|
53
|
+
@values.setter
|
|
54
|
+
def values(self, value):
|
|
55
|
+
self._value_selector.setModel(value)
|
|
56
|
+
self._value_selector.selectAll()
|
|
57
|
+
self.valuesChanged.emit(value)
|
|
58
|
+
self.selectedValuesChanged.emit(self.selectedValues)
|
|
59
|
+
|
|
60
|
+
def setValues(self, values):
|
|
61
|
+
self.values = values
|
|
62
|
+
values.dataChanged.connect(self._handle_values_changed)
|
|
63
|
+
if hasattr(values, 'selectionModel'):
|
|
64
|
+
self._value_selector.setSelectionModel(values.selectionModel)
|
|
65
|
+
|
|
66
|
+
def selectAll(self):
|
|
67
|
+
self._value_selector.selectAll()
|
|
68
|
+
|
|
69
|
+
@auto_property(list)
|
|
70
|
+
def selectedValues(self):
|
|
71
|
+
if isinstance(self._value_selector.model(), QStringListModel):
|
|
72
|
+
string_list = self._value_selector.model().stringList()
|
|
73
|
+
return [
|
|
74
|
+
string_list[i.row()]\
|
|
75
|
+
for i in self._value_selector.selectedIndexes() if i.row() < self._value_selector.model().rowCount()
|
|
76
|
+
]
|
|
77
|
+
return [self._value_selector.model().data(i, DataRole) for i in self._value_selector.selectedIndexes() if i.row() < self._value_selector.model().rowCount()]
|
|
78
|
+
|
|
79
|
+
@selectedValues.setter
|
|
80
|
+
def selectedValues(self, values):
|
|
81
|
+
select_values = (
|
|
82
|
+
self._select_values_stringlist if isinstance(self._value_selector.model(), QStringListModel)
|
|
83
|
+
else self._select_values if self._value_selector.model()
|
|
84
|
+
else None
|
|
85
|
+
)
|
|
86
|
+
if select_values and values != self.selectedValues:
|
|
87
|
+
signals_blocked = self.blockSignals(True)
|
|
88
|
+
self._value_selector.clearSelection()
|
|
89
|
+
select_values(values)
|
|
90
|
+
self.blockSignals(signals_blocked)
|
|
91
|
+
self.selectedValuesChanged.emit(self.selectedValues)
|
|
92
|
+
|
|
93
|
+
def _select_values_stringlist(self, values):
|
|
94
|
+
for value in values:
|
|
95
|
+
index = self._value_selector.model().stringList().index(value)
|
|
96
|
+
model_index = self._value_selector.model().createIndex(index, 0)
|
|
97
|
+
self._value_selector.selectionModel().setCurrentIndex(model_index, QItemSelectionModel.Rows | QItemSelectionModel.Select)
|
|
98
|
+
|
|
99
|
+
def _select_values(self, values):
|
|
100
|
+
model = self._value_selector.model()
|
|
101
|
+
for i in range(model.rowCount()):
|
|
102
|
+
model_index = model.createIndex(i, 0)
|
|
103
|
+
data = model.data(model_index, DataRole)
|
|
104
|
+
if data in values:
|
|
105
|
+
self._value_selector.selectionModel().setCurrentIndex(model_index, QItemSelectionModel.Rows | QItemSelectionModel.Select)
|
|
106
|
+
|
|
107
|
+
def _handle_values_changed(self):
|
|
108
|
+
self.selectedValuesChanged.emit(self.selectedValues)
|
|
109
|
+
|
|
110
|
+
def keyPressEvent(self, event):
|
|
111
|
+
if event.key() in [Qt.Key_Enter, Qt.Key_Return]:
|
|
112
|
+
self.dataCommitted.emit(self.selectedValues)
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
|
18
|
+
from PyQt5.QtWidgets import QCheckBox
|
|
19
|
+
|
|
20
|
+
from pyqttoolkit.properties import AutoProperty
|
|
21
|
+
from pyqttoolkit.views.styleable import make_styleable
|
|
22
|
+
|
|
23
|
+
class BindableCheckBox(QCheckBox):
|
|
24
|
+
def __init__(self, label, parent):
|
|
25
|
+
QCheckBox.__init__(self, label, parent)
|
|
26
|
+
self.clicked.connect(self._handle_clicked)
|
|
27
|
+
self.checkedChanged.connect(self.setChecked)
|
|
28
|
+
|
|
29
|
+
checkedChanged = pyqtSignal(bool)
|
|
30
|
+
|
|
31
|
+
checked = AutoProperty(bool)
|
|
32
|
+
|
|
33
|
+
def _handle_clicked(self):
|
|
34
|
+
self.checked = self.isChecked()
|
|
35
|
+
|
|
36
|
+
BindableCheckBox = make_styleable(BindableCheckBox)
|
|
@@ -0,0 +1,18 @@
|
|
|
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 .code_edit import CodeEdit, PythonCodeEdit
|
|
18
|
+
from .python_syntax import PythonHighlighter
|
|
@@ -0,0 +1,268 @@
|
|
|
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
|
+
import weakref
|
|
18
|
+
from PyQt5.QtCore import Qt, pyqtSignal, QSize, QRect
|
|
19
|
+
from PyQt5.QtWidgets import (
|
|
20
|
+
QWidget, QGridLayout, QPlainTextEdit,
|
|
21
|
+
QSplitter, QTextBrowser,
|
|
22
|
+
QLabel, QTextEdit
|
|
23
|
+
)
|
|
24
|
+
from PyQt5.QtGui import (
|
|
25
|
+
QPainter, QColor, QPalette, QFont,
|
|
26
|
+
QTextOption, QFontMetrics, QTextFormat
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .python_syntax import PythonHighlighter
|
|
30
|
+
|
|
31
|
+
class LineNumberArea(QWidget):
|
|
32
|
+
def __init__(self, editor):
|
|
33
|
+
QWidget.__init__(self, editor)
|
|
34
|
+
self._editor = editor
|
|
35
|
+
|
|
36
|
+
def sizeHint(self):
|
|
37
|
+
return QSize(self._editor.lineNumberAreaWidth(), 0)
|
|
38
|
+
|
|
39
|
+
def paintEvent(self, event):
|
|
40
|
+
self._editor.lineNumberAreaPaintEvent(event)
|
|
41
|
+
|
|
42
|
+
class CodeTextEdit(QPlainTextEdit):
|
|
43
|
+
def __init__(self, parent):
|
|
44
|
+
QPlainTextEdit.__init__(self, parent)
|
|
45
|
+
self.document().setDefaultFont(QFont('Courier', pointSize=7))
|
|
46
|
+
self.setWordWrapMode(QTextOption.NoWrap)
|
|
47
|
+
self.setTabStopWidth(32)
|
|
48
|
+
|
|
49
|
+
self._line_number_area = LineNumberArea(self)
|
|
50
|
+
|
|
51
|
+
self.blockCountChanged.connect(self.updateLineNumberAreaWidth)
|
|
52
|
+
self.updateRequest.connect(self.updateLineNumberArea)
|
|
53
|
+
self.cursorPositionChanged.connect(self.highlightCurrentLine)
|
|
54
|
+
|
|
55
|
+
self.updateLineNumberAreaWidth(0)
|
|
56
|
+
|
|
57
|
+
self._key_press_rules = None
|
|
58
|
+
|
|
59
|
+
editComplete = pyqtSignal(str)
|
|
60
|
+
|
|
61
|
+
def focusInEvent(self, event):
|
|
62
|
+
result = QPlainTextEdit.focusInEvent(self, event)
|
|
63
|
+
self.highlightCurrentLine()
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
def focusOutEvent(self, event):
|
|
67
|
+
self.editComplete.emit(self.toPlainText())
|
|
68
|
+
result = QPlainTextEdit.focusOutEvent(self, event)
|
|
69
|
+
self.highlightCurrentLine()
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
def lineNumberAreaWidth(self):
|
|
73
|
+
digits = 1
|
|
74
|
+
block_count = max(1, self.blockCount())
|
|
75
|
+
while block_count >= 10:
|
|
76
|
+
block_count /= 10
|
|
77
|
+
digits += 1
|
|
78
|
+
|
|
79
|
+
space = 3 + self.fontMetrics().width('9') * digits
|
|
80
|
+
|
|
81
|
+
return space
|
|
82
|
+
|
|
83
|
+
def fontMetrics(self):
|
|
84
|
+
return QFontMetrics(self.document().defaultFont())
|
|
85
|
+
|
|
86
|
+
def updateLineNumberAreaWidth(self, block_count):
|
|
87
|
+
self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
|
|
88
|
+
|
|
89
|
+
def updateLineNumberArea(self, rect, dy):
|
|
90
|
+
if dy:
|
|
91
|
+
self._line_number_area.scroll(0, dy)
|
|
92
|
+
else:
|
|
93
|
+
self._line_number_area.update(0, rect.y(), self._line_number_area.width(), rect.height())
|
|
94
|
+
|
|
95
|
+
if rect.contains(self.viewport().rect()):
|
|
96
|
+
self.updateLineNumberAreaWidth(0)
|
|
97
|
+
|
|
98
|
+
def resizeEvent(self, event):
|
|
99
|
+
QPlainTextEdit.resizeEvent(self, event)
|
|
100
|
+
|
|
101
|
+
contents_rect = self.contentsRect()
|
|
102
|
+
self._line_number_area.setGeometry(QRect(contents_rect.left(), contents_rect.top(), self.lineNumberAreaWidth(), contents_rect.height()))
|
|
103
|
+
|
|
104
|
+
def lineNumberAreaPaintEvent(self, event):
|
|
105
|
+
painter = QPainter(self._line_number_area)
|
|
106
|
+
painter.fillRect(event.rect(), Qt.lightGray)
|
|
107
|
+
painter.setFont(self.document().defaultFont())
|
|
108
|
+
|
|
109
|
+
block = self.firstVisibleBlock()
|
|
110
|
+
block_number = block.blockNumber()
|
|
111
|
+
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
|
|
112
|
+
bottom = top + self.blockBoundingRect(block).height()
|
|
113
|
+
|
|
114
|
+
while block.isValid() and top <= event.rect().bottom():
|
|
115
|
+
if block.isVisible() and bottom >= event.rect().top():
|
|
116
|
+
number = str(block_number + 1)
|
|
117
|
+
painter.setPen(Qt.black)
|
|
118
|
+
painter.drawText(0, int(top), self._line_number_area.width(), self.fontMetrics().height(), Qt.AlignRight, number)
|
|
119
|
+
block = block.next()
|
|
120
|
+
top = bottom
|
|
121
|
+
bottom = top + self.blockBoundingRect(block).height()
|
|
122
|
+
block_number += 1
|
|
123
|
+
|
|
124
|
+
def highlightCurrentLine(self):
|
|
125
|
+
extra_selections = []
|
|
126
|
+
if self.hasFocus() and not self.isReadOnly():
|
|
127
|
+
selection = QTextEdit.ExtraSelection()
|
|
128
|
+
|
|
129
|
+
line_color = QColor(Qt.yellow).lighter(180)
|
|
130
|
+
|
|
131
|
+
selection.format.setBackground(line_color)
|
|
132
|
+
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
|
|
133
|
+
selection.cursor = self.textCursor()
|
|
134
|
+
selection.cursor.clearSelection()
|
|
135
|
+
extra_selections.append(selection)
|
|
136
|
+
self.setExtraSelections(extra_selections)
|
|
137
|
+
|
|
138
|
+
def setKeyPressRules(self, rules):
|
|
139
|
+
self._key_press_rules = rules
|
|
140
|
+
|
|
141
|
+
def keyPressEvent(self, event):
|
|
142
|
+
if event and self._key_press_rules:
|
|
143
|
+
next_text = self._key_press_rules.getText(event.key())
|
|
144
|
+
if next_text:
|
|
145
|
+
self.insertPlainText(next_text)
|
|
146
|
+
return
|
|
147
|
+
QPlainTextEdit.keyPressEvent(self, event)
|
|
148
|
+
|
|
149
|
+
class CodeEdit(QWidget):
|
|
150
|
+
def __init__(self, parent):
|
|
151
|
+
QWidget.__init__(self, parent)
|
|
152
|
+
|
|
153
|
+
self._text_edit = CodeTextEdit(self)
|
|
154
|
+
self._output_pane = QTextBrowser(self)
|
|
155
|
+
self._output_pane.document().setDefaultFont(QFont('Courier', pointSize=7))
|
|
156
|
+
self._output_pane.setWordWrapMode(QTextOption.NoWrap)
|
|
157
|
+
|
|
158
|
+
self._output_pane_container = QWidget(self)
|
|
159
|
+
self._output_pane_layout = QGridLayout(self._output_pane_container)
|
|
160
|
+
self._output_pane_layout.setContentsMargins(0, 16, 0, 0)
|
|
161
|
+
self._output_pane_layout.addWidget(QLabel(self.tr('Script Output'), self), 0, 0)
|
|
162
|
+
self._output_pane_layout.addWidget(self._output_pane)
|
|
163
|
+
|
|
164
|
+
self._splitter = QSplitter(Qt.Vertical, self)
|
|
165
|
+
|
|
166
|
+
self._splitter.addWidget(self._text_edit)
|
|
167
|
+
self._splitter.addWidget(self._output_pane_container)
|
|
168
|
+
self._splitter.setStretchFactor(0, 3)
|
|
169
|
+
self._splitter.setStretchFactor(1, 1)
|
|
170
|
+
|
|
171
|
+
self._layout = QGridLayout(self)
|
|
172
|
+
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
173
|
+
self._layout.addWidget(self._splitter, 0, 0)
|
|
174
|
+
|
|
175
|
+
self._text_edit.editComplete.connect(self.editComplete)
|
|
176
|
+
|
|
177
|
+
editComplete = pyqtSignal(str)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def textEdit(self):
|
|
181
|
+
return self._text_edit
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def outputPane(self):
|
|
185
|
+
return self._output_pane
|
|
186
|
+
|
|
187
|
+
def setFontSize(self, font_size):
|
|
188
|
+
self._text_edit.document().setDefaultFont(QFont('Courier', pointSize=font_size))
|
|
189
|
+
self._output_pane.document().setDefaultFont(QFont('Courier', pointSize=font_size))
|
|
190
|
+
self._text_edit.setTabStopWidth(font_size * 2)
|
|
191
|
+
self._output_pane.setTabStopWidth(font_size * 2)
|
|
192
|
+
|
|
193
|
+
def setModel(self, model):
|
|
194
|
+
self._text_edit.setPlainText(model.text)
|
|
195
|
+
model.textChanged.connect(self._set_text(self._text_edit))
|
|
196
|
+
if hasattr(model, 'output'):
|
|
197
|
+
self._output_pane.setPlainText(model.output.getvalue())
|
|
198
|
+
model.outputChanged.connect(self._set_text(self._output_pane, True))
|
|
199
|
+
self._output_pane.setVisible(True)
|
|
200
|
+
else:
|
|
201
|
+
self._output_pane_container.setVisible(False)
|
|
202
|
+
self.editComplete.connect(self._handle_text_changed(model))
|
|
203
|
+
model.validationMessageChanged.connect(self._handle_validation_message)
|
|
204
|
+
|
|
205
|
+
def _set_text(self, widget, scroll_to_end=False):
|
|
206
|
+
widget_ref = weakref.ref(widget)
|
|
207
|
+
def _(text):
|
|
208
|
+
widget_inst = widget_ref()
|
|
209
|
+
if widget_inst:
|
|
210
|
+
text = text if isinstance(text, str) else text.getvalue()
|
|
211
|
+
if widget_inst.toPlainText() != text:
|
|
212
|
+
widget_inst.setPlainText(text)
|
|
213
|
+
if scroll_to_end:
|
|
214
|
+
widget_inst.verticalScrollBar().setSliderPosition(widget_inst.verticalScrollBar().maximum())
|
|
215
|
+
return _
|
|
216
|
+
|
|
217
|
+
def _handle_text_changed(self, model):
|
|
218
|
+
model_ref = weakref.ref(model)
|
|
219
|
+
def _setter(value):
|
|
220
|
+
model_inst = model_ref()
|
|
221
|
+
if model_inst:
|
|
222
|
+
model_inst.text = value
|
|
223
|
+
return _setter
|
|
224
|
+
|
|
225
|
+
def _handle_validation_message(self, message):
|
|
226
|
+
if message:
|
|
227
|
+
palette = self.palette()
|
|
228
|
+
invalid_color = QColor(Qt.red)
|
|
229
|
+
invalid_color.setAlphaF(0.2)
|
|
230
|
+
palette.setColor(QPalette.Base, invalid_color)
|
|
231
|
+
else:
|
|
232
|
+
palette = self.style().standardPalette()
|
|
233
|
+
self._text_edit.setPalette(palette)
|
|
234
|
+
self._text_edit.setToolTip(message or None)
|
|
235
|
+
|
|
236
|
+
class PythonCodeEdit(CodeEdit):
|
|
237
|
+
def __init__(self, parent):
|
|
238
|
+
CodeEdit.__init__(self, parent)
|
|
239
|
+
self._highlighter = PythonHighlighter(self.textEdit.document())
|
|
240
|
+
self.textEdit.setKeyPressRules(PythonKeyPressRules(self.textEdit, 4))
|
|
241
|
+
|
|
242
|
+
class KeyPressRules:
|
|
243
|
+
def getText(self, key):
|
|
244
|
+
raise NotImplementedError()
|
|
245
|
+
|
|
246
|
+
class PythonKeyPressRules(KeyPressRules):
|
|
247
|
+
def __init__(self, text_edit, tab_width):
|
|
248
|
+
self._text_edit = text_edit
|
|
249
|
+
self._tab_width = tab_width
|
|
250
|
+
|
|
251
|
+
def getText(self, key):
|
|
252
|
+
if key in [Qt.Key_Tab, Qt.Key_Backtab]:
|
|
253
|
+
return ' ' * self._tab_width
|
|
254
|
+
if key in [Qt.Key_Return, Qt.Key_Enter]:
|
|
255
|
+
current_position = self._text_edit.textCursor().positionInBlock()
|
|
256
|
+
current_line = self._text_edit.textCursor().block().text()
|
|
257
|
+
if current_position == len(current_line):
|
|
258
|
+
if current_line.strip() and current_line.strip()[-1] == ':':
|
|
259
|
+
return '\n' + ' ' * self._tab_width
|
|
260
|
+
n_spaces = 0
|
|
261
|
+
for c in current_line:
|
|
262
|
+
if c == ' ':
|
|
263
|
+
n_spaces += 1
|
|
264
|
+
else:
|
|
265
|
+
break
|
|
266
|
+
return '\n' + ' ' * n_spaces
|
|
267
|
+
return None
|
|
268
|
+
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
"""
|
|
18
|
+
PythonHighlighter
|
|
19
|
+
|
|
20
|
+
Source: https://wiki.python.org/moin/PyQt/Python%20syntax%20highlighting
|
|
21
|
+
|
|
22
|
+
BSD 3-Clause License
|
|
23
|
+
|
|
24
|
+
----
|
|
25
|
+
Redistribution and use in source and binary forms, with or without
|
|
26
|
+
modification, are permitted provided that the following conditions are
|
|
27
|
+
met:
|
|
28
|
+
|
|
29
|
+
(1) Redistributions of source code must retain the above copyright
|
|
30
|
+
notice, this list of conditions and the following disclaimer.
|
|
31
|
+
|
|
32
|
+
(2) Redistributions in binary form must reproduce the above copyright
|
|
33
|
+
notice, this list of conditions and the following disclaimer in
|
|
34
|
+
the documentation and/or other materials provided with the
|
|
35
|
+
distribution.
|
|
36
|
+
|
|
37
|
+
(3)The name of the author may not be used to
|
|
38
|
+
endorse or promote products derived from this software without
|
|
39
|
+
specific prior written permission.
|
|
40
|
+
|
|
41
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
42
|
+
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
43
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
44
|
+
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
|
45
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
46
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
47
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
48
|
+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
49
|
+
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
50
|
+
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
51
|
+
POSSIBILITY OF SUCH DAMAGE.
|
|
52
|
+
"""
|
|
53
|
+
#pylint:disable=no-name-in-module
|
|
54
|
+
from PyQt5.QtCore import QRegExp
|
|
55
|
+
from PyQt5.QtGui import QColor, QTextCharFormat, QFont, QSyntaxHighlighter
|
|
56
|
+
#pylint:enable=no-name-in-module
|
|
57
|
+
|
|
58
|
+
def format(color, style=''):
|
|
59
|
+
"""Return a QTextCharFormat with the given attributes.
|
|
60
|
+
"""
|
|
61
|
+
_color = QColor()
|
|
62
|
+
_color.setNamedColor(color)
|
|
63
|
+
|
|
64
|
+
_format = QTextCharFormat()
|
|
65
|
+
_format.setForeground(_color)
|
|
66
|
+
if 'bold' in style:
|
|
67
|
+
_format.setFontWeight(QFont.Bold)
|
|
68
|
+
if 'italic' in style:
|
|
69
|
+
_format.setFontItalic(True)
|
|
70
|
+
|
|
71
|
+
return _format
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Syntax styles that can be shared by all languages
|
|
75
|
+
STYLES = {
|
|
76
|
+
'keyword': format('blue'),
|
|
77
|
+
'operator': format('red'),
|
|
78
|
+
'brace': format('darkGray'),
|
|
79
|
+
'defclass': format('black', 'bold'),
|
|
80
|
+
'string': format('magenta'),
|
|
81
|
+
'string2': format('darkMagenta'),
|
|
82
|
+
'comment': format('darkGreen', 'italic'),
|
|
83
|
+
'self': format('black', 'italic'),
|
|
84
|
+
'numbers': format('brown'),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class PythonHighlighter (QSyntaxHighlighter):
|
|
89
|
+
"""Syntax highlighter for the Python language.
|
|
90
|
+
"""
|
|
91
|
+
# Python keywords
|
|
92
|
+
keywords = [
|
|
93
|
+
'and', 'assert', 'break', 'class', 'continue', 'def',
|
|
94
|
+
'del', 'elif', 'else', 'except', 'exec', 'finally',
|
|
95
|
+
'for', 'from', 'global', 'if', 'import', 'in',
|
|
96
|
+
'is', 'lambda', 'not', 'or', 'pass', 'print',
|
|
97
|
+
'raise', 'return', 'try', 'while', 'yield',
|
|
98
|
+
'None', 'True', 'False',
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# Python operators
|
|
102
|
+
operators = [
|
|
103
|
+
'=',
|
|
104
|
+
# Comparison
|
|
105
|
+
'==', '!=', '<', '<=', '>', '>=',
|
|
106
|
+
# Arithmetic
|
|
107
|
+
'\+', '-', '\*', '/', '//', '\%', '\*\*',
|
|
108
|
+
# In-place
|
|
109
|
+
'\+=', '-=', '\*=', '/=', '\%=',
|
|
110
|
+
# Bitwise
|
|
111
|
+
'\^', '\|', '\&', '\~', '>>', '<<',
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# Python braces
|
|
115
|
+
braces = [
|
|
116
|
+
'\{', '\}', '\(', '\)', '\[', '\]',
|
|
117
|
+
]
|
|
118
|
+
def __init__(self, document):
|
|
119
|
+
QSyntaxHighlighter.__init__(self, document)
|
|
120
|
+
|
|
121
|
+
# Multi-line strings (expression, flag, style)
|
|
122
|
+
# FIXME: The triple-quotes in these two lines will mess up the
|
|
123
|
+
# syntax highlighting from this point onward
|
|
124
|
+
self.tri_single = (QRegExp("'''"), 1, STYLES['string2'])
|
|
125
|
+
self.tri_double = (QRegExp('"""'), 2, STYLES['string2'])
|
|
126
|
+
|
|
127
|
+
rules = []
|
|
128
|
+
|
|
129
|
+
# Keyword, operator, and brace rules
|
|
130
|
+
rules += [(r'\b%s\b' % w, 0, STYLES['keyword'])
|
|
131
|
+
for w in PythonHighlighter.keywords]
|
|
132
|
+
rules += [(r'%s' % o, 0, STYLES['operator'])
|
|
133
|
+
for o in PythonHighlighter.operators]
|
|
134
|
+
rules += [(r'%s' % b, 0, STYLES['brace'])
|
|
135
|
+
for b in PythonHighlighter.braces]
|
|
136
|
+
|
|
137
|
+
# All other rules
|
|
138
|
+
rules += [
|
|
139
|
+
# 'self'
|
|
140
|
+
(r'\bself\b', 0, STYLES['self']),
|
|
141
|
+
|
|
142
|
+
# Double-quoted string, possibly containing escape sequences
|
|
143
|
+
(r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']),
|
|
144
|
+
# Single-quoted string, possibly containing escape sequences
|
|
145
|
+
(r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']),
|
|
146
|
+
|
|
147
|
+
# 'def' followed by an identifier
|
|
148
|
+
(r'\bdef\b\s*(\w+)', 1, STYLES['defclass']),
|
|
149
|
+
# 'class' followed by an identifier
|
|
150
|
+
(r'\bclass\b\s*(\w+)', 1, STYLES['defclass']),
|
|
151
|
+
|
|
152
|
+
# From '#' until a newline
|
|
153
|
+
(r'#[^\n]*', 0, STYLES['comment']),
|
|
154
|
+
|
|
155
|
+
# Numeric literals
|
|
156
|
+
(r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']),
|
|
157
|
+
(r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']),
|
|
158
|
+
(r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# Build a QRegExp for each pattern
|
|
162
|
+
self.rules = [(QRegExp(pat), index, fmt)
|
|
163
|
+
for (pat, index, fmt) in rules]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def highlightBlock(self, text):
|
|
167
|
+
"""Apply syntax highlighting to the given block of text.
|
|
168
|
+
"""
|
|
169
|
+
# Do other syntax formatting
|
|
170
|
+
for expression, nth, format in self.rules:
|
|
171
|
+
index = expression.indexIn(text, 0)
|
|
172
|
+
|
|
173
|
+
while index >= 0:
|
|
174
|
+
# We actually want the index of the nth match
|
|
175
|
+
index = expression.pos(nth)
|
|
176
|
+
length = len(expression.cap(nth))
|
|
177
|
+
self.setFormat(index, length, format)
|
|
178
|
+
index = expression.indexIn(text, index + length)
|
|
179
|
+
|
|
180
|
+
self.setCurrentBlockState(0)
|
|
181
|
+
|
|
182
|
+
# Do multi-line strings
|
|
183
|
+
in_multiline = self.match_multiline(text, *self.tri_single)
|
|
184
|
+
if not in_multiline:
|
|
185
|
+
in_multiline = self.match_multiline(text, *self.tri_double)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def match_multiline(self, text, delimiter, in_state, style):
|
|
189
|
+
"""Do highlighting of multi-line strings. ``delimiter`` should be a
|
|
190
|
+
``QRegExp`` for triple-single-quotes or triple-double-quotes, and
|
|
191
|
+
``in_state`` should be a unique integer to represent the corresponding
|
|
192
|
+
state changes when inside those strings. Returns True if we're still
|
|
193
|
+
inside a multi-line string when this function is finished.
|
|
194
|
+
"""
|
|
195
|
+
# If inside triple-single quotes, start at 0
|
|
196
|
+
if self.previousBlockState() == in_state:
|
|
197
|
+
start = 0
|
|
198
|
+
add = 0
|
|
199
|
+
# Otherwise, look for the delimiter on this line
|
|
200
|
+
else:
|
|
201
|
+
start = delimiter.indexIn(text)
|
|
202
|
+
# Move past this match
|
|
203
|
+
add = delimiter.matchedLength()
|
|
204
|
+
|
|
205
|
+
# As long as there's a delimiter match on this line...
|
|
206
|
+
while start >= 0:
|
|
207
|
+
# Look for the ending delimiter
|
|
208
|
+
end = delimiter.indexIn(text, start + add)
|
|
209
|
+
# Ending delimiter on this line?
|
|
210
|
+
if end >= add:
|
|
211
|
+
length = end - start + add + delimiter.matchedLength()
|
|
212
|
+
self.setCurrentBlockState(0)
|
|
213
|
+
# No; multi-line string
|
|
214
|
+
else:
|
|
215
|
+
self.setCurrentBlockState(in_state)
|
|
216
|
+
length = len(text) - start + add
|
|
217
|
+
# Apply formatting
|
|
218
|
+
self.setFormat(start, length, style)
|
|
219
|
+
# Look for the next match
|
|
220
|
+
start = delimiter.indexIn(text, start + length)
|
|
221
|
+
|
|
222
|
+
# Return True if still inside a multi-line string, False otherwise
|
|
223
|
+
if self.currentBlockState() == in_state:
|
|
224
|
+
return True
|
|
225
|
+
else:
|
|
226
|
+
return False
|