1#!/usr/bin/env python3 2 3#****************************************************************************** 4# treewindow.py, provides a class for the main window and controls 5# 6# TreeLine, an information storage program 7# Copyright (C) 2020, Douglas W. Bell 8# 9# This is free software; you can redistribute it and/or modify it under the 10# terms of the GNU General Public License, either Version 2 or any later 11# version. This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY. See the included LICENSE file for details. 13#****************************************************************************** 14 15import pathlib 16import base64 17from PyQt5.QtCore import QEvent, QRect, QSize, Qt, pyqtSignal 18from PyQt5.QtGui import QGuiApplication, QTextDocument 19from PyQt5.QtWidgets import (QAction, QActionGroup, QApplication, QMainWindow, 20 QSplitter, QStackedWidget, QStatusBar, QTabWidget) 21import treeview 22import breadcrumbview 23import outputview 24import dataeditview 25import titlelistview 26import treenode 27import globalref 28 29 30class TreeWindow(QMainWindow): 31 """Class override for the main window. 32 33 Contains main window views and controls. 34 """ 35 selectChanged = pyqtSignal() 36 nodeModified = pyqtSignal(treenode.TreeNode) 37 treeModified = pyqtSignal() 38 winActivated = pyqtSignal(QMainWindow) 39 winMinimized = pyqtSignal() 40 winClosing = pyqtSignal(QMainWindow) 41 def __init__(self, model, allActions, parent=None): 42 """Initialize the main window. 43 44 Arguments: 45 model -- the initial data model 46 allActions -- a dict containing the upper level actions 47 parent -- the parent window, usually None 48 """ 49 super().__init__(parent) 50 self.allActions = allActions.copy() 51 self.allowCloseFlag = True 52 self.winActions = {} 53 self.toolbars = [] 54 self.rightTabActList = [] 55 self.setAttribute(Qt.WA_DeleteOnClose) 56 self.setAcceptDrops(True) 57 self.setStatusBar(QStatusBar()) 58 self.setCaption() 59 self.setupActions() 60 self.setupMenus() 61 self.setupToolbars() 62 self.restoreToolbarPosition() 63 64 self.treeView = treeview.TreeView(model, self.allActions) 65 self.breadcrumbSplitter = QSplitter(Qt.Vertical) 66 self.setCentralWidget(self.breadcrumbSplitter) 67 self.breadcrumbView = breadcrumbview.BreadcrumbView(self.treeView) 68 self.breadcrumbSplitter.addWidget(self.breadcrumbView) 69 self.breadcrumbView.setVisible(globalref. 70 genOptions['InitShowBreadcrumb']) 71 72 self.treeSplitter = QSplitter() 73 self.breadcrumbSplitter.addWidget(self.treeSplitter) 74 self.treeStack = QStackedWidget() 75 self.treeSplitter.addWidget(self.treeStack) 76 self.treeStack.addWidget(self.treeView) 77 self.treeView.shortcutEntered.connect(self.execShortcut) 78 self.treeView.selectionModel().selectionChanged.connect(self. 79 updateRightViews) 80 self.treeFilterView = None 81 82 self.rightTabs = QTabWidget() 83 self.treeSplitter.addWidget(self.rightTabs) 84 self.rightTabs.setTabPosition(QTabWidget.South) 85 self.rightTabs.tabBar().setFocusPolicy(Qt.NoFocus) 86 87 self.outputSplitter = QSplitter(Qt.Vertical) 88 self.rightTabs.addTab(self.outputSplitter, _('Data Output')) 89 parentOutputView = outputview.OutputView(self.treeView, False) 90 parentOutputView.highlighted[str].connect(self.statusBar().showMessage) 91 self.outputSplitter.addWidget(parentOutputView) 92 childOutputView = outputview.OutputView(self.treeView, True) 93 childOutputView.highlighted[str].connect(self.statusBar().showMessage) 94 self.outputSplitter.addWidget(childOutputView) 95 96 self.editorSplitter = QSplitter(Qt.Vertical) 97 self.rightTabs.addTab(self.editorSplitter, _('Data Edit')) 98 parentEditView = dataeditview.DataEditView(self.treeView, 99 self.allActions, False) 100 parentEditView.shortcutEntered.connect(self.execShortcut) 101 parentEditView.focusOtherView.connect(self.focusNextView) 102 parentEditView.inLinkSelectMode.connect(self.treeView. 103 toggleNoMouseSelectMode) 104 self.treeView.skippedMouseSelect.connect(parentEditView. 105 internalLinkSelected) 106 self.editorSplitter.addWidget(parentEditView) 107 childEditView = dataeditview.DataEditView(self.treeView, 108 self.allActions, True) 109 childEditView.shortcutEntered.connect(self.execShortcut) 110 childEditView.focusOtherView.connect(self.focusNextView) 111 childEditView.inLinkSelectMode.connect(self.treeView. 112 toggleNoMouseSelectMode) 113 self.treeView.skippedMouseSelect.connect(childEditView. 114 internalLinkSelected) 115 parentEditView.hoverFocusActive.connect(childEditView.endEditor) 116 childEditView.hoverFocusActive.connect(parentEditView.endEditor) 117 parentEditView.inLinkSelectMode.connect(childEditView. 118 updateInLinkSelectMode) 119 childEditView.inLinkSelectMode.connect(parentEditView. 120 updateInLinkSelectMode) 121 self.editorSplitter.addWidget(childEditView) 122 123 self.titleSplitter = QSplitter(Qt.Vertical) 124 self.rightTabs.addTab(self.titleSplitter, _('Title List')) 125 parentTitleView = titlelistview.TitleListView(self.treeView, False) 126 parentTitleView.shortcutEntered.connect(self.execShortcut) 127 self.titleSplitter.addWidget(parentTitleView) 128 childTitleView = titlelistview.TitleListView(self.treeView, True) 129 childTitleView.shortcutEntered.connect(self.execShortcut) 130 self.titleSplitter.addWidget(childTitleView) 131 132 self.rightTabs.currentChanged.connect(self.updateRightViews) 133 self.updateFonts() 134 135 def setExternalSignals(self): 136 """Connect widow object signals to signals in this object. 137 138 In a separate method to refresh after local control change. 139 """ 140 self.treeView.selectionModel().selectionChanged.connect(self. 141 selectChanged) 142 for i in range(2): 143 self.editorSplitter.widget(i).nodeModified.connect(self. 144 nodeModified) 145 self.titleSplitter.widget(i).nodeModified.connect(self. 146 nodeModified) 147 self.titleSplitter.widget(i).treeModified.connect(self. 148 treeModified) 149 150 def updateActions(self, allActions): 151 """Use new actions for menus, etc. when the local control changes. 152 153 Arguments: 154 allActions -- a dict containing the upper level actions 155 """ 156 # remove submenu actions that are children of the window 157 self.removeAction(self.allActions['DataNodeType']) 158 self.removeAction(self.allActions['FormatFontSize']) 159 self.allActions = allActions.copy() 160 self.allActions.update(self.winActions) 161 self.menuBar().clear() 162 self.setupMenus() 163 self.addToolbarCommands() 164 self.treeView.allActions = self.allActions 165 for i in range(2): 166 self.editorSplitter.widget(i).allActions = self.allActions 167 168 def updateTreeNode(self, node): 169 """Update all spots for the given node in the tree view. 170 171 Arguments: 172 node -- the node to be updated 173 """ 174 for spot in node.spotRefs: 175 self.treeView.update(spot.index(self.treeView.model())) 176 self.treeView.resizeColumnToContents(0) 177 self.breadcrumbView.updateContents() 178 179 def updateTree(self): 180 """Update the full tree view. 181 """ 182 self.treeView.scheduleDelayedItemsLayout() 183 self.breadcrumbView.updateContents() 184 185 def updateRightViews(self, *args, outputOnly=False): 186 """Update all right-hand views and breadcrumb view. 187 188 Arguments: 189 *args -- dummy arguments to collect args from signals 190 outputOnly -- only update output views (not edit views) 191 """ 192 if globalref.mainControl.activeControl: 193 self.rightTabActList[self.rightTabs. 194 currentIndex()].setChecked(True) 195 self.breadcrumbView.updateContents() 196 splitter = self.rightTabs.currentWidget() 197 if not outputOnly or isinstance(splitter.widget(0), 198 outputview.OutputView): 199 for i in range(2): 200 splitter.widget(i).updateContents() 201 202 def refreshDataEditViews(self): 203 """Refresh the data in non-selected cells in curreent data edit views. 204 """ 205 splitter = self.rightTabs.currentWidget() 206 if isinstance(splitter.widget(0), dataeditview.DataEditView): 207 for i in range(2): 208 splitter.widget(i).updateUnselectedCells() 209 210 def updateCommandsAvail(self): 211 """Set window commands available based on node selections. 212 """ 213 self.allActions['ViewPrevSelect'].setEnabled(len(self.treeView. 214 selectionModel(). 215 prevSpots) > 1) 216 self.allActions['ViewNextSelect'].setEnabled(len(self.treeView. 217 selectionModel(). 218 nextSpots) > 0) 219 220 def updateWinGenOptions(self): 221 """Update tree and data edit windows based on general option changes. 222 """ 223 self.treeView.updateTreeGenOptions() 224 for i in range(2): 225 self.editorSplitter.widget(i).setMouseTracking(globalref. 226 genOptions['EditorOnHover']) 227 228 def updateFonts(self): 229 """Update custom fonts in views. 230 """ 231 treeFont = QTextDocument().defaultFont() 232 treeFontName = globalref.miscOptions['TreeFont'] 233 if treeFontName: 234 treeFont.fromString(treeFontName) 235 self.treeView.setFont(treeFont) 236 self.treeView.updateTreeGenOptions() 237 if self.treeFilterView: 238 self.treeFilterView.setFont(treeFont) 239 ouputFont = QTextDocument().defaultFont() 240 ouputFontName = globalref.miscOptions['OutputFont'] 241 if ouputFontName: 242 ouputFont.fromString(ouputFontName) 243 editorFont = QTextDocument().defaultFont() 244 editorFontName = globalref.miscOptions['EditorFont'] 245 if editorFontName: 246 editorFont.fromString(editorFontName) 247 for i in range(2): 248 self.outputSplitter.widget(i).setFont(ouputFont) 249 self.editorSplitter.widget(i).setFont(editorFont) 250 self.titleSplitter.widget(i).setFont(editorFont) 251 252 def resetTreeModel(self, model): 253 """Change the model assigned to the tree view. 254 255 Arguments: 256 model -- the new model to assign 257 """ 258 self.treeView.resetModel(model) 259 self.treeView.selectionModel().selectionChanged.connect(self. 260 updateRightViews) 261 262 def activateAndRaise(self): 263 """Activate this window and raise it to the front. 264 """ 265 self.activateWindow() 266 self.raise_() 267 268 def setCaption(self, pathObj=None, modified=False): 269 """Change the window caption title based on the file name and path. 270 271 Arguments: 272 pathObj - a path object for the current file 273 """ 274 modFlag = '*' if modified else '' 275 if pathObj: 276 caption = '{0}{1} [{2}] - TreeLine'.format(str(pathObj.name), 277 modFlag, 278 str(pathObj.parent)) 279 else: 280 caption = '- TreeLine' 281 self.setWindowTitle(caption) 282 283 def filterView(self): 284 """Create, show and return a filter view. 285 """ 286 self.removeFilterView() 287 self.treeFilterView = treeview.TreeFilterView(self.treeView, 288 self.allActions) 289 self.treeFilterView.shortcutEntered.connect(self.execShortcut) 290 self.treeView.selectionModel().selectionChanged.connect(self. 291 treeFilterView. 292 updateFromSelectionModel) 293 for i in range(2): 294 editView = self.editorSplitter.widget(i) 295 editView.inLinkSelectMode.connect(self.treeFilterView. 296 toggleNoMouseSelectMode) 297 self.treeFilterView.skippedMouseSelect.connect(editView. 298 internalLinkSelected) 299 self.treeStack.addWidget(self.treeFilterView) 300 self.treeStack.setCurrentWidget(self.treeFilterView) 301 return self.treeFilterView 302 303 def removeFilterView(self): 304 """Hide and delete the current filter view. 305 """ 306 if self.treeFilterView != None: # check for None since False if empty 307 self.treeStack.removeWidget(self.treeFilterView) 308 globalref.mainControl.currentStatusBar().removeWidget(self. 309 treeFilterView. 310 messageLabel) 311 self.treeFilterView.messageLabel.deleteLater() 312 self.treeFilterView = None 313 314 def rightParentView(self): 315 """Return the current right-hand parent view if visible (or None). 316 """ 317 view = self.rightTabs.currentWidget().widget(0) 318 if not view.isVisible() or view.height() == 0 or view.width() == 0: 319 return None 320 return view 321 322 def rightChildView(self): 323 """Return the current right-hand parent view if visible (or None). 324 """ 325 view = self.rightTabs.currentWidget().widget(1) 326 if not view.isVisible() or view.height() == 0 or view.width() == 0: 327 return None 328 return view 329 330 def focusNextView(self, forward=True): 331 """Focus the next pane in the tab focus series. 332 333 Called by a signal from the data edit views. 334 Tab sequences tend to skip views without this. 335 Arguments: 336 forward -- forward in tab series if True 337 """ 338 reason = (Qt.TabFocusReason if forward 339 else Qt.BacktabFocusReason) 340 rightParent = self.rightParentView() 341 rightChild = self.rightChildView() 342 if (self.sender().isChildView == forward or 343 (forward and rightChild == None) or 344 (not forward and rightParent == None)): 345 self.treeView.setFocus(reason) 346 elif forward: 347 rightChild.setFocus(reason) 348 else: 349 rightParent.setFocus(reason) 350 351 def execShortcut(self, key): 352 """Execute an action based on a shortcut key signal from a view. 353 354 Arguments: 355 key -- the QKeySequence shortcut 356 """ 357 keyDict = {action.shortcut().toString(): action for action in 358 self.allActions.values()} 359 try: 360 action = keyDict[key.toString()] 361 except KeyError: 362 return 363 if action.isEnabled(): 364 action.trigger() 365 366 def setupActions(self): 367 """Add the actions for contols at the window level. 368 369 These actions only affect an individual window, 370 they're independent in multiple windows of the same file. 371 """ 372 viewExpandBranchAct = QAction(_('&Expand Full Branch'), self, 373 statusTip=_('Expand all children of the selected nodes')) 374 viewExpandBranchAct.triggered.connect(self.viewExpandBranch) 375 self.winActions['ViewExpandBranch'] = viewExpandBranchAct 376 377 viewCollapseBranchAct = QAction(_('&Collapse Full Branch'), self, 378 statusTip=_('Collapse all children of the selected nodes')) 379 viewCollapseBranchAct.triggered.connect(self.viewCollapseBranch) 380 self.winActions['ViewCollapseBranch'] = viewCollapseBranchAct 381 382 viewPrevSelectAct = QAction(_('&Previous Selection'), self, 383 statusTip=_('Return to the previous tree selection')) 384 viewPrevSelectAct.triggered.connect(self.viewPrevSelect) 385 self.winActions['ViewPrevSelect'] = viewPrevSelectAct 386 387 viewNextSelectAct = QAction(_('&Next Selection'), self, 388 statusTip=_('Go to the next tree selection in history')) 389 viewNextSelectAct.triggered.connect(self.viewNextSelect) 390 self.winActions['ViewNextSelect'] = viewNextSelectAct 391 392 viewRightTabGrp = QActionGroup(self) 393 viewOutputAct = QAction(_('Show Data &Output'), viewRightTabGrp, 394 statusTip=_('Show data output in right view'), 395 checkable=True) 396 self.winActions['ViewDataOutput'] = viewOutputAct 397 398 viewEditAct = QAction(_('Show Data &Editor'), viewRightTabGrp, 399 statusTip=_('Show data editor in right view'), 400 checkable=True) 401 self.winActions['ViewDataEditor'] = viewEditAct 402 403 viewTitleAct = QAction(_('Show &Title List'), viewRightTabGrp, 404 statusTip=_('Show title list in right view'), 405 checkable=True) 406 self.winActions['ViewTitleList'] = viewTitleAct 407 self.rightTabActList = [viewOutputAct, viewEditAct, viewTitleAct] 408 viewRightTabGrp.triggered.connect(self.viewRightTab) 409 410 viewBreadcrumbAct = QAction(_('Show &Breadcrumb View'), self, 411 statusTip=_('Toggle showing breadcrumb ancestor view'), 412 checkable=True) 413 viewBreadcrumbAct.setChecked(globalref. 414 genOptions['InitShowBreadcrumb']) 415 viewBreadcrumbAct.triggered.connect(self.viewBreadcrumb) 416 self.winActions['ViewBreadcrumb'] = viewBreadcrumbAct 417 418 viewChildPaneAct = QAction(_('&Show Child Pane'), self, 419 statusTip=_('Toggle showing right-hand child views'), 420 checkable=True) 421 viewChildPaneAct.setChecked(globalref.genOptions['InitShowChildPane']) 422 viewChildPaneAct.triggered.connect(self.viewShowChildPane) 423 self.winActions['ViewShowChildPane'] = viewChildPaneAct 424 425 viewDescendAct = QAction(_('Show Output &Descendants'), self, 426 statusTip=_('Toggle showing output view indented descendants'), 427 checkable=True) 428 viewDescendAct.setChecked(globalref.genOptions['InitShowDescendants']) 429 viewDescendAct.triggered.connect(self.viewDescendants) 430 self.winActions['ViewShowDescend'] = viewDescendAct 431 432 winCloseAct = QAction(_('&Close Window'), self, 433 statusTip=_('Close this window')) 434 winCloseAct.triggered.connect(self.close) 435 self.winActions['WinCloseWindow'] = winCloseAct 436 437 incremSearchStartAct = QAction(_('Start Incremental Search'), self) 438 incremSearchStartAct.triggered.connect(self.incremSearchStart) 439 self.addAction(incremSearchStartAct) 440 self.winActions['IncremSearchStart'] = incremSearchStartAct 441 442 incremSearchNextAct = QAction(_('Next Incremental Search'), self) 443 incremSearchNextAct.triggered.connect(self.incremSearchNext) 444 self.addAction(incremSearchNextAct) 445 self.winActions['IncremSearchNext'] = incremSearchNextAct 446 447 incremSearchPrevAct = QAction(_('Previous Incremental Search'), self) 448 incremSearchPrevAct.triggered.connect(self.incremSearchPrev) 449 self.addAction(incremSearchPrevAct) 450 self.winActions['IncremSearchPrev'] = incremSearchPrevAct 451 452 for name, action in self.winActions.items(): 453 icon = globalref.toolIcons.getIcon(name.lower()) 454 if icon: 455 action.setIcon(icon) 456 key = globalref.keyboardOptions[name] 457 if not key.isEmpty(): 458 action.setShortcut(key) 459 self.allActions.update(self.winActions) 460 461 def setupToolbars(self): 462 """Add toolbars based on option settings. 463 """ 464 for toolbar in self.toolbars: 465 self.removeToolBar(toolbar) 466 self.toolbars = [] 467 numToolbars = globalref.toolbarOptions['ToolbarQuantity'] 468 iconSize = globalref.toolbarOptions['ToolbarSize'] 469 for num in range(numToolbars): 470 name = 'Toolbar{:d}'.format(num) 471 toolbar = self.addToolBar(name) 472 toolbar.setObjectName(name) 473 toolbar.setIconSize(QSize(iconSize, iconSize)) 474 self.toolbars.append(toolbar) 475 self.addToolbarCommands() 476 477 def addToolbarCommands(self): 478 """Add toolbar commands for current actions. 479 """ 480 for toolbar, commandList in zip(self.toolbars, 481 globalref. 482 toolbarOptions['ToolbarCommands']): 483 toolbar.clear() 484 for command in commandList.split(','): 485 if command: 486 try: 487 toolbar.addAction(self.allActions[command]) 488 except KeyError: 489 pass 490 else: 491 toolbar.addSeparator() 492 493 494 def setupMenus(self): 495 """Add menu items for actions. 496 """ 497 self.fileMenu = self.menuBar().addMenu(_('&File')) 498 self.fileMenu.aboutToShow.connect(self.loadRecentMenu) 499 self.fileMenu.addAction(self.allActions['FileNew']) 500 self.fileMenu.addAction(self.allActions['FileOpen']) 501 self.fileMenu.addAction(self.allActions['FileOpenSample']) 502 self.fileMenu.addAction(self.allActions['FileImport']) 503 self.fileMenu.addSeparator() 504 self.fileMenu.addAction(self.allActions['FileSave']) 505 self.fileMenu.addAction(self.allActions['FileSaveAs']) 506 self.fileMenu.addAction(self.allActions['FileExport']) 507 self.fileMenu.addAction(self.allActions['FileProperties']) 508 self.fileMenu.addSeparator() 509 self.fileMenu.addAction(self.allActions['FilePrintSetup']) 510 self.fileMenu.addAction(self.allActions['FilePrintPreview']) 511 self.fileMenu.addAction(self.allActions['FilePrint']) 512 self.fileMenu.addAction(self.allActions['FilePrintPdf']) 513 self.fileMenu.addSeparator() 514 self.recentFileSep = self.fileMenu.addSeparator() 515 self.fileMenu.addAction(self.allActions['FileQuit']) 516 517 editMenu = self.menuBar().addMenu(_('&Edit')) 518 editMenu.addAction(self.allActions['EditUndo']) 519 editMenu.addAction(self.allActions['EditRedo']) 520 editMenu.addSeparator() 521 editMenu.addAction(self.allActions['EditCut']) 522 editMenu.addAction(self.allActions['EditCopy']) 523 editMenu.addSeparator() 524 editMenu.addAction(self.allActions['EditPaste']) 525 editMenu.addAction(self.allActions['EditPastePlain']) 526 editMenu.addSeparator() 527 editMenu.addAction(self.allActions['EditPasteChild']) 528 editMenu.addAction(self.allActions['EditPasteBefore']) 529 editMenu.addAction(self.allActions['EditPasteAfter']) 530 editMenu.addSeparator() 531 editMenu.addAction(self.allActions['EditPasteCloneChild']) 532 editMenu.addAction(self.allActions['EditPasteCloneBefore']) 533 editMenu.addAction(self.allActions['EditPasteCloneAfter']) 534 535 nodeMenu = self.menuBar().addMenu(_('&Node')) 536 nodeMenu.addAction(self.allActions['NodeRename']) 537 nodeMenu.addSeparator() 538 nodeMenu.addAction(self.allActions['NodeAddChild']) 539 nodeMenu.addAction(self.allActions['NodeInsertBefore']) 540 nodeMenu.addAction(self.allActions['NodeInsertAfter']) 541 nodeMenu.addSeparator() 542 nodeMenu.addAction(self.allActions['NodeDelete']) 543 nodeMenu.addAction(self.allActions['NodeIndent']) 544 nodeMenu.addAction(self.allActions['NodeUnindent']) 545 nodeMenu.addSeparator() 546 nodeMenu.addAction(self.allActions['NodeMoveUp']) 547 nodeMenu.addAction(self.allActions['NodeMoveDown']) 548 nodeMenu.addAction(self.allActions['NodeMoveFirst']) 549 nodeMenu.addAction(self.allActions['NodeMoveLast']) 550 551 dataMenu = self.menuBar().addMenu(_('&Data')) 552 # add action's parent to get the sub-menu 553 dataMenu.addMenu(self.allActions['DataNodeType'].parent()) 554 # add the action to activate the shortcut key 555 self.addAction(self.allActions['DataNodeType']) 556 dataMenu.addAction(self.allActions['DataConfigType']) 557 dataMenu.addAction(self.allActions['DataCopyType']) 558 dataMenu.addAction(self.allActions['DataVisualConfig']) 559 dataMenu.addSeparator() 560 dataMenu.addAction(self.allActions['DataSortNodes']) 561 dataMenu.addAction(self.allActions['DataNumbering']) 562 dataMenu.addAction(self.allActions['DataRegenRefs']) 563 dataMenu.addSeparator() 564 dataMenu.addAction(self.allActions['DataCloneMatches']) 565 dataMenu.addAction(self.allActions['DataDetachClones']) 566 dataMenu.addSeparator() 567 dataMenu.addAction(self.allActions['DataFlatCategory']) 568 dataMenu.addAction(self.allActions['DataAddCategory']) 569 dataMenu.addAction(self.allActions['DataSwapCategory']) 570 571 toolsMenu = self.menuBar().addMenu(_('&Tools')) 572 toolsMenu.addAction(self.allActions['ToolsFindText']) 573 toolsMenu.addAction(self.allActions['ToolsFindCondition']) 574 toolsMenu.addAction(self.allActions['ToolsFindReplace']) 575 toolsMenu.addSeparator() 576 toolsMenu.addAction(self.allActions['ToolsFilterText']) 577 toolsMenu.addAction(self.allActions['ToolsFilterCondition']) 578 toolsMenu.addSeparator() 579 toolsMenu.addAction(self.allActions['ToolsSpellCheck']) 580 toolsMenu.addSeparator() 581 toolsMenu.addAction(self.allActions['ToolsGenOptions']) 582 toolsMenu.addSeparator() 583 toolsMenu.addAction(self.allActions['ToolsShortcuts']) 584 toolsMenu.addAction(self.allActions['ToolsToolbars']) 585 toolsMenu.addAction(self.allActions['ToolsFonts']) 586 toolsMenu.addAction(self.allActions['ToolsColors']) 587 588 formatMenu = self.menuBar().addMenu(_('Fo&rmat')) 589 formatMenu.addAction(self.allActions['FormatBoldFont']) 590 formatMenu.addAction(self.allActions['FormatItalicFont']) 591 formatMenu.addAction(self.allActions['FormatUnderlineFont']) 592 formatMenu.addSeparator() 593 # add action's parent to get the sub-menu 594 formatMenu.addMenu(self.allActions['FormatFontSize'].parent()) 595 # add the action to activate the shortcut key 596 self.addAction(self.allActions['FormatFontSize']) 597 formatMenu.addAction(self.allActions['FormatFontColor']) 598 formatMenu.addSeparator() 599 formatMenu.addAction(self.allActions['FormatExtLink']) 600 formatMenu.addAction(self.allActions['FormatIntLink']) 601 formatMenu.addAction(self.allActions['FormatInsertDate']) 602 formatMenu.addSeparator() 603 formatMenu.addAction(self.allActions['FormatSelectAll']) 604 formatMenu.addAction(self.allActions['FormatClearFormat']) 605 606 viewMenu = self.menuBar().addMenu(_('&View')) 607 viewMenu.addAction(self.allActions['ViewExpandBranch']) 608 viewMenu.addAction(self.allActions['ViewCollapseBranch']) 609 viewMenu.addSeparator() 610 viewMenu.addAction(self.allActions['ViewPrevSelect']) 611 viewMenu.addAction(self.allActions['ViewNextSelect']) 612 viewMenu.addSeparator() 613 viewMenu.addAction(self.allActions['ViewDataOutput']) 614 viewMenu.addAction(self.allActions['ViewDataEditor']) 615 viewMenu.addAction(self.allActions['ViewTitleList']) 616 viewMenu.addSeparator() 617 viewMenu.addAction(self.allActions['ViewBreadcrumb']) 618 viewMenu.addAction(self.allActions['ViewShowChildPane']) 619 viewMenu.addAction(self.allActions['ViewShowDescend']) 620 621 self.windowMenu = self.menuBar().addMenu(_('&Window')) 622 self.windowMenu.aboutToShow.connect(self.loadWindowMenu) 623 self.windowMenu.addAction(self.allActions['WinNewWindow']) 624 self.windowMenu.addAction(self.allActions['WinCloseWindow']) 625 self.windowMenu.addSeparator() 626 627 helpMenu = self.menuBar().addMenu(_('&Help')) 628 helpMenu.addAction(self.allActions['HelpBasic']) 629 helpMenu.addAction(self.allActions['HelpFull']) 630 helpMenu.addSeparator() 631 helpMenu.addAction(self.allActions['HelpAbout']) 632 633 def viewExpandBranch(self): 634 """Expand all children of the selected spots. 635 """ 636 QApplication.setOverrideCursor(Qt.WaitCursor) 637 selectedSpots = self.treeView.selectionModel().selectedSpots() 638 if not selectedSpots: 639 selectedSpots = self.treeView.model().treeStructure.rootSpots() 640 for spot in selectedSpots: 641 self.treeView.expandBranch(spot) 642 QApplication.restoreOverrideCursor() 643 644 def viewCollapseBranch(self): 645 """Collapse all children of the selected spots. 646 """ 647 QApplication.setOverrideCursor(Qt.WaitCursor) 648 selectedSpots = self.treeView.selectionModel().selectedSpots() 649 if not selectedSpots: 650 selectedSpots = self.treeView.model().treeStructure.rootSpots() 651 for spot in selectedSpots: 652 self.treeView.collapseBranch(spot) 653 QApplication.restoreOverrideCursor() 654 655 def viewPrevSelect(self): 656 """Return to the previous tree selection. 657 """ 658 self.treeView.selectionModel().restorePrevSelect() 659 660 def viewNextSelect(self): 661 """Go to the next tree selection in history. 662 """ 663 self.treeView.selectionModel().restoreNextSelect() 664 665 def viewRightTab(self, action): 666 """Show the tab in the right-hand view given by action. 667 668 Arguments: 669 action -- the action triggered in the action group 670 """ 671 if action == self.allActions['ViewDataOutput']: 672 self.rightTabs.setCurrentWidget(self.outputSplitter) 673 elif action == self.allActions['ViewDataEditor']: 674 self.rightTabs.setCurrentWidget(self.editorSplitter) 675 else: 676 self.rightTabs.setCurrentWidget(self.titleSplitter) 677 678 def viewBreadcrumb(self, checked): 679 """Enable or disable the display of the breadcrumb view. 680 681 Arguments: 682 checked -- True if to be shown, False if to be hidden 683 """ 684 self.breadcrumbView.setVisible(checked) 685 if checked: 686 self.updateRightViews() 687 688 def viewShowChildPane(self, checked): 689 """Enable or disable the display of children in a split pane. 690 691 Arguments: 692 checked -- True if to be shown, False if to be hidden 693 """ 694 for tabNum in range(3): 695 for splitNum in range(2): 696 view = self.rightTabs.widget(tabNum).widget(splitNum) 697 view.hideChildView = not checked 698 self.updateRightViews() 699 700 def viewDescendants(self, checked): 701 """Set the output view to show indented descendants if checked. 702 703 Arguments: 704 checked -- True if to be shown, False if to be hidden 705 """ 706 self.outputSplitter.widget(1).showDescendants = checked 707 self.updateRightViews() 708 709 def incremSearchStart(self): 710 """Start an incremental title search. 711 """ 712 if not self.treeFilterView: 713 self.treeView.setFocus() 714 self.treeView.incremSearchStart() 715 716 def incremSearchNext(self): 717 """Go to the next match in an incremental title search. 718 """ 719 if not self.treeFilterView: 720 self.treeView.incremSearchNext() 721 722 def incremSearchPrev(self): 723 """Go to the previous match in an incremental title search. 724 """ 725 if not self.treeFilterView: 726 self.treeView.incremSearchPrev() 727 728 def loadRecentMenu(self): 729 """Load recent file items to file menu before showing. 730 """ 731 for action in self.fileMenu.actions(): 732 text = action.text() 733 if len(text) > 1 and text[0] == '&' and '0' <= text[1] <= '9': 734 self.fileMenu.removeAction(action) 735 self.fileMenu.insertActions(self.recentFileSep, 736 globalref.mainControl.recentFiles. 737 getActions()) 738 739 def loadWindowMenu(self): 740 """Load window list items to window menu before showing. 741 """ 742 for action in self.windowMenu.actions(): 743 text = action.text() 744 if len(text) > 1 and text[0] == '&' and '0' <= text[1] <= '9': 745 self.windowMenu.removeAction(action) 746 self.windowMenu.addActions(globalref.mainControl.windowActions()) 747 748 def saveWindowGeom(self): 749 """Save window geometry parameters to history options. 750 """ 751 contentsRect = self.geometry() 752 frameRect = self.frameGeometry() 753 globalref.histOptions.changeValue('WindowXSize', contentsRect.width()) 754 globalref.histOptions.changeValue('WindowYSize', contentsRect.height()) 755 globalref.histOptions.changeValue('WindowXPos', contentsRect.x()) 756 globalref.histOptions.changeValue('WindowYPos', contentsRect.y()) 757 globalref.histOptions.changeValue('WindowTopMargin', 758 contentsRect.y() - frameRect.y()) 759 globalref.histOptions.changeValue('WindowOtherMargin', 760 contentsRect.x() - frameRect.x()) 761 try: 762 upperWidth, lowerWidth = self.breadcrumbSplitter.sizes() 763 crumbPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) 764 globalref.histOptions.changeValue('CrumbSplitPercent', 765 crumbPercent) 766 767 leftWidth, rightWidth = self.treeSplitter.sizes() 768 treePercent = int(100 * leftWidth / (leftWidth + rightWidth)) 769 globalref.histOptions.changeValue('TreeSplitPercent', treePercent) 770 upperWidth, lowerWidth = self.outputSplitter.sizes() 771 outputPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) 772 globalref.histOptions.changeValue('OutputSplitPercent', 773 outputPercent) 774 upperWidth, lowerWidth = self.editorSplitter.sizes() 775 editorPercent = int(100 * upperWidth / (upperWidth + lowerWidth)) 776 globalref.histOptions.changeValue('EditorSplitPercent', 777 editorPercent) 778 upperWidth, lowerWidth = self.titleSplitter.sizes() 779 titlePercent = int(100 * upperWidth / (upperWidth + lowerWidth)) 780 globalref.histOptions.changeValue('TitleSplitPercent', 781 titlePercent) 782 except ZeroDivisionError: 783 pass # skip if splitter sizes were never set 784 tabNum = self.rightTabs.currentIndex() 785 globalref.histOptions.changeValue('ActiveRightView', tabNum) 786 787 def restoreWindowGeom(self, offset=0): 788 """Restore window geometry from history options. 789 790 Arguments: 791 offset -- number of pixels to offset window, down and to right 792 """ 793 rect = QRect(globalref.histOptions['WindowXPos'], 794 globalref.histOptions['WindowYPos'], 795 globalref.histOptions['WindowXSize'], 796 globalref.histOptions['WindowYSize']) 797 if rect.x() == -1000 and rect.y() == -1000: 798 # let OS position window the first time 799 self.resize(rect.size()) 800 else: 801 if offset: 802 rect.adjust(offset, offset, offset, offset) 803 availRect = QApplication.primaryScreen().availableVirtualGeometry() 804 topMargin = globalref.histOptions['WindowTopMargin'] 805 otherMargin = globalref.histOptions['WindowOtherMargin'] 806 # remove frame space from available rect 807 availRect.adjust(otherMargin, topMargin, 808 -otherMargin, -otherMargin) 809 finalRect = rect.intersected(availRect) 810 if finalRect.isEmpty(): 811 rect.moveTo(0, 0) 812 finalRect = rect.intersected(availRect) 813 if finalRect.isValid(): 814 self.setGeometry(finalRect) 815 crumbWidth = int(self.breadcrumbSplitter.width() / 100 * 816 globalref.histOptions['CrumbSplitPercent']) 817 self.breadcrumbSplitter.setSizes([crumbWidth, 818 self.breadcrumbSplitter.width() - 819 crumbWidth]) 820 treeWidth = int(self.treeSplitter.width() / 100 * 821 globalref.histOptions['TreeSplitPercent']) 822 self.treeSplitter.setSizes([treeWidth, 823 self.treeSplitter.width() - treeWidth]) 824 outHeight = int(self.outputSplitter.height() / 100.0 * 825 globalref.histOptions['OutputSplitPercent']) 826 self.outputSplitter.setSizes([outHeight, 827 self.outputSplitter.height() - outHeight]) 828 editHeight = int(self.editorSplitter.height() / 100.0 * 829 globalref.histOptions['EditorSplitPercent']) 830 self.editorSplitter.setSizes([editHeight, 831 self.editorSplitter.height() - editHeight]) 832 titleHeight = int(self.titleSplitter.height() / 100.0 * 833 globalref.histOptions['TitleSplitPercent']) 834 self.titleSplitter.setSizes([titleHeight, 835 self.titleSplitter.height() - titleHeight]) 836 self.rightTabs.setCurrentIndex(globalref. 837 histOptions['ActiveRightView']) 838 839 def resetWindowGeom(self): 840 """Set all stored window geometry values back to default settings. 841 """ 842 globalref.histOptions.resetToDefaults(['WindowXPos', 'WindowYPos', 843 'WindowXSize', 'WindowYSize', 844 'CrumbSplitPercent', 845 'TreeSplitPercent', 846 'OutputSplitPercent', 847 'EditorSplitPercent', 848 'TitleSplitPercent', 849 'ActiveRightView']) 850 851 def saveToolbarPosition(self): 852 """Save the toolbar position to the toolbar options. 853 """ 854 toolbarPos = base64.b64encode(self.saveState().data()).decode('ascii') 855 globalref.toolbarOptions.changeValue('ToolbarPosition', toolbarPos) 856 globalref.toolbarOptions.writeFile() 857 858 def restoreToolbarPosition(self): 859 """Restore the toolbar position from the toolbar options. 860 """ 861 toolbarPos = globalref.toolbarOptions['ToolbarPosition'] 862 if toolbarPos: 863 self.restoreState(base64.b64decode(bytes(toolbarPos, 'ascii'))) 864 865 def dragEnterEvent(self, event): 866 """Accept drags of files to this window. 867 868 Arguments: 869 event -- the drag event object 870 """ 871 if event.mimeData().hasUrls(): 872 event.accept() 873 874 def dropEvent(self, event): 875 """Open a file dropped onto this window. 876 877 Arguments: 878 event -- the drop event object 879 """ 880 fileList = event.mimeData().urls() 881 if fileList: 882 path = pathlib.Path(fileList[0].toLocalFile()) 883 globalref.mainControl.openFile(path, checkModified=True) 884 885 def changeEvent(self, event): 886 """Detect an activation of the main window and emit a signal. 887 888 Arguments: 889 event -- the change event object 890 """ 891 super().changeEvent(event) 892 if (event.type() == QEvent.ActivationChange and 893 QApplication.activeWindow() == self): 894 self.winActivated.emit(self) 895 elif (event.type() == QEvent.WindowStateChange and 896 globalref.genOptions['MinToSysTray'] and self.isMinimized()): 897 self.winMinimized.emit() 898 899 def closeEvent(self, event): 900 """Signal that the view is closing and close if the flag allows it. 901 902 Also save window status if necessary. 903 Arguments: 904 event -- the close event object 905 """ 906 self.winClosing.emit(self) 907 if self.allowCloseFlag: 908 event.accept() 909 else: 910 event.ignore() 911