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