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