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.
- pyqt_code_editor/__init__.py +5 -0
- pyqt_code_editor/_settings.py +232 -0
- pyqt_code_editor/app.py +438 -0
- pyqt_code_editor/code_editors/__init__.py +1 -0
- pyqt_code_editor/code_editors/code_editors.py +33 -0
- pyqt_code_editor/code_editors/languages/__init__.py +0 -0
- pyqt_code_editor/code_editors/languages/generic.py +11 -0
- pyqt_code_editor/code_editors/languages/markdown.py +11 -0
- pyqt_code_editor/code_editors/languages/python.py +13 -0
- pyqt_code_editor/code_editors/languages/text.py +9 -0
- pyqt_code_editor/components/__init__.py +0 -0
- pyqt_code_editor/components/editor_panel.py +399 -0
- pyqt_code_editor/components/find_in_files.py +199 -0
- pyqt_code_editor/components/jupyter_console.py +487 -0
- pyqt_code_editor/components/project_explorer.py +515 -0
- pyqt_code_editor/components/settings_panel.py +162 -0
- pyqt_code_editor/components/sigmund.py +75 -0
- pyqt_code_editor/components/workspace_explorer.py +162 -0
- pyqt_code_editor/mixins/__init__.py +15 -0
- pyqt_code_editor/mixins/auto_pair.py +201 -0
- pyqt_code_editor/mixins/base.py +142 -0
- pyqt_code_editor/mixins/check.py +94 -0
- pyqt_code_editor/mixins/comment.py +91 -0
- pyqt_code_editor/mixins/complete.py +482 -0
- pyqt_code_editor/mixins/execute.py +182 -0
- pyqt_code_editor/mixins/file_link.py +166 -0
- pyqt_code_editor/mixins/line_number.py +206 -0
- pyqt_code_editor/mixins/python_auto_indent.py +239 -0
- pyqt_code_editor/mixins/search_replace.py +396 -0
- pyqt_code_editor/mixins/shortcuts.py +228 -0
- pyqt_code_editor/mixins/smart_backspace_delete.py +120 -0
- pyqt_code_editor/mixins/symbols.py +45 -0
- pyqt_code_editor/mixins/theme.py +99 -0
- pyqt_code_editor/mixins/widgets/__init__.py +0 -0
- pyqt_code_editor/mixins/zoom.py +51 -0
- pyqt_code_editor/python_utils.py +936 -0
- pyqt_code_editor/signal_router.py +116 -0
- pyqt_code_editor/syntax_highlighters/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/__init__.py +0 -0
- pyqt_code_editor/syntax_highlighters/languages/generic.py +86 -0
- pyqt_code_editor/syntax_highlighters/languages/python.py +62 -0
- pyqt_code_editor/syntax_highlighters/syntax_highlighter.py +26 -0
- pyqt_code_editor/themes.py +36 -0
- pyqt_code_editor/utils.py +96 -0
- pyqt_code_editor/watchdog.py +141 -0
- pyqt_code_editor/widgets/__init__.py +6 -0
- pyqt_code_editor/widgets/calltip_widget.py +44 -0
- pyqt_code_editor/widgets/completion_popup.py +89 -0
- pyqt_code_editor/widgets/dock.py +15 -0
- pyqt_code_editor/widgets/quick_open_dialog.py +196 -0
- pyqt_code_editor/widgets/quick_open_file_dialog.py +32 -0
- pyqt_code_editor/widgets/quick_symbol_dialog.py +13 -0
- pyqt_code_editor/widgets/search_replace_frame.py +128 -0
- pyqt_code_editor/widgets/tab_splitter.py +22 -0
- pyqt_code_editor/widgets/tabbed_editor.py +351 -0
- pyqt_code_editor/worker/__init__.py +0 -0
- pyqt_code_editor/worker/languages/__init__.py +0 -0
- pyqt_code_editor/worker/languages/generic.py +15 -0
- pyqt_code_editor/worker/languages/python.py +17 -0
- pyqt_code_editor/worker/manager.py +113 -0
- pyqt_code_editor/worker/process.py +128 -0
- pyqt_code_editor/worker/providers/__init__.py +1 -0
- pyqt_code_editor/worker/providers/codestral.py +56 -0
- pyqt_code_editor/worker/providers/jedi.py +115 -0
- pyqt_code_editor/worker/providers/ruff.py +45 -0
- pyqt_code_editor/worker/providers/sigmund.py +29 -0
- pyqt_code_editor/worker/providers/symbol.py +78 -0
- pyqt_code_editor/worker/settings.py +1 -0
- pyqt_code_editor-0.0.1.dist-info/METADATA +69 -0
- pyqt_code_editor-0.0.1.dist-info/RECORD +73 -0
- pyqt_code_editor-0.0.1.dist-info/WHEEL +4 -0
- pyqt_code_editor-0.0.1.dist-info/entry_points.txt +4 -0
- pyqt_code_editor-0.0.1.dist-info/licenses/COPYING +674 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from qtpy.QtCore import QObject, Signal, QSettings
|
|
4
|
+
import logging
|
|
5
|
+
logging.basicConfig(level=logging.INFO, force=True)
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SettingProperty:
|
|
10
|
+
"""
|
|
11
|
+
Descriptor for setting properties that emit signals when changed.
|
|
12
|
+
|
|
13
|
+
This class uses Python's descriptor protocol to allow settings to be accessed
|
|
14
|
+
like normal attributes (settings.font_size) while providing custom behavior
|
|
15
|
+
when they are accessed or modified.
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, default_value, category: str = "General"):
|
|
18
|
+
self.name = None # Will be set when the class is created
|
|
19
|
+
self.default_value = default_value
|
|
20
|
+
self.category = category
|
|
21
|
+
|
|
22
|
+
def __set_name__(self, owner, name):
|
|
23
|
+
"""Called when the descriptor is assigned to a class attribute"""
|
|
24
|
+
self.name = name
|
|
25
|
+
|
|
26
|
+
def __get__(self, instance, owner):
|
|
27
|
+
"""
|
|
28
|
+
Called when the attribute is accessed (settings.font_size)
|
|
29
|
+
|
|
30
|
+
This is why settings.font_size returns an int instead of a SettingProperty.
|
|
31
|
+
When you access settings.font_size, Python calls this method instead of
|
|
32
|
+
returning the descriptor object itself.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
instance: The Settings instance (or None if accessed from the class)
|
|
36
|
+
owner: The Settings class
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The actual setting value (not the descriptor)
|
|
40
|
+
"""
|
|
41
|
+
if instance is None:
|
|
42
|
+
return self # When accessed from the class (Settings.font_size), return the descriptor
|
|
43
|
+
return instance._values.get(self.name, self.default_value)
|
|
44
|
+
|
|
45
|
+
def __set__(self, instance, value):
|
|
46
|
+
"""Called when the attribute is set (settings.font_size = 18)"""
|
|
47
|
+
old_value = self.__get__(instance, type(instance))
|
|
48
|
+
if old_value != value:
|
|
49
|
+
instance._values[self.name] = value
|
|
50
|
+
logger.info(f"Setting changed: {self.name} = {value} (was {old_value})")
|
|
51
|
+
instance.setting_changed.emit(self.name, value)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Settings(QObject):
|
|
55
|
+
"""Qt-based settings class with change signals and persistence"""
|
|
56
|
+
|
|
57
|
+
# Signal emitted when any setting changes: (setting_name, new_value)
|
|
58
|
+
setting_changed = Signal(str, object)
|
|
59
|
+
|
|
60
|
+
# Singleton instance
|
|
61
|
+
_instance = None
|
|
62
|
+
|
|
63
|
+
def __new__(cls):
|
|
64
|
+
if cls._instance is None:
|
|
65
|
+
cls._instance = super(Settings, cls).__new__(cls)
|
|
66
|
+
cls._instance._initialized = False
|
|
67
|
+
return cls._instance
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
if self._initialized:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
super().__init__()
|
|
74
|
+
self._values = {}
|
|
75
|
+
self._qsettings = QSettings("cogscinl", "Sigmund Analyst")
|
|
76
|
+
self._initialized = True
|
|
77
|
+
logger.info("Initializing Settings manager")
|
|
78
|
+
self._load_settings()
|
|
79
|
+
|
|
80
|
+
# Appearance
|
|
81
|
+
font_size = SettingProperty(16, "Appearance")
|
|
82
|
+
font_family = SettingProperty('Ubuntu Mono', "Appearance")
|
|
83
|
+
color_scheme = SettingProperty('monokai', "Appearance")
|
|
84
|
+
tab_width = SettingProperty(4, "Appearance")
|
|
85
|
+
word_wrap = SettingProperty(False, "Appearance")
|
|
86
|
+
character_ruler = SettingProperty(80, "Appearance") # None to disable
|
|
87
|
+
search_replace_background = SettingProperty("#fdff74", "Appearance")
|
|
88
|
+
search_replace_foreground = SettingProperty("#000000", "Appearance")
|
|
89
|
+
|
|
90
|
+
# Files and Folders
|
|
91
|
+
current_folder = SettingProperty(os.path.expanduser('~'), "Files")
|
|
92
|
+
default_filename = SettingProperty('untitled.txt', "Files")
|
|
93
|
+
default_encoding = SettingProperty('utf-8', "Files")
|
|
94
|
+
|
|
95
|
+
# Keyboard shortcuts
|
|
96
|
+
shortcut_move_line_up = SettingProperty('Alt+Up', "Shortcuts")
|
|
97
|
+
shortcut_move_line_down = SettingProperty('Alt+Down', "Shortcuts")
|
|
98
|
+
shortcut_duplicate_line = SettingProperty('Ctrl+D', "Shortcuts")
|
|
99
|
+
shortcut_comment = SettingProperty('Ctrl+/', "Shortcuts")
|
|
100
|
+
shortcut_split_horizontally = SettingProperty('Ctrl+Shift+H', "Shortcuts")
|
|
101
|
+
shortcut_split_vertically = SettingProperty('Ctrl+Shift+V', "Shortcuts")
|
|
102
|
+
shortcut_quick_open_file = SettingProperty('Ctrl+P', "Shortcuts")
|
|
103
|
+
shortcut_symbols = SettingProperty('Ctrl+R', "Shortcuts")
|
|
104
|
+
shortcut_previous_tab = SettingProperty('Ctrl+Shift+Tab', "Shortcuts")
|
|
105
|
+
shortcut_close_all_tabs = SettingProperty('Ctrl+Shift+W', "Shortcuts")
|
|
106
|
+
shortcut_close_other_tabs = SettingProperty('Ctrl+Alt+W', "Shortcuts")
|
|
107
|
+
shortcut_open_folder = SettingProperty('Ctrl+Shift+O', "Shortcuts")
|
|
108
|
+
shortcut_find_in_files = SettingProperty('Ctrl+Shift+F', "Shortcuts")
|
|
109
|
+
shortcut_execute_code = SettingProperty('F9', "Shortcuts")
|
|
110
|
+
shortcut_execute_file = SettingProperty('F5', "Shortcuts")
|
|
111
|
+
shortcut_toggle_jupyter_console = SettingProperty('Ctrl+Shift+J', "Shortcuts")
|
|
112
|
+
shortcut_toggle_sigmund = SettingProperty('Ctrl+Shift+I', "Shortcuts")
|
|
113
|
+
shortcut_toggle_workspace_explorer = SettingProperty('Ctrl+Shift+X', "Shortcuts")
|
|
114
|
+
shortcut_toggle_project_explorers = SettingProperty('Ctrl+\\', "Shortcuts")
|
|
115
|
+
|
|
116
|
+
# Project explorer
|
|
117
|
+
max_files = SettingProperty(1000, "ProjectExplorer")
|
|
118
|
+
ignored_folders = SettingProperty(['.git'], "ProjectExplorer")
|
|
119
|
+
|
|
120
|
+
# Completions
|
|
121
|
+
max_completions = SettingProperty(5, "Completion")
|
|
122
|
+
full_completion_delay = SettingProperty(250, "Completion")
|
|
123
|
+
hide_completion_delay = SettingProperty(500, "Completion")
|
|
124
|
+
|
|
125
|
+
# Code checking
|
|
126
|
+
check_debounce_delay = SettingProperty(500, "Checking")
|
|
127
|
+
check_interval_delay = SettingProperty(5000, "Checking")
|
|
128
|
+
|
|
129
|
+
# Sigmund provider
|
|
130
|
+
sigmund_max_context = SettingProperty(2000, "Sigmund")
|
|
131
|
+
sigmund_fim_endpoint = SettingProperty('http://localhost:5000/code_completion/fim', "Sigmund")
|
|
132
|
+
sigmund_token = SettingProperty(None, "Sigmund")
|
|
133
|
+
sigmund_timeout = SettingProperty(1, "Sigmund") # seconds
|
|
134
|
+
|
|
135
|
+
# Codestral provider
|
|
136
|
+
codestral_max_context = SettingProperty(2000, "Codestral")
|
|
137
|
+
codestral_min_context = SettingProperty(100, "Codestral")
|
|
138
|
+
codestral_model = SettingProperty('codestral-latest', "Codestral")
|
|
139
|
+
codestral_api_key = SettingProperty(os.environ.get('CODESTRAL_API_KEY', ''), "Codestral")
|
|
140
|
+
codestral_url = SettingProperty('https://codestral.mistral.ai', "Codestral")
|
|
141
|
+
codestral_timeout = SettingProperty(5000, "Codestral")
|
|
142
|
+
|
|
143
|
+
# Window geometry
|
|
144
|
+
window_geometry = SettingProperty('', "Window")
|
|
145
|
+
window_state = SettingProperty('', "Window")
|
|
146
|
+
|
|
147
|
+
def save(self):
|
|
148
|
+
"""Save all settings to persistent storage"""
|
|
149
|
+
logger.info("Saving settings to persistent storage")
|
|
150
|
+
for setting_name, value in self._values.items():
|
|
151
|
+
self._qsettings.setValue(setting_name, value)
|
|
152
|
+
self._qsettings.sync()
|
|
153
|
+
logger.info(f"Saved {len(self._values)} settings")
|
|
154
|
+
|
|
155
|
+
def _load_settings(self):
|
|
156
|
+
"""Load settings from persistent storage"""
|
|
157
|
+
logger.info("Loading settings from persistent storage")
|
|
158
|
+
setting_names = self.get_all_setting_names()
|
|
159
|
+
loaded_count = 0
|
|
160
|
+
|
|
161
|
+
for setting_name in setting_names:
|
|
162
|
+
if self._qsettings.contains(setting_name):
|
|
163
|
+
value = self._qsettings.value(setting_name)
|
|
164
|
+
# Convert the value back to the correct type based on default
|
|
165
|
+
default = getattr(type(self), setting_name).default_value
|
|
166
|
+
|
|
167
|
+
if isinstance(default, bool) and not isinstance(value, bool):
|
|
168
|
+
value = value.lower() == 'true'
|
|
169
|
+
elif isinstance(default, int) and not isinstance(value, int):
|
|
170
|
+
try:
|
|
171
|
+
value = int(value)
|
|
172
|
+
except (ValueError, TypeError):
|
|
173
|
+
logger.warning(f"Failed to convert setting '{setting_name}' value to int: {value}")
|
|
174
|
+
value = default
|
|
175
|
+
elif isinstance(default, float) and not isinstance(value, float):
|
|
176
|
+
try:
|
|
177
|
+
value = float(value)
|
|
178
|
+
except (ValueError, TypeError):
|
|
179
|
+
logger.warning(f"Failed to convert setting '{setting_name}' value to float: {value}")
|
|
180
|
+
value = default
|
|
181
|
+
|
|
182
|
+
self._values[setting_name] = value
|
|
183
|
+
loaded_count += 1
|
|
184
|
+
logger.info(f"Loaded setting: {setting_name} = {value}")
|
|
185
|
+
|
|
186
|
+
logger.info(f"Loaded {loaded_count} settings ({len(setting_names) - loaded_count} using defaults)")
|
|
187
|
+
|
|
188
|
+
def reset_to_defaults(self):
|
|
189
|
+
"""Reset all settings to their default values"""
|
|
190
|
+
logger.info("Resetting all settings to defaults")
|
|
191
|
+
self._values.clear()
|
|
192
|
+
self.save()
|
|
193
|
+
for setting_name in self.get_all_setting_names():
|
|
194
|
+
# Access each property to trigger signal emission with default value
|
|
195
|
+
default_value = getattr(type(self), setting_name).default_value
|
|
196
|
+
logger.info(f"Reset setting: {setting_name} = {default_value} (default)")
|
|
197
|
+
self.setting_changed.emit(setting_name, default_value)
|
|
198
|
+
|
|
199
|
+
def get_all_setting_names(self) -> list[str]:
|
|
200
|
+
"""Get a list of all setting names"""
|
|
201
|
+
names = [name for name, attr in vars(type(self)).items()
|
|
202
|
+
if isinstance(attr, SettingProperty)]
|
|
203
|
+
logger.info(f"Found {len(names)} settings in total")
|
|
204
|
+
return names
|
|
205
|
+
|
|
206
|
+
def get_settings_by_category(self) -> dict[str, list[str]]:
|
|
207
|
+
"""Get a dictionary of settings grouped by category"""
|
|
208
|
+
result = {}
|
|
209
|
+
for name, attr in vars(type(self)).items():
|
|
210
|
+
if isinstance(attr, SettingProperty):
|
|
211
|
+
category = attr.category
|
|
212
|
+
if category not in result:
|
|
213
|
+
result[category] = []
|
|
214
|
+
result[category].append(name)
|
|
215
|
+
|
|
216
|
+
categories_count = len(result)
|
|
217
|
+
logger.info(f"Settings organized into {categories_count} categories")
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
def __iter__(self):
|
|
221
|
+
"""Iterate over all settings"""
|
|
222
|
+
for name in self.get_all_setting_names():
|
|
223
|
+
yield name, getattr(self, name)
|
|
224
|
+
|
|
225
|
+
def __str__(self):
|
|
226
|
+
"""Returns all settings as a JSON array of objects."""
|
|
227
|
+
# todo
|
|
228
|
+
return json.dumps({name: value for name, value in self})
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# Create the singleton instance
|
|
232
|
+
settings = Settings()
|
pyqt_code_editor/app.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
from qtpy.QtWidgets import QMainWindow, QShortcut, QMessageBox, \
|
|
5
|
+
QDockWidget, QToolBar, QAction, QApplication
|
|
6
|
+
from qtpy.QtCore import Qt, QByteArray, QSize
|
|
7
|
+
from qtpy.QtGui import QKeySequence
|
|
8
|
+
import qtawesome as qta
|
|
9
|
+
from .widgets import QuickOpenFileDialog
|
|
10
|
+
from .components.editor_panel import EditorPanel
|
|
11
|
+
from .components.project_explorer import ProjectExplorer
|
|
12
|
+
from .components.find_in_files import FindInFiles
|
|
13
|
+
from .components.jupyter_console import JupyterConsole
|
|
14
|
+
from .components.workspace_explorer import WorkspaceExplorer
|
|
15
|
+
from .components.sigmund import Sigmund
|
|
16
|
+
from .components.settings_panel import SettingsPanel
|
|
17
|
+
from . import settings, watchdog
|
|
18
|
+
from pyqt_code_editor.signal_router import signal_router
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO, force=True)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MainWindow(QMainWindow):
|
|
25
|
+
def __init__(self, root_path=None):
|
|
26
|
+
super().__init__()
|
|
27
|
+
logger.info("MainWindow initialized")
|
|
28
|
+
# If no path is provided, fall back to current dir
|
|
29
|
+
if not root_path:
|
|
30
|
+
root_path = settings.current_folder
|
|
31
|
+
|
|
32
|
+
self.setWindowTitle("Sigmund Analyst")
|
|
33
|
+
# The editor panel provides splittable editor tabs
|
|
34
|
+
self._editor_panel = EditorPanel()
|
|
35
|
+
self.setCentralWidget(self._editor_panel)
|
|
36
|
+
self._project_explorers = []
|
|
37
|
+
# Track if project explorers are hidden as a group
|
|
38
|
+
self._project_explorers_hidden = False
|
|
39
|
+
# Open initial project explorer
|
|
40
|
+
self._open_project_explorer(root_path)
|
|
41
|
+
|
|
42
|
+
# Set up the workspace explorer
|
|
43
|
+
self._workspace_explorer = WorkspaceExplorer(parent=self)
|
|
44
|
+
self.addDockWidget(Qt.RightDockWidgetArea, self._workspace_explorer)
|
|
45
|
+
self._setup_dock_widget(self._workspace_explorer, "Workspace Explorer")
|
|
46
|
+
|
|
47
|
+
# Set up the jupyter console
|
|
48
|
+
self._jupyter_console = JupyterConsole(parent=self)
|
|
49
|
+
self._jupyter_console.workspace_updated.connect(
|
|
50
|
+
self._workspace_explorer.update
|
|
51
|
+
)
|
|
52
|
+
self.addDockWidget(Qt.BottomDockWidgetArea, self._jupyter_console)
|
|
53
|
+
self._setup_dock_widget(self._jupyter_console, "Jupyter Console")
|
|
54
|
+
|
|
55
|
+
# Set up Sigmund
|
|
56
|
+
self._sigmund = Sigmund(parent=self, editor_panel=self._editor_panel)
|
|
57
|
+
self.addDockWidget(Qt.RightDockWidgetArea, self._sigmund)
|
|
58
|
+
self._setup_dock_widget(self._sigmund, "Sigmund")
|
|
59
|
+
|
|
60
|
+
# Set up settings panel
|
|
61
|
+
self._settings_panel = SettingsPanel(parent=self)
|
|
62
|
+
self.addDockWidget(Qt.RightDockWidgetArea, self._settings_panel)
|
|
63
|
+
self._setup_dock_widget(self._settings_panel, "Settings")
|
|
64
|
+
|
|
65
|
+
# Shortcuts
|
|
66
|
+
self._quick_open_shortcut = QShortcut(
|
|
67
|
+
QKeySequence(settings.shortcut_quick_open_file), self
|
|
68
|
+
)
|
|
69
|
+
self._quick_open_shortcut.activated.connect(self._show_quick_open)
|
|
70
|
+
|
|
71
|
+
self._open_folder_shortcut = QShortcut(
|
|
72
|
+
QKeySequence(settings.shortcut_open_folder), self
|
|
73
|
+
)
|
|
74
|
+
self._open_folder_shortcut.activated.connect(self._open_folder)
|
|
75
|
+
|
|
76
|
+
self._find_in_files_shortcut = QShortcut(
|
|
77
|
+
QKeySequence(settings.shortcut_find_in_files), self
|
|
78
|
+
)
|
|
79
|
+
self._find_in_files_shortcut.activated.connect(self._find_in_files)
|
|
80
|
+
|
|
81
|
+
# Add dock widget visibility toggle shortcuts
|
|
82
|
+
self._workspace_toggle_shortcut = QShortcut(
|
|
83
|
+
QKeySequence(settings.shortcut_toggle_workspace_explorer), self
|
|
84
|
+
)
|
|
85
|
+
self._workspace_toggle_shortcut.activated.connect(
|
|
86
|
+
lambda: self._toggle_dock_widget(self._workspace_explorer)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self._jupyter_toggle_shortcut = QShortcut(
|
|
90
|
+
QKeySequence(settings.shortcut_toggle_jupyter_console), self
|
|
91
|
+
)
|
|
92
|
+
self._jupyter_toggle_shortcut.activated.connect(
|
|
93
|
+
lambda: self._toggle_dock_widget(self._jupyter_console)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self._sigmund_toggle_shortcut = QShortcut(
|
|
97
|
+
QKeySequence(settings.shortcut_toggle_sigmund), self
|
|
98
|
+
)
|
|
99
|
+
self._sigmund_toggle_shortcut.activated.connect(
|
|
100
|
+
lambda: self._toggle_dock_widget(self._sigmund)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Add project explorer group toggle shortcut
|
|
104
|
+
self._project_toggle_shortcut = QShortcut(
|
|
105
|
+
QKeySequence(settings.shortcut_toggle_project_explorers), self
|
|
106
|
+
)
|
|
107
|
+
self._project_toggle_shortcut.activated.connect(
|
|
108
|
+
self._toggle_project_explorers
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Connect to dynamically created signals
|
|
112
|
+
signal_router.connect_to_signal("execute_code", self._execute_code)
|
|
113
|
+
signal_router.connect_to_signal("execute_file", self._execute_file)
|
|
114
|
+
|
|
115
|
+
self._setup_toolbar()
|
|
116
|
+
|
|
117
|
+
# If we have no saved geometry/state, hide the docks by default
|
|
118
|
+
if not settings.window_geometry or not settings.window_state:
|
|
119
|
+
self._workspace_explorer.hide()
|
|
120
|
+
self._jupyter_console.hide()
|
|
121
|
+
self._settings_panel.hide()
|
|
122
|
+
else:
|
|
123
|
+
# Restore geometry and state
|
|
124
|
+
try:
|
|
125
|
+
geom = QByteArray.fromBase64(
|
|
126
|
+
settings.window_geometry.encode('utf-8')
|
|
127
|
+
)
|
|
128
|
+
state = QByteArray.fromBase64(
|
|
129
|
+
settings.window_state.encode('utf-8')
|
|
130
|
+
)
|
|
131
|
+
self.restoreGeometry(geom)
|
|
132
|
+
self.restoreState(state)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.warning(f"Could not restore window geometry/state: {e}")
|
|
135
|
+
|
|
136
|
+
def _setup_toolbar(self):
|
|
137
|
+
"""Setup the main toolbar with Material Design icons."""
|
|
138
|
+
# Create toolbar
|
|
139
|
+
self.toolbar = QToolBar("Main Toolbar")
|
|
140
|
+
self.toolbar.setObjectName('main_toolbar')
|
|
141
|
+
self.toolbar.setIconSize(QSize(32, 32))
|
|
142
|
+
self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
|
|
143
|
+
self.addToolBar(self.toolbar)
|
|
144
|
+
|
|
145
|
+
# FILE OPERATIONS
|
|
146
|
+
new_file_action = QAction(qta.icon('mdi6.file-plus'), "New File", self)
|
|
147
|
+
new_file_action.setStatusTip("Create a new file")
|
|
148
|
+
new_file_action.triggered.connect(
|
|
149
|
+
lambda: self._editor_panel.open_file()
|
|
150
|
+
)
|
|
151
|
+
self.toolbar.addAction(new_file_action)
|
|
152
|
+
|
|
153
|
+
open_file_action = QAction(qta.icon('mdi6.folder-open'), "Open File", self)
|
|
154
|
+
open_file_action.setStatusTip("Open an existing file")
|
|
155
|
+
open_file_action.triggered.connect(
|
|
156
|
+
lambda: self._editor_panel.select_and_open_file()
|
|
157
|
+
)
|
|
158
|
+
self.toolbar.addAction(open_file_action)
|
|
159
|
+
|
|
160
|
+
open_folder_action = QAction(qta.icon('mdi6.folder-multiple'), "Open Folder", self)
|
|
161
|
+
open_folder_action.setStatusTip("Open a folder as project")
|
|
162
|
+
open_folder_action.triggered.connect(self._open_folder)
|
|
163
|
+
self.toolbar.addAction(open_folder_action)
|
|
164
|
+
|
|
165
|
+
self.toolbar.addSeparator()
|
|
166
|
+
|
|
167
|
+
# QUICK ACCESS
|
|
168
|
+
quick_file_action = QAction(qta.icon('mdi6.file-find'), "Quick Select File", self)
|
|
169
|
+
quick_file_action.setStatusTip("Quickly find and open a file")
|
|
170
|
+
quick_file_action.triggered.connect(self._show_quick_open)
|
|
171
|
+
self.toolbar.addAction(quick_file_action)
|
|
172
|
+
|
|
173
|
+
quick_symbol_action = QAction(qta.icon('mdi6.code-tags'), "Quick Select Symbol", self)
|
|
174
|
+
quick_symbol_action.setStatusTip("Quickly find a symbol in current file")
|
|
175
|
+
quick_symbol_action.triggered.connect(
|
|
176
|
+
lambda: self._trigger_editor_shortcut('symbol_shortcut')
|
|
177
|
+
)
|
|
178
|
+
self.toolbar.addAction(quick_symbol_action)
|
|
179
|
+
|
|
180
|
+
self.toolbar.addSeparator()
|
|
181
|
+
|
|
182
|
+
# EXECUTION CONTROLS
|
|
183
|
+
run_file_action = QAction(qta.icon('mdi6.play'), "Run File", self)
|
|
184
|
+
run_file_action.setStatusTip("Run the current file")
|
|
185
|
+
run_file_action.triggered.connect(
|
|
186
|
+
lambda: self._trigger_editor_shortcut('execute_file_shortcut')
|
|
187
|
+
)
|
|
188
|
+
self.toolbar.addAction(run_file_action)
|
|
189
|
+
|
|
190
|
+
run_selection_action = QAction(
|
|
191
|
+
qta.icon('mdi6.play-box-outline'),
|
|
192
|
+
"Run Cell or Selection",
|
|
193
|
+
self
|
|
194
|
+
)
|
|
195
|
+
run_selection_action.setStatusTip("Run the selected cell or lines")
|
|
196
|
+
run_selection_action.triggered.connect(
|
|
197
|
+
lambda: self._trigger_editor_shortcut('execute_code_shortcut')
|
|
198
|
+
)
|
|
199
|
+
self.toolbar.addAction(run_selection_action)
|
|
200
|
+
|
|
201
|
+
break_kernel_action = QAction(qta.icon('mdi6.stop'), "Break Kernel", self)
|
|
202
|
+
break_kernel_action.setStatusTip("Interrupt the kernel")
|
|
203
|
+
break_kernel_action.triggered.connect(
|
|
204
|
+
lambda: self._jupyter_console.interrupt_current_kernel()
|
|
205
|
+
)
|
|
206
|
+
self.toolbar.addAction(break_kernel_action)
|
|
207
|
+
|
|
208
|
+
restart_kernel_action = QAction(qta.icon('mdi6.restart'), "Restart Kernel", self)
|
|
209
|
+
restart_kernel_action.setStatusTip("Restart the kernel")
|
|
210
|
+
restart_kernel_action.triggered.connect(
|
|
211
|
+
lambda: self._jupyter_console.restart_current_kernel()
|
|
212
|
+
)
|
|
213
|
+
self.toolbar.addAction(restart_kernel_action)
|
|
214
|
+
|
|
215
|
+
self.toolbar.addSeparator()
|
|
216
|
+
|
|
217
|
+
# FIND IN FILES
|
|
218
|
+
find_in_files_action = QAction(qta.icon('mdi6.file-search'), "Find in Files", self)
|
|
219
|
+
find_in_files_action.setStatusTip("Search across multiple files")
|
|
220
|
+
find_in_files_action.triggered.connect(self._find_in_files)
|
|
221
|
+
self.toolbar.addAction(find_in_files_action)
|
|
222
|
+
|
|
223
|
+
self.toolbar.addSeparator()
|
|
224
|
+
|
|
225
|
+
# SPLIT VIEWS
|
|
226
|
+
split_vert_action = QAction(qta.icon('mdi6.arrow-split-horizontal'), "Split Vertically", self)
|
|
227
|
+
split_vert_action.setStatusTip("Split the editor vertically")
|
|
228
|
+
split_vert_action.triggered.connect(
|
|
229
|
+
lambda: self._editor_panel.split(Qt.Vertical)
|
|
230
|
+
)
|
|
231
|
+
self.toolbar.addAction(split_vert_action)
|
|
232
|
+
|
|
233
|
+
split_horz_action = QAction(qta.icon('mdi6.arrow-split-vertical'), "Split Horizontally", self)
|
|
234
|
+
split_horz_action.setStatusTip("Split the editor horizontally")
|
|
235
|
+
split_horz_action.triggered.connect(
|
|
236
|
+
lambda: self._editor_panel.split(Qt.Horizontal)
|
|
237
|
+
)
|
|
238
|
+
self.toolbar.addAction(split_horz_action)
|
|
239
|
+
|
|
240
|
+
self.toolbar.addSeparator()
|
|
241
|
+
|
|
242
|
+
# DOCK TOGGLES – now checkable
|
|
243
|
+
toggle_settings_panel_action = QAction(qta.icon('mdi6.cogs'), "Toggle Settings", self)
|
|
244
|
+
toggle_settings_panel_action.setCheckable(True)
|
|
245
|
+
toggle_settings_panel_action.triggered.connect(
|
|
246
|
+
lambda checked: self._toggle_dock_widget(self._settings_panel, show=checked)
|
|
247
|
+
)
|
|
248
|
+
self._settings_panel.visibilityChanged.connect(toggle_settings_panel_action.setChecked)
|
|
249
|
+
self.toolbar.addAction(toggle_settings_panel_action)
|
|
250
|
+
|
|
251
|
+
self.toolbar.addSeparator()
|
|
252
|
+
|
|
253
|
+
self._toggle_project_action = QAction(qta.icon('mdi6.folder-outline'), "Toggle Project Explorer", self)
|
|
254
|
+
self._toggle_project_action.setCheckable(True)
|
|
255
|
+
self._toggle_project_action.setChecked(True)
|
|
256
|
+
self._toggle_project_action.setStatusTip("Show/hide project explorers")
|
|
257
|
+
self._toggle_project_action.triggered.connect(self._toggle_project_explorers)
|
|
258
|
+
self.toolbar.addAction(self._toggle_project_action)
|
|
259
|
+
|
|
260
|
+
toggle_jupyter_action = QAction(qta.icon('mdi6.console'), "Toggle Jupyter Console", self)
|
|
261
|
+
toggle_jupyter_action.setCheckable(True)
|
|
262
|
+
toggle_jupyter_action.triggered.connect(
|
|
263
|
+
lambda checked: self._toggle_dock_widget(self._jupyter_console, show=checked)
|
|
264
|
+
)
|
|
265
|
+
self._jupyter_console.visibilityChanged.connect(toggle_jupyter_action.setChecked)
|
|
266
|
+
self.toolbar.addAction(toggle_jupyter_action)
|
|
267
|
+
|
|
268
|
+
toggle_workspace_action = QAction(qta.icon('mdi6.view-list'), "Toggle Workspace Explorer", self)
|
|
269
|
+
toggle_workspace_action.setCheckable(True)
|
|
270
|
+
toggle_workspace_action.triggered.connect(
|
|
271
|
+
lambda checked: self._toggle_dock_widget(self._workspace_explorer, show=checked)
|
|
272
|
+
)
|
|
273
|
+
self._workspace_explorer.visibilityChanged.connect(toggle_workspace_action.setChecked)
|
|
274
|
+
self.toolbar.addAction(toggle_workspace_action)
|
|
275
|
+
|
|
276
|
+
toggle_sigmund_action = QAction(qta.icon('mdi6.brain'), "Toggle Sigmund", self)
|
|
277
|
+
toggle_sigmund_action.setCheckable(True)
|
|
278
|
+
toggle_sigmund_action.triggered.connect(
|
|
279
|
+
lambda checked: self._toggle_dock_widget(self._sigmund, show=checked)
|
|
280
|
+
)
|
|
281
|
+
self._sigmund.visibilityChanged.connect(toggle_sigmund_action.setChecked)
|
|
282
|
+
self.toolbar.addAction(toggle_sigmund_action)
|
|
283
|
+
|
|
284
|
+
def _trigger_editor_shortcut(self, shortcut):
|
|
285
|
+
editor = self._editor_panel.active_editor()
|
|
286
|
+
if editor is not None and hasattr(editor, shortcut):
|
|
287
|
+
getattr(editor, shortcut).activated.emit()
|
|
288
|
+
|
|
289
|
+
def _setup_dock_widget(self, dock_widget, name):
|
|
290
|
+
"""Configures dock widget to hide on close instead of actually closing."""
|
|
291
|
+
if isinstance(dock_widget, QDockWidget):
|
|
292
|
+
# Override close behavior
|
|
293
|
+
dock_widget.closeEvent = lambda event: self._handle_dock_close(event, dock_widget)
|
|
294
|
+
else:
|
|
295
|
+
# If the dock widget is a custom class with a close_requested signal
|
|
296
|
+
if hasattr(dock_widget, 'close_requested'):
|
|
297
|
+
dock_widget.close_requested.connect(
|
|
298
|
+
lambda: self._toggle_dock_widget(dock_widget, show=False)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _handle_dock_close(self, event, dock_widget):
|
|
302
|
+
"""Handles close events for dock widgets by hiding them instead."""
|
|
303
|
+
event.ignore() # Prevent the actual close
|
|
304
|
+
dock_widget.hide()
|
|
305
|
+
|
|
306
|
+
def _toggle_dock_widget(self, dock_widget, show=None):
|
|
307
|
+
"""Toggle the visibility of a dock widget."""
|
|
308
|
+
if show is None:
|
|
309
|
+
# Toggle visibility
|
|
310
|
+
dock_widget.setVisible(not dock_widget.isVisible())
|
|
311
|
+
else:
|
|
312
|
+
# Set to specific state
|
|
313
|
+
dock_widget.setVisible(show)
|
|
314
|
+
|
|
315
|
+
def _toggle_project_explorers(self):
|
|
316
|
+
"""Toggle visibility of all project explorers as a group."""
|
|
317
|
+
# Toggle the hidden state
|
|
318
|
+
self._project_explorers_hidden = not self._project_explorers_hidden
|
|
319
|
+
self._toggle_project_action.setChecked(not self._project_explorers_hidden)
|
|
320
|
+
# Apply the visibility to all project explorers
|
|
321
|
+
for explorer in self._project_explorers:
|
|
322
|
+
explorer.setVisible(not self._project_explorers_hidden)
|
|
323
|
+
|
|
324
|
+
def _normalize_line_breaks(self, text):
|
|
325
|
+
"""Convert paragraph separators (U+2029) to standard newlines."""
|
|
326
|
+
return text.replace('\u2029', '\n')
|
|
327
|
+
|
|
328
|
+
def _execute_code(self, code):
|
|
329
|
+
editor = self._editor_panel.active_editor()
|
|
330
|
+
if editor is not None and editor.code_editor_file_path is not None:
|
|
331
|
+
self._jupyter_console.change_directory(
|
|
332
|
+
os.path.dirname(editor.code_editor_file_path)
|
|
333
|
+
)
|
|
334
|
+
self._jupyter_console.execute_code(code)
|
|
335
|
+
|
|
336
|
+
def _execute_file(self, path):
|
|
337
|
+
self._jupyter_console.execute_file(path)
|
|
338
|
+
|
|
339
|
+
def _find_in_files(self):
|
|
340
|
+
file_list = []
|
|
341
|
+
for project_explorer in self._project_explorers:
|
|
342
|
+
file_list.extend(project_explorer.list_files())
|
|
343
|
+
editor = self._editor_panel.active_editor()
|
|
344
|
+
if editor is not None:
|
|
345
|
+
selected_text = editor.textCursor().selectedText()
|
|
346
|
+
# Normalize line breaks
|
|
347
|
+
selected_text = self._normalize_line_breaks(selected_text)
|
|
348
|
+
lines = selected_text.splitlines()
|
|
349
|
+
if len(lines) == 1:
|
|
350
|
+
needle = lines[0].strip()
|
|
351
|
+
else:
|
|
352
|
+
needle = None
|
|
353
|
+
else:
|
|
354
|
+
needle = None
|
|
355
|
+
find_in_files = FindInFiles(file_list, parent=self, needle=needle)
|
|
356
|
+
find_in_files.open_file_requested.connect(self._open_found_file)
|
|
357
|
+
self.addDockWidget(Qt.RightDockWidgetArea, find_in_files)
|
|
358
|
+
|
|
359
|
+
def _open_found_file(self, path, line_number):
|
|
360
|
+
self._editor_panel.open_file(path, line_number)
|
|
361
|
+
|
|
362
|
+
def _open_project_explorer(self, path):
|
|
363
|
+
project_explorer = ProjectExplorer(self._editor_panel, path, parent=self)
|
|
364
|
+
project_explorer.closed.connect(self._close_project_explorer)
|
|
365
|
+
self.addDockWidget(Qt.LeftDockWidgetArea, project_explorer)
|
|
366
|
+
self._project_explorers.append(project_explorer)
|
|
367
|
+
|
|
368
|
+
# If project explorers were hidden as a group, make them all visible again
|
|
369
|
+
if self._project_explorers_hidden:
|
|
370
|
+
self._project_explorers_hidden = False
|
|
371
|
+
for explorer in self._project_explorers:
|
|
372
|
+
explorer.setVisible(True)
|
|
373
|
+
|
|
374
|
+
def _close_project_explorer(self, project_explorer):
|
|
375
|
+
self._project_explorers.remove(project_explorer)
|
|
376
|
+
|
|
377
|
+
def _open_folder(self):
|
|
378
|
+
project_explorer = ProjectExplorer.open_folder(
|
|
379
|
+
self._editor_panel, parent=self
|
|
380
|
+
)
|
|
381
|
+
if project_explorer is None:
|
|
382
|
+
return
|
|
383
|
+
project_explorer.closed.connect(self._close_project_explorer)
|
|
384
|
+
self.addDockWidget(Qt.LeftDockWidgetArea, project_explorer)
|
|
385
|
+
self._project_explorers.append(project_explorer)
|
|
386
|
+
|
|
387
|
+
# If project explorers were hidden as a group, make them all visible again
|
|
388
|
+
if self._project_explorers_hidden:
|
|
389
|
+
self._project_explorers_hidden = False
|
|
390
|
+
for explorer in self._project_explorers:
|
|
391
|
+
explorer.setVisible(True)
|
|
392
|
+
|
|
393
|
+
def closeEvent(self, event):
|
|
394
|
+
# Check if there are unsaved changes, and ask for confirmation if there
|
|
395
|
+
# are
|
|
396
|
+
if self._editor_panel.unsaved_changes():
|
|
397
|
+
message_box = QMessageBox(self)
|
|
398
|
+
message_box.setWindowTitle("Save changes?")
|
|
399
|
+
message_box.setText("Unsaved changes will be permanently lost.")
|
|
400
|
+
message_box.setStandardButtons(
|
|
401
|
+
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
|
|
402
|
+
)
|
|
403
|
+
message_box.setDefaultButton(QMessageBox.Cancel)
|
|
404
|
+
answer = message_box.exec()
|
|
405
|
+
if answer == QMessageBox.Cancel:
|
|
406
|
+
event.ignore()
|
|
407
|
+
return
|
|
408
|
+
if answer == QMessageBox.Yes:
|
|
409
|
+
self._editor_panel.save_all_unsaved_changes()
|
|
410
|
+
|
|
411
|
+
# Persist window geometry/state
|
|
412
|
+
geom_str = self.saveGeometry().toBase64().data().decode('utf-8')
|
|
413
|
+
state_str = self.saveState().toBase64().data().decode('utf-8')
|
|
414
|
+
settings.window_geometry = geom_str
|
|
415
|
+
settings.window_state = state_str
|
|
416
|
+
|
|
417
|
+
settings.save()
|
|
418
|
+
watchdog.shutdown()
|
|
419
|
+
super().closeEvent(event)
|
|
420
|
+
|
|
421
|
+
def _show_quick_open(self):
|
|
422
|
+
"""Show a dialog with all files in the project, filtered as user types."""
|
|
423
|
+
file_list = []
|
|
424
|
+
for project_explorer in self._project_explorers:
|
|
425
|
+
file_list.extend(project_explorer.list_files())
|
|
426
|
+
dlg = QuickOpenFileDialog(
|
|
427
|
+
parent=self,
|
|
428
|
+
file_list=file_list,
|
|
429
|
+
open_file_callback=self._editor_panel.open_file,
|
|
430
|
+
)
|
|
431
|
+
dlg.exec_()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def launch_app():
|
|
435
|
+
app = QApplication(sys.argv)
|
|
436
|
+
window = MainWindow()
|
|
437
|
+
window.show()
|
|
438
|
+
sys.exit(app.exec_())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .code_editors import create_editor
|