spiceditor 0.0.4__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.

Potentially problematic release.


This version of spiceditor might be problematic. Click here for more details.

Files changed (51) hide show
  1. pyspice/__init__.py +0 -0
  2. pyspice/dialogs.py +103 -0
  3. pyspice/editor_widget.py +196 -0
  4. pyspice/file_browser.py +143 -0
  5. pyspice/highlighter.py +74 -0
  6. pyspice/line_number_text_edit.py +54 -0
  7. pyspice/magic_scrollbar.py +11 -0
  8. pyspice/main.py +435 -0
  9. pyspice/resources.py +1120 -0
  10. pyspice/spice_console.py +282 -0
  11. pyspice/spice_magic_editor.py +389 -0
  12. pyspice/splitter.py +118 -0
  13. pyspice/term.py +63 -0
  14. pyspice/textract.py +597 -0
  15. pyspice/utils.py +33 -0
  16. spiceditor/__init__.py +0 -0
  17. spiceditor/dialogs.py +103 -0
  18. spiceditor/editor_widget.py +196 -0
  19. spiceditor/file_browser.py +143 -0
  20. spiceditor/highlighter.py +74 -0
  21. spiceditor/line_number_text_edit.py +54 -0
  22. spiceditor/magic_scrollbar.py +11 -0
  23. spiceditor/main.py +435 -0
  24. spiceditor/resources.py +1120 -0
  25. spiceditor/spice_console.py +282 -0
  26. spiceditor/spice_magic_editor.py +389 -0
  27. spiceditor/splitter.py +118 -0
  28. spiceditor/term.py +63 -0
  29. spiceditor/textract.py +597 -0
  30. spiceditor/utils.py +33 -0
  31. spiceditor-0.0.4.dist-info/LICENSE +674 -0
  32. spiceditor-0.0.4.dist-info/METADATA +31 -0
  33. spiceditor-0.0.4.dist-info/RECORD +51 -0
  34. spiceditor-0.0.4.dist-info/WHEEL +5 -0
  35. spiceditor-0.0.4.dist-info/entry_points.txt +2 -0
  36. spiceditor-0.0.4.dist-info/top_level.txt +1 -0
  37. spyce/__init__.py +0 -0
  38. spyce/dialogs.py +103 -0
  39. spyce/editor_widget.py +196 -0
  40. spyce/file_browser.py +143 -0
  41. spyce/highlighter.py +74 -0
  42. spyce/line_number_text_edit.py +54 -0
  43. spyce/magic_scrollbar.py +11 -0
  44. spyce/main.py +435 -0
  45. spyce/resources.py +1120 -0
  46. spyce/spice_console.py +282 -0
  47. spyce/spice_magic_editor.py +389 -0
  48. spyce/splitter.py +118 -0
  49. spyce/term.py +63 -0
  50. spyce/textract.py +597 -0
  51. spyce/utils.py +33 -0
pyspice/__init__.py ADDED
File without changes
pyspice/dialogs.py ADDED
@@ -0,0 +1,103 @@
1
+ from PyQt5.QtCore import Qt
2
+ from PyQt5.QtWidgets import QPushButton, QTextEdit, QVBoxLayout, QDialog
3
+
4
+
5
+
6
+ class Author(QDialog):
7
+ def __init__(self):
8
+ super().__init__()
9
+ self.setLayout(QVBoxLayout())
10
+ self.setMinimumSize(400, 350)
11
+ self.setWindowTitle("About")
12
+ textEdit = QTextEdit()
13
+ textEdit.setReadOnly(True)
14
+ textEdit.setHtml("""<!DOCTYPE html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="UTF-8">
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ <title>SPICE - About</title>
20
+ <style>
21
+ body {
22
+ font-family: 'Arial', sans-serif;
23
+ background-color: #f4f4f9;
24
+ color: #333;
25
+ margin: 0;
26
+ padding: 0;
27
+ display: flex;
28
+ justify-content: center;
29
+ align-items: center;
30
+ height: 100vh;
31
+ text-align: center;
32
+ }
33
+
34
+ .container {
35
+ background-color: #ffffff;
36
+ padding: 20px;
37
+ border-radius: 8px;
38
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
39
+ width: 80%;
40
+ max-width: 600px;
41
+ }
42
+
43
+ h1 {
44
+ font-size: 3em;
45
+ color: #2c3e50;
46
+ margin-bottom: 20px;
47
+ }
48
+
49
+ p {
50
+ font-size: 1.2em;
51
+ line-height: 1.6;
52
+ }
53
+
54
+ ul {
55
+ list-style: none;
56
+ padding: 0;
57
+ }
58
+
59
+ ul li {
60
+ font-size: 1.1em;
61
+ margin: 5px 0;
62
+ }
63
+
64
+ .footer {
65
+ font-size: 1em;
66
+ color: #7f8c8d;
67
+ margin-top: 20px;
68
+ }
69
+
70
+ .footer p {
71
+ margin: 0;
72
+ }
73
+
74
+ </style>
75
+ </head>
76
+ <body>
77
+
78
+ <div class="container">
79
+ <h1>Spice</h1>
80
+ <h2><strong>Slides and Python for Interactive and Creative Education</strong></h2>
81
+
82
+ <h4><strong>Developed by:</strong></h4>
83
+ <h2>Danilo Tardioli</h2>
84
+ <h3>Email: <a href="mailto:dantard@unizar.es">dantard@unizar.es</a></h3>
85
+
86
+ <p><strong>Year:</strong> 2024</p>
87
+
88
+ <div class="footer">
89
+ <p><strong>Learn more at:</strong> <a href="https://github.com/dantard/coder">https://github.com/dantard/coder</a></p>
90
+ </div>
91
+ </div>
92
+
93
+ </body>
94
+ </html>
95
+
96
+ """)
97
+ self.layout().addWidget(textEdit)
98
+ close_button = QPushButton("Close")
99
+ close_button.setMaximumWidth(100)
100
+ # center the button
101
+ self.layout().addWidget(close_button)
102
+ self.layout().setAlignment(close_button, Qt.AlignRight)
103
+ close_button.clicked.connect(self.close)
@@ -0,0 +1,196 @@
1
+ import os
2
+
3
+ from PyQt5.QtCore import Qt
4
+ from PyQt5.QtGui import QFont, QIcon
5
+ from PyQt5.QtWidgets import QVBoxLayout, QToolBar, QStatusBar, QWidget, QComboBox, QShortcut, QTabWidget, QFileDialog, \
6
+ QApplication, QDialog, QMessageBox
7
+ from spiceditor import utils
8
+
9
+ import spiceditor.resources # noqa
10
+
11
+
12
+ class DynamicComboBox(QComboBox):
13
+ def __init__(self, folder=None, parent=None):
14
+ super().__init__(parent)
15
+ self.folder = folder
16
+ self.populate()
17
+
18
+ def set_folder(self, folder):
19
+ self.folder = folder
20
+ self.populate()
21
+
22
+
23
+ def populate(self, item=None):
24
+ self.blockSignals(True)
25
+ self.clear() # Clear the current items
26
+ self.addItem("[Free Coding]")
27
+ if self.folder is not None and os.path.exists(self.folder):
28
+ files = list(x for x in os.listdir(self.folder) if x.endswith(".py"))
29
+
30
+ files = [f for f in files if os.path.isfile(os.path.join(self.folder, f))]
31
+ files.sort()
32
+ self.addItems(files)
33
+ if item is not None:
34
+ index = self.findText(item)
35
+ if index >=0:
36
+ self.setCurrentIndex(index)
37
+ self.blockSignals(False)
38
+
39
+
40
+
41
+ class EditorWidget(QWidget):
42
+
43
+ def __init__(self, language_editor, console, config):
44
+ super().__init__()
45
+ self.path = None
46
+ self.config = config
47
+ editor = config.root().getSubSection("editor", pretty="Editor")
48
+ self.cfg_keep_code = editor.getCheckBox("keep_code",
49
+ pretty="Keep Code on Run",
50
+ default=False)
51
+ self.cfg_show_all = editor.getCheckBox("show_all",
52
+ pretty="Show all Code on Open",
53
+ default=False)
54
+ self.cfg_autocomplete = editor.getString("autocomplete",
55
+ pretty="Autocomplete",
56
+ default="")
57
+ self.cfg_delay = editor.getSlider("delay",
58
+ pretty="Delay",
59
+ min=0, max=100,
60
+ default=25,
61
+ den=1,
62
+ show_value=True,
63
+ suffix=" ms")
64
+
65
+ self.cfg_show_sb = editor.getCheckBox("show_tb",
66
+ pretty="Show Toolbar",
67
+ default=False)
68
+
69
+
70
+ self.language_editor = language_editor
71
+ self.console = console
72
+
73
+ # Left side layout
74
+ left_layout = QVBoxLayout()
75
+
76
+ self.language_editor.ctrl_enter.connect(self.execute_code)
77
+ self.language_editor.info.connect(self.update_status_bar)
78
+
79
+ bar = QToolBar()
80
+
81
+ a1 = bar.addAction("Play", self.execute_code)
82
+ a2 = bar.addAction("Clear", self.clear)
83
+ a3 = bar.addAction("Show", self.language_editor.show_code)
84
+
85
+ self.keep_banner = bar.addAction("Keep Code on Console")
86
+ self.keep_banner.setCheckable(True)
87
+ self.keep_banner.setChecked(False)
88
+
89
+ self.show_all = bar.addAction("Show all Code on Load")
90
+ self.show_all.setIcon(QIcon(":/icons/radio-button.svg"))
91
+ self.show_all.setCheckable(True)
92
+
93
+ self.text_edit_group = [a1, a2, a3, self.keep_banner]
94
+ bar.addSeparator()
95
+
96
+ left_layout.addWidget(bar)
97
+ left_layout.addWidget(self.language_editor)
98
+
99
+ self.sb = QStatusBar()
100
+ left_layout.addWidget(self.sb)
101
+ left_layout.setSpacing(0)
102
+
103
+ self.setLayout(left_layout)
104
+ self.setLayout(left_layout)
105
+
106
+ def update_config(self):
107
+ self.keep_banner.setChecked(self.cfg_keep_code.get())
108
+ self.show_all.setChecked(self.cfg_show_all.get())
109
+ self.language_editor.append_autocomplete(self.cfg_autocomplete.get())
110
+ self.language_editor.set_delay(self.cfg_delay.get())
111
+ self.language_editor.set_font_size(self.config.font_size.get() + 10)
112
+
113
+ def set_font_size(self, font_size):
114
+ self.language_editor.set_font_size(font_size)
115
+ self.console.set_font_size(font_size)
116
+
117
+ def load_program(self, path, show_all=False):
118
+ self.path = path
119
+ with open(path) as f:
120
+ self.language_editor.set_code(f.read())
121
+ self.console.clear()
122
+
123
+ if self.show_all.isChecked() or show_all:
124
+ self.show_all_code()
125
+
126
+ def save_program(self, path, save_as):
127
+ if self.path is None or save_as:
128
+ ext = self.console.get_file_extension()
129
+ filename, ok = QFileDialog.getSaveFileName(self, "Save code", filter="Language files (*" + ext + ")",
130
+ directory=path)
131
+ if not filename:
132
+ return
133
+
134
+ self.path = filename.replace(".py","") + ".py"
135
+
136
+ with open(self.path, "w") as f:
137
+ f.write(self.language_editor.toPlainText())
138
+
139
+ name = os.path.basename(self.path)
140
+ tab_wiget: QTabWidget = self.parent().parent() # noqa
141
+ index = tab_wiget.indexOf(self)
142
+ tab_wiget.setTabText(index, name)
143
+ self.sb.showMessage("Saved in " + self.path, 2000)
144
+
145
+ def clear(self):
146
+ self.language_editor.clear()
147
+ # self.console_widget.clear()
148
+
149
+ def execute_code(self):
150
+ self.language_editor.format_code()
151
+ self.console.execute(self.language_editor.toPlainText(), not self.keep_banner.isChecked())
152
+
153
+ def set_dark_mode(self, dark):
154
+ self.language_editor.set_dark_mode(dark)
155
+ color = Qt.white if dark else Qt.black
156
+ a1, a2, a3, a4 = self.text_edit_group
157
+ a1.setIcon(QIcon(utils.color(":/icons/play.svg", color)))
158
+ a2.setIcon(QIcon(utils.color(":/icons/refresh.svg", color)))
159
+ a3.setIcon(QIcon(utils.color(":/icons/download.svg", color)))
160
+ a4.setIcon(QIcon(utils.color(":/icons/hash.svg", color)))
161
+
162
+ def get_text(self):
163
+ self.language_editor.format_code()
164
+ return self.language_editor.text_edit.toPlainText()
165
+
166
+ def get_font_size(self):
167
+ return self.language_editor.font().pixelSize()
168
+
169
+ def append_autocomplete(self, value, val):
170
+ self.language_editor.append_autocomplete(value, val)
171
+
172
+ def set_delay(self, value):
173
+ self.language_editor.set_delay(value)
174
+
175
+ def show_all_code(self):
176
+ self.language_editor.show_all_code()
177
+
178
+ def set_progs_path(self, path):
179
+ value = self.prog_cb.currentText()
180
+ self.prog_cb.set_folder(path)
181
+ self.populate_progs()
182
+ self.prog_cb.setCurrentIndex(self.prog_cb.findText(value))
183
+
184
+ def update_status_bar(self, x, diff, timeout):
185
+ if self.cfg_show_sb.get_value():
186
+ if timeout != 0:
187
+ x = "{:5d} | {}".format(diff, x)
188
+ self.sb.showMessage(x, 1000)
189
+ else:
190
+ self.sb.showMessage(x)
191
+
192
+ # red if diff is negative
193
+ if diff < 10:
194
+ self.sb.setStyleSheet("color: red")
195
+ else:
196
+ self.sb.setStyleSheet("color: black")
@@ -0,0 +1,143 @@
1
+ import os
2
+ import shutil
3
+
4
+ from PyQt5 import QtGui
5
+ from PyQt5.QtCore import QObject, pyqtSignal, QDir, QItemSelectionModel, QModelIndex, Qt
6
+ from PyQt5.QtWidgets import QWidget, QTreeView, QFileSystemModel, QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QMenu, QMessageBox, QToolBar, QInputDialog
7
+
8
+
9
+ class Tree(QTreeView):
10
+ delete_requested = pyqtSignal(str)
11
+
12
+ def filter_rows(self):
13
+ for i in range(self.model().rowCount(self.rootIndex())):
14
+ child_index = self.model().index(i, 0, self.rootIndex()) # Get index of each row
15
+ filename = self.model().data(child_index) # Get file name
16
+ if filename == "__pycache__":
17
+ self.setRowHidden(i, self.rootIndex(), True)
18
+
19
+ def contextMenuEvent(self, a0: QtGui.QContextMenuEvent) -> None:
20
+ super().contextMenuEvent(a0)
21
+ indexes = self.selectedIndexes()
22
+ if indexes:
23
+ index = self.indexAt(a0.pos())
24
+ if index.isValid():
25
+ dirModel = self.model()
26
+ path = dirModel.fileInfo(index).absoluteFilePath()
27
+ menu = QMenu()
28
+ delete = menu.addAction("Delete")
29
+ res = menu.exec_(self.viewport().mapToGlobal(a0.pos()))
30
+ if res == delete:
31
+ self.delete_requested.emit(path)
32
+
33
+
34
+ class FileBrowser(QWidget):
35
+ class Signals(QObject):
36
+ file_selected = pyqtSignal(str)
37
+
38
+ def __init__(self, path, filters=None, hide_details=True):
39
+ super().__init__()
40
+ if filters is None:
41
+ filters = ["*.pdf"]
42
+ self.signals = self.Signals()
43
+ self.path = path
44
+ self.treeview = Tree()
45
+ self.treeview.delete_requested.connect(self.delete_requested)
46
+ self.dirModel = QFileSystemModel()
47
+ self.dirModel.directoryLoaded.connect(self.treeview.filter_rows)
48
+ self.dirModel.setNameFilters(filters)
49
+ self.dirModel.setNameFilterDisables(False)
50
+
51
+ self.treeview.setModel(self.dirModel)
52
+ self.treeview.setRootIndex(self.dirModel.setRootPath(path))
53
+
54
+ vlayout = QVBoxLayout(self)
55
+ vlayout.setSpacing(0)
56
+ vlayout.setContentsMargins(0, 0, 0, 0)
57
+ self.setLayout(vlayout)
58
+ #tb = QToolBar()
59
+ #tb.addAction("🗀", self.refresh)
60
+
61
+ #vlayout.addWidget(tb)
62
+
63
+ self.layout().addWidget(self.treeview)
64
+ self.treeview.selectionModel().selectionChanged.connect(self.on_current_changed)
65
+ self.treeview.doubleClicked.connect(self.on_double_clicked)
66
+
67
+ if hide_details:
68
+ for i in range(1, self.treeview.model().columnCount()):
69
+ self.treeview.header().hideSection(i)
70
+
71
+ def delete_requested(self, path):
72
+ if QMessageBox.question(self, "Delete", f"Are you sure you want to delete {path}?", QMessageBox.Yes | QMessageBox.No) == QMessageBox.No:
73
+ return
74
+ if os.path.isdir(path):
75
+ shutil.rmtree(path)
76
+ else:
77
+ os.remove(path)
78
+
79
+
80
+ def on_double_clicked(self, index):
81
+ # Map the proxy index to the source model index
82
+ path = self.dirModel.fileInfo(index).absoluteFilePath()
83
+ if os.path.isdir(path):
84
+ return
85
+ self.signals.file_selected.emit(path)
86
+
87
+ def btn_up_clicked(self):
88
+ index = self.treeview.rootIndex()
89
+ if index.isValid():
90
+ index = index.parent()
91
+ self.set_root_index(index)
92
+
93
+ def set_root(self, path):
94
+ self.treeview.setRootIndex(self.dirModel.setRootPath(path))
95
+
96
+ def set_root_index(self, index):
97
+ self.treeview.setRootIndex(index)
98
+ path = self.dirModel.fileInfo(index).absoluteFilePath()
99
+ self.label.setText(path)
100
+
101
+ def select(self, filename, emit=True):
102
+ if not emit:
103
+ self.treeview.selectionModel().blockSignals(True)
104
+ index = self.dirModel.index(filename)
105
+ indices = []
106
+ while index.isValid():
107
+ indices.append(index)
108
+ index = index.parent()
109
+
110
+ for index in reversed(indices):
111
+ self.treeview.setExpanded(index, True)
112
+
113
+ self.treeview.setCurrentIndex(index)
114
+ self.treeview.selectionModel().blockSignals(False)
115
+
116
+ def on_current_changed(self, selected, deselected):
117
+ pass
118
+ # if deselected.indexes():
119
+ # print("deselected1", deselected.indexes())
120
+ # # Check if the deselected index is valid
121
+ # for index in deselected.indexes():
122
+ # if not os.path.isfile(self.dirModel.filePath(index)):
123
+ # self.treeview.clearSelection()
124
+ # return
125
+ #
126
+ # if selected is None or len(selected.indexes()) < 1:
127
+ # return
128
+ # path = self.dirModel.fileInfo(selected.indexes()[0]).absoluteFilePath()
129
+ # # self.listview.setRootIndex(self.fileModel.setRootPath(path))
130
+ # if os.path.isfile(path):
131
+ # self.signals.file_selected.emit(path)
132
+
133
+ def new_folder(self):
134
+ current_path = self.dirModel.rootPath()
135
+
136
+ # If the toolbar refresh button was clicked with no arguments,
137
+ # show the folder creation dialog
138
+ folder_name, ok = QInputDialog.getText(self, "Folder Name", "Enter the folder name")
139
+ if ok and folder_name:
140
+ new_folder_path = os.path.join(self.path, folder_name)
141
+ os.makedirs(new_folder_path, exist_ok=True)
142
+ self.dirModel.setRootPath("")
143
+ self.dirModel.setRootPath(current_path)
pyspice/highlighter.py ADDED
@@ -0,0 +1,74 @@
1
+ from PyQt5.QtCore import Qt, QRegExp
2
+ from PyQt5.QtGui import QSyntaxHighlighter, QTextCharFormat, QColor, QFont
3
+
4
+
5
+ class SyntaxHighlighter(QSyntaxHighlighter):
6
+
7
+ def __init__(self, keywords):
8
+ super().__init__(None)
9
+ self.highlighting_rules = []
10
+ self.keywords = keywords
11
+ self.keyword_color = Qt.blue
12
+ self.apply_scheme()
13
+
14
+ def set_dark_mode(self, dark):
15
+ self.keyword_color = Qt.cyan if dark else Qt.blue
16
+ self.highlighting_rules.clear()
17
+ self.apply_scheme()
18
+
19
+ def apply_scheme(self):
20
+ keyword_format = QTextCharFormat()
21
+ keyword_format.setForeground(self.keyword_color)
22
+ keyword_format.setFontWeight(QFont.Bold)
23
+
24
+ self.highlighting_rules += [(f"\\b{k}\\b", keyword_format) for k in self.keywords]
25
+
26
+ string_format = QTextCharFormat()
27
+ string_format.setForeground(Qt.magenta)
28
+ self.highlighting_rules.append((r'".*"', string_format))
29
+ self.highlighting_rules.append((r"'.*'", string_format))
30
+
31
+ comment_format = QTextCharFormat()
32
+ comment_format.setForeground(QColor("green"))
33
+ comment_format.setFontItalic(True)
34
+ self.highlighting_rules.append((r"#.*", comment_format))
35
+
36
+ def highlightBlock(self, text):
37
+ for pattern, fmt in self.highlighting_rules:
38
+ expression = QRegExp(pattern)
39
+ index = expression.indexIn(text)
40
+ while index >= 0:
41
+ length = expression.matchedLength()
42
+ self.setFormat(index, length, fmt)
43
+ index = expression.indexIn(text, index + length)
44
+
45
+ def get_keywords(self):
46
+ return self.keywords
47
+
48
+
49
+ class PythonHighlighter(SyntaxHighlighter):
50
+ def __init__(self, dark=False):
51
+ super().__init__(['return', 'nonlocal', 'elif', 'assert', 'or', 'yield', 'finally',
52
+ 'from', 'global', 'del', 'print', 'None', 'pass', 'class', 'as',
53
+ 'break', 'while', 'await', 'async', 'range', 'is', 'True', 'lambda',
54
+ 'False', 'in', 'import', 'except', 'continue', 'and', 'raise', 'with',
55
+ 'if', 'try', 'for', 'else', 'not', 'def', "input", "int", "float", "str",
56
+ "list", "dict", "input", "print", "open", "read", "write", "close", "split",
57
+ ])
58
+
59
+
60
+ class PascalHighlighter(SyntaxHighlighter):
61
+ def __init__(self, dark=False):
62
+ super().__init__([
63
+ "and", "array", "asm", "begin", "case", "const", "constructor", "destructor",
64
+ "div", "do", "downto", "else", "end", "file", "for", "function", "goto", "if",
65
+ "implementation", "in", "inherited", "inline", "interface", "label", "mod", "nil",
66
+ "not", "object", "of", "or", "packed", "procedure", "program", "record", "repeat",
67
+ "set", "shl", "shr", "string", "then", "to", "type", "unit", "until", "uses",
68
+ "var", "while", "with", "xor", "AND", "ARRAY", "ASM", "BEGIN", "CASE", "CONST", "CONSTRUCTOR", "DESTRUCTOR",
69
+ "DIV", "DO", "DOWNTO", "ELSE", "END", "FILE", "FOR", "FUNCTION", "GOTO", "IF",
70
+ "IMPLEMENTATION", "IN", "INHERITED", "INLINE", "INTERFACE", "LABEL", "MOD", "NIL",
71
+ "NOT", "OBJECT", "OF", "OR", "PACKED", "PROCEDURE", "PROGRAM", "RECORD", "REPEAT",
72
+ "SET", "SHL", "SHR", "STRING", "THEN", "TO", "TYPE", "UNIT", "UNTIL", "USES",
73
+ "VAR", "WHILE", "WITH", "XOR"
74
+ ])
@@ -0,0 +1,54 @@
1
+ from PyQt5.QtCore import Qt
2
+ from PyQt5.QtGui import QTextCursor, QTextCharFormat, QColor, QTextBlockFormat
3
+ from PyQt5.QtWidgets import QTextEdit
4
+
5
+ from spiceditor.magic_scrollbar import MagicScrollBar
6
+
7
+
8
+ class LineNumberTextEdit(QTextEdit):
9
+ def __init__(self, parent=None):
10
+ super().__init__(parent)
11
+ self.line_highlighter_color = QColor(255, 255, 255, 100)
12
+ self.setReadOnly(True)
13
+ self.setLineWrapMode(QTextEdit.NoWrap)
14
+ self.setHorizontalScrollBar(MagicScrollBar())
15
+ self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
16
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
17
+
18
+
19
+ def highlight_line(self, line_number):
20
+ cursor = self.textCursor()
21
+ position = cursor.position()
22
+ cursor.movePosition(QTextCursor.Start)
23
+
24
+ # Select all text and reset formatting
25
+ cursor.select(QTextCursor.Document)
26
+ default_format = QTextCharFormat() # Default format (no highlights)
27
+ cursor.setCharFormat(default_format)
28
+
29
+ cursor.movePosition(QTextCursor.Start)
30
+ for _ in range(line_number):
31
+ cursor.movePosition(QTextCursor.Down)
32
+
33
+ cursor.select(QTextCursor.LineUnderCursor)
34
+
35
+ highlight_format = QTextCharFormat()
36
+ highlight_format.setBackground(self.line_highlighter_color)
37
+ cursor.setCharFormat(highlight_format)
38
+ cursor.setPosition(position)
39
+
40
+ # blockFmt = QTextBlockFormat()
41
+ # blockFmt.setLineHeight(40, QTextBlockFormat.FixedHeight)
42
+ #
43
+ # theCursor = self.textCursor()
44
+ # theCursor.clearSelection()
45
+ # theCursor.select(QTextCursor.Document)
46
+ # theCursor.mergeBlockFormat(blockFmt)
47
+
48
+ def set_line_highlighter_color(self, color):
49
+ self.line_highlighter_color = color
50
+ self.update()
51
+
52
+ def set_dark_mode(self, dark):
53
+ self.set_line_highlighter_color(
54
+ QColor(0, 0, 0, 50) if not dark else QColor(255, 255, 255, 100))
@@ -0,0 +1,11 @@
1
+ from PyQt5.QtGui import QPainter
2
+ from PyQt5.QtWidgets import QScrollBar
3
+
4
+
5
+ class MagicScrollBar(QScrollBar):
6
+ def paintEvent(self, a0) -> None:
7
+ super().paintEvent(a0)
8
+ if self.maximum() == 0:
9
+ p = QPainter(self)
10
+ p.fillRect(self.rect(), self.parent().palette().base().color())
11
+