├── .gitignore
├── .vscode
└── tasks.json
├── LICENSE
├── README.md
├── project.toml
├── screenshots
├── Screenshot1.png
├── Screenshot2.png
├── Screenshot3.png
└── Screenshot4.png
├── src
├── __pycache__
│ ├── editor.cpython-310.pyc
│ ├── lexer.cpython-310.pyc
│ └── resources.cpython-310.pyc
├── autcompleter.py
├── css
│ └── style.qss
├── editor.py
├── file_manager.py
├── fuzzy_searcher.py
├── icons
│ ├── close-icon.svg
│ ├── folder-icon-blue.svg
│ ├── resouces.qrc
│ └── search-icon.svg
├── lexer.py
├── main.py
└── resources.py
└── theme.json
/.gitignore:
--------------------------------------------------------------------------------
1 | /venv
2 | src/__pycache__/*.pyc
3 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "Start PPM",
8 | "type": "shell",
9 | "command": "ppm start"
10 | },
11 | {
12 | "label": "Build Qrc",
13 | "type": "shell",
14 | "command": "ppm run build-qrc"
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Asif Hossain
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pyqt-code-editor-yt
2 |
3 | [](https://ko-fi.com/A0A0ETK5O)
4 |
5 | Part of a youtube series (Discontinued for now but still has alot of content)
6 | You can find it here: https://www.youtube.com/playlist?list=PL_4y7TZU8zkc20vgwQSUG_FcwjxedfsjD
7 |
8 | ### Improved version
9 | [**Neutron**](https://github.com/Fus3n/neutron), This is what the actual plan for the series was and where it was suposed to go, this still has alot missing but its overal alot improved from the last commit of the yt series, so if you know what you are doing you can take code from this to improve the current youtube version.
10 |
11 | ## Screenshots
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | More features will be added in the future
25 |
--------------------------------------------------------------------------------
/project.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "editor-yt"
3 | version = "0.1.0"
4 | description = ""
5 | main_script = "./src/main.py"
6 |
7 | [packages]
8 | PyQt5 = "5.15.7"
9 | QScintilla = "2.13.3"
10 | jedi = "0.18.1"
11 |
12 | [scripts]
13 | build-qrc = "pyrcc5 ./src/icons/resouces.qrc -o ./src/resources.py"
14 | upgrade-pip = "python -m pip install --upgrade pip"
15 |
--------------------------------------------------------------------------------
/screenshots/Screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/screenshots/Screenshot1.png
--------------------------------------------------------------------------------
/screenshots/Screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/screenshots/Screenshot2.png
--------------------------------------------------------------------------------
/screenshots/Screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/screenshots/Screenshot3.png
--------------------------------------------------------------------------------
/screenshots/Screenshot4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/screenshots/Screenshot4.png
--------------------------------------------------------------------------------
/src/__pycache__/editor.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/src/__pycache__/editor.cpython-310.pyc
--------------------------------------------------------------------------------
/src/__pycache__/lexer.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/src/__pycache__/lexer.cpython-310.pyc
--------------------------------------------------------------------------------
/src/__pycache__/resources.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/src/__pycache__/resources.cpython-310.pyc
--------------------------------------------------------------------------------
/src/autcompleter.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QThread
2 | from PyQt5.Qsci import QsciAPIs
3 | from jedi import Script
4 | from jedi.api import Completion
5 |
6 |
7 | class AutoCompleter(QThread):
8 | def __init__(self, file_path, api):
9 | super(AutoCompleter, self).__init__(None)
10 | self.file_path = file_path
11 | self.script: Script = None
12 | self.api: QsciAPIs = api
13 | self.completions: list[Completion] = None
14 |
15 |
16 | self.line = 0
17 | self.index = 0
18 | self.text = ""
19 |
20 | def run(self):
21 | try:
22 | self.script = Script(self.text, path=self.file_path)
23 | self.completions = self.script.complete(self.line, self.index)
24 | self.load_autocomplete(self.completions)
25 | except Exception as err:
26 | print(err)
27 |
28 | self.finished.emit()
29 |
30 | def load_autocomplete(self, completions):
31 | self.api.clear()
32 | [self.api.add(i.name) for i in completions]
33 | self.api.prepare()
34 |
35 | def get_completions(self, line: int, index: int, text: str):
36 | self.line = line
37 | self.index = index
38 | self.text = text
39 | self.start()
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/css/style.qss:
--------------------------------------------------------------------------------
1 | QMainWindow {
2 | background-color: #282c34;
3 | color: #d3d3d3;
4 | }
5 |
6 |
7 | QTabWidget {
8 | background-color: #282c34;
9 | color: #d3d3d3;
10 | }
11 |
12 |
13 | QTabBar::tab {
14 | background-color: rgb(36, 36, 36);
15 | color: #d3d3d3;
16 | min-width: 10ex;
17 | min-height: 10ex;
18 | padding: 10px 20px;
19 | border-style: none;
20 | }
21 |
22 |
23 | QTabBar::tab::selected {
24 | color: #d3d3d3;
25 | border-style: none;
26 | background-color: #2d2d2d;
27 | border-bottom: 2px solid #d3d3d3;
28 | }
29 |
30 |
31 | QTabBar::close-button {
32 | image: url(":/icons/close-icon.svg");
33 | }
34 |
35 |
36 | QSplitter {
37 | background-color: #282c34;
38 | }
39 |
40 | QSplitter::handle {
41 | background-color: #2d2d2d;
42 | }
43 |
44 |
45 | QScrollBar:vertical {
46 | border: none;
47 | width: 14px;
48 | margin: 15px 0 15px 0;
49 | background-color: #2d2d2d;
50 | border-radius: 0px;
51 | }
52 |
53 |
54 | QScrollBar:handle:vertical {
55 | background-color: #3e3d3d;
56 | }
57 |
58 | QScrollBar:handle:vertical:hover {
59 | background-color: #4c4a4a;
60 | }
61 |
62 |
63 | QScrollBar:handle:vertical:pressed {
64 | background-color: #5c5b5b;
65 | }
66 |
67 |
68 | QScrollBar::sub-line::vertical {
69 | border: none;
70 | background: none;
71 | height: 15px;
72 | border-top-left-radius: 7px;
73 | border-top-right-radius: 7px;
74 | subcontrol-position: top;
75 | subcontrol-origin: margin;
76 | }
77 |
78 | QScrollBar::add-line::vertical {
79 | border: none;
80 | background: none;
81 | height: 15px;
82 | border-top-left-radius: 7px;
83 | border-top-right-radius: 7px;
84 | subcontrol-position: bottom;
85 | subcontrol-origin: margin;
86 | }
87 |
88 |
89 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
90 | background: none;
91 | }
92 |
93 |
94 | QScrollBar:horizontal {
95 | border: none;
96 | height: 14px;
97 | margin: 0px 0 0 0;
98 | background-color: #2d2d2d;
99 | border-radius: 0px;
100 | }
101 |
102 |
103 | QScrollBar:handle:horizontal {
104 | background-color: #3e3d3d;
105 | }
106 |
107 | QScrollBar:handle:horizontal:hover {
108 | background-color: #4c4a4a;
109 | }
110 |
111 | QScrollBar:handle:horizontal:pressed {
112 | background-color: #5c5b5b;
113 | }
114 |
115 |
116 | QScrollBar::sub-line::horizontal {
117 | border: none;
118 | background: none;
119 | width: 15px;
120 | border-top-left-radius: 7px;
121 | border-top-right-radius: 7px;
122 | subcontrol-position: top;
123 | subcontrol-origin: margin;
124 | }
125 |
126 | QScrollBar::add-line::horizontal {
127 | border: none;
128 | background: none;
129 | width: 15px;
130 | border-top-left-radius: 7px;
131 | border-top-right-radius: 7px;
132 | subcontrol-position: bottom;
133 | subcontrol-origin: margin;
134 | }
135 |
136 |
137 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
138 | background: none;
139 | }
140 |
141 |
142 | QsciScintilla {
143 | border: none;
144 | background-color: #282c34;
145 | color: #D3D3D3;
146 | }
147 |
148 |
149 | QMessageBox {
150 | color: white;
151 | border-radius: 30px;
152 | }
153 |
154 | QLineEdit {
155 | background-color: #21252b;
156 | border-radius: 4px;
157 | border: 1px solid #d3d3d3;
158 | padding: 3px;
159 | color: #D3D3D3;
160 | min-height: 6ex;
161 | }
162 |
163 | QLineEditBox:hover {
164 | color: white;
165 | }
166 |
167 | QListView {
168 | background-color: #21252b;
169 | border-radius: 5px;
170 | border: 1px solid #D3D3D3;
171 | padding: 5px;
172 | color: #d3d3d3;
173 | }
174 | QListView::item:hover {
175 | background-color: #323842;
176 | border: 1px soid #d3d3d3;
177 | border-radius: 3px;
178 | color: #d3d3d3;
179 | }
180 |
181 | QListView::item:selected {
182 | background-color: #323842;
183 | border-style: none;
184 | color: #D3D3D3;
185 | }
186 |
187 | QMenuBar {
188 | background-color: #282c34;
189 | border: 1px soid #21252b;
190 | padding: 3px;
191 | color: #D3D3D3;
192 | font-size: 15px;
193 | }
194 |
195 | QMenuBar::item {
196 | background-color: #282c34;
197 | padding: 3px;
198 | color: #d3d3d3;
199 | }
--------------------------------------------------------------------------------
/src/editor.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import *
2 | from PyQt5.QtCore import *
3 | from PyQt5.QtGui import *
4 |
5 | from PyQt5.Qsci import *
6 |
7 |
8 | import keyword
9 | import pkgutil
10 | from pathlib import Path
11 | from lexer import PyCustomLexer
12 | from autcompleter import AutoCompleter
13 | from typing import TYPE_CHECKING
14 |
15 | if TYPE_CHECKING:
16 | from main import MainWindow
17 |
18 | import resources
19 |
20 | class Editor(QsciScintilla):
21 |
22 | def __init__(self, main_window, parent=None, path: Path = None, is_python_file=True):
23 | super(Editor, self).__init__(parent)
24 |
25 | self.main_window: MainWindow = main_window
26 | self._current_file_changed = False
27 | self.first_launch = True
28 |
29 | self.path = path
30 | self.full_path = self.path.absolute()
31 | self.is_python_file = is_python_file
32 |
33 | # EDITOR
34 | self.cursorPositionChanged.connect(self._cusorPositionChanged)
35 | self.textChanged.connect(self._textChanged)
36 |
37 | # encoding
38 | self.setUtf8(True)
39 | # Font
40 | self.window_font = QFont("Fire Code") # font needs to be installed in your computer if its not use something else
41 | self.window_font.setPointSize(12)
42 | self.setFont(self.window_font)
43 |
44 | # brace matching
45 | self.setBraceMatching(QsciScintilla.SloppyBraceMatch)
46 |
47 | # indentation
48 | self.setIndentationGuides(True)
49 | self.setTabWidth(4)
50 | self.setIndentationsUseTabs(False)
51 | self.setAutoIndent(True)
52 |
53 | # autocomplete
54 | self.setAutoCompletionSource(QsciScintilla.AcsAll)
55 | self.setAutoCompletionThreshold(1)
56 | self.setAutoCompletionCaseSensitivity(False)
57 | self.setAutoCompletionUseSingle(QsciScintilla.AcusNever)
58 |
59 |
60 | # caret
61 | self.setCaretForegroundColor(QColor("#dedcdc"))
62 | self.setCaretLineVisible(True)
63 | self.setCaretWidth(2)
64 | self.setCaretLineBackgroundColor(QColor("#2c313c"))
65 |
66 |
67 | # EOL
68 | self.setEolMode(QsciScintilla.EolWindows)
69 | self.setEolVisibility(False)
70 |
71 |
72 | if self.is_python_file:
73 | # lexer for syntax highlighting
74 | self.pylexer = PyCustomLexer(self)
75 | self.pylexer.setDefaultFont(self.window_font)
76 |
77 | self.__api = QsciAPIs(self.pylexer)
78 |
79 | self.auto_completer = AutoCompleter(self.full_path, self.__api)
80 | self.auto_completer.finished.connect(self.loaded_autocomplete) # you can use this callback to do something
81 |
82 | self.setLexer(self.pylexer)
83 | else:
84 | self.setPaper(QColor("#282c34"))
85 | self.setColor(QColor("#abb2bf"))
86 |
87 |
88 | # line numbers
89 | self.setMarginType(0, QsciScintilla.NumberMargin)
90 | self.setMarginWidth(0, "000")
91 | self.setMarginsForegroundColor(QColor("#ff888888"))
92 | self.setMarginsBackgroundColor(QColor("#282c34"))
93 | self.setMarginsFont(self.window_font)
94 |
95 | # key press
96 | # self.keyPressEvent = self.handle_editor_press
97 |
98 | @property
99 | def current_file_changed(self):
100 | return self._current_file_changed
101 |
102 | @current_file_changed.setter
103 | def current_file_changed(self, value: bool):
104 | curr_index = self.main_window.tab_view.currentIndex()
105 | if value:
106 | self.main_window.tab_view.setTabText(curr_index, "*"+self.path.name)
107 | self.main_window.setWindowTitle(f"*{self.path.name} - {self.main_window.app_name}")
108 | else:
109 | if self.main_window.tab_view.tabText(curr_index).startswith("*"):
110 | self.main_window.tab_view.setTabText(
111 | curr_index,
112 | self.main_window.tab_view.tabText(curr_index)[1:]
113 | )
114 | self.main_window.setWindowTitle(self.main_window.windowTitle()[1:])
115 |
116 | self._current_file_changed = value
117 |
118 | def toggle_comment(self, text: str):
119 | lines = text.split('\n')
120 | toggled_lines = []
121 | for line in lines:
122 | if line.startswith('#'):
123 | toggled_lines.append(line[1:].lstrip())
124 | else:
125 | toggled_lines.append("# " + line)
126 |
127 | return '\n'.join(toggled_lines)
128 |
129 | def keyPressEvent(self, e: QKeyEvent) -> None:
130 | if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_Space:
131 | if self.is_python_file:
132 | pos = self.getCursorPosition()
133 | self.auto_completer.get_completions(pos[0]+1, pos[1], self.text())
134 | self.autoCompleteFromAPIs()
135 | return
136 |
137 | if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_X: # CUT SHORTCUT
138 | if not self.hasSelectedText():
139 | line, index = self.getCursorPosition()
140 | self.setSelection(line, 0, line, self.lineLength(line))
141 | self.cut()
142 | return
143 |
144 | if e.modifiers() == Qt.ControlModifier and e.text() == "/": # COMMENT SHORTCUT
145 | if self.hasSelectedText():
146 | start, srow, end, erow = self.getSelection()
147 | self.setSelection(start, 0, end, self.lineLength(end)-1)
148 | self.replaceSelectedText(self.toggle_comment(self.selectedText()))
149 | self.setSelection(start, srow, end, erow)
150 | else:
151 | line, _ = self.getCursorPosition()
152 | self.setSelection(line, 0, line, self.lineLength(line)-1)
153 | self.replaceSelectedText(self.toggle_comment(self.selectedText()))
154 | self.setSelection(-1, -1, -1, -1) # reset selection
155 | return
156 |
157 | return super().keyPressEvent(e)
158 |
159 | def _cusorPositionChanged(self, line: int, index: int) -> None:
160 | if self.is_python_file:
161 | self.auto_completer.get_completions(line+1, index, self.text())
162 |
163 | def loaded_autocomplete(self):
164 | pass
165 |
166 | def _textChanged(self):
167 | if not self.current_file_changed and not self.first_launch:
168 | self.current_file_changed = True
169 |
170 | if self.first_launch:
171 | self.first_launch = False
--------------------------------------------------------------------------------
/src/file_manager.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import *
2 | from PyQt5.QtCore import *
3 | from PyQt5.QtGui import *
4 | from PyQt5.Qsci import *
5 |
6 | from pathlib import Path
7 | import shutil
8 | import os
9 | import sys
10 | import subprocess
11 |
12 | from editor import Editor
13 |
14 |
15 | class FileManager(QTreeView):
16 | def __init__(self, tab_view, set_new_tab=None, main_window=None):
17 | super(FileManager, self).__init__(None)
18 |
19 | self.set_new_tab = set_new_tab
20 | self.tab_view = tab_view
21 | self.main_window = main_window
22 |
23 | # renaming feature variables
24 | #...
25 |
26 | self.manager_font = QFont("FiraCode", 13) # font must be installed on pc
27 |
28 | self.model: QFileSystemModel = QFileSystemModel()
29 | self.model.setRootPath(os.getcwd())
30 | # File system filters
31 | self.model.setFilter(QDir.NoDotAndDotDot | QDir.AllDirs | QDir.Files | QDir.Drives)
32 | self.model.setReadOnly(False)
33 | self.setFocusPolicy(Qt.NoFocus)
34 |
35 | self.setFont(self.manager_font)
36 | self.setModel(self.model)
37 | self.setRootIndex(self.model.index(os.getcwd()))
38 | self.setSelectionMode(QAbstractItemView.ExtendedSelection)
39 | self.setSelectionBehavior(QTreeView.SelectRows)
40 | self.setEditTriggers(QTreeView.EditTrigger.NoEditTriggers)
41 | # addntext menu
42 | self.setContextMenuPolicy(Qt.CustomContextMenu)
43 | self.customContextMenuRequested.connect(self.show_context_menu)
44 | # hank
45 | self.clicked.connect(self.tree_view_clicked)
46 | self.setIndentation(10)
47 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
48 | # Hidnd hide other columns except for name
49 | self.setHeaderHidden(True) # hiding header
50 | self.setColumnHidden(1, True)
51 | self.setColumnHidden(2, True)
52 | self.setColumnHidden(3, True)
53 |
54 | # enable drag and drop
55 | self.setDragEnabled(True)
56 | self.setAcceptDrops(True)
57 | self.setDropIndicatorShown(True)
58 | self.setDragDropMode(QAbstractItemView.DragDrop)
59 |
60 | # enable file name editing
61 |
62 | # renaming
63 | self.previous_rename_name = None
64 | self.is_renaming = False
65 | self.current_edit_index = None
66 |
67 | self.itemDelegate().closeEditor.connect(self._on_closeEditor)
68 |
69 | def _on_closeEditor(self, editor: QLineEdit):
70 | if self.is_renaming:
71 | self.rename_file_with_index()
72 |
73 | def tree_view_clicked(self, index: QModelIndex):
74 | path = self.model.filePath(index)
75 | p = Path(path)
76 | if p.is_file():
77 | self.set_new_tab(p)
78 |
79 | def show_context_menu(self, pos: QPoint):
80 | ix = self.indexAt(pos)
81 | menu = QMenu()
82 | menu.addAction("New File")
83 | menu.addAction("New Folder")
84 | menu.addAction("Open In File Manager")
85 |
86 | if ix.column() == 0:
87 | menu.addAction("Rename")
88 | menu.addAction("Delete")
89 |
90 | action = menu.exec_(self.viewport().mapToGlobal(pos))
91 |
92 | if not action:
93 | return
94 |
95 | if action.text() == "Rename":
96 | self.action_rename(ix)
97 | elif action.text() == "Delete":
98 | self.action_delete(ix)
99 | elif action.text() == "New Folder":
100 | self.action_new_folder()
101 | elif action.text() == "New File":
102 | self.action_new_file(ix)
103 | elif action.text() == "Open In File Manager":
104 | self.action_open_in_file_manager(ix)
105 | else:
106 | pass
107 |
108 | def show_dialog(self, title, msg) -> int:
109 | dialog = QMessageBox(self)
110 | dialog.setFont(self.manager_font)
111 | dialog.font().setPointSize(14)
112 | dialog.setWindowTitle(title)
113 | dialog.setWindowIcon(QIcon(":/icons/close-icon.svg"))
114 | dialog.setText(msg)
115 | dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
116 | dialog.setDefaultButton(QMessageBox.No)
117 | dialog.setIcon(QMessageBox.Warning)
118 | return dialog.exec_()
119 |
120 | def rename_file_with_index(self):
121 | new_name = self.model.fileName(self.current_edit_index)
122 | if self.previous_rename_name == new_name:
123 | return
124 |
125 | # loop over all the tabs open and find the one with the old name
126 | for editor in self.tab_view.findChildren(Editor): # finding all children of type Editor
127 | if editor.path.name == self.previous_rename_name: # the editor should keep a path vatriable
128 | editor.path = editor.path.parent / new_name
129 | self.tab_view.setTabText(
130 | self.tab_view.indexOf(editor), new_name
131 | )
132 | self.tab_view.repaint()
133 | editor.full_path = editor.path.absolute() # changing the editor instances full_path variable
134 | self.main_window.current_file = editor.path
135 | break
136 |
137 | def action_rename(self, ix):
138 | self.edit(ix)
139 | self.previous_rename_name = self.model.fileName(ix)
140 | self.is_renaming = True
141 | self.current_edit_index = ix
142 |
143 | def delete_file(self, path: Path):
144 | if path.is_dir():
145 | shutil.rmtree(path)
146 | else:
147 | path.unlink()
148 |
149 | def action_delete(self, ix):
150 | # check if selection
151 | file_name = self.model.fileName(ix)
152 | dialog = self.show_dialog(
153 | "Delete", f"Are you sure you want to delete {file_name}"
154 | )
155 | if dialog == QMessageBox.Yes:
156 | if self.selectionModel().selectedRows():
157 | for i in self.selectionModel().selectedRows():
158 | path = Path(self.model.filePath(i))
159 | self.delete_file(path)
160 | for editor in self.tab_view.findChildren(Editor):
161 | if editor.path.name == path.name:
162 | self.tab_view.removeTab(
163 | self.tab_view.indexOf(editor)
164 | )
165 |
166 | def action_new_file(self, ix: QModelIndex):
167 | root_path = self.model.rootPath()
168 | if ix.column() != -1 and self.model.isDir(ix):
169 | self.expand(ix)
170 | root_path = self.model.filePath(ix)
171 |
172 | f = Path(root_path) / "file"
173 | count = 1
174 | while f.exists():
175 | f = Path(f.parent / f"file{count}")
176 | count += 1
177 | f.touch()
178 | idx = self.model.index(str(f.absolute()))
179 | self.edit(idx)
180 |
181 | def action_new_folder(self):
182 | f = Path(self.model.rootPath()) / "New Folder"
183 | count = 1
184 | while f.exists():
185 | f = Path(f.parent / f"New Folder{count}")
186 | count += 1
187 | idx = self.model.mkdir(self.rootIndex(), f.name)
188 | # edit that index
189 | self.edit(idx)
190 |
191 | def action_open_in_file_manager(self, ix: QModelIndex):
192 | path = os.path.abspath(self.model.filePath(ix))
193 | is_dir = self.model.isDir(ix)
194 | if os.name == "nt":
195 | # Windows
196 | if is_dir:
197 | subprocess.Popen(f'explorer "{path}"')
198 | else:
199 | subprocess.Popen(f'explorer /select,"{path}"')
200 | elif os.name == "posix":
201 | # Linux or Mac OS
202 | if sys.platform == "darwin":
203 | # macOS
204 | if is_dir:
205 | subprocess.Popen(["open", path])
206 | else:
207 | subprocess.Popen(["open", "-R", path])
208 | else:
209 | # Linux
210 | subprocess.Popen(["xdg-open", os.path.dirname(path)])
211 | else:
212 | raise OSError(f"Unsupported platform {os.name}")
213 |
214 | # drag and drop functionality
215 | def dragEnterEvent(self, e: QDragEnterEvent) -> None:
216 | if e.mimeData().hasUrls():
217 | e.accept()
218 | else:
219 | e.ignore()
220 |
221 | def dropEvent(self, e: QDropEvent) -> None:
222 | root_path = Path(self.model.rootPath())
223 | if e.mimeData().hasUrls():
224 | for url in e.mimeData().urls():
225 | path = Path(url.toLocalFile())
226 | if path.is_dir():
227 | shutil.copytree(path, root_path / path.name)
228 | else:
229 | if root_path.samefile(self.model.rootPath()):
230 | idx: QModelIndex = self.indexAt(e.pos())
231 | if idx.column() == -1:
232 | shutil.move(path, root_path / path.name)
233 | else:
234 | folder_path = Path(self.model.filePath(idx))
235 | shutil.move(path, folder_path / path.name)
236 | else:
237 | shutil.copy(path, root_path / path.name)
238 | e.accept()
239 |
240 | return super().dropEvent(e)
241 |
242 |
--------------------------------------------------------------------------------
/src/fuzzy_searcher.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtCore import QThread, pyqtSignal
2 | from PyQt5.QtWidgets import QListWidgetItem
3 |
4 | import os
5 | from pathlib import Path
6 | import re
7 |
8 |
9 |
10 | class SearchItem(QListWidgetItem):
11 | def __init__(self, name, full_path, lineno, end, line):
12 | self.name = name
13 | self.full_path = full_path
14 | self.lineno = lineno
15 | self.end = end
16 | self.line = line
17 | self.formatted = f'{self.name}:{self.lineno}:{self.end} - {self.line} ...'
18 | super().__init__(self.formatted)
19 |
20 |
21 | def __str__(self):
22 | return self.formatted
23 |
24 | def __repr__(self):
25 | return self.formatted
26 |
27 |
28 | class SearchWorker(QThread):
29 | finished = pyqtSignal(list)
30 |
31 | def __init__(self):
32 | super(SearchWorker, self).__init__(None)
33 | self.items = []
34 | self.search_path: str = None
35 | self.search_text: str = None
36 | self.search_project: bool = None
37 |
38 | def is_binary(self, path):
39 | '''
40 | Check if file is binary
41 | '''
42 | with open(path, 'rb') as f:
43 | return b'\0' in f.read(1024)
44 |
45 | def walkdir(self, path, exclude_dirs: list, exclude_files: list):
46 | for root, dirs, files, in os.walk(path, topdown=True):
47 | # filtering
48 | dirs[:] = [d for d in dirs if d not in exclude_dirs]
49 | files[:] = [f for f in files if Path(f).suffix not in exclude_files]
50 | yield root, dirs, files
51 |
52 | def search(self):
53 | debug = False
54 | self.items = []
55 | # you can add more
56 | exclude_dirs = set([".git", ".svn", ".hg", ".bzr", ".idea", "__pycache__", "venv"])
57 | if self.search_project:
58 | exclude_dirs.remove("venv")
59 | exclude_files = set([".svg", ".png", ".exe", ".pyc", ".qm"])
60 |
61 | for root, _, files in self.walkdir(self.search_path, exclude_dirs, exclude_files):
62 | # total search limit
63 | if len(self.items) > 5_000:
64 | break
65 | for file_ in files:
66 | full_path = os.path.join(root, file_)
67 | if self.is_binary(full_path):
68 | break
69 |
70 | try:
71 | with open(full_path, 'r', encoding='utf8') as f:
72 | try:
73 | reg = re.compile(self.search_text, re.IGNORECASE)
74 | for i, line in enumerate(f):
75 | if m := reg.search(line):
76 | fd = SearchItem(
77 | file_,
78 | full_path,
79 | i,
80 | m.end(),
81 | line[m.start():].strip()[:50], # limiting to 50 chars
82 | )
83 | self.items.append(fd)
84 | except re.error as e:
85 | if debug: print(e)
86 | except UnicodeDecodeError as e:
87 | if debug: print(e)
88 | continue
89 |
90 | self.finished.emit(self.items)
91 |
92 | def run(self):
93 | self.search()
94 |
95 | def update(self, pattern, path, search_project):
96 | self.search_text = pattern
97 | self.search_path = path
98 | self.search_project = search_project
99 | self.start()
100 |
--------------------------------------------------------------------------------
/src/icons/close-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/folder-icon-blue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/resouces.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | close-icon.svg
4 | folder-icon-blue.svg
5 | search-icon.svg
6 |
7 |
--------------------------------------------------------------------------------
/src/icons/search-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lexer.py:
--------------------------------------------------------------------------------
1 | import re
2 | import keyword
3 | import builtins
4 | import types
5 | import json
6 |
7 | from PyQt5.Qsci import QsciLexerCustom, QsciScintilla
8 | from PyQt5.QtGui import QFont, QColor
9 | from PyQt5.QtCore import *
10 | from PyQt5.QtWidgets import *
11 |
12 |
13 | # config type
14 | DefaultConfig = dict[str, str, tuple[str, int]]
15 |
16 | class NeutronLexer(QsciLexerCustom):
17 | """Base Custo Lexer class for all language"""
18 |
19 | def __init__(self, language_name, editor, theme=None, defaults: DefaultConfig = None):
20 | super(NeutronLexer, self).__init__(editor)
21 |
22 | self.editor = editor
23 | self.language_name = language_name
24 | self.theme_json = None
25 | if theme is None:
26 | self.theme = "./theme.json"
27 | else:
28 | self.theme = theme
29 |
30 | self.token_list: list[str, str] = []
31 |
32 | self.keywords_list = []
33 | self.builtin_names = []
34 |
35 | if defaults is None:
36 | defaults: DefaultConfig = {}
37 | defaults["color"] = "#abb2bf"
38 | defaults["paper"] = "#282c34"
39 | defaults["font"] = ("Consolas", 14)
40 |
41 |
42 | # Default text settings
43 | self.setDefaultColor(QColor(defaults["color"]))
44 | self.setDefaultPaper(QColor(defaults["paper"]))
45 | self.setDefaultFont(QFont(defaults["font"][0], defaults["font"][1]))
46 |
47 | self._init_theme_vars()
48 | self._init_theme()
49 |
50 | def setKeywords(self, keywords: list[str]):
51 | """Set List of strings that considered keywords for this language."""
52 | self.keywords_list =keywords
53 |
54 | def setBuiltinNames(self, buitin_names: list[str]):
55 | """Set list of builtin names"""
56 | self.builtin_names = buitin_names
57 |
58 | def _init_theme_vars(self):
59 | # color per style
60 |
61 | self.DEFAULT = 0
62 | self.KEYWORD = 1
63 | self.TYPES = 2
64 | self.STRING = 3
65 | self.KEYARGS = 4
66 | self.BRACKETS = 5
67 | self.COMMENTS = 6
68 | self.CONSTANTS = 7
69 | self.FUNCTIONS = 8
70 | self.CLASSES = 9
71 | self.FUNCTION_DEF = 10
72 |
73 | self.default_names = [
74 | "default",
75 | "keyword",
76 | "types",
77 | "string",
78 | "keyargs",
79 | "brackets",
80 | "comments",
81 | "constants",
82 | "functions",
83 | "classes",
84 | "function_def"
85 | ]
86 |
87 | self.font_weights = {
88 | "thin": QFont.Thin,
89 | "extralight": QFont.ExtraLight,
90 | "light": QFont.Light,
91 | "normal": QFont.Normal,
92 | "medium": QFont.Medium,
93 | "demibold": QFont.DemiBold,
94 | "bold": QFont.Bold,
95 | "extrabold": QFont.ExtraBold,
96 | "black": QFont.Black,
97 | }
98 |
99 | def _init_theme(self):
100 | with open(self.theme, "r") as f:
101 | self.theme_json = json.load(f)
102 |
103 | colors = self.theme_json["theme"]["syntax"]
104 |
105 | for clr in colors:
106 | name: str = list(clr.keys())[0]
107 |
108 | if name not in self.default_names:
109 | print(f"Theme error: {name} is not a valid style name")
110 | continue
111 |
112 | for k, v in clr[name].items():
113 | if k == "color":
114 | self.setColor(QColor(v), getattr(self, name.upper()))
115 | elif k == "paper-color":
116 | self.setPaper(QColor(v), getattr(self, name.upper()))
117 | elif k == "font":
118 | try:
119 | self.setFont(
120 | QFont(
121 | v.get("family", "Consolas"),
122 | v.get("font-size", 14),
123 | self.font_weights.get(v.get("font-weight", QFont.Normal)),
124 | v.get("italic", False)
125 | ),
126 | getattr(self, name.upper())
127 | )
128 | except AttributeError as e:
129 | print(f"theme error: {e}")
130 |
131 |
132 | def language(self) -> str:
133 | return self.language_name
134 |
135 | def description(self, style: int) -> str:
136 | if style == self.DEFAULT:
137 | return "DEFAULT"
138 | elif style == self.KEYWORD:
139 | return "KEYWORD"
140 | elif style == self.TYPES:
141 | return "TYPES"
142 | elif style == self.STRING:
143 | return "STRING"
144 | elif style == self.KEYARGS:
145 | return "KEYARGS"
146 | elif style == self.BRACKETS:
147 | return "BRACKETS"
148 | elif style == self.COMMENTS:
149 | return "COMMENTS"
150 | elif style == self.CONSTANTS:
151 | return "CONSTANTS"
152 | elif style == self.FUNCTIONS:
153 | return "FUNCTIONS"
154 | elif style == self.CLASSES:
155 | return "CLASSES"
156 | elif style == self.FUNCTION_DEF:
157 | return "FUNCTION_DEF"
158 |
159 | return ""
160 |
161 | def generate_token(self, text):
162 | # 3. Tokenize the text
163 | # ---------------------
164 | p = re.compile(r"[*]\/|\/[*]|\s+|\w+|\W")
165 |
166 | # 'token_list' is a list of tuples: (token_name, token_len), ex: '(class, 5)'
167 | self.token_list = [ (token, len(bytearray(token, "utf-8"))) for token in p.findall(text)]
168 |
169 | def next_tok(self, skip: int = None):
170 | if len(self.token_list) > 0:
171 | if skip is not None and skip != 0:
172 | for _ in range(skip-1):
173 | if len(self.token_list) > 0:
174 | self.token_list.pop(0)
175 | return self.token_list.pop(0)
176 | else:
177 | return None
178 |
179 | def peek_tok(self, n=0):
180 | try:
181 | return self.token_list[n]
182 | except IndexError:
183 | return ['']
184 |
185 | def skip_spaces_peek(self, skip=None):
186 | """find he next non-space token but using peek without consuming it"""
187 | i = 0
188 | tok = " "
189 | if skip is not None:
190 | i = skip
191 | while tok[0].isspace():
192 | tok = self.peek_tok(i)
193 | i += 1
194 | return tok, i
195 |
196 |
197 | class PyCustomLexer(NeutronLexer):
198 | """Custom lexer for python"""
199 |
200 | def __init__(self, editor):
201 | super(PyCustomLexer, self).__init__("Python", editor)
202 |
203 | self.setKeywords(keyword.kwlist)
204 | self.setBuiltinNames([
205 | name
206 | for name, obj in vars(builtins).items()
207 | if isinstance(obj, types.BuiltinFunctionType)
208 | ])
209 |
210 | def styleText(self, start: int, end: int) -> None:
211 | # 1. Start styling procedure
212 | self.startStyling(start)
213 |
214 | # 2. Slice out part from the text
215 | text = self.editor.text()[start:end]
216 |
217 | # 3. Tokenize the text
218 | self.generate_token(text)
219 |
220 | # Flags
221 | string_flag = False
222 | comment_flag = False
223 |
224 | if start > 0:
225 | prev_style = self.editor.SendScintilla(self.editor.SCI_GETSTYLEAT, start -1)
226 | if prev_style == self.COMMENTS:
227 | comment_flag = False
228 |
229 | while True:
230 | curr_token = self.next_tok()
231 |
232 | if curr_token is None:
233 | break
234 |
235 | tok: str = curr_token[0]
236 | tok_len: int = curr_token[1]
237 |
238 | if comment_flag:
239 | self.setStyling(tok_len, self.COMMENTS)
240 | if tok.startswith("\n"):
241 | comment_flag = False
242 | continue
243 |
244 |
245 | if string_flag:
246 | self.setStyling(tok_len, self.STRING)
247 | if tok == '"' or tok == "'":
248 | string_flag = False
249 | continue
250 |
251 | if tok == "class":
252 | name, ni = self.skip_spaces_peek()
253 | brac_or_colon, _ = self.skip_spaces_peek(ni)
254 | if name[0].isidentifier() and brac_or_colon[0] in (":", "("):
255 | self.setStyling(tok_len, self.KEYWORD)
256 | _ = self.next_tok(ni)
257 | self.setStyling(name[1]+1, self.CLASSES)
258 | continue
259 | else:
260 | self.setStyling(tok_len, self.KEYWORD)
261 | continue
262 | elif tok == "def":
263 | name, ni = self.skip_spaces_peek()
264 | if name[0].isidentifier():
265 | self.setStyling(tok_len, self.KEYWORD)
266 | _ = self.next_tok(ni)
267 | self.setStyling(name[1]+1, self.FUNCTION_DEF)
268 | continue
269 | else:
270 | self.setStyling(tok_len, self.KEYWORD)
271 | continue
272 | elif tok in self.keywords_list:
273 | self.setStyling(tok_len, self.KEYWORD)
274 | elif tok.strip() == "." and self.peek_tok()[0].isidentifier():
275 | self.setStyling(tok_len, self.DEFAULT)
276 | curr_token = self.next_tok()
277 | tok: str = curr_token[0]
278 | tok_len: int = curr_token[1]
279 | if self.peek_tok()[0] == "(":
280 | self.setStyling(tok_len, self.FUNCTIONS)
281 | else:
282 | self.setStyling(tok_len, self.DEFAULT)
283 | continue
284 | elif tok.isnumeric() or tok == 'self':
285 | self.setStyling(tok_len, self.CONSTANTS)
286 | elif tok in ["(", ")", "{", "}", "[", "]"]:
287 | self.setStyling(tok_len, self.BRACKETS)
288 | elif tok == '"' or tok == "'":
289 | self.setStyling(tok_len, self.STRING)
290 | string_flag = True
291 | elif tok == "#":
292 | self.setStyling(tok_len, self.COMMENTS)
293 | comment_flag = True
294 | elif tok in self.builtin_names or tok in ['+', '-', '*', '/', '%', '=', '<', '>']:
295 | self.setStyling(tok_len, self.TYPES)
296 | else:
297 | self.setStyling(tok_len, self.DEFAULT)
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | from PyQt5.QtWidgets import *
3 | from PyQt5.QtCore import *
4 | from PyQt5.QtGui import *
5 |
6 | from PyQt5.Qsci import *
7 | # test
8 |
9 | import sys
10 | from pathlib import Path
11 |
12 | from editor import Editor
13 | from fuzzy_searcher import SearchItem, SearchWorker
14 | from file_manager import FileManager
15 |
16 |
17 | class MainWindow(QMainWindow):
18 | def __init__(self):
19 | super(QMainWindow, self).__init__()
20 | # add before init
21 | self.side_bar_clr = "#282c34"
22 |
23 | self.init_ui()
24 |
25 | self.current_file = None
26 | self.current_side_bar = None
27 |
28 | def init_ui(self):
29 | self.app_name = "PYQT EDITOR"
30 | self.setWindowTitle(self.app_name)
31 | self.resize(1300, 900)
32 |
33 | self.setStyleSheet(open("./src/css/style.qss", "r").read())
34 |
35 | # alternative Consolas font
36 | self.window_font = QFont("Fire Code") # font needs to be installed in your computer if its not use something else
37 | self.window_font.setPointSize(12)
38 | self.setFont(self.window_font)
39 |
40 |
41 | self.set_up_menu()
42 | self.set_up_body()
43 | self.set_up_status_bar()
44 |
45 | self.show()
46 |
47 | def set_up_status_bar(self):
48 | # Create status bar
49 | stat = QStatusBar(self)
50 | stat.setStyleSheet("color: #D3D3D3;")
51 | stat.showMessage("Ready", 3000)
52 | self.setStatusBar(stat)
53 |
54 | def set_up_menu(self):
55 | menu_bar = self.menuBar()
56 |
57 | # File Menu
58 | file_menu = menu_bar.addMenu("File")
59 |
60 | new_file = file_menu.addAction("New")
61 | new_file.setShortcut("Ctrl+N")
62 | new_file.triggered.connect(self.new_file)
63 |
64 | open_file = file_menu.addAction("Open File")
65 | open_file.setShortcut("Ctrl+O")
66 | open_file.triggered.connect(self.open_file)
67 |
68 | open_folder = file_menu.addAction("Open Folder")
69 | open_folder.setShortcut("Ctrl+K")
70 | open_folder.triggered.connect(self.open_folder)
71 |
72 | file_menu.addSeparator()
73 |
74 | save_file = file_menu.addAction("Save")
75 | save_file.setShortcut("Ctrl+S")
76 | save_file.triggered.connect(self.save_file)
77 |
78 | save_as = file_menu.addAction("Save As")
79 | save_as.setShortcut("Ctrl+Shift+S")
80 | save_as.triggered.connect(self.save_as)
81 |
82 |
83 | # Edit menu
84 | edit_menu = menu_bar.addMenu("Edit")
85 |
86 | copy_action = edit_menu.addAction("Copy")
87 | copy_action.setShortcut("Ctrl+C")
88 | copy_action.triggered.connect(self.copy)
89 | # you can add more
90 |
91 | def get_editor(self, path: Path = None, is_python_file=True) -> QsciScintilla:
92 | editor = Editor(self, path=path, is_python_file=is_python_file)
93 | return editor
94 |
95 | def is_binary(self, path):
96 | '''
97 | Check if file is binary
98 | '''
99 | with open(path, 'rb') as f:
100 | return b'\0' in f.read(1024)
101 |
102 | def set_new_tab(self, path: Path, is_new_file=False):
103 | if not is_new_file and self.is_binary(path):
104 | self.statusBar().showMessage("Cannot Open Binary File", 2000)
105 | return
106 |
107 | if path.is_dir():
108 | return
109 |
110 | # add whichever extentions you consider as python file
111 | editor = self.get_editor(path, path.suffix in {".py", ".pyw"})
112 |
113 | if is_new_file:
114 | self.tab_view.addTab(editor, "untitled")
115 | self.setWindowTitle("untitled - " + self.app_name)
116 | self.statusBar().showMessage("Opened untitled")
117 | self.tab_view.setCurrentIndex(self.tab_view.count() - 1)
118 | self.current_file = None
119 | return
120 |
121 | # check if file already open
122 | for i in range(self.tab_view.count()):
123 | if self.tab_view.tabText(i) == path.name or self.tab_view.tabText(i) == "*"+path.name:
124 | self.tab_view.setCurrentIndex(i)
125 | self.current_file = path
126 | return
127 |
128 | # create new tab
129 | self.tab_view.addTab(editor, path.name)
130 | editor.setText(path.read_text(encoding="utf-8"))
131 | self.setWindowTitle(f"{path.name} - {self.app_name}")
132 | self.current_file = path
133 | self.tab_view.setCurrentIndex(self.tab_view.count() - 1)
134 | self.statusBar().showMessage(f"Opened {path.name}", 2000)
135 |
136 | def set_cursor_pointer(self, e):
137 | self.setCursor(Qt.PointingHandCursor)
138 |
139 | def set_cursor_arrow(self, e):
140 | self.setCursor(Qt.ArrowCursor)
141 |
142 | def get_side_bar_label(self, path, name):
143 | label = QLabel()
144 | label.setPixmap(QPixmap(path).scaled(QSize(30, 30)))
145 | label.setAlignment(Qt.AlignmentFlag.AlignTop)
146 | label.setFont(self.window_font)
147 | label.mousePressEvent = lambda e: self.show_hide_tab(e, name)
148 | # Chaning Cursor on hover
149 | label.enterEvent = self.set_cursor_pointer
150 | label.leaveEvent = self.set_cursor_arrow
151 | return label
152 |
153 | def get_frame(self) -> QFrame:
154 | frame = QFrame()
155 | frame.setFrameShape(QFrame.NoFrame)
156 | frame.setFrameShadow(QFrame.Plain)
157 | frame.setContentsMargins(0, 0, 0, 0)
158 | frame.setStyleSheet('''
159 | QFrame {
160 | background-color: #21252b;
161 | border-radius: 5px;
162 | border: none;
163 | padding: 5px;
164 | color: #D3D3D3;
165 | }
166 | QFrame:hover {
167 | color: white;
168 | }
169 | ''')
170 | return frame
171 |
172 | def set_up_body(self):
173 |
174 | # Body
175 | body_frame = QFrame()
176 | body_frame.setFrameShape(QFrame.NoFrame)
177 | body_frame.setFrameShadow(QFrame.Plain)
178 | body_frame.setLineWidth(0)
179 | body_frame.setMidLineWidth(0)
180 | body_frame.setContentsMargins(0, 0, 0, 0)
181 | body_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
182 | body = QHBoxLayout()
183 | body.setContentsMargins(0, 0, 0, 0)
184 | body.setSpacing(0)
185 | body_frame.setLayout(body)
186 |
187 | ##############################
188 | ###### TAB VIEW ##########
189 |
190 | # Tab Widget to add editor to
191 | self.tab_view = QTabWidget()
192 | self.tab_view.setContentsMargins(0, 0, 0, 0)
193 | self.tab_view.setTabsClosable(True)
194 | self.tab_view.setMovable(True)
195 | self.tab_view.setDocumentMode(True)
196 | self.tab_view.tabCloseRequested.connect(self.close_tab)
197 |
198 | ##############################
199 | ###### SIDE BAR ##########
200 | self.side_bar = QFrame()
201 | self.side_bar.setFrameShape(QFrame.StyledPanel)
202 | self.side_bar.setFrameShadow(QFrame.Plain)
203 | self.side_bar.setStyleSheet(f'''
204 | background-color: {self.side_bar_clr};
205 | ''')
206 | side_bar_layout = QVBoxLayout()
207 | side_bar_layout.setContentsMargins(5, 10, 5, 0)
208 | side_bar_layout.setSpacing(0)
209 | side_bar_layout.setAlignment(Qt.AlignTop | Qt.AlignCenter)
210 |
211 | # setup labels
212 | folder_label = self.get_side_bar_label("./src/icons/folder-icon-blue.svg", "folder-icon")
213 | side_bar_layout.addWidget(folder_label)
214 |
215 | search_label = self.get_side_bar_label("./src/icons/search-icon", "search-icon")
216 | side_bar_layout.addWidget(search_label)
217 |
218 | self.side_bar.setLayout(side_bar_layout)
219 |
220 |
221 | # split view
222 | self.hsplit = QSplitter(Qt.Horizontal)
223 |
224 | ##############################
225 | ###### FILE MANAGER ##########
226 |
227 | # frame and layout to hold tree view (file manager)
228 | self.file_manager_frame = self.get_frame()
229 | self.file_manager_frame.setMaximumWidth(400)
230 | self.file_manager_frame.setMinimumWidth(200)
231 |
232 | self.file_manager_layout = QVBoxLayout()
233 | self.file_manager_layout.setContentsMargins(0, 0, 0, 0)
234 | self.file_manager_layout.setSpacing(0)
235 |
236 | self.file_manager = FileManager(
237 | tab_view=self.tab_view,
238 | set_new_tab=self.set_new_tab,
239 | main_window=self
240 | )
241 |
242 | # setup layout
243 | self.file_manager_layout.addWidget(self.file_manager)
244 | self.file_manager_frame.setLayout(self.file_manager_layout)
245 |
246 |
247 | ##############################
248 | ###### SEARCH VIEW ##########
249 | self.search_frame = self.get_frame()
250 | self.search_frame.setMaximumWidth(400)
251 | self.search_frame.setMinimumWidth(200)
252 |
253 | search_layout = QVBoxLayout()
254 | search_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
255 | search_layout.setContentsMargins(0, 10, 0, 0)
256 | search_layout.setSpacing(0)
257 |
258 | search_input = QLineEdit()
259 | search_input.setPlaceholderText("Search")
260 | search_input.setFont(self.window_font)
261 | search_input.setAlignment(Qt.AlignmentFlag.AlignTop)
262 |
263 | ############# CHECKBOX ################
264 | self.search_checkbox = QCheckBox("Search in modules")
265 | self.search_checkbox.setFont(self.window_font)
266 | self.search_checkbox.setStyleSheet("color: white; margin-bottom: 10px;")
267 |
268 | self.search_worker = SearchWorker()
269 | self.search_worker.finished.connect(self.search_finshed)
270 |
271 | search_input.textChanged.connect(
272 | lambda text: self.search_worker.update(
273 | text,
274 | self.file_manager.model.rootDirectory().absolutePath(),
275 | self.search_checkbox.isChecked()
276 | )
277 | )
278 |
279 |
280 | ##############################
281 | ###### SEARCH ListView ##########
282 | self.search_list_view = QListWidget()
283 | self.search_list_view.setFont(QFont("FiraCode", 13))
284 | self.search_list_view.setStyleSheet("""
285 | QListWidget {
286 | background-color: #21252b;
287 | border-radius: 5px;
288 | border: 1px solid #D3D3D3;
289 | padding: 5px;
290 | color: #D3D3D3;
291 | }
292 | """)
293 |
294 | self.search_list_view.itemClicked.connect(self.search_list_view_clicked)
295 |
296 |
297 | search_layout.addWidget(self.search_checkbox)
298 | search_layout.addWidget(search_input)
299 | search_layout.addSpacerItem(QSpacerItem(5, 5, QSizePolicy.Minimum, QSizePolicy.Minimum))
300 |
301 | search_layout.addWidget(self.search_list_view)
302 | self.search_frame.setLayout(search_layout)
303 |
304 |
305 |
306 |
307 | ##############################
308 | ###### SETUP WIDGETS ##########
309 |
310 | # add tree view and tab view
311 | self.hsplit.addWidget(self.file_manager_frame)
312 | self.hsplit.addWidget(self.tab_view)
313 |
314 | body.addWidget(self.side_bar)
315 | body.addWidget(self.hsplit)
316 |
317 | body_frame.setLayout(body)
318 |
319 | self.setCentralWidget(body_frame)
320 |
321 | def search_finshed(self, items):
322 | self.search_list_view.clear()
323 | for i in items:
324 | self.search_list_view.addItem(i)
325 |
326 | def search_list_view_clicked(self, item: SearchItem):
327 | self.set_new_tab(Path(item.full_path))
328 | editor: Editor = self.tab_view.currentWidget()
329 | editor.setCursorPosition(item.lineno, item.end)
330 | editor.setFocus()
331 |
332 | def show_dialog(self, title, msg) -> int:
333 | dialog = QMessageBox(self)
334 | dialog.setFont(self.font())
335 | dialog.font().setPointSize(14)
336 | dialog.setWindowTitle(title)
337 | dialog.setWindowIcon(QIcon(":/icons/close-icon.svg"))
338 | dialog.setText(msg)
339 | dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
340 | dialog.setDefaultButton(QMessageBox.No)
341 | dialog.setIcon(QMessageBox.Warning)
342 | return dialog.exec_()
343 |
344 | def close_tab(self, index):
345 | editor: Editor = self.tab_view.currentWidget()
346 | if editor.current_file_changed:
347 | dialog = self.show_dialog(
348 | "Close", f"Do you want to save the changes made to {self.current_file.name}?"
349 | )
350 | if dialog == QMessageBox.Yes:
351 | self.save_file()
352 |
353 | self.tab_view.removeTab(index)
354 |
355 | def show_hide_tab(self, e, type_):
356 | if type_ == "folder-icon":
357 | if not (self.file_manager_frame in self.hsplit.children()):
358 | self.hsplit.replaceWidget(0, self.file_manager_frame)
359 | elif type_ == "search-icon":
360 | if not (self.search_frame in self.hsplit.children()):
361 | self.hsplit.replaceWidget(0, self.search_frame)
362 |
363 | if self.current_side_bar == type_:
364 | frame = self.hsplit.children()[0]
365 | if frame.isHidden():
366 | frame.show()
367 | else:
368 | frame.hide()
369 |
370 | self.current_side_bar = type_
371 |
372 |
373 | def tree_view_context_menu(self, pos):
374 | ...
375 |
376 | def new_file(self):
377 | self.set_new_tab(Path("untitled"), is_new_file=True)
378 |
379 | def save_file(self):
380 | if self.current_file is None and self.tab_view.count() > 0:
381 | self.save_as()
382 |
383 | editor = self.tab_view.currentWidget()
384 | self.current_file.write_text(editor.text())
385 | self.statusBar().showMessage(f"Saved {self.current_file.name}", 2000)
386 | editor.current_file_changed = False
387 |
388 | def save_as(self):
389 | # save as
390 | editor = self.tab_view.currentWidget()
391 | if editor is None:
392 | return
393 |
394 | file_path = QFileDialog.getSaveFileName(self, "Save As", os.getcwd())[0]
395 | if file_path == '':
396 | self.statusBar().showMessage("Cancelled", 2000)
397 | return
398 | path = Path(file_path)
399 | path.write_text(editor.text())
400 | self.tab_view.setTabText(self.tab_view.currentIndex(), path.name)
401 | self.statusBar().showMessage(f"Saved {path.name}", 2000)
402 | self.current_file = path
403 | editor.current_file_changed = False
404 |
405 | def open_file(self):
406 | # open file
407 | ops = QFileDialog.Options() # this is optional
408 | ops |= QFileDialog.DontUseNativeDialog
409 | # i will add support for opening multiple files later for now it can only open one at a time
410 | new_file, _ = QFileDialog.getOpenFileName(self,
411 | "Pick A File", "", "All Files (*);;Python Files (*.py)",
412 | options=ops)
413 | if new_file == '':
414 | self.statusBar().showMessage("Cancelled", 2000)
415 | return
416 | f = Path(new_file)
417 | self.set_new_tab(f)
418 |
419 |
420 | def open_folder(self):
421 | # open folder
422 | ops = QFileDialog.Options() # this is optional
423 | ops |= QFileDialog.DontUseNativeDialog
424 |
425 | new_folder = QFileDialog.getExistingDirectory(self, "Pick A Folder", "", options=ops)
426 | if new_folder:
427 | self.model.setRootPath(new_folder)
428 | self.tree_view.setRootIndex(self.model.index(new_folder))
429 | self.statusBar().showMessage(f"Opened {new_folder}", 2000)
430 |
431 | def copy(self):
432 | editor = self.tab_view.currentWidget()
433 | if editor is not None:
434 | editor.copy()
435 |
436 |
437 |
438 |
439 | if __name__ == '__main__':
440 | app = QApplication([])
441 | window = MainWindow()
442 | sys.exit(app.exec())
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
--------------------------------------------------------------------------------
/src/resources.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | # Resource object code
4 | #
5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
6 | #
7 | # WARNING! All changes made in this file will be lost!
8 |
9 | from PyQt5 import QtCore
10 |
11 | qt_resource_data = b"\
12 | \x00\x00\x02\x2c\
13 | \x3c\
14 | \x73\x76\x67\x20\x66\x69\x6c\x6c\x3d\x22\x23\x32\x32\x38\x42\x45\
15 | \x36\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\
16 | \x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\
17 | \x2f\x73\x76\x67\x22\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\
18 | \x30\x20\x30\x20\x34\x38\x20\x34\x38\x22\x20\x77\x69\x64\x74\x68\
19 | \x3d\x22\x31\x34\x34\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\
20 | \x22\x31\x34\x34\x70\x78\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\
21 | \x22\x4d\x20\x33\x39\x2e\x34\x38\x36\x33\x32\x38\x20\x36\x2e\x39\
22 | \x37\x38\x35\x31\x35\x36\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\
23 | \x20\x31\x2e\x35\x30\x30\x31\x35\x20\x30\x20\x30\x20\x30\x20\x33\
24 | \x38\x2e\x34\x33\x39\x34\x35\x33\x20\x37\x2e\x34\x33\x39\x34\x35\
25 | \x33\x31\x20\x4c\x20\x32\x34\x20\x32\x31\x2e\x38\x37\x38\x39\x30\
26 | \x36\x20\x4c\x20\x39\x2e\x35\x36\x30\x35\x34\x36\x39\x20\x37\x2e\
27 | \x34\x33\x39\x34\x35\x33\x31\x20\x41\x20\x31\x2e\x35\x30\x30\x31\
28 | \x35\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x30\x20\x30\x20\x30\x20\
29 | \x38\x2e\x34\x38\x34\x33\x37\x35\x20\x36\x2e\x39\x38\x34\x33\x37\
30 | \x35\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x31\x2e\x35\x30\
31 | \x30\x31\x35\x20\x30\x20\x30\x20\x30\x20\x37\x2e\x34\x33\x39\x34\
32 | \x35\x33\x31\x20\x39\x2e\x35\x36\x30\x35\x34\x36\x39\x20\x4c\x20\
33 | \x32\x31\x2e\x38\x37\x38\x39\x30\x36\x20\x32\x34\x20\x4c\x20\x37\
34 | \x2e\x34\x33\x39\x34\x35\x33\x31\x20\x33\x38\x2e\x34\x33\x39\x34\
35 | \x35\x33\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x31\x2e\x35\
36 | \x30\x30\x31\x35\x20\x30\x20\x31\x20\x30\x20\x39\x2e\x35\x36\x30\
37 | \x35\x34\x36\x39\x20\x34\x30\x2e\x35\x36\x30\x35\x34\x37\x20\x4c\
38 | \x20\x32\x34\x20\x32\x36\x2e\x31\x32\x31\x30\x39\x34\x20\x4c\x20\
39 | \x33\x38\x2e\x34\x33\x39\x34\x35\x33\x20\x34\x30\x2e\x35\x36\x30\
40 | \x35\x34\x37\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x31\x2e\
41 | \x35\x30\x30\x31\x35\x20\x30\x20\x31\x20\x30\x20\x34\x30\x2e\x35\
42 | \x36\x30\x35\x34\x37\x20\x33\x38\x2e\x34\x33\x39\x34\x35\x33\x20\
43 | \x4c\x20\x32\x36\x2e\x31\x32\x31\x30\x39\x34\x20\x32\x34\x20\x4c\
44 | \x20\x34\x30\x2e\x35\x36\x30\x35\x34\x37\x20\x39\x2e\x35\x36\x30\
45 | \x35\x34\x36\x39\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x31\
46 | \x2e\x35\x30\x30\x31\x35\x20\x30\x20\x30\x20\x30\x20\x33\x39\x2e\
47 | \x34\x38\x36\x33\x32\x38\x20\x36\x2e\x39\x37\x38\x35\x31\x35\x36\
48 | \x20\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\
49 | \x00\x00\x03\x54\
50 | \x3c\
51 | \x73\x76\x67\x20\x66\x69\x6c\x6c\x3d\x22\x23\x32\x32\x38\x42\x45\
52 | \x36\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\
53 | \x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\
54 | \x2f\x73\x76\x67\x22\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\
55 | \x30\x20\x30\x20\x34\x38\x20\x34\x38\x22\x20\x77\x69\x64\x74\x68\
56 | \x3d\x22\x31\x34\x34\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\
57 | \x22\x31\x34\x34\x70\x78\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\
58 | \x22\x4d\x20\x38\x2e\x35\x20\x38\x20\x43\x20\x36\x2e\x30\x33\x32\
59 | \x34\x39\x39\x31\x20\x38\x20\x34\x20\x31\x30\x2e\x30\x33\x32\x34\
60 | \x39\x39\x20\x34\x20\x31\x32\x2e\x35\x20\x4c\x20\x34\x20\x33\x35\
61 | \x2e\x35\x20\x43\x20\x34\x20\x33\x37\x2e\x39\x36\x37\x35\x30\x31\
62 | \x20\x36\x2e\x30\x33\x32\x34\x39\x39\x31\x20\x34\x30\x20\x38\x2e\
63 | \x35\x20\x34\x30\x20\x4c\x20\x33\x39\x2e\x35\x20\x34\x30\x20\x43\
64 | \x20\x34\x31\x2e\x39\x36\x37\x35\x30\x31\x20\x34\x30\x20\x34\x34\
65 | \x20\x33\x37\x2e\x39\x36\x37\x35\x30\x31\x20\x34\x34\x20\x33\x35\
66 | \x2e\x35\x20\x4c\x20\x34\x34\x20\x31\x37\x2e\x35\x20\x43\x20\x34\
67 | \x34\x20\x31\x35\x2e\x30\x33\x32\x34\x39\x39\x20\x34\x31\x2e\x39\
68 | \x36\x37\x35\x30\x31\x20\x31\x33\x20\x33\x39\x2e\x35\x20\x31\x33\
69 | \x20\x4c\x20\x32\x34\x2e\x30\x34\x32\x39\x36\x39\x20\x31\x33\x20\
70 | \x4c\x20\x31\x39\x2e\x35\x37\x32\x32\x36\x36\x20\x39\x2e\x32\x37\
71 | \x35\x33\x39\x30\x36\x20\x43\x20\x31\x38\x2e\x35\x38\x34\x30\x35\
72 | \x35\x20\x38\x2e\x34\x35\x32\x31\x31\x30\x35\x20\x31\x37\x2e\x33\
73 | \x33\x39\x31\x36\x32\x20\x38\x20\x31\x36\x2e\x30\x35\x32\x37\x33\
74 | \x34\x20\x38\x20\x4c\x20\x38\x2e\x35\x20\x38\x20\x7a\x20\x4d\x20\
75 | \x38\x2e\x35\x20\x31\x31\x20\x4c\x20\x31\x36\x2e\x30\x35\x32\x37\
76 | \x33\x34\x20\x31\x31\x20\x43\x20\x31\x36\x2e\x36\x33\x38\x33\x30\
77 | \x37\x20\x31\x31\x20\x31\x37\x2e\x32\x30\x32\x35\x35\x35\x20\x31\
78 | \x31\x2e\x32\x30\x35\x33\x35\x38\x20\x31\x37\x2e\x36\x35\x32\x33\
79 | \x34\x34\x20\x31\x31\x2e\x35\x38\x30\x30\x37\x38\x20\x4c\x20\x32\
80 | \x31\x2e\x31\x35\x36\x32\x35\x20\x31\x34\x2e\x35\x20\x4c\x20\x31\
81 | \x37\x2e\x36\x35\x32\x33\x34\x34\x20\x31\x37\x2e\x34\x31\x39\x39\
82 | \x32\x32\x20\x43\x20\x31\x37\x2e\x32\x30\x32\x35\x35\x35\x20\x31\
83 | \x37\x2e\x37\x39\x34\x36\x34\x32\x20\x31\x36\x2e\x36\x33\x38\x33\
84 | \x30\x37\x20\x31\x38\x20\x31\x36\x2e\x30\x35\x32\x37\x33\x34\x20\
85 | \x31\x38\x20\x4c\x20\x37\x20\x31\x38\x20\x4c\x20\x37\x20\x31\x32\
86 | \x2e\x35\x20\x43\x20\x37\x20\x31\x31\x2e\x36\x35\x33\x35\x30\x31\
87 | \x20\x37\x2e\x36\x35\x33\x35\x30\x30\x39\x20\x31\x31\x20\x38\x2e\
88 | \x35\x20\x31\x31\x20\x7a\x20\x4d\x20\x32\x34\x2e\x30\x34\x32\x39\
89 | \x36\x39\x20\x31\x36\x20\x4c\x20\x33\x39\x2e\x35\x20\x31\x36\x20\
90 | \x43\x20\x34\x30\x2e\x33\x34\x36\x34\x39\x39\x20\x31\x36\x20\x34\
91 | \x31\x20\x31\x36\x2e\x36\x35\x33\x35\x30\x31\x20\x34\x31\x20\x31\
92 | \x37\x2e\x35\x20\x4c\x20\x34\x31\x20\x33\x35\x2e\x35\x20\x43\x20\
93 | \x34\x31\x20\x33\x36\x2e\x33\x34\x36\x34\x39\x39\x20\x34\x30\x2e\
94 | \x33\x34\x36\x34\x39\x39\x20\x33\x37\x20\x33\x39\x2e\x35\x20\x33\
95 | \x37\x20\x4c\x20\x38\x2e\x35\x20\x33\x37\x20\x43\x20\x37\x2e\x36\
96 | \x35\x33\x35\x30\x30\x39\x20\x33\x37\x20\x37\x20\x33\x36\x2e\x33\
97 | \x34\x36\x34\x39\x39\x20\x37\x20\x33\x35\x2e\x35\x20\x4c\x20\x37\
98 | \x20\x32\x31\x20\x4c\x20\x31\x36\x2e\x30\x35\x32\x37\x33\x34\x20\
99 | \x32\x31\x20\x43\x20\x31\x37\x2e\x33\x33\x39\x31\x36\x32\x20\x32\
100 | \x31\x20\x31\x38\x2e\x35\x38\x34\x30\x35\x35\x20\x32\x30\x2e\x35\
101 | \x34\x37\x38\x38\x39\x20\x31\x39\x2e\x35\x37\x32\x32\x36\x36\x20\
102 | \x31\x39\x2e\x37\x32\x34\x36\x30\x39\x20\x4c\x20\x32\x34\x2e\x30\
103 | \x34\x32\x39\x36\x39\x20\x31\x36\x20\x7a\x22\x2f\x3e\x3c\x2f\x73\
104 | \x76\x67\x3e\
105 | "
106 |
107 | qt_resource_name = b"\
108 | \x00\x05\
109 | \x00\x6f\xa6\x53\
110 | \x00\x69\
111 | \x00\x63\x00\x6f\x00\x6e\x00\x73\
112 | \x00\x0e\
113 | \x00\xb2\x47\x47\
114 | \x00\x63\
115 | \x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x2d\x00\x69\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\
116 | \x00\x14\
117 | \x04\x53\x5f\xa7\
118 | \x00\x66\
119 | \x00\x6f\x00\x6c\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x69\x00\x63\x00\x6f\x00\x6e\x00\x2d\x00\x62\x00\x6c\x00\x75\x00\x65\x00\x2e\
120 | \x00\x73\x00\x76\x00\x67\
121 | "
122 |
123 | qt_resource_struct_v1 = b"\
124 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
125 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
126 | \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
127 | \x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x02\x30\
128 | "
129 |
130 | qt_resource_struct_v2 = b"\
131 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
132 | \x00\x00\x00\x00\x00\x00\x00\x00\
133 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
134 | \x00\x00\x00\x00\x00\x00\x00\x00\
135 | \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
136 | \x00\x00\x01\x82\x02\x59\x86\x74\
137 | \x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x02\x30\
138 | \x00\x00\x01\x82\x02\x57\xa4\xdd\
139 | "
140 |
141 | qt_version = [int(v) for v in QtCore.qVersion().split('.')]
142 | if qt_version < [5, 8, 0]:
143 | rcc_version = 1
144 | qt_resource_struct = qt_resource_struct_v1
145 | else:
146 | rcc_version = 2
147 | qt_resource_struct = qt_resource_struct_v2
148 |
149 | def qInitResources():
150 | QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)
151 |
152 | def qCleanupResources():
153 | QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data)
154 |
155 | qInitResources()
156 |
--------------------------------------------------------------------------------
/theme.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme": {
3 | "name": "default-dark",
4 | "version": "1.0",
5 |
6 | "syntax": [
7 | {
8 | "default": {
9 | "color": "#abb2bf",
10 | "paper-color": "#282c34",
11 | "font": {
12 | "family": "consolas",
13 | "font-size": 14,
14 | "font-weight": "bold",
15 | "italic": false
16 | }
17 | }
18 | },
19 | {
20 | "keyword": {
21 | "color": "#c678dd",
22 | "paper-color": "#282c34",
23 | "font": {
24 | "family": "consolas",
25 | "font-size": 14,
26 | "font-weight": "bold",
27 | "italic": false
28 | }
29 | }
30 | },
31 | {
32 | "classes": {
33 | "color": "#C68F55",
34 | "paper-color": "#282c34",
35 | "font": {
36 | "family": "consolas",
37 | "font-size": 14,
38 | "font-weight": "bold",
39 | "italic": false
40 | }
41 | }
42 | },
43 | {
44 | "functions": {
45 | "color": "#61afd1",
46 | "paper-color": "#282c34",
47 | "font": {
48 | "family": "consolas",
49 | "font-size": 14,
50 | "font-weight": "bold",
51 | "italic": false
52 | }
53 | }
54 | },
55 | {
56 | "function_def": {
57 | "color": "#61afd1",
58 | "paper-color": "#282c34",
59 | "font": {
60 | "family": "consolas",
61 | "font-size": 14,
62 | "font-weight": "bold",
63 | "italic": true
64 | }
65 | }
66 | },
67 | {
68 | "string": {
69 | "color": "#98c379",
70 | "paper-color": "#282c34"
71 | }
72 | },
73 | {
74 | "types": {
75 | "color": "#56b6c2",
76 | "paper-color": "#282c34"
77 | }
78 | },
79 | {
80 | "keyargs": {
81 | "color": "#c678dd",
82 | "paper-color": "#282c34"
83 | }
84 | },
85 | {
86 | "brackets": {
87 | "color": "#c678dd",
88 | "paper-color": "#282c34"
89 | }
90 | },
91 | {
92 | "comments": {
93 | "color": "#777777",
94 | "paper-color": "#282c34"
95 | }
96 | },
97 | {
98 | "constants": {
99 | "color": "#d19a5e",
100 | "paper-color": "#282c34"
101 | }
102 | }
103 | ]
104 | }
105 | }
106 |
--------------------------------------------------------------------------------