1#!/usr/bin/env python3
2
3#******************************************************************************
4# miscdialogs.py, provides classes for various control dialogs
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 enum
16import re
17import sys
18import operator
19import collections
20import datetime
21import platform
22import traceback
23from PyQt5.QtCore import Qt, pyqtSignal, PYQT_VERSION_STR, qVersion
24from PyQt5.QtGui import QFont, QKeySequence, QTextDocument, QTextOption
25from PyQt5.QtWidgets import (QAbstractItemView, QApplication, QButtonGroup,
26                             QCheckBox, QComboBox, QDialog, QGridLayout,
27                             QGroupBox, QHBoxLayout, QLabel, QLineEdit,
28                             QListWidget, QListWidgetItem, QMenu, QMessageBox,
29                             QPlainTextEdit, QPushButton, QRadioButton,
30                             QScrollArea, QSpinBox, QTabWidget, QTextEdit,
31                             QTreeWidget, QTreeWidgetItem, QVBoxLayout,
32                             QWidget)
33import options
34import printdialogs
35import undo
36import globalref
37try:
38    from __main__ import __version__
39except ImportError:
40    __version__ = ''
41
42
43class RadioChoiceDialog(QDialog):
44    """Dialog for choosing between a list of text items (radio buttons).
45
46    Dialog title, group heading, button text and return text can be set.
47    """
48    def __init__(self, title, heading, choiceList, parent=None):
49        """Create the radio choice dialog.
50
51        Arguments:
52            title -- the window title
53            heading -- the groupbox text
54            choiceList -- tuples of button text and return values
55            parent -- the parent window
56        """
57        super().__init__(parent)
58        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
59                            Qt.WindowCloseButtonHint)
60        self.setWindowTitle(title)
61        topLayout = QVBoxLayout(self)
62        self.setLayout(topLayout)
63
64        groupBox = QGroupBox(heading)
65        topLayout.addWidget(groupBox)
66        groupLayout = QVBoxLayout(groupBox)
67        self.buttonGroup = QButtonGroup(self)
68        for text, value in choiceList:
69            if value != None:
70                button = QRadioButton(text)
71                button.returnValue = value
72                groupLayout.addWidget(button)
73                self.buttonGroup.addButton(button)
74            else:  # add heading if no return value
75                label = QLabel('<b>{0}:</b>'.format(text))
76                groupLayout.addWidget(label)
77        self.buttonGroup.buttons()[0].setChecked(True)
78
79        ctrlLayout = QHBoxLayout()
80        topLayout.addLayout(ctrlLayout)
81        ctrlLayout.addStretch(0)
82        okButton = QPushButton(_('&OK'))
83        ctrlLayout.addWidget(okButton)
84        okButton.clicked.connect(self.accept)
85        cancelButton = QPushButton(_('&Cancel'))
86        ctrlLayout.addWidget(cancelButton)
87        cancelButton.clicked.connect(self.reject)
88        groupBox.setFocus()
89
90    def addLabelBox(self, heading, text):
91        """Add a group box with text above the radio button group.
92
93        Arguments:
94            heading -- the groupbox text
95            text - the label text
96        """
97        labelBox = QGroupBox(heading)
98        self.layout().insertWidget(0, labelBox)
99        labelLayout =  QVBoxLayout(labelBox)
100        label = QLabel(text)
101        labelLayout.addWidget(label)
102
103    def selectedButton(self):
104        """Return the value of the selected button.
105        """
106        return self.buttonGroup.checkedButton().returnValue
107
108
109class FieldSelectDialog(QDialog):
110    """Dialog for selecting a sequence from a list of field names.
111    """
112    def __init__(self, title, heading, fieldList, parent=None):
113        """Create the field select dialog.
114
115        Arguments:
116            title -- the window title
117            heading -- the groupbox text
118            fieldList -- the list of field names to select
119            parent -- the parent window
120        """
121        super().__init__(parent)
122        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
123                            Qt.WindowCloseButtonHint)
124        self.setWindowTitle(title)
125        self.selectedFields = []
126        topLayout = QVBoxLayout(self)
127        self.setLayout(topLayout)
128        groupBox = QGroupBox(heading)
129        topLayout.addWidget(groupBox)
130        groupLayout = QVBoxLayout(groupBox)
131
132        self.listView = QTreeWidget()
133        groupLayout.addWidget(self.listView)
134        self.listView.setHeaderLabels(['#', _('Fields')])
135        self.listView.setRootIsDecorated(False)
136        self.listView.setSortingEnabled(False)
137        self.listView.setSelectionMode(QAbstractItemView.MultiSelection)
138        for field in fieldList:
139            QTreeWidgetItem(self.listView, ['', field])
140        self.listView.resizeColumnToContents(0)
141        self.listView.resizeColumnToContents(1)
142        self.listView.itemSelectionChanged.connect(self.updateSelectedFields)
143
144        ctrlLayout = QHBoxLayout()
145        topLayout.addLayout(ctrlLayout)
146        ctrlLayout.addStretch(0)
147        self.okButton = QPushButton(_('&OK'))
148        ctrlLayout.addWidget(self.okButton)
149        self.okButton.clicked.connect(self.accept)
150        self.okButton.setEnabled(False)
151        cancelButton = QPushButton(_('&Cancel'))
152        ctrlLayout.addWidget(cancelButton)
153        cancelButton.clicked.connect(self.reject)
154        self.listView.setFocus()
155
156    def updateSelectedFields(self):
157        """Update the TreeView and the list of selected fields.
158        """
159        itemList = [self.listView.topLevelItem(i) for i in
160                    range(self.listView.topLevelItemCount())]
161        for item in itemList:
162            if item.isSelected():
163                if item.text(1) not in self.selectedFields:
164                    self.selectedFields.append(item.text(1))
165            elif item.text(1) in self.selectedFields:
166                self.selectedFields.remove(item.text(1))
167        for item in itemList:
168            if item.isSelected():
169                item.setText(0, str(self.selectedFields.index(item.text(1))
170                                    + 1))
171            else:
172                item.setText(0, '')
173        self.okButton.setEnabled(len(self.selectedFields))
174
175
176class FilePropertiesDialog(QDialog):
177    """Dialog for setting file parameters like compression and encryption.
178    """
179    def __init__(self, localControl, parent=None):
180        """Create the file properties dialog.
181
182        Arguments:
183            localControl -- a reference to the file's local control
184            parent -- the parent window
185        """
186        super().__init__(parent)
187        self.localControl = localControl
188        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
189                            Qt.WindowCloseButtonHint)
190        self.setWindowTitle(_('File Properties'))
191        topLayout = QVBoxLayout(self)
192        self.setLayout(topLayout)
193
194        groupBox = QGroupBox(_('File Storage'))
195        topLayout.addWidget(groupBox)
196        groupLayout = QVBoxLayout(groupBox)
197        self.compressCheck = QCheckBox(_('&Use file compression'))
198        self.compressCheck.setChecked(localControl.compressed)
199        groupLayout.addWidget(self.compressCheck)
200        self.encryptCheck = QCheckBox(_('Use file &encryption'))
201        self.encryptCheck.setChecked(localControl.encrypted)
202        groupLayout.addWidget(self.encryptCheck)
203
204        groupBox = QGroupBox(_('Spell Check'))
205        topLayout.addWidget(groupBox)
206        groupLayout = QHBoxLayout(groupBox)
207        label = QLabel(_('Language code or\ndictionary (optional)'))
208        groupLayout.addWidget(label)
209        self.spellCheckEdit = QLineEdit()
210        self.spellCheckEdit.setText(self.localControl.spellCheckLang)
211        groupLayout.addWidget(self.spellCheckEdit)
212
213        groupBox = QGroupBox(_('Math Fields'))
214        topLayout.addWidget(groupBox)
215        groupLayout = QVBoxLayout(groupBox)
216        self.zeroBlanks = QCheckBox(_('&Treat blank fields as zeros'))
217        self.zeroBlanks.setChecked(localControl.structure.mathZeroBlanks)
218        groupLayout.addWidget(self.zeroBlanks)
219
220        ctrlLayout = QHBoxLayout()
221        topLayout.addLayout(ctrlLayout)
222        ctrlLayout.addStretch(0)
223        okButton = QPushButton(_('&OK'))
224        ctrlLayout.addWidget(okButton)
225        okButton.clicked.connect(self.accept)
226        cancelButton = QPushButton(_('&Cancel'))
227        ctrlLayout.addWidget(cancelButton)
228        cancelButton.clicked.connect(self.reject)
229
230    def accept(self):
231        """Store the results.
232        """
233        if (self.localControl.compressed != self.compressCheck.isChecked() or
234            self.localControl.encrypted != self.encryptCheck.isChecked() or
235            self.localControl.spellCheckLang != self.spellCheckEdit.text() or
236            self.localControl.structure.mathZeroBlanks !=
237            self.zeroBlanks.isChecked()):
238            undo.ParamUndo(self.localControl.structure.undoList,
239                           [(self.localControl, 'compressed'),
240                            (self.localControl, 'encrypted'),
241                            (self.localControl, 'spellCheckLang'),
242                            (self.localControl.structure, 'mathZeroBlanks')])
243            self.localControl.compressed = self.compressCheck.isChecked()
244            self.localControl.encrypted = self.encryptCheck.isChecked()
245            self.localControl.spellCheckLang = self.spellCheckEdit.text()
246            self.localControl.structure.mathZeroBlanks = (self.zeroBlanks.
247                                                          isChecked())
248            super().accept()
249        else:
250            super().reject()
251
252
253class PasswordDialog(QDialog):
254    """Dialog for password entry and optional re-entry.
255    """
256    remember = True
257    def __init__(self, retype=True, fileLabel='', parent=None):
258        """Create the password dialog.
259
260        Arguments:
261            retype -- require a 2nd password entry if True
262            fileLabel -- file name to show if given
263            parent -- the parent window
264        """
265        super().__init__(parent)
266        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
267                            Qt.WindowCloseButtonHint)
268        self.setWindowTitle(_('Encrypted File Password'))
269        self.password = ''
270        topLayout = QVBoxLayout(self)
271        self.setLayout(topLayout)
272        if fileLabel:
273            prompt = _('Type Password for "{0}":').format(fileLabel)
274        else:
275            prompt = _('Type Password:')
276        self.editors = [self.addEditor(prompt, topLayout)]
277        self.editors[0].setFocus()
278        if retype:
279            self.editors.append(self.addEditor(_('Re-Type Password:'),
280                                               topLayout))
281            self.editors[0].returnPressed.connect(self.editors[1].setFocus)
282        self.editors[-1].returnPressed.connect(self.accept)
283        self.rememberCheck = QCheckBox(_('Remember password during this '
284                                               'session'))
285        self.rememberCheck.setChecked(PasswordDialog.remember)
286        topLayout.addWidget(self.rememberCheck)
287
288        ctrlLayout = QHBoxLayout()
289        topLayout.addLayout(ctrlLayout)
290        ctrlLayout.addStretch(0)
291        okButton = QPushButton(_('&OK'))
292        okButton.setAutoDefault(False)
293        ctrlLayout.addWidget(okButton)
294        okButton.clicked.connect(self.accept)
295        cancelButton = QPushButton(_('&Cancel'))
296        cancelButton.setAutoDefault(False)
297        ctrlLayout.addWidget(cancelButton)
298        cancelButton.clicked.connect(self.reject)
299
300    def addEditor(self, labelText, layout):
301        """Add a password editor to this dialog and return it.
302
303        Arguments:
304            labelText -- the text for the label
305            layout -- the layout to append it
306        """
307        label = QLabel(labelText)
308        layout.addWidget(label)
309        editor = QLineEdit()
310        editor.setEchoMode(QLineEdit.Password)
311        layout.addWidget(editor)
312        return editor
313
314    def accept(self):
315        """Check for valid password and store the result.
316        """
317        self.password = self.editors[0].text()
318        PasswordDialog.remember = self.rememberCheck.isChecked()
319        if not self.password:
320            QMessageBox.warning(self, 'TreeLine',
321                                  _('Zero-length passwords are not permitted'))
322        elif len(self.editors) > 1 and self.editors[1].text() != self.password:
323             QMessageBox.warning(self, 'TreeLine',
324                                       _('Re-typed password did not match'))
325        else:
326            super().accept()
327        for editor in self.editors:
328            editor.clear()
329        self.editors[0].setFocus()
330
331
332class TemplateFileItem:
333    """Helper class to store template paths and info.
334    """
335    nameExp = re.compile(r'(\d+)([a-zA-Z]+?)_(.+)')
336    def __init__(self, pathObj):
337        """Initialize the path.
338
339        Arguments:
340            pathObj -- the full path object
341        """
342        self.pathObj = pathObj
343        self.number = sys.maxsize
344        self.name = ''
345        self.displayName = ''
346        self.langCode = ''
347        if pathObj:
348            self.name = pathObj.stem
349            match = TemplateFileItem.nameExp.match(self.name)
350            if match:
351                num, self.langCode, self.name = match.groups()
352                self.number = int(num)
353            self.displayName = self.name.replace('_', ' ')
354
355    def sortKey(self):
356        """Return a key for sorting the items by number then name.
357        """
358        return (self.number, self.displayName)
359
360    def __eq__(self, other):
361        """Comparison to detect equivalent items.
362
363        Arguments:
364            other -- the TemplateFileItem to compare
365        """
366        return (self.displayName == other.displayName and
367                self.langCode == other.langCode)
368
369    def __hash__(self):
370        """Return a hash code for use in sets and dictionaries.
371        """
372        return hash((self.langCode, self.displayName))
373
374
375class TemplateFileDialog(QDialog):
376    """Dialog for listing available template files.
377    """
378    def __init__(self, title, heading, searchPaths, addDefault=True,
379                 parent=None):
380        """Create the template dialog.
381
382        Arguments:
383            title -- the window title
384            heading -- the groupbox text
385            searchPaths -- list of path objects with available templates
386            addDefault -- if True, add a default (no path) entry
387            parent -- the parent window
388        """
389        super().__init__(parent)
390        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
391                            Qt.WindowCloseButtonHint)
392        self.setWindowTitle(title)
393        self.templateItems = []
394        if addDefault:
395            item = TemplateFileItem(None)
396            item.number = -1
397            item.displayName = _('Default - Single Line Text')
398            self.templateItems.append(item)
399
400        topLayout = QVBoxLayout(self)
401        self.setLayout(topLayout)
402        groupBox = QGroupBox(heading)
403        topLayout.addWidget(groupBox)
404        boxLayout = QVBoxLayout(groupBox)
405        self.listBox = QListWidget()
406        boxLayout.addWidget(self.listBox)
407        self.listBox.itemDoubleClicked.connect(self.accept)
408
409        ctrlLayout = QHBoxLayout()
410        topLayout.addLayout(ctrlLayout)
411        ctrlLayout.addStretch(0)
412        self.okButton = QPushButton(_('&OK'))
413        ctrlLayout.addWidget(self.okButton)
414        self.okButton.clicked.connect(self.accept)
415        cancelButton = QPushButton(_('&Cancel'))
416        ctrlLayout.addWidget(cancelButton)
417        cancelButton.clicked.connect(self.reject)
418
419        self.readTemplates(searchPaths)
420        self.loadListBox()
421
422    def readTemplates(self, searchPaths):
423        """Read template file paths into the templateItems list.
424
425        Arguments:
426            searchPaths -- list of path objects with available templates
427        """
428        templateItems = set()
429        for path in searchPaths:
430            for templatePath in path.glob('*.trln'):
431                templateItem = TemplateFileItem(templatePath)
432                if templateItem not in templateItems:
433                    templateItems.add(templateItem)
434        availLang = set([item.langCode for item in templateItems])
435        if len(availLang) > 1:
436            lang = 'en'
437            if globalref.lang[:2] in availLang:
438                lang = globalref.lang[:2]
439            templateItems = [item for item in templateItems if
440                             item.langCode == lang or not item.langCode]
441        self.templateItems.extend(list(templateItems))
442        self.templateItems.sort(key = operator.methodcaller('sortKey'))
443
444    def loadListBox(self):
445        """Load the list box with items from the templateItems list.
446        """
447        self.listBox.clear()
448        self.listBox.addItems([item.displayName for item in
449                               self.templateItems])
450        self.listBox.setCurrentRow(0)
451        self.okButton.setEnabled(self.listBox.count() > 0)
452
453    def selectedPath(self):
454        """Return the path object from the selected item.
455        """
456        item = self.templateItems[self.listBox.currentRow()]
457        return item.pathObj
458
459    def selectedName(self):
460        """Return the displayed name with underscores from the selected item.
461        """
462        item = self.templateItems[self.listBox.currentRow()]
463        return item.name
464
465
466class ExceptionDialog(QDialog):
467    """Dialog for showing debug info from an unhandled exception.
468    """
469    def __init__(self, excType, value, tb, parent=None):
470        """Initialize the exception dialog.
471
472        Arguments:
473            excType -- execption class
474            value -- execption error text
475            tb -- the traceback object
476        """
477        super().__init__(parent)
478        self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint)
479        self.setWindowTitle(_('TreeLine - Serious Error'))
480
481        topLayout = QVBoxLayout(self)
482        self.setLayout(topLayout)
483        label = QLabel(_('A serious error has occurred.  TreeLine could be '
484                         'in an unstable state.\n'
485                         'Recommend saving any file changes under another '
486                         'filename and restart TreeLine.\n\n'
487                         'The debugging info shown below can be copied '
488                         'and emailed to doug101@bellz.org along with\n'
489                         'an explanation of the circumstances.\n'))
490        topLayout.addWidget(label)
491        textBox = QTextEdit()
492        textBox.setReadOnly(True)
493        pyVersion = '.'.join([repr(num) for num in sys.version_info[:3]])
494        textLines = ['When:  {0}\n'.format(datetime.datetime.now().
495                                           isoformat(' ')),
496                     'TreeLine Version:  {0}\n'.format(__version__),
497                     'Python Version:  {0}\n'.format(pyVersion),
498                     'Qt Version:  {0}\n'.format(qVersion()),
499                     'PyQt Version:  {0}\n'.format(PYQT_VERSION_STR),
500                     'OS:  {0}\n'.format(platform.platform()), '\n']
501        textLines.extend(traceback.format_exception(excType, value, tb))
502        textBox.setPlainText(''.join(textLines))
503        topLayout.addWidget(textBox)
504
505        ctrlLayout = QHBoxLayout()
506        topLayout.addLayout(ctrlLayout)
507        ctrlLayout.addStretch(0)
508        closeButton = QPushButton(_('&Close'))
509        ctrlLayout.addWidget(closeButton)
510        closeButton.clicked.connect(self.close)
511
512
513FindScope = enum.IntEnum('FindScope', 'fullData titlesOnly')
514FindType = enum.IntEnum('FindType', 'keyWords fullWords fullPhrase regExp')
515
516class FindFilterDialog(QDialog):
517    """Dialog for searching for text within tree titles and data.
518    """
519    dialogShown = pyqtSignal(bool)
520    def __init__(self, isFilterDialog=False, parent=None):
521        """Initialize the find dialog.
522
523        Arguments:
524            isFilterDialog -- True for filter dialog, False for find dialog
525            parent -- the parent window
526        """
527        super().__init__(parent)
528        self.isFilterDialog = isFilterDialog
529        self.setAttribute(Qt.WA_QuitOnClose, False)
530        self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint)
531
532        topLayout = QVBoxLayout(self)
533        self.setLayout(topLayout)
534
535        textBox = QGroupBox(_('&Search Text'))
536        topLayout.addWidget(textBox)
537        textLayout = QVBoxLayout(textBox)
538        self.textEntry = QLineEdit()
539        textLayout.addWidget(self.textEntry)
540        self.textEntry.textEdited.connect(self.updateAvail)
541
542        horizLayout = QHBoxLayout()
543        topLayout.addLayout(horizLayout)
544
545        whatBox = QGroupBox(_('What to Search'))
546        horizLayout.addWidget(whatBox)
547        whatLayout = QVBoxLayout(whatBox)
548        self.whatButtons = QButtonGroup(self)
549        button = QRadioButton(_('Full &data'))
550        self.whatButtons.addButton(button, FindScope.fullData)
551        whatLayout.addWidget(button)
552        button = QRadioButton(_('&Titles only'))
553        self.whatButtons.addButton(button, FindScope.titlesOnly)
554        whatLayout.addWidget(button)
555        self.whatButtons.button(FindScope.fullData).setChecked(True)
556
557        howBox = QGroupBox(_('How to Search'))
558        horizLayout.addWidget(howBox)
559        howLayout = QVBoxLayout(howBox)
560        self.howButtons = QButtonGroup(self)
561        button = QRadioButton(_('&Key words'))
562        self.howButtons.addButton(button, FindType.keyWords)
563        howLayout.addWidget(button)
564        button = QRadioButton(_('Key full &words'))
565        self.howButtons.addButton(button, FindType.fullWords)
566        howLayout.addWidget(button)
567        button = QRadioButton(_('F&ull phrase'))
568        self.howButtons.addButton(button, FindType.fullPhrase)
569        howLayout.addWidget(button)
570        button = QRadioButton(_('&Regular expression'))
571        self.howButtons.addButton(button, FindType.regExp)
572        howLayout.addWidget(button)
573        self.howButtons.button(FindType.keyWords).setChecked(True)
574
575        ctrlLayout = QHBoxLayout()
576        topLayout.addLayout(ctrlLayout)
577        if not self.isFilterDialog:
578            self.setWindowTitle(_('Find'))
579            self.previousButton = QPushButton(_('Find &Previous'))
580            ctrlLayout.addWidget(self.previousButton)
581            self.previousButton.clicked.connect(self.findPrevious)
582            self.nextButton = QPushButton(_('Find &Next'))
583            self.nextButton.setDefault(True)
584            ctrlLayout.addWidget(self.nextButton)
585            self.nextButton.clicked.connect(self.findNext)
586            self.resultLabel = QLabel()
587            topLayout.addWidget(self.resultLabel)
588        else:
589            self.setWindowTitle(_('Filter'))
590            self.filterButton = QPushButton(_('&Filter'))
591            ctrlLayout.addWidget(self.filterButton)
592            self.filterButton.clicked.connect(self.startFilter)
593            self.endFilterButton = QPushButton(_('&End Filter'))
594            ctrlLayout.addWidget(self.endFilterButton)
595            self.endFilterButton.clicked.connect(self.endFilter)
596        closeButton = QPushButton(_('&Close'))
597        ctrlLayout.addWidget(closeButton)
598        closeButton.clicked.connect(self.close)
599        self.updateAvail('')
600
601    def selectAllText(self):
602        """Select all line edit text to prepare for a new entry.
603        """
604        self.textEntry.selectAll()
605        self.textEntry.setFocus()
606
607    def updateAvail(self, text='', fileChange=False):
608        """Make find buttons available if search text exists.
609
610        Arguments:
611            text -- placeholder for signal text (not used)
612            fileChange -- True if window changed while dialog open
613        """
614        hasEntry = len(self.textEntry.text().strip()) > 0
615        if not self.isFilterDialog:
616            self.previousButton.setEnabled(hasEntry)
617            self.nextButton.setEnabled(hasEntry)
618            self.resultLabel.setText('')
619        else:
620            window = globalref.mainControl.activeControl.activeWindow
621            if fileChange and window.treeFilterView:
622                filterView = window.treeFilterView
623                self.textEntry.setText(filterView.filterStr)
624                self.whatButtons.button(filterView.filterWhat).setChecked(True)
625                self.howButtons.button(filterView.filterHow).setChecked(True)
626            self.filterButton.setEnabled(hasEntry)
627            self.endFilterButton.setEnabled(window.treeFilterView != None)
628
629    def find(self, forward=True):
630        """Find another match in the indicated direction.
631
632        Arguments:
633            forward -- next if True, previous if False
634        """
635        self.resultLabel.setText('')
636        text = self.textEntry.text()
637        titlesOnly = self.whatButtons.checkedId() == (FindScope.titlesOnly)
638        control = globalref.mainControl.activeControl
639        if self.howButtons.checkedId() == FindType.regExp:
640            try:
641                regExp = re.compile(text)
642            except re.error:
643                QMessageBox.warning(self, 'TreeLine',
644                                    _('Error - invalid regular expression'))
645                return
646            result = control.findNodesByRegExp([regExp], titlesOnly, forward)
647        elif self.howButtons.checkedId() == FindType.fullWords:
648            regExpList = []
649            for word in text.lower().split():
650                regExpList.append(re.compile(r'(?i)\b{}\b'.
651                                             format(re.escape(word))))
652            result = control.findNodesByRegExp(regExpList, titlesOnly, forward)
653        elif self.howButtons.checkedId() == FindType.keyWords:
654            wordList = text.lower().split()
655            result = control.findNodesByWords(wordList, titlesOnly, forward)
656        else:         # full phrase
657            wordList = [text.lower().strip()]
658            result = control.findNodesByWords(wordList, titlesOnly, forward)
659        if not result:
660            self.resultLabel.setText(_('Search string "{0}" not found').
661                                     format(text))
662
663    def findPrevious(self):
664        """Find the previous match.
665        """
666        self.find(False)
667
668    def findNext(self):
669        """Find the next match.
670        """
671        self.find(True)
672
673    def startFilter(self):
674        """Start filtering nodes.
675        """
676        if self.howButtons.checkedId() == FindType.regExp:
677            try:
678                re.compile(self.textEntry.text())
679            except re.error:
680                QMessageBox.warning(self, 'TreeLine',
681                                       _('Error - invalid regular expression'))
682                return
683        filterView = (globalref.mainControl.activeControl.activeWindow.
684                      filterView())
685        filterView.filterWhat = self.whatButtons.checkedId()
686        filterView.filterHow = self.howButtons.checkedId()
687        filterView.filterStr = self.textEntry.text()
688        filterView.updateContents()
689        self.updateAvail()
690
691    def endFilter(self):
692        """Stop filtering nodes.
693        """
694        globalref.mainControl.activeControl.activeWindow.removeFilterView()
695        self.updateAvail()
696
697    def closeEvent(self, event):
698        """Signal that the dialog is closing.
699
700        Arguments:
701            event -- the close event
702        """
703        self.dialogShown.emit(False)
704
705
706FindReplaceType = enum.IntEnum('FindReplaceType', 'anyMatch fullWord regExp')
707
708class FindReplaceDialog(QDialog):
709    """Dialog for finding and replacing text in the node data.
710    """
711    dialogShown = pyqtSignal(bool)
712    def __init__(self, parent=None):
713        """Initialize the find and replace dialog.
714
715        Arguments:
716            parent -- the parent window
717        """
718        super().__init__(parent)
719        self.setAttribute(Qt.WA_QuitOnClose, False)
720        self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint)
721        self.setWindowTitle(_('Find and Replace'))
722
723        self.matchedSpot = None
724        topLayout = QGridLayout(self)
725        self.setLayout(topLayout)
726
727        textBox = QGroupBox(_('&Search Text'))
728        topLayout.addWidget(textBox, 0, 0)
729        textLayout = QVBoxLayout(textBox)
730        self.textEntry = QLineEdit()
731        textLayout.addWidget(self.textEntry)
732        self.textEntry.textEdited.connect(self.clearMatch)
733
734        replaceBox = QGroupBox(_('Replacement &Text'))
735        topLayout.addWidget(replaceBox, 0, 1)
736        replaceLayout = QVBoxLayout(replaceBox)
737        self.replaceEntry = QLineEdit()
738        replaceLayout.addWidget(self.replaceEntry)
739
740        howBox = QGroupBox(_('How to Search'))
741        topLayout.addWidget(howBox, 1, 0, 2, 1)
742        howLayout = QVBoxLayout(howBox)
743        self.howButtons = QButtonGroup(self)
744        button = QRadioButton(_('Any &match'))
745        self.howButtons.addButton(button, FindReplaceType.anyMatch)
746        howLayout.addWidget(button)
747        button = QRadioButton(_('Full &words'))
748        self.howButtons.addButton(button, FindReplaceType.fullWord)
749        howLayout.addWidget(button)
750        button = QRadioButton(_('Re&gular expression'))
751        self.howButtons.addButton(button, FindReplaceType.regExp)
752        howLayout.addWidget(button)
753        self.howButtons.button(FindReplaceType.anyMatch).setChecked(True)
754        self.howButtons.buttonClicked.connect(self.clearMatch)
755
756        typeBox = QGroupBox(_('&Node Type'))
757        topLayout.addWidget(typeBox, 1, 1)
758        typeLayout = QVBoxLayout(typeBox)
759        self.typeCombo = QComboBox()
760        typeLayout.addWidget(self.typeCombo)
761        self.typeCombo.currentIndexChanged.connect(self.loadFieldNames)
762
763        fieldBox = QGroupBox(_('N&ode Fields'))
764        topLayout.addWidget(fieldBox, 2, 1)
765        fieldLayout = QVBoxLayout(fieldBox)
766        self.fieldCombo = QComboBox()
767        fieldLayout.addWidget(self.fieldCombo)
768        self.fieldCombo.currentIndexChanged.connect(self.clearMatch)
769
770        ctrlLayout = QHBoxLayout()
771        topLayout.addLayout(ctrlLayout, 3, 0, 1, 2)
772        self.previousButton = QPushButton(_('Find &Previous'))
773        ctrlLayout.addWidget(self.previousButton)
774        self.previousButton.clicked.connect(self.findPrevious)
775        self.nextButton = QPushButton(_('&Find Next'))
776        self.nextButton.setDefault(True)
777        ctrlLayout.addWidget(self.nextButton)
778        self.nextButton.clicked.connect(self.findNext)
779        self.replaceButton = QPushButton(_('&Replace'))
780        ctrlLayout.addWidget(self.replaceButton)
781        self.replaceButton.clicked.connect(self.replace)
782        self.replaceAllButton = QPushButton(_('Replace &All'))
783        ctrlLayout.addWidget(self.replaceAllButton)
784        self.replaceAllButton.clicked.connect(self.replaceAll)
785        closeButton = QPushButton(_('&Close'))
786        ctrlLayout.addWidget(closeButton)
787        closeButton.clicked.connect(self.close)
788
789        self.resultLabel = QLabel()
790        topLayout.addWidget(self.resultLabel, 4, 0, 1, 2)
791        self.loadTypeNames()
792        self.updateAvail()
793
794    def updateAvail(self):
795        """Set find & replace buttons available if search text & matches exist.
796        """
797        hasEntry = (len(self.textEntry.text().strip()) > 0 or
798                    self.howButtons.checkedId() == FindReplaceType.anyMatch)
799        self.previousButton.setEnabled(hasEntry)
800        self.nextButton.setEnabled(hasEntry)
801        match = bool(self.matchedSpot and self.matchedSpot is
802                     globalref.mainControl.activeControl.
803                     currentSelectionModel().currentSpot())
804        self.replaceButton.setEnabled(match)
805        self.replaceAllButton.setEnabled(match)
806        self.resultLabel.setText('')
807
808    def clearMatch(self):
809        """Remove reference to matched node if search criteria changes.
810        """
811        self.matchedSpot = None
812        globalref.mainControl.activeControl.findReplaceSpotRef = (None, 0)
813        self.updateAvail()
814
815    def loadTypeNames(self):
816        """Load format type names into combo box.
817        """
818        origTypeName = self.typeCombo.currentText()
819        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
820        self.typeCombo.blockSignals(True)
821        self.typeCombo.clear()
822        typeNames = nodeFormats.typeNames()
823        self.typeCombo.addItems([_('[All Types]')] + typeNames)
824        origPos = self.typeCombo.findText(origTypeName)
825        if origPos >= 0:
826            self.typeCombo.setCurrentIndex(origPos)
827        self.typeCombo.blockSignals(False)
828        self.loadFieldNames()
829
830    def loadFieldNames(self):
831        """Load field names into combo box.
832        """
833        origFieldName = self.fieldCombo.currentText()
834        nodeFormats = globalref.mainControl.activeControl.structure.treeFormats
835        typeName = self.typeCombo.currentText()
836        fieldNames = []
837        if typeName.startswith('['):
838            for typeName in nodeFormats.typeNames():
839                for fieldName in nodeFormats[typeName].fieldNames():
840                    if fieldName not in fieldNames:
841                        fieldNames.append(fieldName)
842        else:
843            fieldNames.extend(nodeFormats[typeName].fieldNames())
844        self.fieldCombo.clear()
845        self.fieldCombo.addItems([_('[All Fields]')] + fieldNames)
846        origPos = self.fieldCombo.findText(origFieldName)
847        if origPos >= 0:
848            self.fieldCombo.setCurrentIndex(origPos)
849        self.matchedSpot = None
850        self.updateAvail()
851
852    def findParameters(self):
853        """Create search parameters based on the dialog settings.
854
855        Return a tuple of searchText, regExpObj, typeName, and fieldName.
856        """
857        text = self.textEntry.text()
858        searchText = ''
859        regExpObj = None
860        if self.howButtons.checkedId() == FindReplaceType.anyMatch:
861            searchText = text.lower().strip()
862        elif self.howButtons.checkedId() == FindReplaceType.fullWord:
863            regExpObj = re.compile(r'(?i)\b{}\b'.format(re.escape(text)))
864        else:
865            regExpObj = re.compile(text)
866        typeName = self.typeCombo.currentText()
867        if typeName.startswith('['):
868            typeName = ''
869        fieldName = self.fieldCombo.currentText()
870        if fieldName.startswith('['):
871            fieldName = ''
872        return (searchText, regExpObj, typeName, fieldName)
873
874    def find(self, forward=True):
875        """Find another match in the indicated direction.
876
877        Arguments:
878            forward -- next if True, previous if False
879        """
880        self.matchedSpot = None
881        try:
882            searchText, regExpObj, typeName, fieldName = self.findParameters()
883        except re.error:
884            QMessageBox.warning(self, 'TreeLine',
885                                _('Error - invalid regular expression'))
886            self.updateAvail()
887            return
888        control = globalref.mainControl.activeControl
889        if control.findNodesForReplace(searchText, regExpObj, typeName,
890                                       fieldName, forward):
891            self.matchedSpot = control.currentSelectionModel().currentSpot()
892            self.updateAvail()
893        else:
894            self.updateAvail()
895            self.resultLabel.setText(_('Search text "{0}" not found').
896                                     format(self.textEntry.text()))
897
898    def findPrevious(self):
899        """Find the previous match.
900        """
901        self.find(False)
902
903    def findNext(self):
904        """Find the next match.
905        """
906        self.find(True)
907
908    def replace(self):
909        """Replace the currently found text.
910        """
911        searchText, regExpObj, typeName, fieldName = self.findParameters()
912        replaceText = self.replaceEntry.text()
913        control = globalref.mainControl.activeControl
914        if control.replaceInCurrentNode(searchText, regExpObj, typeName,
915                                        fieldName, replaceText):
916            self.find()
917        else:
918            QMessageBox.warning(self, 'TreeLine',
919                                      _('Error - replacement failed'))
920            self.matchedSpot = None
921            self.updateAvail()
922
923    def replaceAll(self):
924        """Replace all text matches.
925        """
926        searchText, regExpObj, typeName, fieldName = self.findParameters()
927        replaceText = self.replaceEntry.text()
928        control = globalref.mainControl.activeControl
929        qty = control.replaceAll(searchText, regExpObj, typeName, fieldName,
930                                 replaceText)
931        self.matchedSpot = None
932        self.updateAvail()
933        self.resultLabel.setText(_('Replaced {0} matches').format(qty))
934
935    def closeEvent(self, event):
936        """Signal that the dialog is closing.
937
938        Arguments:
939            event -- the close event
940        """
941        self.dialogShown.emit(False)
942
943
944SortWhat = enum.IntEnum('SortWhat',
945                        'fullTree selectBranch selectChildren selectSiblings')
946SortMethod = enum.IntEnum('SortMethod', 'fieldSort titleSort')
947SortDirection = enum.IntEnum('SortDirection', 'forward reverse')
948
949class SortDialog(QDialog):
950    """Dialog for defining sort operations.
951    """
952    dialogShown = pyqtSignal(bool)
953    def __init__(self, parent=None):
954        """Initialize the sort dialog.
955
956        Arguments:
957            parent -- the parent window
958        """
959        super().__init__(parent)
960        self.setAttribute(Qt.WA_QuitOnClose, False)
961        self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint)
962        self.setWindowTitle(_('Sort Nodes'))
963
964        topLayout = QVBoxLayout(self)
965        self.setLayout(topLayout)
966        horizLayout = QHBoxLayout()
967        topLayout.addLayout(horizLayout)
968        whatBox = QGroupBox(_('What to Sort'))
969        horizLayout.addWidget(whatBox)
970        whatLayout = QVBoxLayout(whatBox)
971        self.whatButtons = QButtonGroup(self)
972        button = QRadioButton(_('&Entire tree'))
973        self.whatButtons.addButton(button, SortWhat.fullTree)
974        whatLayout.addWidget(button)
975        button = QRadioButton(_('Selected &branches'))
976        self.whatButtons.addButton(button, SortWhat.selectBranch)
977        whatLayout.addWidget(button)
978        button = QRadioButton(_('Selection\'s childre&n'))
979        self.whatButtons.addButton(button, SortWhat.selectChildren)
980        whatLayout.addWidget(button)
981        button = QRadioButton(_('Selection\'s &siblings'))
982        self.whatButtons.addButton(button, SortWhat.selectSiblings)
983        whatLayout.addWidget(button)
984        self.whatButtons.button(SortWhat.fullTree).setChecked(True)
985
986        vertLayout =  QVBoxLayout()
987        horizLayout.addLayout(vertLayout)
988        methodBox = QGroupBox(_('Sort Method'))
989        vertLayout.addWidget(methodBox)
990        methodLayout = QVBoxLayout(methodBox)
991        self.methodButtons = QButtonGroup(self)
992        button = QRadioButton(_('&Predefined Key Fields'))
993        self.methodButtons.addButton(button, SortMethod.fieldSort)
994        methodLayout.addWidget(button)
995        button = QRadioButton(_('Node &Titles'))
996        self.methodButtons.addButton(button, SortMethod.titleSort)
997        methodLayout.addWidget(button)
998        self.methodButtons.button(SortMethod.fieldSort).setChecked(True)
999
1000        directionBox = QGroupBox(_('Sort Direction'))
1001        vertLayout.addWidget(directionBox)
1002        directionLayout =  QVBoxLayout(directionBox)
1003        self.directionButtons = QButtonGroup(self)
1004        button = QRadioButton(_('&Forward'))
1005        self.directionButtons.addButton(button, SortDirection.forward)
1006        directionLayout.addWidget(button)
1007        button = QRadioButton(_('&Reverse'))
1008        self.directionButtons.addButton(button, SortDirection.reverse)
1009        directionLayout.addWidget(button)
1010        self.directionButtons.button(SortDirection.forward).setChecked(True)
1011
1012        ctrlLayout = QHBoxLayout()
1013        topLayout.addLayout(ctrlLayout)
1014        ctrlLayout.addStretch()
1015        okButton = QPushButton(_('&OK'))
1016        ctrlLayout.addWidget(okButton)
1017        okButton.clicked.connect(self.sortAndClose)
1018        applyButton = QPushButton(_('&Apply'))
1019        ctrlLayout.addWidget(applyButton)
1020        applyButton.clicked.connect(self.sortNodes)
1021        closeButton = QPushButton(_('&Close'))
1022        ctrlLayout.addWidget(closeButton)
1023        closeButton.clicked.connect(self.close)
1024        self.updateCommandsAvail()
1025
1026    def updateCommandsAvail(self):
1027        """Set what to sort options available based on tree selections.
1028        """
1029        selModel = globalref.mainControl.activeControl.currentSelectionModel()
1030        hasChild = False
1031        hasSibling = False
1032        for spot in selModel.selectedSpots():
1033            if spot.nodeRef.childList:
1034                hasChild = True
1035            if spot.parentSpot and len(spot.parentSpot.nodeRef.childList) > 1:
1036                hasSibling = True
1037        self.whatButtons.button(SortWhat.selectBranch).setEnabled(hasChild)
1038        self.whatButtons.button(SortWhat.selectChildren).setEnabled(hasChild)
1039        self.whatButtons.button(SortWhat.selectSiblings).setEnabled(hasSibling)
1040        if not self.whatButtons.checkedButton().isEnabled():
1041            self.whatButtons.button(SortWhat.fullTree).setChecked(True)
1042
1043    def sortNodes(self):
1044        """Perform the sorting operation.
1045        """
1046        QApplication.setOverrideCursor(Qt.WaitCursor)
1047        control = globalref.mainControl.activeControl
1048        selSpots = control.currentSelectionModel().selectedSpots()
1049        if self.whatButtons.checkedId() == SortWhat.fullTree:
1050            selSpots = [control.structure.spotByNumber(0)]
1051        elif self.whatButtons.checkedId() == SortWhat.selectSiblings:
1052            selSpots = [spot.parentSpot for spot in selSpots]
1053        if self.whatButtons.checkedId() in (SortWhat.fullTree,
1054                                            SortWhat.selectBranch):
1055            rootSpots = selSpots[:]
1056            selSpots = []
1057            for root in rootSpots:
1058                for spot in root.spotDescendantGen():
1059                    if spot.nodeRef.childList:
1060                        selSpots.append(spot)
1061        undo.ChildListUndo(control.structure.undoList,
1062                           [spot.nodeRef for spot in selSpots])
1063        forward = self.directionButtons.checkedId() == SortDirection.forward
1064        if self.methodButtons.checkedId() == SortMethod.fieldSort:
1065            for spot in selSpots:
1066                spot.nodeRef.sortChildrenByField(False, forward)
1067            # reset temporary sort field storage
1068            for nodeFormat in control.structure.treeFormats.values():
1069                nodeFormat.sortFields = []
1070        else:
1071            for spot in selSpots:
1072                spot.nodeRef.sortChildrenByTitle(False, forward)
1073        control.updateAll()
1074        QApplication.restoreOverrideCursor()
1075
1076    def sortAndClose(self):
1077        """Perform the sorting operation and close the dialog.
1078        """
1079        self.sortNodes()
1080        self.close()
1081
1082    def closeEvent(self, event):
1083        """Signal that the dialog is closing.
1084
1085        Arguments:
1086            event -- the close event
1087        """
1088        self.dialogShown.emit(False)
1089
1090
1091NumberingScope = enum.IntEnum('NumberingScope',
1092                              'fullTree selectBranch selectChildren')
1093NumberingNoField = enum.IntEnum('NumberingNoField',
1094                            'ignoreNoField restartAfterNoField reserveNoField')
1095
1096class NumberingDialog(QDialog):
1097    """Dialog for updating node nuumbering fields.
1098    """
1099    dialogShown = pyqtSignal(bool)
1100    def __init__(self, parent=None):
1101        """Initialize the numbering dialog.
1102
1103        Arguments:
1104            parent -- the parent window
1105        """
1106        super().__init__(parent)
1107        self.setAttribute(Qt.WA_QuitOnClose, False)
1108        self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint)
1109        self.setWindowTitle(_('Update Node Numbering'))
1110
1111        topLayout = QVBoxLayout(self)
1112        self.setLayout(topLayout)
1113        whatBox = QGroupBox(_('What to Update'))
1114        topLayout.addWidget(whatBox)
1115        whatLayout = QVBoxLayout(whatBox)
1116        self.whatButtons = QButtonGroup(self)
1117        button = QRadioButton(_('&Entire tree'))
1118        self.whatButtons.addButton(button, NumberingScope.fullTree)
1119        whatLayout.addWidget(button)
1120        button = QRadioButton(_('Selected &branches'))
1121        self.whatButtons.addButton(button, NumberingScope.selectBranch)
1122        whatLayout.addWidget(button)
1123        button = QRadioButton(_('&Selection\'s children'))
1124        self.whatButtons.addButton(button, NumberingScope.selectChildren)
1125        whatLayout.addWidget(button)
1126        self.whatButtons.button(NumberingScope.fullTree).setChecked(True)
1127
1128        rootBox = QGroupBox(_('Root Node'))
1129        topLayout.addWidget(rootBox)
1130        rootLayout = QVBoxLayout(rootBox)
1131        self.rootCheck = QCheckBox(_('Include top-level nodes'))
1132        rootLayout.addWidget(self.rootCheck)
1133        self.rootCheck.setChecked(True)
1134
1135        noFieldBox = QGroupBox(_('Handling Nodes without Numbering '
1136                                       'Fields'))
1137        topLayout.addWidget(noFieldBox)
1138        noFieldLayout =  QVBoxLayout(noFieldBox)
1139        self.noFieldButtons = QButtonGroup(self)
1140        button = QRadioButton(_('&Ignore and skip'))
1141        self.noFieldButtons.addButton(button, NumberingNoField.ignoreNoField)
1142        noFieldLayout.addWidget(button)
1143        button = QRadioButton(_('&Restart numbers for next siblings'))
1144        self.noFieldButtons.addButton(button,
1145                                      NumberingNoField.restartAfterNoField)
1146        noFieldLayout.addWidget(button)
1147        button = QRadioButton(_('Reserve &numbers'))
1148        self.noFieldButtons.addButton(button, NumberingNoField.reserveNoField)
1149        noFieldLayout.addWidget(button)
1150        self.noFieldButtons.button(NumberingNoField.
1151                                   ignoreNoField).setChecked(True)
1152
1153        ctrlLayout = QHBoxLayout()
1154        topLayout.addLayout(ctrlLayout)
1155        ctrlLayout.addStretch()
1156        okButton = QPushButton(_('&OK'))
1157        ctrlLayout.addWidget(okButton)
1158        okButton.clicked.connect(self.numberAndClose)
1159        applyButton = QPushButton(_('&Apply'))
1160        ctrlLayout.addWidget(applyButton)
1161        applyButton.clicked.connect(self.updateNumbering)
1162        closeButton = QPushButton(_('&Close'))
1163        ctrlLayout.addWidget(closeButton)
1164        closeButton.clicked.connect(self.close)
1165        self.updateCommandsAvail()
1166
1167    def updateCommandsAvail(self):
1168        """Set branch numbering available based on tree selections.
1169        """
1170        selNodes = globalref.mainControl.activeControl.currentSelectionModel()
1171        hasChild = False
1172        for node in selNodes.selectedNodes():
1173            if node.childList:
1174                hasChild = True
1175        self.whatButtons.button(NumberingScope.
1176                                selectChildren).setEnabled(hasChild)
1177        if not self.whatButtons.checkedButton().isEnabled():
1178            self.whatButtons.button(NumberingScope.fullTree).setChecked(True)
1179
1180    def checkForNumberingFields(self):
1181        """Check that the tree formats have numbering formats.
1182
1183        Return a dict of numbering field names by node format name.
1184        If not found, warn user.
1185        """
1186        fieldDict = (globalref.mainControl.activeControl.structure.treeFormats.
1187                     numberingFieldDict())
1188        if not fieldDict:
1189            QMessageBox.warning(self, _('TreeLine Numbering'),
1190                             _('No numbering fields were found in data types'))
1191        return fieldDict
1192
1193    def updateNumbering(self):
1194        """Perform the numbering update operation.
1195        """
1196        QApplication.setOverrideCursor(Qt.WaitCursor)
1197        fieldDict = self.checkForNumberingFields()
1198        if fieldDict:
1199            control = globalref.mainControl.activeControl
1200            selNodes = control.currentSelectionModel().selectedNodes()
1201            if (self.whatButtons.checkedId() == NumberingScope.fullTree or
1202                len(selNodes) == 0):
1203                selNodes = control.structure.childList
1204            undo.DataUndo(control.structure.undoList, selNodes, addBranch=True)
1205            reserveNums = (self.noFieldButtons.checkedId() ==
1206                           NumberingNoField.reserveNoField)
1207            restartSetting = (self.noFieldButtons.checkedId() ==
1208                              NumberingNoField.restartAfterNoField)
1209            includeRoot = self.rootCheck.isChecked()
1210            if self.whatButtons.checkedId() == NumberingScope.selectChildren:
1211                levelLimit = 2
1212            else:
1213                levelLimit = sys.maxsize
1214            startNum = [1]
1215            completedClones = set()
1216            for node in selNodes:
1217                node.updateNumbering(fieldDict, startNum, levelLimit,
1218                                     completedClones, includeRoot,
1219                                     reserveNums, restartSetting)
1220            control.updateAll()
1221        QApplication.restoreOverrideCursor()
1222
1223    def numberAndClose(self):
1224        """Perform the numbering update operation and close the dialog.
1225        """
1226        self.updateNumbering()
1227        self.close()
1228
1229    def closeEvent(self, event):
1230        """Signal that the dialog is closing.
1231
1232        Arguments:
1233            event -- the close event
1234        """
1235        self.dialogShown.emit(False)
1236
1237
1238menuNames = collections.OrderedDict([(N_('File Menu'), _('File')),
1239                                     (N_('Edit Menu'), _('Edit')),
1240                                     (N_('Node Menu'), _('Node')),
1241                                     (N_('Data Menu'), _('Data')),
1242                                     (N_('Tools Menu'), _('Tools')),
1243                                     (N_('Format Menu'), _('Format')),
1244                                     (N_('View Menu'), _('View')),
1245                                     (N_('Window Menu'), _('Window')),
1246                                     (N_('Help Menu'), _('Help'))])
1247
1248class CustomShortcutsDialog(QDialog):
1249    """Dialog for customizing keyboard commands.
1250    """
1251    def __init__(self, allActions, parent=None):
1252        """Create a shortcuts selection dialog.
1253
1254        Arguments:
1255            allActions -- dict of all actions from a window
1256            parent -- the parent window
1257        """
1258        super().__init__(parent)
1259        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
1260                            Qt.WindowCloseButtonHint)
1261        self.setWindowTitle(_('Keyboard Shortcuts'))
1262        topLayout = QVBoxLayout(self)
1263        self.setLayout(topLayout)
1264        scrollArea = QScrollArea()
1265        topLayout.addWidget(scrollArea)
1266        viewport = QWidget()
1267        viewLayout = QGridLayout(viewport)
1268        scrollArea.setWidget(viewport)
1269        scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
1270        scrollArea.setWidgetResizable(True)
1271
1272        self.editors = []
1273        for i, keyOption in enumerate(globalref.keyboardOptions.values()):
1274            category = menuNames.get(keyOption.category, _('No menu'))
1275            try:
1276                action = allActions[keyOption.name]
1277            except KeyError:
1278                pass
1279            else:
1280                text = '{0} > {1}'.format(category, action.toolTip())
1281                label = QLabel(text)
1282                viewLayout.addWidget(label, i, 0)
1283                editor = KeyLineEdit(keyOption, action, self)
1284                viewLayout.addWidget(editor, i, 1)
1285                self.editors.append(editor)
1286
1287        ctrlLayout = QHBoxLayout()
1288        topLayout.addLayout(ctrlLayout)
1289        restoreButton = QPushButton(_('&Restore Defaults'))
1290        ctrlLayout.addWidget(restoreButton)
1291        restoreButton.clicked.connect(self.restoreDefaults)
1292        ctrlLayout.addStretch(0)
1293        self.okButton = QPushButton(_('&OK'))
1294        ctrlLayout.addWidget(self.okButton)
1295        self.okButton.clicked.connect(self.accept)
1296        cancelButton = QPushButton(_('&Cancel'))
1297        ctrlLayout.addWidget(cancelButton)
1298        cancelButton.clicked.connect(self.reject)
1299        self.editors[0].setFocus()
1300
1301    def restoreDefaults(self):
1302        """Restore all default keyboard shortcuts.
1303        """
1304        for editor in self.editors:
1305            editor.loadDefaultKey()
1306
1307    def accept(self):
1308        """Save any changes to options and actions before closing.
1309        """
1310        modified = False
1311        for editor in self.editors:
1312            if editor.modified:
1313                editor.saveChange()
1314                modified = True
1315        if modified:
1316            globalref.keyboardOptions.writeFile()
1317        super().accept()
1318
1319
1320class KeyLineEdit(QLineEdit):
1321    """Line editor for keyboad sequence entry.
1322    """
1323    usedKeySet = set()
1324    blankText = ' ' * 8
1325    def __init__(self, keyOption, action, parent=None):
1326        """Create a key editor.
1327
1328        Arguments:
1329            keyOption -- the KeyOptionItem for this editor
1330            action -- the action to update on changes
1331            parent -- the parent dialog
1332        """
1333        super().__init__(parent)
1334        self.keyOption = keyOption
1335        self.keyAction = action
1336        self.key = None
1337        self.modified = False
1338        self.setReadOnly(True)
1339        self.loadKey()
1340
1341    def loadKey(self):
1342        """Load the initial key shortcut from the option.
1343        """
1344        key = self.keyOption.value
1345        if key:
1346            self.setKey(key)
1347        else:
1348            self.setText(KeyLineEdit.blankText)
1349
1350    def loadDefaultKey(self):
1351        """Change to the default key shortcut from the option.
1352
1353        Arguments:
1354            useDefault -- if True, load the default key
1355        """
1356        key = self.keyOption.defaultValue
1357        if key == self.key:
1358            return
1359        if key:
1360            self.setKey(key)
1361            self.modified = True
1362        else:
1363            self.clearKey(False)
1364
1365    def setKey(self, key):
1366        """Set this editor to the given key and add to the used key set.
1367
1368        Arguments:
1369            key - the QKeySequence to add
1370        """
1371        keyText = key.toString(QKeySequence.NativeText)
1372        self.setText(keyText)
1373        self.key = key
1374        KeyLineEdit.usedKeySet.add(keyText)
1375
1376    def clearKey(self, staySelected=True):
1377        """Remove any existing key.
1378        """
1379        self.setText(KeyLineEdit.blankText)
1380        if staySelected:
1381            self.selectAll()
1382        if self.key:
1383            KeyLineEdit.usedKeySet.remove(self.key.toString(QKeySequence.
1384                                                            NativeText))
1385            self.key = None
1386            self.modified = True
1387
1388    def saveChange(self):
1389        """Save any change to the option and action.
1390        """
1391        if self.modified:
1392            self.keyOption.setValue(self.key)
1393            if self.key:
1394                self.keyAction.setShortcut(self.key)
1395            else:
1396                self.keyAction.setShortcut(QKeySequence())
1397
1398    def keyPressEvent(self, event):
1399        """Capture key strokes and update the editor if valid.
1400
1401        Arguments:
1402            event -- the key press event
1403        """
1404        if event.key() in (Qt.Key_Shift, Qt.Key_Control,
1405                           Qt.Key_Meta, Qt.Key_Alt,
1406                           Qt.Key_AltGr, Qt.Key_CapsLock,
1407                           Qt.Key_NumLock, Qt.Key_ScrollLock,
1408                           Qt.Key_Pause, Qt.Key_Print,
1409                           Qt.Key_Cancel):
1410            event.ignore()
1411        elif event.key() in (Qt.Key_Backspace, Qt.Key_Escape):
1412            self.clearKey()
1413            event.accept()
1414        else:
1415            modifier = event.modifiers()
1416            if modifier & Qt.KeypadModifier:
1417                modifier = modifier ^ Qt.KeypadModifier
1418            key = QKeySequence(event.key() + int(modifier))
1419            if key != self.key:
1420                keyText = key.toString(QKeySequence.NativeText)
1421                if keyText not in KeyLineEdit.usedKeySet:
1422                    if self.key:
1423                        KeyLineEdit.usedKeySet.remove(self.key.
1424                                                   toString(QKeySequence.
1425                                                            NativeText))
1426                    self.setKey(key)
1427                    self.selectAll()
1428                    self.modified = True
1429                else:
1430                    text = _('Key {0} is already used').format(keyText)
1431                    QMessageBox.warning(self.parent(), 'TreeLine', text)
1432            event.accept()
1433
1434    def contextMenuEvent(self, event):
1435        """Change to a context menu with a clear command.
1436
1437        Arguments:
1438            event -- the menu event
1439        """
1440        menu = QMenu(self)
1441        menu.addAction(_('Clear &Key'), self.clearKey)
1442        menu.exec_(event.globalPos())
1443
1444    def mousePressEvent(self, event):
1445        """Capture mouse clicks to avoid selection loss.
1446
1447        Arguments:
1448            event -- the mouse event
1449        """
1450        event.accept()
1451
1452    def mouseReleaseEvent(self, event):
1453        """Capture mouse clicks to avoid selection loss.
1454
1455        Arguments:
1456            event -- the mouse event
1457        """
1458        event.accept()
1459
1460    def mouseMoveEvent(self, event):
1461        """Capture mouse clicks to avoid selection loss.
1462
1463        Arguments:
1464            event -- the mouse event
1465        """
1466        event.accept()
1467
1468    def mouseDoubleClickEvent(self, event):
1469        """Capture mouse clicks to avoid selection loss.
1470
1471        Arguments:
1472            event -- the mouse event
1473        """
1474        event.accept()
1475
1476    def focusInEvent(self, event):
1477        """Select contents when focussed.
1478
1479        Arguments:
1480            event -- the focus event
1481        """
1482        self.selectAll()
1483        super().focusInEvent(event)
1484
1485
1486class CustomToolbarDialog(QDialog):
1487    """Dialog for customizing toolbar buttons.
1488    """
1489    separatorString = _('--Separator--')
1490    def __init__(self, allActions, updateFunction, parent=None):
1491        """Create a toolbar buttons customization dialog.
1492
1493        Arguments:
1494            allActions -- dict of all actions from a window
1495            updateFunction -- a function ref for updating window toolbars
1496            parent -- the parent window
1497        """
1498        super().__init__(parent)
1499        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
1500                            Qt.WindowCloseButtonHint)
1501        self.setWindowTitle(_('Customize Toolbars'))
1502        self.allActions = allActions
1503        self.updateFunction = updateFunction
1504        self.availableCommands = []
1505        self.modified = False
1506        self.numToolbars = 0
1507        self.availableCommands = []
1508        self.toolbarLists = []
1509
1510        topLayout = QVBoxLayout(self)
1511        self.setLayout(topLayout)
1512        gridLayout = QGridLayout()
1513        topLayout.addLayout(gridLayout)
1514
1515        sizeBox = QGroupBox(_('Toolbar &Size'))
1516        gridLayout.addWidget(sizeBox, 0, 0, 1, 2)
1517        sizeLayout = QVBoxLayout(sizeBox)
1518        self.sizeCombo = QComboBox()
1519        sizeLayout.addWidget(self.sizeCombo)
1520        self.sizeCombo.addItems([_('Small Icons'), _('Large Icons')])
1521        self.sizeCombo.currentIndexChanged.connect(self.setModified)
1522
1523        numberBox = QGroupBox(_('Toolbar Quantity'))
1524        gridLayout.addWidget(numberBox, 0, 2)
1525        numberLayout = QHBoxLayout(numberBox)
1526        self.quantitySpin = QSpinBox()
1527        numberLayout.addWidget(self.quantitySpin)
1528        self.quantitySpin.setRange(0, 20)
1529        numberlabel = QLabel(_('&Toolbars'))
1530        numberLayout.addWidget(numberlabel)
1531        numberlabel.setBuddy(self.quantitySpin)
1532        self.quantitySpin.valueChanged.connect(self.changeQuantity)
1533
1534        availableBox = QGroupBox(_('A&vailable Commands'))
1535        gridLayout.addWidget(availableBox, 1, 0)
1536        availableLayout = QVBoxLayout(availableBox)
1537        menuCombo = QComboBox()
1538        availableLayout.addWidget(menuCombo)
1539        menuCombo.addItems([_(name) for name in menuNames.keys()])
1540        menuCombo.currentIndexChanged.connect(self.updateAvailableCommands)
1541
1542        self.availableListWidget = QListWidget()
1543        availableLayout.addWidget(self.availableListWidget)
1544
1545        buttonLayout = QVBoxLayout()
1546        gridLayout.addLayout(buttonLayout, 1, 1)
1547        self.addButton = QPushButton('>>')
1548        buttonLayout.addWidget(self.addButton)
1549        self.addButton.setMaximumWidth(self.addButton.sizeHint().height())
1550        self.addButton.clicked.connect(self.addTool)
1551
1552        self.removeButton = QPushButton('<<')
1553        buttonLayout.addWidget(self.removeButton)
1554        self.removeButton.setMaximumWidth(self.removeButton.sizeHint().
1555                                          height())
1556        self.removeButton.clicked.connect(self.removeTool)
1557
1558        toolbarBox = QGroupBox(_('Tool&bar Commands'))
1559        gridLayout.addWidget(toolbarBox, 1, 2)
1560        toolbarLayout = QVBoxLayout(toolbarBox)
1561        self.toolbarCombo = QComboBox()
1562        toolbarLayout.addWidget(self.toolbarCombo)
1563        self.toolbarCombo.currentIndexChanged.connect(self.
1564                                                      updateToolbarCommands)
1565
1566        self.toolbarListWidget = QListWidget()
1567        toolbarLayout.addWidget(self.toolbarListWidget)
1568        self.toolbarListWidget.currentRowChanged.connect(self.
1569                                                         setButtonsAvailable)
1570
1571        moveLayout = QHBoxLayout()
1572        toolbarLayout.addLayout(moveLayout)
1573        self.moveUpButton = QPushButton(_('Move &Up'))
1574        moveLayout.addWidget(self.moveUpButton)
1575        self.moveUpButton.clicked.connect(self.moveUp)
1576        self.moveDownButton = QPushButton(_('Move &Down'))
1577        moveLayout.addWidget(self.moveDownButton)
1578        self.moveDownButton.clicked.connect(self.moveDown)
1579
1580        ctrlLayout = QHBoxLayout()
1581        topLayout.addLayout(ctrlLayout)
1582        restoreButton = QPushButton(_('&Restore Defaults'))
1583        ctrlLayout.addWidget(restoreButton)
1584        restoreButton.clicked.connect(self.restoreDefaults)
1585        ctrlLayout.addStretch()
1586        self.okButton = QPushButton(_('&OK'))
1587        ctrlLayout.addWidget(self.okButton)
1588        self.okButton.clicked.connect(self.accept)
1589        self.applyButton = QPushButton(_('&Apply'))
1590        ctrlLayout.addWidget(self.applyButton)
1591        self.applyButton.clicked.connect(self.applyChanges)
1592        self.applyButton.setEnabled(False)
1593        cancelButton = QPushButton(_('&Cancel'))
1594        ctrlLayout.addWidget(cancelButton)
1595        cancelButton.clicked.connect(self.reject)
1596
1597        self.updateAvailableCommands(0)
1598        self.loadToolbars()
1599
1600    def setModified(self):
1601        """Set modified flag and make apply button available.
1602        """
1603        self.modified = True
1604        self.applyButton.setEnabled(True)
1605
1606    def setButtonsAvailable(self):
1607        """Enable or disable buttons based on toolbar list state.
1608        """
1609        toolbarNum = numCommands = commandNum = 0
1610        if self.numToolbars:
1611            toolbarNum = self.toolbarCombo.currentIndex()
1612            numCommands = len(self.toolbarLists[toolbarNum])
1613            if self.toolbarLists[toolbarNum]:
1614                commandNum = self.toolbarListWidget.currentRow()
1615        self.addButton.setEnabled(self.numToolbars > 0)
1616        self.removeButton.setEnabled(self.numToolbars and numCommands)
1617        self.moveUpButton.setEnabled(self.numToolbars and numCommands > 1 and
1618                                     commandNum > 0)
1619        self.moveDownButton.setEnabled(self.numToolbars and numCommands > 1 and
1620                                       commandNum < numCommands - 1)
1621
1622    def loadToolbars(self, defaultOnly=False):
1623        """Load all toolbar data from options.
1624
1625        Arguments:
1626            defaultOnly -- if True, load default settings
1627        """
1628        size = (globalref.toolbarOptions['ToolbarSize'] if not defaultOnly else
1629                globalref.toolbarOptions.getDefaultValue('ToolbarSize'))
1630        self.sizeCombo.blockSignals(True)
1631        if size < 24:
1632            self.sizeCombo.setCurrentIndex(0)
1633        else:
1634            self.sizeCombo.setCurrentIndex(1)
1635        self.sizeCombo.blockSignals(False)
1636        self.numToolbars = (globalref.toolbarOptions['ToolbarQuantity'] if not
1637                            defaultOnly else globalref.toolbarOptions.
1638                            getDefaultValue('ToolbarQuantity'))
1639        self.quantitySpin.blockSignals(True)
1640        self.quantitySpin.setValue(self.numToolbars)
1641        self.quantitySpin.blockSignals(False)
1642        self.toolbarLists = []
1643        commands = (globalref.toolbarOptions['ToolbarCommands'] if not
1644                    defaultOnly else globalref.toolbarOptions.
1645                    getDefaultValue('ToolbarCommands'))
1646        self.toolbarLists = [cmd.split(',') for cmd in commands]
1647        # account for toolbar quantity mismatch (should not happen)
1648        del self.toolbarLists[self.numToolbars:]
1649        while len(self.toolbarLists) < self.numToolbars:
1650            self.toolbarLists.append([])
1651        self.updateToolbarCombo()
1652
1653    def updateToolbarCombo(self):
1654        """Fill combo with toolbar numbers for current quantity.
1655        """
1656        self.toolbarCombo.clear()
1657        if self.numToolbars:
1658            self.toolbarCombo.addItems(['Toolbar {0}'.format(num + 1) for
1659                                        num in range(self.numToolbars)])
1660        else:
1661            self.toolbarListWidget.clear()
1662            self.setButtonsAvailable()
1663
1664    def updateAvailableCommands(self, menuNum):
1665        """Fill in available command list for given menu.
1666
1667        Arguments:
1668            menuNum -- the index of the current menu selected
1669        """
1670        menuName = list(menuNames.keys())[menuNum]
1671        self.availableCommands = []
1672        self.availableListWidget.clear()
1673        for option in globalref.keyboardOptions.values():
1674            if option.category == menuName:
1675                action = self.allActions[option.name]
1676                icon = action.icon()
1677                if not icon.isNull():
1678                    self.availableCommands.append(option.name)
1679                    QListWidgetItem(icon, action.toolTip(),
1680                                          self.availableListWidget)
1681        QListWidgetItem(CustomToolbarDialog.separatorString,
1682                              self.availableListWidget)
1683        self.availableListWidget.setCurrentRow(0)
1684
1685    def updateToolbarCommands(self, toolbarNum):
1686        """Fill in toolbar commands for given toolbar.
1687
1688        Arguments:
1689            toolbarNum -- the number of the toolbar to update
1690        """
1691        self.toolbarListWidget.clear()
1692        if self.numToolbars == 0:
1693            return
1694        for command in self.toolbarLists[toolbarNum]:
1695            if command:
1696                action = self.allActions[command]
1697                QListWidgetItem(action.icon(), action.toolTip(),
1698                                      self.toolbarListWidget)
1699            else:  # separator
1700                QListWidgetItem(CustomToolbarDialog.separatorString,
1701                                      self.toolbarListWidget)
1702        if self.toolbarLists[toolbarNum]:
1703            self.toolbarListWidget.setCurrentRow(0)
1704        self.setButtonsAvailable()
1705
1706    def changeQuantity(self, qty):
1707        """Change the toolbar quantity based on a spin box signal.
1708
1709        Arguments:
1710            qty -- the new toolbar quantity
1711        """
1712        self.numToolbars = qty
1713        while qty > len(self.toolbarLists):
1714            self.toolbarLists.append([])
1715        self.updateToolbarCombo()
1716        self.setModified()
1717
1718    def addTool(self):
1719        """Add the selected command to the current toolbar.
1720        """
1721        toolbarNum = self.toolbarCombo.currentIndex()
1722        try:
1723            command = self.availableCommands[self.availableListWidget.
1724                                             currentRow()]
1725            action = self.allActions[command]
1726            item = QListWidgetItem(action.icon(), action.toolTip())
1727        except IndexError:
1728            command = ''
1729            item = QListWidgetItem(CustomToolbarDialog.separatorString)
1730        if self.toolbarLists[toolbarNum]:
1731            pos = self.toolbarListWidget.currentRow() + 1
1732        else:
1733            pos = 0
1734        self.toolbarLists[toolbarNum].insert(pos, command)
1735        self.toolbarListWidget.insertItem(pos, item)
1736        self.toolbarListWidget.setCurrentRow(pos)
1737        self.toolbarListWidget.scrollToItem(item)
1738        self.setModified()
1739
1740    def removeTool(self):
1741        """Remove the selected command from the current toolbar.
1742        """
1743        toolbarNum = self.toolbarCombo.currentIndex()
1744        pos = self.toolbarListWidget.currentRow()
1745        del self.toolbarLists[toolbarNum][pos]
1746        self.toolbarListWidget.takeItem(pos)
1747        if self.toolbarLists[toolbarNum]:
1748            if pos == len(self.toolbarLists[toolbarNum]):
1749                pos -= 1
1750            self.toolbarListWidget.setCurrentRow(pos)
1751        self.setModified()
1752
1753    def moveUp(self):
1754        """Raise the selected command.
1755        """
1756        toolbarNum = self.toolbarCombo.currentIndex()
1757        pos = self.toolbarListWidget.currentRow()
1758        command = self.toolbarLists[toolbarNum].pop(pos)
1759        self.toolbarLists[toolbarNum].insert(pos - 1, command)
1760        item = self.toolbarListWidget.takeItem(pos)
1761        self.toolbarListWidget.insertItem(pos - 1, item)
1762        self.toolbarListWidget.setCurrentRow(pos - 1)
1763        self.toolbarListWidget.scrollToItem(item)
1764        self.setModified()
1765
1766    def moveDown(self):
1767        """Lower the selected command.
1768        """
1769        toolbarNum = self.toolbarCombo.currentIndex()
1770        pos = self.toolbarListWidget.currentRow()
1771        command = self.toolbarLists[toolbarNum].pop(pos)
1772        self.toolbarLists[toolbarNum].insert(pos + 1, command)
1773        item = self.toolbarListWidget.takeItem(pos)
1774        self.toolbarListWidget.insertItem(pos + 1, item)
1775        self.toolbarListWidget.setCurrentRow(pos + 1)
1776        self.toolbarListWidget.scrollToItem(item)
1777        self.setModified()
1778
1779    def restoreDefaults(self):
1780        """Restore all default toolbar settings.
1781        """
1782        self.loadToolbars(True)
1783        self.setModified()
1784
1785    def applyChanges(self):
1786        """Apply any changes from the dialog.
1787        """
1788        size = 16 if self.sizeCombo.currentIndex() == 0 else 32
1789        globalref.toolbarOptions.changeValue('ToolbarSize', size)
1790        globalref.toolbarOptions.changeValue('ToolbarQuantity',
1791                                             self.numToolbars)
1792        del self.toolbarLists[self.numToolbars:]
1793        commands = [','.join(cmds) for cmds in self.toolbarLists]
1794        globalref.toolbarOptions.changeValue('ToolbarCommands', commands)
1795        globalref.toolbarOptions.writeFile()
1796        self.modified = False
1797        self.applyButton.setEnabled(False)
1798        self.updateFunction()
1799
1800    def accept(self):
1801        """Apply changes and close the dialog.
1802        """
1803        if self.modified:
1804            self.applyChanges()
1805        super().accept()
1806
1807
1808class CustomFontData:
1809    """Class to store custom font settings.
1810
1811    Acts as a stand-in for PrintData class in the font page of the dialog.
1812    """
1813    def __init__(self, fontOption, useAppDefault=True):
1814        """Initialize the font data.
1815
1816        Arguments:
1817            fontOption -- the name of the font setting to retrieve
1818            useAppDefault -- use app default if true, o/w use sys default
1819        """
1820        self.fontOption = fontOption
1821        if useAppDefault:
1822            self.defaultFont = QTextDocument().defaultFont()
1823        else:
1824            self.defaultFont = QFont(globalref.mainControl.systemFont)
1825        self.useDefaultFont = True
1826        self.mainFont = QFont(self.defaultFont)
1827        fontName = globalref.miscOptions[self.fontOption]
1828        if fontName:
1829            self.mainFont.fromString(fontName)
1830            self.useDefaultFont = False
1831
1832    def recordChanges(self):
1833        """Record the updated font info to the option settings.
1834        """
1835        if self.useDefaultFont:
1836            globalref.miscOptions.changeValue(self.fontOption, '')
1837        else:
1838            globalref.miscOptions.changeValue(self.fontOption,
1839                                              self.mainFont.toString())
1840
1841
1842class CustomFontDialog(QDialog):
1843    """Dialog for selecting custom fonts.
1844
1845    Uses the print setup dialog's font page for the details.
1846    """
1847    updateRequired = pyqtSignal()
1848    def __init__(self, parent=None):
1849        """Create a font customization dialog.
1850
1851        Arguments:
1852            parent -- the parent window
1853        """
1854        super().__init__(parent)
1855        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
1856                            Qt.WindowCloseButtonHint)
1857        self.setWindowTitle(_('Customize Fonts'))
1858
1859        topLayout = QVBoxLayout(self)
1860        self.setLayout(topLayout)
1861        self.tabs = QTabWidget()
1862        topLayout.addWidget(self.tabs)
1863        self.tabs.setUsesScrollButtons(False)
1864        self.tabs.currentChanged.connect(self.updateTabDefault)
1865
1866        self.pages = []
1867        defaultLabel = _('&Use system default font')
1868        appFontPage = printdialogs.FontPage(CustomFontData('AppFont', False),
1869                                            defaultLabel)
1870        self.pages.append(appFontPage)
1871        self.tabs.addTab(appFontPage, _('App Default Font'))
1872        defaultLabel = _('&Use app default font')
1873        treeFontPage = printdialogs.FontPage(CustomFontData('TreeFont'),
1874                                             defaultLabel)
1875        self.pages.append(treeFontPage)
1876        self.tabs.addTab(treeFontPage, _('Tree View Font'))
1877        outputFontPage = printdialogs.FontPage(CustomFontData('OutputFont'),
1878                                               defaultLabel)
1879        self.pages.append(outputFontPage)
1880        self.tabs.addTab(outputFontPage, _('Output View Font'))
1881        editorFontPage = printdialogs.FontPage(CustomFontData('EditorFont'),
1882                                               defaultLabel)
1883        self.pages.append(editorFontPage)
1884        self.tabs.addTab(editorFontPage, _('Editor View Font'))
1885
1886        ctrlLayout = QHBoxLayout()
1887        topLayout.addLayout(ctrlLayout)
1888        ctrlLayout.addStretch()
1889        self.okButton = QPushButton(_('&OK'))
1890        ctrlLayout.addWidget(self.okButton)
1891        self.okButton.clicked.connect(self.accept)
1892        self.applyButton = QPushButton(_('&Apply'))
1893        ctrlLayout.addWidget(self.applyButton)
1894        self.applyButton.clicked.connect(self.applyChanges)
1895        cancelButton = QPushButton(_('&Cancel'))
1896        ctrlLayout.addWidget(cancelButton)
1897        cancelButton.clicked.connect(self.reject)
1898
1899    def updateTabDefault(self):
1900        """Update the default font on the newly shown page.
1901        """
1902        appFontWidget = self.tabs.widget(0)
1903        currentWidget = self.tabs.currentWidget()
1904        if appFontWidget is not currentWidget:
1905            if appFontWidget.defaultCheck.isChecked():
1906                defaultFont = QFont(globalref.mainControl.systemFont)
1907            else:
1908                defaultFont = appFontWidget.readFont()
1909            if defaultFont:
1910                currentWidget.printData.defaultFont = defaultFont
1911                if currentWidget.defaultCheck.isChecked():
1912                    currentWidget.printData.mainFont = QFont(defaultFont)
1913                    currentWidget.currentFont = (currentWidget.printData.
1914                                                 mainFont)
1915                    currentWidget.setFont(defaultFont)
1916
1917    def applyChanges(self):
1918        """Apply any changes from the dialog.
1919        """
1920        modified = False
1921        for page in self.pages:
1922            if page.saveChanges():
1923                page.printData.recordChanges()
1924                modified = True
1925        if modified:
1926            globalref.miscOptions.writeFile()
1927            self.updateRequired.emit()
1928
1929    def accept(self):
1930        """Apply changes and close the dialog.
1931        """
1932        self.applyChanges()
1933        super().accept()
1934
1935
1936class AboutDialog(QDialog):
1937    """Show program info in a text box.
1938    """
1939    def __init__(self, title, textLines, icon=None, parent=None):
1940        """Create the dialog.
1941
1942        Arguments:
1943            title -- the window title text
1944            textLines -- a list of lines to show
1945            icon -- an icon to show if given
1946            parent -- the parent window
1947        """
1948        super().__init__(parent)
1949        self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint |
1950                            Qt.WindowCloseButtonHint)
1951        self.setWindowTitle(title)
1952
1953        topLayout = QVBoxLayout(self)
1954        self.setLayout(topLayout)
1955        mainLayout = QHBoxLayout()
1956        topLayout.addLayout(mainLayout)
1957        iconLabel = QLabel()
1958        iconLabel.setPixmap(icon.pixmap(128, 128))
1959        mainLayout.addWidget(iconLabel)
1960        textBox = QPlainTextEdit()
1961        textBox.setReadOnly(True)
1962        textBox.setWordWrapMode(QTextOption.NoWrap)
1963        textBox.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
1964        textBox.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
1965        text = '\n'.join(textLines)
1966        textBox.setPlainText(text)
1967        size = textBox.fontMetrics().size(0, text)
1968        size.setHeight(size.height() + 10)
1969        size.setWidth(size.width() + 10)
1970        textBox.setMinimumSize(size)
1971        mainLayout.addWidget(textBox)
1972
1973        ctrlLayout = QHBoxLayout()
1974        topLayout.addLayout(ctrlLayout)
1975        ctrlLayout.addStretch()
1976        okButton = QPushButton(_('&OK'))
1977        ctrlLayout.addWidget(okButton)
1978        okButton.clicked.connect(self.accept)
1979