-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathmainWindow.py
445 lines (356 loc) · 17.6 KB
/
mainWindow.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
from __future__ import annotations
import os
import uuid
from math import floor
from typing import Optional, TypeVar, ClassVar
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QCloseEvent, QKeySequence, QDragEnterEvent, QDropEvent, QIcon
from PyQt5.QtWidgets import QMainWindow
from base.model.application import getApp
from cat.GUI import CORNERS, NO_OVERLAP, SizePolicy, RoundedCorners
from cat.GUI import pythonGUI
from cat.GUI.components import Widgets, catWidgetMixins
from cat.GUI.enums import TAB_POSITION_NORTH_SOUTH, TabPosition, MessageBoxStyle, MessageBoxButton, FileExtensionFilter
from cat.GUI.framelessWindow.catFramelessWindowMixin import CatFramelessWindowMixin
from cat.GUI.icons import iconCombiner, CompositionMode
from gui.icons import icons, _Icons
from base.gui.newProjectDialog import NewProjectDialog
from base.gui.documentsViewEditor import DocumentsViewsContainerEditor
from gui.profileParsingDialog import ProfileParsingDialog
from base.model import theme
from keySequences import KEY_SEQUENCES
from base.model.session import getSession, saveSessionToFile, GLOBAL_SIGNALS
from base.model.documents import Document, DocumentTypeDescription, createNewDocument, getDocumentTypes, getAllFileExtensionFilters, getDocumentTypeForDocument
from base.gui.checkAllDialog import CheckAllDialog
from base.gui.searchAllDialog import SearchAllDialog
from base.gui.spotlightSearch import SpotlightSearchGui
from gui.datapackEditorGUI import DatapackEditorGUI
from base.plugin import PLUGIN_SERVICE, SideBarOptions
from base.model.applicationSettings import applicationSettings
from base.gui.settingsDialog import SettingsDialog
_TT = TypeVar('_TT')
class _DpeIcons:
__slots__ = ()
_shadowedChevronDown = (
(_Icons.chevronDown, dict(mode=CompositionMode.Erase, scale=.5, offset=(+0.00 + .3, -0.10 + .3))),
(_Icons.chevronDown, dict(mode=CompositionMode.Erase, scale=.5, offset=(-0.051 + .3, -0.051 + .3))),
(_Icons.chevronDown, dict(mode=CompositionMode.Erase, scale=.5, offset=(-0.10 + .3, +0.00 + .3))),
(_Icons.chevronDown, dict(scale=.5, offset=(+0.00 + .3, +0.00 + .3)))
)
project_chevronDown: QIcon = iconCombiner(_Icons.project, *_shadowedChevronDown)
dpeIcons = _DpeIcons()
ALL_FILES_FILTER: FileExtensionFilter = ('All files', '*')
def frange(a: float, b: float, jump: float, *, includeLAst: bool = False):
cnt = int(floor(abs(b - a) / jump))
cnt = cnt + 1 if includeLAst else cnt
for i in range(cnt):
yield a + jump * i
class MainWindow(CatFramelessWindowMixin, QMainWindow): # QtWidgets.QWidget):
TIP_dataDir = 'Directory containing the raw data.'
__allMainWindows: ClassVar[dict[uuid.UUID, MainWindow]] = {}
@classmethod
def registerMainWindow(cls, window: MainWindow, windowId: uuid.UUID):
"""
this is to prevent a very strange bug, where a main window gets closed when:
- it is not stored in a python object and therefore can be garbage collected AND
- and the user selects one (or sometimes more) roles in the non-modal searchAllDialog dialog with a treeView
this materialized in following behavior:
- RuntimeError: wrapped C/C++ object of type QGridLayout has been deleted
- when the user selects one (or sometimes more) roles in the non-modal searchAllDialog dialog for the first time
- while a treeView is in the searchAllDialog (the results list is a treeView)
- and no treeView is visible in the main window.
slightly different behavior is shown:
- when the searchAllDialog is missing its results list --> no crash
- when the searchAllDialog is modal --> no crash
- when a t
:param window:
:param windowId:
:return:
"""
cls.__allMainWindows[windowId] = window
window._uuid = windowId
@classmethod
def deregisterMainWindow(cls, windowId: uuid.UUID):
cls.__allMainWindows.pop(windowId, None)
def __init__(self, windowId: uuid.UUID):
super().__init__(GUICls=DatapackEditorGUI)
self._uuid: uuid.UUID = windowId
MainWindow.registerMainWindow(self, windowId)
self._gui._name = f'main Window GUI {windowId}'
self.disableContentMargins = True
self.disableSidebarMargins = True
self.disableBottombarMargins = True
self.drawTitleToolbarBorder = True
self.roundedCorners = CORNERS.ALL
# TODO: change initial _lastOpenPath:
self._lastOpenPath = ''
#GUI
self.checkAllDialog = CheckAllDialog(self)
self.searchAllDialog = SearchAllDialog(self)
self.settingsDialog = SettingsDialog(self, GUICls=DatapackEditorGUI)
self.profileParsingDialog = ProfileParsingDialog(self)
self.currentDocumenSubGUI: Optional[DatapackEditorGUI] = None
self.setAcceptDrops(True)
GLOBAL_SIGNALS.onCanCloseModifiedDocument = self._canCloseModifiedDocument
GLOBAL_SIGNALS.onError.reconnect('showError', lambda e, title: self._gui.showWarningDialog(title, str(e)))
GLOBAL_SIGNALS.onWarning.reconnect('showWarning', lambda e, title: self._gui.showWarningDialog(title, '' if e is None else str(e)))
GLOBAL_SIGNALS.onAskUser = lambda title, message: self._gui.askUser(title, message)
# close document as shortcut:
# self.closeDocumentShortcut = QShortcut(KEY_SEQUENCES.CLOSE_DOCUMENT, self, lambda d=document, s=self: self._safelyCloseDocument(gui, getSession().selectedDocument),
# TODO: lambda d=document, s=self: asdasdasdasdasd s._safelyCloseDocument(gui, d), Qt.WidgetWithChildrenShortcut)
@property
def uuid(self) -> uuid.UUID:
return self._uuid
def closeEvent(self, event: QCloseEvent):
self._saveSession()
MainWindow.deregisterMainWindow(self._uuid)
event.accept()
def dragEnterEvent(self, e: QDragEnterEvent):
if e.mimeData().hasUrls() and all(url.isValid() for url in e.mimeData().urls()):
e.acceptProposedAction()
def dropEvent(self, e: QDropEvent):
for url in e.mimeData().urls():
filePath = url.toLocalFile()
getSession().tryOpenOrSelectDocument(filePath)
# properties:
@property
def isToolbarInTitleBar(self) -> bool:
return applicationSettings.appearance.useCompactLayout
@property
def disableStatusbarMargins(self) -> bool:
return applicationSettings.appearance.useCompactLayout
def _updateApplicationDisplayName(self) -> None:
displayName = getApp().info.appDisplayName
if currentProjectName := getSession().project.name.strip():
displayName = f'{displayName} - {currentProjectName}'
if getApp().qApp.applicationDisplayName() != displayName:
getApp().qApp.setApplicationDisplayName(displayName)
if self.windowTitle() != displayName:
self.setWindowTitle(displayName)
# GUI:
def OnGUI(self, gui: DatapackEditorGUI):
self._updateApplicationDisplayName()
gui.editor(DocumentsViewsContainerEditor, getSession().documents.viewsC, seamless=True).redrawLater('MainWindow.OnGUI(...)')
self._saveSession()
def OnToolbarGUI(self, gui: DatapackEditorGUI):
self.toolBarGUI2(gui)
def OnStatusbarGUI(self, gui: DatapackEditorGUI):
mg = self._gui.margin if self.disableStatusbarMargins else 0
with gui.hLayout(contentsMargins=(mg, 0, mg, 0)):
gui.label("this is a status bar.")
def OnSidebarGUI(self, gui: DatapackEditorGUI):
tabs: list[SideBarOptions] = []
for plugin in PLUGIN_SERVICE.activePlugins:
tabs.extend(plugin.sideBarTabs())
self.barGUI(gui, TabPosition.West, tabs)
def OnBottombarGUI(self, gui: DatapackEditorGUI):
tabs: list[SideBarOptions] = []
for plugin in PLUGIN_SERVICE.activePlugins:
tabs.extend(plugin.bottomBarTabs())
self.barGUI(gui, TabPosition.South, tabs)
def barGUI(self, gui: DatapackEditorGUI, position: TabPosition, tabs: list[SideBarOptions]):
tabPosition = gui._select(position, TabPosition.North, TabPosition.West, TabPosition.North, TabPosition.West)
with gui.tabWidget(
drawBase=True,
documentMode=True,
expanding=False,
position=tabPosition,
cornerGUI=lambda idx: gui.editor(tabs[idx].toolButtons, model=None, seamless=True) if idx in range(len(tabs)) and tabs[idx].toolButtons is not None else None,
) as tabWidget:
for id_, sideBar in enumerate(tabs):
with tabWidget.addView(sideBar.tabOptions, str(id_), seamless=True):
subGui = gui.editor(sideBar.content, model=None, seamless=True)
if str(id_) == tabWidget.selectedView:
subGui.redrawLater()
def documentToolBarGUI(self, gui: DatapackEditorGUI, button, btnCorners, btnOverlap, btnMargins):
button = gui.framelessButton
document = self.selectedDocument
btnKwArgs = dict(roundedCorners=btnCorners, overlap=btnOverlap, margins=btnMargins, hSizePolicy=SizePolicy.Fixed.value)
with gui.hLayout():
if button(icon=icons.file, tip='New File', **btnKwArgs, windowShortcut=KEY_SEQUENCES.NEW):
self._createNewDocument(gui)
if button(icon=icons.open, tip='Open File', **btnKwArgs, windowShortcut=QKeySequence.Open):
filePath = gui.showFileDialog(self._lastOpenPath, [*getAllFileExtensionFilters(), ALL_FILES_FILTER], selectedFilter=ALL_FILES_FILTER, style='open')
if filePath:
getSession().tryOpenOrSelectDocument(filePath)
if button(icon=icons.save, tip='Save File', **btnKwArgs, enabled=bool(document), windowShortcut=QKeySequence.Save):
self._saveOrSaveAs(gui, document)
if button(icon=icons.saveAs, tip='Save As', **btnKwArgs, enabled=bool(document), windowShortcut=KEY_SEQUENCES.SAVE_AS):
self._saveAs(gui, document)
if button(icon=icons.refresh, tip='Reload File', **btnKwArgs, enabled=bool(document) and not document.isNew, windowShortcut=QKeySequence.Refresh):
if not document.isNew:
getSession().reloadDocument(document)
with gui.hLayout(seamless=True, roundedCorners=btnCorners, overlap=btnOverlap):
if button(icon=icons.undo, tip='Undo', margins=btnMargins, hSizePolicy=SizePolicy.Fixed.value, enabled=bool(document), windowShortcut=QKeySequence.Undo):
document.undoRedoStack.undoOnce()
if button(icon=icons.redo, tip='Redo', margins=btnMargins, hSizePolicy=SizePolicy.Fixed.value, enabled=bool(document), windowShortcut=QKeySequence.Redo):
document.undoRedoStack.redoOnce()
def projectMenu(self, gui: DatapackEditorGUI):
hasOpenedProject = getSession().hasOpenedProject
with gui.popupMenu() as menu:
menu.addAction('Open / Create Project', lambda: self._openOrCreateProjectDialog(gui), icon=icons.project),
menu.addAction('Close Project', lambda: self._closeProjectDialog(gui), icon=icons.project, enabled=hasOpenedProject),
def toolBarGUI2(self, gui: DatapackEditorGUI):
# TODO: INVESTIGATE calculation of hSpacing:
sdm = gui.smallDefaultMargins
dm = gui.defaultMargins
avgMarginsDiff = ((dm[0] - sdm[0]) + (dm[1] - sdm[1])) / 2
hSpacing = gui.smallSpacing # + int(avgMarginsDiff * gui.scale)
button = gui.framelessButton
btnCorners = CORNERS.ALL
btnMargins = gui.smallDefaultMargins
btnOverlap = (0, 0, 0, 1) if self.isToolbarInTitleBar else NO_OVERLAP
btnKwArgs = dict(roundedCorners=btnCorners, overlap=btnOverlap, margins=btnMargins, hSizePolicy=SizePolicy.Fixed.value)
with gui.hLayout(horizontalSpacing=hSpacing):
hasOpenedProject = getSession().hasOpenedProject
if button(icon=dpeIcons.project_chevronDown, tip='Project...', **btnKwArgs, default=not hasOpenedProject):
self.projectMenu(gui)
docToolBarGUI = gui.subGUI(type(gui), lambda g: self.documentToolBarGUI(g, button=button, btnCorners=btnCorners, btnOverlap=btnOverlap, btnMargins=btnMargins), hSizePolicy=SizePolicy.Fixed.value)
getSession().documents.onSelectedDocumentChanged.reconnect('documentToolBarGUI', docToolBarGUI.host.redraw)
docToolBarGUI.redrawGUI()
docToolBarGUI._name = 'docToolBarGUI'
gui.addHSpacer(0, SizePolicy.Expanding)
self.fileSearchFieldGUI(gui, roundedCorners=btnCorners, overlap=btnOverlap)
with gui.hLayout(horizontalSpacing=hSpacing):
gui.addHSpacer(0, SizePolicy.Expanding)
if button(icon=icons.search, tip='Search all', **btnKwArgs, windowShortcut=KEY_SEQUENCES.FIND_ALL):
self.searchAllDialog.show()
if button(icon=icons.spellCheck, tip='Check all Files', **btnKwArgs, enabled=True):
self.checkAllDialog.show()
if applicationSettings.debugging.isDeveloperMode:
if button(icon=icons.color, tip='Reload Color Scheme', **btnKwArgs, enabled=True):
theme.reloadAllColorSchemes()
if button(icon=icons.settings, tip='Settings', **btnKwArgs, windowShortcut=QKeySequence.Preferences):
self._showSettingsDialog(gui)
if applicationSettings.debugging.isDeveloperMode:
gui.hSeparator()
if button(icon=icons.chevronDown, tip='developer tools', **btnKwArgs, enabled=True):
self.devToolsDropDownGUI(gui)
# if button(icon=icons.stopwatch, tip='Profile Parsing', **btnKwArgs, enabled=True):
# self.profileParsingDialog.show()
# pythonGUI.PROFILING_ENABLED = gui.toggleSwitch(pythonGUI.PROFILING_ENABLED, enabled=True)
# gui.label('P')
if self.isToolbarInTitleBar:
gui.hSeparator()
def devToolsDropDownGUI(self, gui: DatapackEditorGUI):
def setProfilingEnabled(checked: bool) -> None:
pythonGUI.PROFILING_ENABLED = checked
def setLayoutInfoAsToolTip(checked):
pythonGUI.ADD_LAYOUT_INFO_AS_TOOL_TIP = checked
def setDebugLayout(checked):
Widgets.DEBUG_LAYOUT = checked
def setDebugPaintEvent(checked):
catWidgetMixins.DO_DEBUG_PAINT_EVENT = checked
with gui.popupMenu(atMousePosition=False) as popup:
popup.addAction('Profile Parsing', self.profileParsingDialog.show, icon=icons.stopwatch)
popup.addToggle('profiling Enabled', pythonGUI.PROFILING_ENABLED, setProfilingEnabled, icon=icons.stopwatch)
popup.addToggle('layout info as tool tip', pythonGUI.ADD_LAYOUT_INFO_AS_TOOL_TIP, setLayoutInfoAsToolTip)
popup.addToggle('debug layout', Widgets.DEBUG_LAYOUT, setDebugLayout)
popup.addToggle('debug paint event', catWidgetMixins.DO_DEBUG_PAINT_EVENT, setDebugPaintEvent, enabled=not catWidgetMixins.NEVER_DO_DEBUG_PAINT_EVENT)
# Dialogs:
def _showSettingsDialog(self, gui: DatapackEditorGUI) -> None:
with gui.overlay():
self.settingsDialog.exec()
def _saveAsDialog(self, gui: DatapackEditorGUI, document: Document) -> str:
dt = getDocumentTypeForDocument(document)
if dt is not None:
selectedFilter = dt.fileExtensionFilter
selectedFilter = [(selectedFilter[0], f) for f in selectedFilter[1]][0]
else:
selectedFilter = ALL_FILES_FILTER
filePath = gui.showFileDialog(document.unitedFilePath, [*getAllFileExtensionFilters(expanded=True), ALL_FILES_FILTER], selectedFilter=selectedFilter, style='save')
if filePath:
self._lastOpenPath = os.path.dirname(filePath)
return filePath
@staticmethod
def _selectDocumentTypeDialog(gui: DatapackEditorGUI) -> Optional[DocumentTypeDescription]:
documentType: Optional[DocumentTypeDescription] = None
documentType = gui.searchableChoicePopup(
documentType,
'New File',
getDocumentTypes(),
getSearchStr=lambda x: x.name,
labelMaker=lambda x, i: (x.name, ', '.join(x.extensions))[i],
iconMaker=lambda x, i: (getattr(icons, x.icon, None) if x.icon is not None else None, None)[i],
toolTipMaker=lambda x, i: x.tip,
columnCount=2,
width=200,
height=175,
)
return documentType
@staticmethod
def _closeProjectDialog(gui: DatapackEditorGUI) -> None:
session = getSession()
if gui.askUser("Close Project", "Are you sure?", style=MessageBoxStyle.Warning):
getSession().closeProject()
@staticmethod
def _openOrCreateProjectDialog(gui: DatapackEditorGUI) -> None:
with gui.overlay():
page, isOk = NewProjectDialog.showModal(width=int(680 * gui.scale), height=int(680 * gui.scale))
if not isOk:
return
with gui.waitCursor():
page.acceptAction(gui)
# Fields:
@staticmethod
def fileSearchFieldGUI(gui: DatapackEditorGUI, roundedCorners: RoundedCorners = CORNERS.ALL, **kwargs) -> None:
gui.customWidget(
SpotlightSearchGui,
placeholderText=f'Press \'{KEY_SEQUENCES.GO_TO_FILE.toString()}\' to find a file',
roundedCorners=roundedCorners,
alignment=Qt.AlignCenter,
windowShortcut=KEY_SEQUENCES.GO_TO_FILE,
**kwargs
)
# Functionality:
@staticmethod
def _saveSession():
saveSessionToFile()
def _createNewDocument(self, gui: DatapackEditorGUI):
docType = self._selectDocumentTypeDialog(gui)
if docType is None:
return
# create document:
doc = getSession().documents.insertInCurrentView(createNewDocument(docType, None))
getSession().documents.selectDocument(doc)
self.redraw()
def _saveAs(self, gui: DatapackEditorGUI, document: Document) -> bool:
filePath = self._saveAsDialog(gui, document)
if filePath:
document.filePath = filePath
getSession().saveDocument(document)
return False
def _saveOrSaveAs(self, gui: DatapackEditorGUI, document: Document) -> bool:
if document.isNew:
return self._saveAs(gui, document)
else:
return getSession().saveDocument(document)
def _safelyCloseDocument(self, gui: DatapackEditorGUI, document: Document):
getSession().documents.safelyCloseDocument(document)
self._gui.redrawGUI()
self._gui.redrawGUI()
def _canCloseModifiedDocument(self, doc: Document) -> bool:
if doc.documentChanged:
msgBoxResult = self._gui.showMessageDialog(
# f'Save Changes?',
f'Do you want to save the changes made to the file {doc.fileName}?',
# '{document.fileName} has been modified. \nSave changes?',
f'You can discard to undo the changes since you have last saved / opened the file.',
MessageBoxStyle.Warning,
{MessageBoxButton.Save, MessageBoxButton.Discard, MessageBoxButton.Cancel}
)
if msgBoxResult == MessageBoxButton.Save:
self._saveOrSaveAs(self._gui, doc) # will reset documentChanged
return not doc.documentChanged
elif msgBoxResult == MessageBoxButton.Discard:
return True # close it anyway
else: # Cancel
return False
@property
def selectedDocument(self) -> Optional[Document]:
return getSession().documents.currentView.selectedDocument
def _select(pos: TabPosition, north: _TT, east: _TT, south: _TT, west: _TT) -> _TT:
if pos in TAB_POSITION_NORTH_SOUTH:
return north if pos is TabPosition.North else south
else:
return east if pos is TabPosition.East else west