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,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