pyqt-code-editor 0.0.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 (73) hide show
  1. pyqt_code_editor/__init__.py +5 -0
  2. pyqt_code_editor/_settings.py +232 -0
  3. pyqt_code_editor/app.py +438 -0
  4. pyqt_code_editor/code_editors/__init__.py +1 -0
  5. pyqt_code_editor/code_editors/code_editors.py +33 -0
  6. pyqt_code_editor/code_editors/languages/__init__.py +0 -0
  7. pyqt_code_editor/code_editors/languages/generic.py +11 -0
  8. pyqt_code_editor/code_editors/languages/markdown.py +11 -0
  9. pyqt_code_editor/code_editors/languages/python.py +13 -0
  10. pyqt_code_editor/code_editors/languages/text.py +9 -0
  11. pyqt_code_editor/components/__init__.py +0 -0
  12. pyqt_code_editor/components/editor_panel.py +399 -0
  13. pyqt_code_editor/components/find_in_files.py +199 -0
  14. pyqt_code_editor/components/jupyter_console.py +487 -0
  15. pyqt_code_editor/components/project_explorer.py +515 -0
  16. pyqt_code_editor/components/settings_panel.py +162 -0
  17. pyqt_code_editor/components/sigmund.py +75 -0
  18. pyqt_code_editor/components/workspace_explorer.py +162 -0
  19. pyqt_code_editor/mixins/__init__.py +15 -0
  20. pyqt_code_editor/mixins/auto_pair.py +201 -0
  21. pyqt_code_editor/mixins/base.py +142 -0
  22. pyqt_code_editor/mixins/check.py +94 -0
  23. pyqt_code_editor/mixins/comment.py +91 -0
  24. pyqt_code_editor/mixins/complete.py +482 -0
  25. pyqt_code_editor/mixins/execute.py +182 -0
  26. pyqt_code_editor/mixins/file_link.py +166 -0
  27. pyqt_code_editor/mixins/line_number.py +206 -0
  28. pyqt_code_editor/mixins/python_auto_indent.py +239 -0
  29. pyqt_code_editor/mixins/search_replace.py +396 -0
  30. pyqt_code_editor/mixins/shortcuts.py +228 -0
  31. pyqt_code_editor/mixins/smart_backspace_delete.py +120 -0
  32. pyqt_code_editor/mixins/symbols.py +45 -0
  33. pyqt_code_editor/mixins/theme.py +99 -0
  34. pyqt_code_editor/mixins/widgets/__init__.py +0 -0
  35. pyqt_code_editor/mixins/zoom.py +51 -0
  36. pyqt_code_editor/python_utils.py +936 -0
  37. pyqt_code_editor/signal_router.py +116 -0
  38. pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
  39. pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
  40. pyqt_code_editor/syntax_highlighters/languages/generic.py +86 -0
  41. pyqt_code_editor/syntax_highlighters/languages/python.py +62 -0
  42. pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +26 -0
  43. pyqt_code_editor/themes.py +36 -0
  44. pyqt_code_editor/utils.py +96 -0
  45. pyqt_code_editor/watchdog.py +141 -0
  46. pyqt_code_editor/widgets/__init__.py +6 -0
  47. pyqt_code_editor/widgets/calltip_widget.py +44 -0
  48. pyqt_code_editor/widgets/completion_popup.py +89 -0
  49. pyqt_code_editor/widgets/dock.py +15 -0
  50. pyqt_code_editor/widgets/quick_open_dialog.py +196 -0
  51. pyqt_code_editor/widgets/quick_open_file_dialog.py +32 -0
  52. pyqt_code_editor/widgets/quick_symbol_dialog.py +13 -0
  53. pyqt_code_editor/widgets/search_replace_frame.py +128 -0
  54. pyqt_code_editor/widgets/tab_splitter.py +22 -0
  55. pyqt_code_editor/widgets/tabbed_editor.py +351 -0
  56. pyqt_code_editor/worker/__init__.py +0 -0
  57. pyqt_code_editor/worker/languages/__init__.py +0 -0
  58. pyqt_code_editor/worker/languages/generic.py +15 -0
  59. pyqt_code_editor/worker/languages/python.py +17 -0
  60. pyqt_code_editor/worker/manager.py +113 -0
  61. pyqt_code_editor/worker/process.py +128 -0
  62. pyqt_code_editor/worker/providers/__init__.py +1 -0
  63. pyqt_code_editor/worker/providers/codestral.py +56 -0
  64. pyqt_code_editor/worker/providers/jedi.py +115 -0
  65. pyqt_code_editor/worker/providers/ruff.py +45 -0
  66. pyqt_code_editor/worker/providers/sigmund.py +29 -0
  67. pyqt_code_editor/worker/providers/symbol.py +78 -0
  68. pyqt_code_editor/worker/settings.py +1 -0
  69. pyqt_code_editor-0.0.1.dist-info/METADATA +69 -0
  70. pyqt_code_editor-0.0.1.dist-info/RECORD +73 -0
  71. pyqt_code_editor-0.0.1.dist-info/WHEEL +4 -0
  72. pyqt_code_editor-0.0.1.dist-info/entry_points.txt +4 -0
  73. pyqt_code_editor-0.0.1.dist-info/licenses/COPYING +674 -0
@@ -0,0 +1,228 @@
1
+ import logging
2
+ from qtpy.QtWidgets import QShortcut
3
+ from qtpy.QtGui import QTextCursor, QKeySequence
4
+ from qtpy.QtCore import Qt, QEvent
5
+ from .. import settings
6
+ logger = logging.getLogger(__name__)
7
+ logging.basicConfig(level=logging.INFO, force=True)
8
+
9
+
10
+ class Shortcuts:
11
+ """
12
+ Mixin that adds common editor shortcuts to a QPlainTextEdit.
13
+ """
14
+ def __init__(self, *args, **kwargs):
15
+ super().__init__(*args, **kwargs)
16
+ # Move line up
17
+ self._shortcut_move_line_up = QShortcut(
18
+ QKeySequence(settings.shortcut_move_line_up), self)
19
+ self._shortcut_move_line_up.setContext(Qt.WidgetShortcut)
20
+ self._shortcut_move_line_up.activated.connect(self._move_line_up)
21
+
22
+ # Move line down
23
+ self._shortcut_move_line_down = QShortcut(
24
+ QKeySequence(settings.shortcut_move_line_down), self)
25
+ self._shortcut_move_line_down.setContext(Qt.WidgetShortcut)
26
+ self._shortcut_move_line_down.activated.connect(self._move_line_down)
27
+
28
+ # Duplicate line
29
+ self._shortcut_duplicate_line = QShortcut(
30
+ QKeySequence(settings.shortcut_duplicate_line), self)
31
+ self._shortcut_duplicate_line.setContext(Qt.WidgetShortcut)
32
+ self._shortcut_duplicate_line.activated.connect(self._duplicate_line)
33
+
34
+ def eventFilter(self, obj, event):
35
+ """Filters out and handles the 'Cut' shortcut, cross-platform."""
36
+ if obj == self and event.type() == QEvent.KeyPress:
37
+ if event.matches(QKeySequence.Cut):
38
+ if not self.textCursor().hasSelection():
39
+ self._delete_current_line()
40
+ return False
41
+ return super().eventFilter(obj, event)
42
+
43
+ def _delete_current_line(self):
44
+ """
45
+ Removes the current line, preserving undo/redo.
46
+ """
47
+ cursor = self.textCursor()
48
+ start, end = self._current_line_bounds(cursor)
49
+ doc_length = self.document().characterCount()
50
+
51
+ cursor.beginEditBlock()
52
+ # We want to remove the trailing or preceding newline, but not both.
53
+ if end < doc_length - 1:
54
+ end += 1
55
+ elif start > 0:
56
+ start -= 1
57
+ cursor.setPosition(start)
58
+ cursor.setPosition(end, QTextCursor.KeepAnchor)
59
+ cursor.removeSelectedText()
60
+ cursor.endEditBlock()
61
+
62
+ def _current_line_bounds(self, cursor=None):
63
+ """
64
+ Return (start, end) positions of the current line in the document.
65
+ """
66
+ if cursor is None:
67
+ cursor = self.textCursor()
68
+ original_pos = cursor.position()
69
+
70
+ cursor.movePosition(QTextCursor.StartOfBlock)
71
+ start = cursor.position()
72
+ cursor.movePosition(QTextCursor.EndOfBlock)
73
+ end = cursor.position()
74
+
75
+ # Restore cursor
76
+ cursor.setPosition(original_pos)
77
+ self.setTextCursor(cursor)
78
+ return start, end
79
+
80
+ def _move_lines_impl(self, direction: int):
81
+ """
82
+ Step 1 of the plan:
83
+ - Expand selection to cover the line before and the line after the
84
+ current selection (or the current line if nothing is selected).
85
+ - Ignore 'direction' for now.
86
+ """
87
+ doc = self.document()
88
+ cursor = self.textCursor()
89
+ cursor.beginEditBlock()
90
+
91
+ anchor = cursor.anchor()
92
+ pos = cursor.position()
93
+
94
+ # Identify which line is top vs bottom
95
+ anchor_block = doc.findBlock(anchor)
96
+ pos_block = doc.findBlock(pos)
97
+ if anchor_block.firstLineNumber() > pos_block.firstLineNumber():
98
+ anchor_block, pos_block = pos_block, anchor_block
99
+
100
+ # Expand upward if possible
101
+ prev_block = anchor_block.previous()
102
+ if prev_block.isValid():
103
+ has_preceding_line = True
104
+ anchor_block = prev_block
105
+ else:
106
+ has_preceding_line = False
107
+
108
+ # Expand downward if possible
109
+ next_block = pos_block.next()
110
+ if next_block.isValid():
111
+ has_following_line = True
112
+ pos_block = next_block
113
+ else:
114
+ has_following_line = False
115
+
116
+ # Don't move lines up or down if they're already at the start or end
117
+ if not has_preceding_line and direction < 0:
118
+ logger.info('cannot move line up')
119
+ return
120
+ if not has_following_line and direction > 0:
121
+ logger.info('cannot move line down')
122
+ return
123
+
124
+ # Build new selection from start of anchor_block to end of pos_block
125
+ start_pos = anchor_block.position()
126
+ end_pos = pos_block.position() + pos_block.length()
127
+
128
+ # Set the new selection
129
+ cursor.setPosition(start_pos)
130
+ if end_pos >= doc.characterCount():
131
+ # If the end of the selection is the end of the document, then we
132
+ # need to set it one character sooner, and manually add a newline
133
+ # to the end to compensate for this, but then not pad with a
134
+ # newline to in turn compensate for the fact that we already padded
135
+ # the selection
136
+ cursor.setPosition(end_pos - 1, QTextCursor.KeepAnchor)
137
+ selection = cursor.selectedText() + '\n'
138
+ insert_padding = ''
139
+ else:
140
+ cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
141
+ selection = cursor.selectedText()
142
+ insert_padding = '\n'
143
+
144
+ # Get the current selection, and swap the lines according to the
145
+ # direction in which we're moving. We have special cases for when there
146
+ # is no preceding or following line.
147
+ lines = selection.splitlines()
148
+ if direction < 0:
149
+ anchor -= len(lines[0]) + 1
150
+ pos -= len(lines[0]) + 1
151
+ if has_following_line:
152
+ lines = lines[1:-1] + [lines[0], lines[-1]]
153
+ else:
154
+ lines = lines[1:] + [lines[0]]
155
+ else:
156
+ anchor += len(lines[-1]) + 1
157
+ pos += len(lines[-1]) + 1
158
+ if has_preceding_line:
159
+ lines = [lines[0], lines[-1]] + lines[1:-1]
160
+ else:
161
+ lines = [lines[-1]] + lines[:-1]
162
+ cursor.removeSelectedText()
163
+ cursor.insertText('\n'.join(lines) + insert_padding)
164
+ # Restore the original selection
165
+ cursor.setPosition(anchor)
166
+ cursor.setPosition(pos, QTextCursor.KeepAnchor)
167
+ self.setTextCursor(cursor)
168
+ cursor.endEditBlock()
169
+ self.refresh()
170
+
171
+ def _move_line_up(self):
172
+ """Move the currently selected lines (or single line) up by one line."""
173
+ self._move_lines_impl(direction=-1)
174
+
175
+ def _move_line_down(self):
176
+ """Move the currently selected lines (or single line) down by one line."""
177
+ self._move_lines_impl(direction=1)
178
+
179
+ def _duplicate_line(self):
180
+ """
181
+ If there is no selection, duplicate the current block. If there is
182
+ a selection, all blocks that are part of the selection should be duplicated.
183
+ The anchor and cursor should be preserved on the lower of the duplicated blocks.
184
+ In other words, it should appear as if the duplication is inserted above.
185
+ """
186
+ cursor = self.textCursor()
187
+ cursor.beginEditBlock()
188
+
189
+ # 1) Remember original anchor and cursor positions
190
+ original_anchor = cursor.anchor()
191
+ original_position = cursor.position()
192
+
193
+ # 2) Determine start_pos and end_pos based on anchor / cursor
194
+ start_pos = min(original_anchor, original_position)
195
+ end_pos = max(original_anchor, original_position)
196
+
197
+ # 3) Move the cursor to the start of the block that contains start_pos
198
+ cursor.setPosition(start_pos)
199
+ cursor.movePosition(cursor.StartOfBlock)
200
+ start_block_pos = cursor.position()
201
+
202
+ # 4) Move the cursor to the end of the block containing end_pos (keeping anchor)
203
+ cursor.setPosition(end_pos, cursor.KeepAnchor)
204
+ cursor.movePosition(cursor.EndOfBlock, cursor.KeepAnchor)
205
+ end_block_pos = cursor.position()
206
+
207
+ # 5) The text that is now selected should be duplicated
208
+ duplication_text = cursor.selectedText()
209
+ # QPlainTextEdit often uses U+2029 for newlines
210
+ duplication_text = duplication_text.replace(u'\u2029', '\n')
211
+
212
+ # 6) Insert '\n' + duplication_text just after the selection
213
+ # But first, move cursor to the end of the selected blocks without a selection
214
+ cursor.setPosition(end_block_pos, cursor.MoveAnchor)
215
+ # Now insert a new line plus the duplicated text
216
+ cursor.insertText('\n' + duplication_text)
217
+
218
+ # Figure out how many characters were inserted
219
+ inserted_length = 1 + len(duplication_text)
220
+
221
+ # 7) Restore anchor/cursor so that they point to the *new* lines (the lower portion).
222
+ # According to your docstring, we want the final “visual” anchor/cursor to effectively
223
+ # be as if the duplication is inserted above, leaving anchor/cursor on the newly inserted lines.
224
+ cursor.setPosition(original_anchor + inserted_length, cursor.MoveAnchor)
225
+ cursor.setPosition(original_position + inserted_length, cursor.KeepAnchor)
226
+
227
+ self.setTextCursor(cursor)
228
+ cursor.endEditBlock()
@@ -0,0 +1,120 @@
1
+ from qtpy.QtCore import Qt
2
+ from qtpy.QtGui import QKeyEvent, QTextCursor
3
+ import logging
4
+ logging.basicConfig(level=logging.INFO, force=True)
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ class SmartBackspaceDelete:
9
+ """
10
+ This is meant to be used as a mixin for QPlainTextEdit to enhance the
11
+ functionality of backspace and delete in a code editor. It re-implements
12
+ keyPressEvent(), supporting the following behavior:
13
+
14
+ 1) Backspace anywhere in the whitespace at the end of a line:
15
+ - Full right-trim of the whitespace for that line (including whitespace
16
+ before the cursor).
17
+ - Place the cursor at the end of the trimmed line.
18
+ 2) Backspace at the very start of a line:
19
+ - Delete the preceding newline and trailing whitespace on the previous
20
+ line (up to the first non-whitespace or newline).
21
+ - Place the cursor at the end of the trimmed preceding line.
22
+ 3) Delete anywhere in the whitespace at the end of a line (except the
23
+ very last position):
24
+ - Right-trim from the cursor position onward.
25
+ - The cursor does not move, so it can remain at the same block/column
26
+ index once the whitespace is removed.
27
+ 4) Delete at the very end of a line:
28
+ - Delete the next newline and the leading whitespace on the next line
29
+ up to the first non-whitespace or newline.
30
+ - Keep the cursor where it was, effectively pulling up the next line.
31
+ """
32
+
33
+ def keyPressEvent(self, event: QKeyEvent) -> None:
34
+ """
35
+ Handle the specific 'backspace anywhere in the whitespace at the
36
+ end of a line' logic. If not applicable, fall back to default behavior.
37
+ """
38
+ c = self.textCursor()
39
+ # We don't handle special cases if there is a selection
40
+ if c.hasSelection():
41
+ super().keyPressEvent(event)
42
+ return
43
+ key = event.key()
44
+ block = c.block()
45
+ pos = c.positionInBlock()
46
+ text = block.text()
47
+ block_length = len(text)
48
+ stripped_block_length = len(text.rstrip())
49
+ in_trailing_whitespace = text[pos:].isspace()
50
+ has_trailing_whitespace = block_length > stripped_block_length
51
+ at_block_end = pos == block_length
52
+ # We don't do anything with empty lines unless we're at the start of the
53
+ # block
54
+ if text.isspace() and pos > 0:
55
+ super().keyPressEvent(event)
56
+ return
57
+ # If we're at the very start of the line, then we're also eating up the
58
+ # trailing whitespace of the previous line. We do this by first moving to
59
+ # the end of the previous line, then handling this case recursively, and
60
+ # finally deleting the newline.
61
+ if key == Qt.Key_Backspace and pos == 0 and block.previous().isValid():
62
+ logger.info('moving to end of previous block')
63
+ c.movePosition(QTextCursor.PreviousBlock, QTextCursor.MoveAnchor)
64
+ c.movePosition(QTextCursor.EndOfBlock, QTextCursor.MoveAnchor)
65
+ self.setTextCursor(c)
66
+ # We only delete trailing whitespace if the previous line has
67
+ # trailing whitespace
68
+ prev_text = c.block().text()
69
+ if prev_text.rstrip() != prev_text:
70
+ logger.info('removing trailing whitespace from previous block')
71
+ self.keyPressEvent(event)
72
+ else:
73
+ logger.info('previous block doesn\'t have trailing whitespace')
74
+ logger.info('removing neweline')
75
+ c = self.textCursor()
76
+ c.movePosition(QTextCursor.NextBlock, QTextCursor.KeepAnchor)
77
+ c.removeSelectedText()
78
+ self.setTextCursor(c)
79
+ return
80
+ # If we're in the whitespace at the end of the block, we delete all
81
+ # trailing whitespace.
82
+ if key == Qt.Key_Backspace and has_trailing_whitespace and \
83
+ (in_trailing_whitespace or at_block_end):
84
+ n_trim = block_length - stripped_block_length
85
+ logger.info(f'trimming {n_trim} whitespace from end of block')
86
+ c.movePosition(QTextCursor.EndOfBlock)
87
+ c.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, n_trim)
88
+ c.removeSelectedText()
89
+ self.setTextCursor(c)
90
+ event.accept()
91
+ return
92
+ # If we're in the whitespace at the end of the block, we delete all
93
+ # trailing whitespace that follows the cursor.
94
+ if key == Qt.Key_Delete and in_trailing_whitespace:
95
+ n_trim = block_length - pos
96
+ logger.info(f'trimming {n_trim} whitespace from end of block')
97
+ c.movePosition(QTextCursor.EndOfBlock)
98
+ c.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, n_trim)
99
+ c.removeSelectedText()
100
+ self.setTextCursor(c)
101
+ event.accept()
102
+ return
103
+ # If we're at the end of a line, then delete removes the next line break
104
+ # and the leading whitespace on the next line. We do this by first moving
105
+ # to the next block, calculating how much leading whitespace there is,
106
+ # then getting a fresh cursor and removing this leading whitespace plus
107
+ # one character for the newline.
108
+ if key == Qt.Key_Delete and at_block_end and block.next().isValid():
109
+ c.movePosition(QTextCursor.NextBlock, QTextCursor.MoveAnchor)
110
+ next_text = c.block().text()
111
+ n_trim = len(next_text) - len(next_text.lstrip()) + 1
112
+ c = self.textCursor()
113
+ c.movePosition(
114
+ QTextCursor.NextCharacter, QTextCursor.KeepAnchor, n_trim)
115
+ c.removeSelectedText()
116
+ self.setTextCursor(c)
117
+ event.accept()
118
+ return
119
+ # In any situation, fall back
120
+ super().keyPressEvent(event)
@@ -0,0 +1,45 @@
1
+ import logging
2
+ from qtpy.QtGui import QShortcut
3
+ from qtpy.QtCore import Qt, Signal
4
+ from .. import settings
5
+ from ..widgets import QuickSymbolDialog
6
+ logging.basicConfig(level=logging.INFO, force=True)
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class Symbols:
11
+ """
12
+ A mixin for QPlainTextEdit that provides all symbols (functions, classes, etc.)
13
+ for the current file.
14
+ """
15
+ symbols_available = Signal(list)
16
+
17
+ def __init__(self, *args, **kwargs):
18
+ super().__init__(*args, **kwargs)
19
+ logger.info("Initializing symbols")
20
+ self.symbol_shortcut = QShortcut(settings.shortcut_symbols, self)
21
+ self.symbol_shortcut.setContext(Qt.WidgetWithChildrenShortcut)
22
+ self.symbol_shortcut.activated.connect(self.request_symbols)
23
+
24
+ def request_symbols(self):
25
+ logger.info("Requesting symbols")
26
+ code = self.toPlainText()
27
+ self.send_worker_request(action='symbols', code=code,
28
+ path=self.code_editor_file_path,
29
+ language=self.code_editor_language)
30
+
31
+ def handle_worker_result(self, action, result):
32
+ """Check for completion or calltip results from the external worker."""
33
+ super().handle_worker_result(action, result)
34
+ if action != 'symbols':
35
+ return
36
+ QuickSymbolDialog(self, result['symbols'], self.jump_to_symbol).exec()
37
+
38
+ def jump_to_symbol(self, symbol):
39
+ block_nr = symbol['line'] - 1
40
+ cursor = self.textCursor()
41
+ # Jump to block number
42
+ cursor.setPosition(self.document().findBlockByNumber(block_nr).position())
43
+ self.setTextCursor(cursor)
44
+ self.centerCursor()
45
+ self.ensureCursorVisible()
@@ -0,0 +1,99 @@
1
+ from pygments.token import Token
2
+ from .. import settings
3
+ from ..syntax_highlighters.syntax_highlighter import create_syntax_highlighter
4
+ import logging
5
+ from qtpy.QtGui import QPainter, QColor, QFont, QFontMetrics
6
+ from qtpy.QtWidgets import QPlainTextEdit
7
+
8
+ logging.basicConfig(level=logging.INFO, force=True)
9
+ logger = logging.getLogger(__name__)
10
+
11
+ class Theme:
12
+ """
13
+ Mixin for QPlainTextEdit that provides theming. It handles the following:
14
+
15
+ - Apply syntax highlighting
16
+ - Set a stylesheet
17
+ - Set code_editor_colors property so that other mixins can use it
18
+ - Toggle visibility of character ruler (based on settings.character_ruler)
19
+ - Toggle word wrap (based on settings.word_wrap)
20
+ """
21
+
22
+ def __init__(self, *args, **kwargs):
23
+ super().__init__(*args, **kwargs)
24
+ self._highlighter = create_syntax_highlighter(
25
+ self.code_editor_language, self.document(),
26
+ color_scheme=settings.color_scheme)
27
+ style = self._highlighter._style
28
+ self.code_editor_colors = {
29
+ 'background': style.background_color,
30
+ 'highlight': style.highlight_color,
31
+ 'text': '#' + style.style_for_token(Token.Text)['color'],
32
+ 'line-number': '#' + style.style_for_token(Token.Comment)['color'],
33
+ 'border': '#' + style.style_for_token(Token.Comment)['color']
34
+ }
35
+ self._apply_stylesheet()
36
+ self._apply_word_wrap()
37
+ self._apply_tab_width()
38
+
39
+ def refresh(self):
40
+ super().refresh()
41
+ self._highlighter.rehighlight()
42
+
43
+ def update_theme(self):
44
+ super().update_theme()
45
+ self._apply_stylesheet()
46
+ self._apply_word_wrap()
47
+ self._apply_tab_width
48
+ self.viewport().update()
49
+
50
+ def _apply_stylesheet(self):
51
+ stylesheet = f"""
52
+ QPlainTextEdit {{
53
+ background-color: {self.code_editor_colors['background']};
54
+ font: {settings.font_size}pt '{settings.font_family}';
55
+ color: {self.code_editor_colors['text']};
56
+ }}
57
+ QToolTip {{
58
+ color: {self.code_editor_colors['text']};
59
+ background-color: {self.code_editor_colors['background']};
60
+ font: {settings.font_size}pt '{settings.font_family}';
61
+ border-color: {self.code_editor_colors['border']};
62
+ border-width: 1px;
63
+ border-style: solid;
64
+ border-radius: 4px;
65
+ padding: 4px;
66
+ }}
67
+ """
68
+ self.setStyleSheet(stylesheet)
69
+
70
+ def paintEvent(self, event):
71
+ super().paintEvent(event)
72
+ if settings.character_ruler:
73
+ char_width = self.fontMetrics().width("x")
74
+ x_pos = int(char_width * settings.character_ruler + self.contentOffset().x())
75
+ y_pos = self.viewport().height()
76
+ painter = QPainter(self.viewport())
77
+ painter.setPen(QColor(self.code_editor_colors['line-number']))
78
+ painter.drawLine(x_pos, 0, x_pos, y_pos)
79
+
80
+ def _apply_word_wrap(self):
81
+ """
82
+ Toggles word wrap mode based on settings.word_wrap
83
+ """
84
+ if settings.word_wrap:
85
+ self.setLineWrapMode(QPlainTextEdit.WidgetWidth)
86
+ else:
87
+ self.setLineWrapMode(QPlainTextEdit.NoWrap)
88
+
89
+ def _apply_tab_width(self):
90
+ """Sets the tab-stop distance. This uses a QFont rather than relying on
91
+ the standard font metrics, because these are not immediately applied on
92
+ initialization.
93
+ """
94
+ font = QFont()
95
+ font.setFamily(settings.font_family)
96
+ font.setPointSize(settings.font_size)
97
+ self.setTabStopDistance(
98
+ settings.tab_width * QFontMetrics(font).horizontalAdvance(' '))
99
+
File without changes
@@ -0,0 +1,51 @@
1
+ from qtpy.QtCore import Qt
2
+ from .. import settings
3
+
4
+
5
+ class Zoom:
6
+ """
7
+ A mixin that implements zoom in/out with:
8
+ - Ctrl+Plus
9
+ - Ctrl+Minus
10
+ - Ctrl+ScrollUp/Down
11
+ It expects the QPlainTextEdit to have a 'code_editor_font_size'
12
+ attribute and a 'setFont(...)' method.
13
+ """
14
+
15
+ def keyPressEvent(self, event):
16
+ if (event.modifiers() & Qt.ControlModifier):
17
+ if event.key() == Qt.Key_Plus:
18
+ self.zoom_in()
19
+ return
20
+ elif event.key() == Qt.Key_Minus:
21
+ self.zoom_out()
22
+ return
23
+
24
+ super().keyPressEvent(event)
25
+
26
+ def wheelEvent(self, event):
27
+ """
28
+ Zoom in/out when Ctrl is pressed and the user scrolls the wheel.
29
+ """
30
+ if event.modifiers() & Qt.ControlModifier:
31
+ # angleDelta().y() is typically 120 per step on many systems.
32
+ delta = event.angleDelta().y()
33
+ if delta > 0:
34
+ self.zoom_in()
35
+ else:
36
+ self.zoom_out()
37
+ # Do not pass event along so normal scrolling doesn't occur
38
+ event.accept()
39
+ else:
40
+ super().wheelEvent(event)
41
+
42
+ def zoom_in(self):
43
+ """ Increase the editor's font size by 1 point. """
44
+ settings.font_size += 1
45
+ self.update_theme()
46
+
47
+ def zoom_out(self):
48
+ """ Decrease the editor's font size by 1 point, down to a minimum of 1. """
49
+ if settings.font_size > 1:
50
+ settings.font_size -= 1
51
+ self.update_theme()