1#!/usr/bin/env python3 2 3#****************************************************************************** 4# treemaincontrol.py, provides a class for global tree commands 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 sys 16import pathlib 17import os.path 18import ast 19import io 20import gzip 21import zlib 22import platform 23from PyQt5.QtCore import QIODevice, QObject, Qt, PYQT_VERSION_STR, qVersion 24from PyQt5.QtGui import QColor, QFont, QPalette 25from PyQt5.QtNetwork import QLocalServer, QLocalSocket 26from PyQt5.QtWidgets import (QAction, QApplication, QDialog, QFileDialog, 27 QMessageBox, QStyleFactory, QSystemTrayIcon, qApp) 28import globalref 29import treelocalcontrol 30import options 31import optiondefaults 32import recentfiles 33import p3 34import icondict 35import imports 36import configdialog 37import miscdialogs 38import conditional 39import colorset 40import helpview 41try: 42 from __main__ import __version__, __author__ 43except ImportError: 44 __version__ = '' 45 __author__ = '' 46try: 47 from __main__ import docPath, iconPath, templatePath, samplePath 48except ImportError: 49 docPath = None 50 iconPath = None 51 templatePath = None 52 samplePath = None 53 54encryptPrefix = b'>>TL+enc' 55 56 57class TreeMainControl(QObject): 58 """Class to handle all global controls. 59 60 Provides methods for all controls and stores local control objects. 61 """ 62 def __init__(self, pathObjects, parent=None): 63 """Initialize the main tree controls 64 65 Arguments: 66 pathObjects -- a list of file objects to open 67 parent -- the parent QObject if given 68 """ 69 super().__init__(parent) 70 self.localControls = [] 71 self.activeControl = None 72 self.trayIcon = None 73 self.isTrayMinimized = False 74 self.configDialog = None 75 self.sortDialog = None 76 self.numberingDialog = None 77 self.findTextDialog = None 78 self.findConditionDialog = None 79 self.findReplaceDialog = None 80 self.filterTextDialog = None 81 self.filterConditionDialog = None 82 self.basicHelpView = None 83 self.passwords = {} 84 self.creatingLocalControlFlag = False 85 globalref.mainControl = self 86 self.allActions = {} 87 try: 88 # check for existing TreeLine session 89 socket = QLocalSocket() 90 socket.connectToServer('treeline3-session', 91 QIODevice.WriteOnly) 92 # if found, send files to open and exit TreeLine 93 if socket.waitForConnected(1000): 94 socket.write(bytes(repr([str(path) for path in pathObjects]), 95 'utf-8')) 96 if socket.waitForBytesWritten(1000): 97 socket.close() 98 sys.exit(0) 99 # start local server to listen for attempt to start new session 100 self.serverSocket = QLocalServer() 101 # remove any old servers still around after a crash in linux 102 self.serverSocket.removeServer('treeline3-session') 103 self.serverSocket.listen('treeline3-session') 104 self.serverSocket.newConnection.connect(self.getSocket) 105 except AttributeError: 106 print(_('Warning: Could not create local socket')) 107 mainVersion = '.'.join(__version__.split('.')[:2]) 108 globalref.genOptions = options.Options('general', 'TreeLine', 109 mainVersion, 'bellz') 110 optiondefaults.setGenOptionDefaults(globalref.genOptions) 111 globalref.miscOptions = options.Options('misc') 112 optiondefaults.setMiscOptionDefaults(globalref.miscOptions) 113 globalref.histOptions = options.Options('history') 114 optiondefaults.setHistOptionDefaults(globalref.histOptions) 115 globalref.toolbarOptions = options.Options('toolbar') 116 optiondefaults.setToolbarOptionDefaults(globalref.toolbarOptions) 117 globalref.keyboardOptions = options.Options('keyboard') 118 optiondefaults.setKeyboardOptionDefaults(globalref.keyboardOptions) 119 try: 120 globalref.genOptions.readFile() 121 globalref.miscOptions.readFile() 122 globalref.histOptions.readFile() 123 globalref.toolbarOptions.readFile() 124 globalref.keyboardOptions.readFile() 125 except IOError: 126 errorDir = options.Options.basePath 127 if not errorDir: 128 errorDir = _('missing directory') 129 QMessageBox.warning(None, 'TreeLine', 130 _('Error - could not write config file to {}'). 131 format(errorDir)) 132 options.Options.basePath = None 133 iconPathList = self.findResourcePaths('icons', iconPath) 134 globalref.toolIcons = icondict.IconDict([path / 'toolbar' for path 135 in iconPathList], 136 ['', '32x32', '16x16']) 137 globalref.toolIcons.loadAllIcons() 138 windowIcon = globalref.toolIcons.getIcon('treelogo') 139 if windowIcon: 140 QApplication.setWindowIcon(windowIcon) 141 globalref.treeIcons = icondict.IconDict(iconPathList, ['', 'tree']) 142 icon = globalref.treeIcons.getIcon('default') 143 qApp.setStyle(QStyleFactory.create('Fusion')) 144 self.colorSet = colorset.ColorSet() 145 if globalref.miscOptions['ColorTheme'] != 'system': 146 self.colorSet.setAppColors() 147 self.recentFiles = recentfiles.RecentFileList() 148 if globalref.genOptions['AutoFileOpen'] and not pathObjects: 149 recentPath = self.recentFiles.firstPath() 150 if recentPath: 151 pathObjects = [recentPath] 152 self.setupActions() 153 self.systemFont = QApplication.font() 154 self.updateAppFont() 155 if globalref.genOptions['MinToSysTray']: 156 self.createTrayIcon() 157 qApp.focusChanged.connect(self.updateActionsAvail) 158 if pathObjects: 159 for pathObj in pathObjects: 160 self.openFile(pathObj, True) 161 else: 162 self.createLocalControl() 163 164 def getSocket(self): 165 """Open a socket from an attempt to open a second Treeline instance. 166 167 Opens the file (or raise and focus if open) in this instance. 168 """ 169 socket = self.serverSocket.nextPendingConnection() 170 if socket and socket.waitForReadyRead(1000): 171 data = str(socket.readAll(), 'utf-8') 172 try: 173 paths = ast.literal_eval(data) 174 if paths: 175 for path in paths: 176 pathObj = pathlib.Path(path) 177 if pathObj != self.activeControl.filePathObj: 178 self.openFile(pathObj, True) 179 else: 180 self.activeControl.activeWindow.activateAndRaise() 181 else: 182 self.activeControl.activeWindow.activateAndRaise() 183 except(SyntaxError, ValueError, TypeError, RuntimeError): 184 pass 185 186 def findResourcePaths(self, resourceName, preferredPath=''): 187 """Return list of potential non-empty pathlib objects for the resource. 188 189 List includes preferred, module and user option paths. 190 Arguments: 191 resourceName -- the typical name of the resource directory 192 preferredPath -- add this as the second path if given 193 """ 194 # use abspath() - pathlib's resolve() can be buggy with network drives 195 modPath = pathlib.Path(os.path.abspath(sys.path[0])) 196 if modPath.is_file(): 197 modPath = modPath.parent # for frozen binary 198 pathList = [modPath / '..' / resourceName, modPath / resourceName] 199 if options.Options.basePath: 200 basePath = pathlib.Path(options.Options.basePath) 201 pathList.insert(0, basePath / resourceName) 202 if preferredPath: 203 pathList.insert(1, pathlib.Path(preferredPath)) 204 return [pathlib.Path(os.path.abspath(str(path))) for path in pathList 205 if path.is_dir() and list(path.iterdir())] 206 207 def findResourceFile(self, fileName, resourceName, preferredPath=''): 208 """Return a path object for a resource file. 209 210 Add a language code before the extension if it exists. 211 Arguments: 212 fileName -- the name of the file to find 213 resourceName -- the typical name of the resource directory 214 preferredPath -- search this path first if given 215 """ 216 fileList = [fileName] 217 if globalref.lang and globalref.lang != 'C': 218 fileList[0:0] = [fileName.replace('.', '_{0}.'. 219 format(globalref.lang)), 220 fileName.replace('.', '_{0}.'. 221 format(globalref.lang[:2]))] 222 for fileName in fileList: 223 for path in self.findResourcePaths(resourceName, preferredPath): 224 if (path / fileName).is_file(): 225 return path / fileName 226 return None 227 228 def defaultPathObj(self, dirOnly=False): 229 """Return a reasonable default file path object. 230 231 Used for open, save-as, import and export. 232 Arguments: 233 dirOnly -- if True, do not include basename of file 234 """ 235 pathObj = None 236 if self.activeControl: 237 pathObj = self.activeControl.filePathObj 238 if not pathObj: 239 pathObj = self.recentFiles.firstDir() 240 if not pathObj: 241 pathObj = pathlib.Path.home() 242 if dirOnly: 243 pathObj = pathObj.parent 244 return pathObj 245 246 def openFile(self, pathObj, forceNewWindow=False, checkModified=False, 247 importOnFail=True): 248 """Open the file given by path if not already open. 249 250 If already open in a different window, focus and raise the window. 251 Arguments: 252 pathObj -- the path object to read 253 forceNewWindow -- if True, use a new window regardless of option 254 checkModified -- if True & not new win, prompt if file modified 255 importOnFail -- if True, prompts for import on non-TreeLine files 256 """ 257 match = [control for control in self.localControls if 258 pathObj == control.filePathObj] 259 if match and self.activeControl not in match: 260 control = match[0] 261 control.activeWindow.activateAndRaise() 262 self.updateLocalControlRef(control) 263 return 264 if checkModified and not (forceNewWindow or 265 globalref.genOptions['OpenNewWindow'] or 266 self.activeControl.checkSaveChanges()): 267 return 268 if not self.checkAutoSave(pathObj): 269 if not self.localControls: 270 self.createLocalControl() 271 return 272 QApplication.setOverrideCursor(Qt.WaitCursor) 273 try: 274 self.createLocalControl(pathObj, None, forceNewWindow) 275 self.recentFiles.addItem(pathObj) 276 if not (globalref.genOptions['SaveTreeStates'] and 277 self.recentFiles.retrieveTreeState(self.activeControl)): 278 self.activeControl.expandRootNodes() 279 self.activeControl.selectRootSpot() 280 QApplication.restoreOverrideCursor() 281 except IOError: 282 QApplication.restoreOverrideCursor() 283 QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', 284 _('Error - could not read file {0}'). 285 format(pathObj)) 286 self.recentFiles.removeItem(pathObj) 287 except (ValueError, KeyError, TypeError): 288 fileObj = pathObj.open('rb') 289 fileObj, encrypted = self.decryptFile(fileObj) 290 if not fileObj: 291 if not self.localControls: 292 self.createLocalControl() 293 QApplication.restoreOverrideCursor() 294 return 295 fileObj, compressed = self.decompressFile(fileObj) 296 if compressed or encrypted: 297 try: 298 textFileObj = io.TextIOWrapper(fileObj, encoding='utf-8') 299 self.createLocalControl(textFileObj, None, forceNewWindow) 300 fileObj.close() 301 textFileObj.close() 302 self.recentFiles.addItem(pathObj) 303 if not (globalref.genOptions['SaveTreeStates'] and 304 self.recentFiles.retrieveTreeState(self. 305 activeControl)): 306 self.activeControl.expandRootNodes() 307 self.activeControl.selectRootSpot() 308 self.activeControl.compressed = compressed 309 self.activeControl.encrypted = encrypted 310 QApplication.restoreOverrideCursor() 311 return 312 except (ValueError, KeyError, TypeError): 313 pass 314 fileObj.close() 315 importControl = imports.ImportControl(pathObj) 316 structure = importControl.importOldTreeLine() 317 if structure: 318 self.createLocalControl(pathObj, structure, forceNewWindow) 319 self.activeControl.printData.readData(importControl. 320 treeLineRootAttrib) 321 self.recentFiles.addItem(pathObj) 322 self.activeControl.expandRootNodes() 323 self.activeControl.imported = True 324 QApplication.restoreOverrideCursor() 325 return 326 QApplication.restoreOverrideCursor() 327 if importOnFail: 328 importControl = imports.ImportControl(pathObj) 329 structure = importControl.interactiveImport(True) 330 if structure: 331 self.createLocalControl(pathObj, structure, forceNewWindow) 332 self.activeControl.imported = True 333 return 334 else: 335 QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', 336 _('Error - invalid TreeLine file {0}'). 337 format(pathObj)) 338 self.recentFiles.removeItem(pathObj) 339 if not self.localControls: 340 self.createLocalControl() 341 342 def decryptFile(self, fileObj): 343 """Check for encryption and decrypt the fileObj if needed. 344 345 Return a tuple of the file object and True if it was encrypted. 346 Return None for the file object if the user cancels. 347 Arguments: 348 fileObj -- the file object to check and decrypt 349 """ 350 if fileObj.read(len(encryptPrefix)) != encryptPrefix: 351 fileObj.seek(0) 352 return (fileObj, False) 353 while True: 354 pathObj = pathlib.Path(fileObj.name) 355 password = self.passwords.get(pathObj, '') 356 if not password: 357 QApplication.restoreOverrideCursor() 358 dialog = miscdialogs.PasswordDialog(False, pathObj.name, 359 QApplication. 360 activeWindow()) 361 if dialog.exec_() != QDialog.Accepted: 362 fileObj.close() 363 return (None, True) 364 QApplication.setOverrideCursor(Qt.WaitCursor) 365 password = dialog.password 366 if miscdialogs.PasswordDialog.remember: 367 self.passwords[pathObj] = password 368 try: 369 text = p3.p3_decrypt(fileObj.read(), password.encode()) 370 fileIO = io.BytesIO(text) 371 fileIO.name = fileObj.name 372 fileObj.close() 373 return (fileIO, True) 374 except p3.CryptError: 375 try: 376 del self.passwords[pathObj] 377 except KeyError: 378 pass 379 380 def decompressFile(self, fileObj): 381 """Check for compression and decompress the fileObj if needed. 382 383 Return a tuple of the file object and True if it was compressed. 384 Arguments: 385 fileObj -- the file object to check and decompress 386 """ 387 prefix = fileObj.read(2) 388 fileObj.seek(0) 389 if prefix != b'\037\213': 390 return (fileObj, False) 391 try: 392 newFileObj = gzip.GzipFile(fileobj=fileObj) 393 except zlib.error: 394 return (fileObj, False) 395 newFileObj.name = fileObj.name 396 return (newFileObj, True) 397 398 def checkAutoSave(self, pathObj): 399 """Check for presence of auto save file & prompt user. 400 401 Return True if OK to contimue, False if aborting or already loaded. 402 Arguments: 403 pathObj -- the base path object to search for a backup 404 """ 405 if not globalref.genOptions['AutoSaveMinutes']: 406 return True 407 basePath = pathObj 408 pathObj = pathlib.Path(str(pathObj) + '~') 409 if not pathObj.is_file(): 410 return True 411 msgBox = QMessageBox(QMessageBox.Information, 'TreeLine', 412 _('Backup file "{}" exists.\nA previous ' 413 'session may have crashed'). 414 format(pathObj), QMessageBox.NoButton, 415 QApplication.activeWindow()) 416 restoreButton = msgBox.addButton(_('&Restore Backup'), 417 QMessageBox.ApplyRole) 418 deleteButton = msgBox.addButton(_('&Delete Backup'), 419 QMessageBox.DestructiveRole) 420 cancelButton = msgBox.addButton(_('&Cancel File Open'), 421 QMessageBox.RejectRole) 422 msgBox.exec_() 423 if msgBox.clickedButton() == restoreButton: 424 self.openFile(pathObj) 425 if self.activeControl.filePathObj != pathObj: 426 return False 427 try: 428 basePath.unlink() 429 pathObj.rename(basePath) 430 except OSError: 431 QMessageBox.warning(QApplication.activeWindow(), 432 'TreeLine', 433 _('Error - could not rename "{0}" to "{1}"'). 434 format(pathObj, basePath)) 435 return False 436 self.activeControl.filePathObj = basePath 437 self.activeControl.updateWindowCaptions() 438 self.recentFiles.removeItem(pathObj) 439 self.recentFiles.addItem(basePath) 440 return False 441 elif msgBox.clickedButton() == deleteButton: 442 try: 443 pathObj.unlink() 444 except OSError: 445 QMessageBox.warning(QApplication.activeWindow(), 446 'TreeLine', 447 _('Error - could not remove backup file {}'). 448 format(pathObj)) 449 else: # cancel button 450 return False 451 return True 452 453 def createLocalControl(self, pathObj=None, treeStruct=None, 454 forceNewWindow=False): 455 """Create a new local control object and add it to the list. 456 457 Use an imported structure if given or open the file if path is given. 458 Arguments: 459 pathObj -- the path object or file object for the control to open 460 treeStruct -- the imported structure to use 461 forceNewWindow -- if True, use a new window regardless of option 462 """ 463 self.creatingLocalControlFlag = True 464 localControl = treelocalcontrol.TreeLocalControl(self.allActions, 465 pathObj, treeStruct, 466 forceNewWindow) 467 localControl.controlActivated.connect(self.updateLocalControlRef) 468 localControl.controlClosed.connect(self.removeLocalControlRef) 469 self.localControls.append(localControl) 470 self.updateLocalControlRef(localControl) 471 self.creatingLocalControlFlag = False 472 localControl.updateRightViews() 473 localControl.updateCommandsAvail() 474 475 def updateLocalControlRef(self, localControl): 476 """Set the given local control as active. 477 478 Called by signal from a window becoming active. 479 Also updates non-modal dialogs. 480 Arguments: 481 localControl -- the new active local control 482 """ 483 if localControl != self.activeControl: 484 self.activeControl = localControl 485 if self.configDialog and self.configDialog.isVisible(): 486 self.configDialog.setRefs(self.activeControl) 487 488 def removeLocalControlRef(self, localControl): 489 """Remove ref to local control based on a closing signal. 490 491 Also do application exit clean ups if last control closing. 492 Arguments: 493 localControl -- the local control that is closing 494 """ 495 try: 496 self.localControls.remove(localControl) 497 except ValueError: 498 return # skip for unreporducible bug - odd race condition? 499 if globalref.genOptions['SaveTreeStates']: 500 self.recentFiles.saveTreeState(localControl) 501 if not self.localControls and not self.creatingLocalControlFlag: 502 if globalref.genOptions['SaveWindowGeom']: 503 localControl.windowList[0].saveWindowGeom() 504 else: 505 localControl.windowList[0].resetWindowGeom() 506 self.recentFiles.writeItems() 507 localControl.windowList[0].saveToolbarPosition() 508 globalref.histOptions.writeFile() 509 if self.trayIcon: 510 self.trayIcon.hide() 511 # stop listening for session connections 512 try: 513 self.serverSocket.close() 514 del self.serverSocket 515 except AttributeError: 516 pass 517 if self.localControls: 518 # make sure a window is active (may not be focused), to avoid 519 # bugs due to a deleted current window 520 newControl = self.localControls[0] 521 newControl.setActiveWin(newControl.windowList[0]) 522 localControl.deleteLater() 523 524 def createTrayIcon(self): 525 """Create a new system tray icon if not already created. 526 """ 527 if QSystemTrayIcon.isSystemTrayAvailable: 528 if not self.trayIcon: 529 self.trayIcon = QSystemTrayIcon(qApp.windowIcon(), qApp) 530 self.trayIcon.activated.connect(self.toggleTrayShow) 531 self.trayIcon.show() 532 533 def trayMinimize(self): 534 """Minimize to tray based on window minimize signal. 535 """ 536 if self.trayIcon and QSystemTrayIcon.isSystemTrayAvailable: 537 # skip minimize to tray if not all windows minimized 538 for control in self.localControls: 539 for window in control.windowList: 540 if not window.isMinimized(): 541 return 542 for control in self.localControls: 543 for window in control.windowList: 544 window.hide() 545 self.isTrayMinimized = True 546 547 def toggleTrayShow(self): 548 """Toggle show and hide application based on system tray icon click. 549 """ 550 if self.isTrayMinimized: 551 for control in self.localControls: 552 for window in control.windowList: 553 window.show() 554 window.showNormal() 555 self.activeControl.activeWindow.treeView.setFocus() 556 else: 557 for control in self.localControls: 558 for window in control.windowList: 559 window.hide() 560 self.isTrayMinimized = not self.isTrayMinimized 561 562 def updateConfigDialog(self): 563 """Update the config dialog for changes if it exists. 564 """ 565 if self.configDialog: 566 self.configDialog.reset() 567 568 def currentStatusBar(self): 569 """Return the status bar from the current main window. 570 """ 571 return self.activeControl.activeWindow.statusBar() 572 573 def windowActions(self): 574 """Return a list of window menu actions from each local control. 575 """ 576 actions = [] 577 for control in self.localControls: 578 actions.extend(control.windowActions(len(actions) + 1, 579 control == self.activeControl)) 580 return actions 581 582 def updateActionsAvail(self, oldWidget, newWidget): 583 """Update command availability based on focus changes. 584 585 Arguments: 586 oldWidget -- the previously focused widget 587 newWidget -- the newly focused widget 588 """ 589 self.allActions['FormatSelectAll'].setEnabled(hasattr(newWidget, 590 'selectAll') and 591 not hasattr(newWidget, 592 'editTriggers')) 593 594 def setupActions(self): 595 """Add the actions for contols at the global level. 596 """ 597 fileNewAct = QAction(_('&New...'), self, toolTip=_('New File'), 598 statusTip=_('Start a new file')) 599 fileNewAct.triggered.connect(self.fileNew) 600 self.allActions['FileNew'] = fileNewAct 601 602 fileOpenAct = QAction(_('&Open...'), self, toolTip=_('Open File'), 603 statusTip=_('Open a file from disk')) 604 fileOpenAct.triggered.connect(self.fileOpen) 605 self.allActions['FileOpen'] = fileOpenAct 606 607 fileSampleAct = QAction(_('Open Sa&mple...'), self, 608 toolTip=_('Open Sample'), 609 statusTip=_('Open a sample file')) 610 fileSampleAct.triggered.connect(self.fileOpenSample) 611 self.allActions['FileOpenSample'] = fileSampleAct 612 613 fileImportAct = QAction(_('&Import...'), self, 614 statusTip=_('Open a non-TreeLine file')) 615 fileImportAct.triggered.connect(self.fileImport) 616 self.allActions['FileImport'] = fileImportAct 617 618 fileQuitAct = QAction(_('&Quit'), self, 619 statusTip=_('Exit the application')) 620 fileQuitAct.triggered.connect(self.fileQuit) 621 self.allActions['FileQuit'] = fileQuitAct 622 623 dataConfigAct = QAction(_('&Configure Data Types...'), self, 624 statusTip=_('Modify data types, fields & output lines'), 625 checkable=True) 626 dataConfigAct.triggered.connect(self.dataConfigDialog) 627 self.allActions['DataConfigType'] = dataConfigAct 628 629 dataVisualConfigAct = QAction(_('Show C&onfiguration Structure...'), 630 self, 631 statusTip=_('Show read-only visualization of type structure')) 632 dataVisualConfigAct.triggered.connect(self.dataVisualConfig) 633 self.allActions['DataVisualConfig'] = dataVisualConfigAct 634 635 dataSortAct = QAction(_('Sor&t Nodes...'), self, 636 statusTip=_('Define node sort operations'), 637 checkable=True) 638 dataSortAct.triggered.connect(self.dataSortDialog) 639 self.allActions['DataSortNodes'] = dataSortAct 640 641 dataNumberingAct = QAction(_('Update &Numbering...'), self, 642 statusTip=_('Update node numbering fields'), 643 checkable=True) 644 dataNumberingAct.triggered.connect(self.dataNumberingDialog) 645 self.allActions['DataNumbering'] = dataNumberingAct 646 647 toolsFindTextAct = QAction(_('&Find Text...'), self, 648 statusTip=_('Find text in node titles & data'), 649 checkable=True) 650 toolsFindTextAct.triggered.connect(self.toolsFindTextDialog) 651 self.allActions['ToolsFindText'] = toolsFindTextAct 652 653 toolsFindConditionAct = QAction(_('&Conditional Find...'), self, 654 statusTip=_('Use field conditions to find nodes'), 655 checkable=True) 656 toolsFindConditionAct.triggered.connect(self.toolsFindConditionDialog) 657 self.allActions['ToolsFindCondition'] = toolsFindConditionAct 658 659 toolsFindReplaceAct = QAction(_('Find and &Replace...'), self, 660 statusTip=_('Replace text strings in node data'), 661 checkable=True) 662 toolsFindReplaceAct.triggered.connect(self.toolsFindReplaceDialog) 663 self.allActions['ToolsFindReplace'] = toolsFindReplaceAct 664 665 toolsFilterTextAct = QAction(_('&Text Filter...'), self, 666 statusTip=_('Filter nodes to only show text matches'), 667 checkable=True) 668 toolsFilterTextAct.triggered.connect(self.toolsFilterTextDialog) 669 self.allActions['ToolsFilterText'] = toolsFilterTextAct 670 671 toolsFilterConditionAct = QAction(_('C&onditional Filter...'), 672 self, 673 statusTip=_('Use field conditions to filter nodes'), 674 checkable=True) 675 toolsFilterConditionAct.triggered.connect(self. 676 toolsFilterConditionDialog) 677 self.allActions['ToolsFilterCondition'] = toolsFilterConditionAct 678 679 toolsGenOptionsAct = QAction(_('&General Options...'), self, 680 statusTip=_('Set user preferences for all files')) 681 toolsGenOptionsAct.triggered.connect(self.toolsGenOptions) 682 self.allActions['ToolsGenOptions'] = toolsGenOptionsAct 683 684 toolsShortcutAct = QAction(_('Set &Keyboard Shortcuts...'), self, 685 statusTip=_('Customize keyboard commands')) 686 toolsShortcutAct.triggered.connect(self.toolsCustomShortcuts) 687 self.allActions['ToolsShortcuts'] = toolsShortcutAct 688 689 toolsToolbarAct = QAction(_('C&ustomize Toolbars...'), self, 690 statusTip=_('Customize toolbar buttons')) 691 toolsToolbarAct.triggered.connect(self.toolsCustomToolbars) 692 self.allActions['ToolsToolbars'] = toolsToolbarAct 693 694 toolsFontsAct = QAction(_('Customize Fo&nts...'), self, 695 statusTip=_('Customize fonts in various views')) 696 toolsFontsAct.triggered.connect(self.toolsCustomFonts) 697 self.allActions['ToolsFonts'] = toolsFontsAct 698 699 toolsColorsAct = QAction(_('Custo&mize Colors...'), self, 700 statusTip=_('Customize GUI colors and themes')) 701 toolsColorsAct.triggered.connect(self.toolsCustomColors) 702 self.allActions['ToolsColors'] = toolsColorsAct 703 704 formatSelectAllAct = QAction(_('&Select All'), self, 705 statusTip=_('Select all text in an editor')) 706 formatSelectAllAct.setEnabled(False) 707 formatSelectAllAct.triggered.connect(self.formatSelectAll) 708 self.allActions['FormatSelectAll'] = formatSelectAllAct 709 710 helpBasicAct = QAction(_('&Basic Usage...'), self, 711 statusTip=_('Display basic usage instructions')) 712 helpBasicAct.triggered.connect(self.helpViewBasic) 713 self.allActions['HelpBasic'] = helpBasicAct 714 715 helpFullAct = QAction(_('&Full Documentation...'), self, 716 statusTip=_('Open a TreeLine file with full documentation')) 717 helpFullAct.triggered.connect(self.helpViewFull) 718 self.allActions['HelpFull'] = helpFullAct 719 720 helpAboutAct = QAction(_('&About TreeLine...'), self, 721 statusTip=_('Display version info about this program')) 722 helpAboutAct.triggered.connect(self.helpAbout) 723 self.allActions['HelpAbout'] = helpAboutAct 724 725 for name, action in self.allActions.items(): 726 icon = globalref.toolIcons.getIcon(name.lower()) 727 if icon: 728 action.setIcon(icon) 729 key = globalref.keyboardOptions[name] 730 if not key.isEmpty(): 731 action.setShortcut(key) 732 733 def fileNew(self): 734 """Start a new blank file. 735 """ 736 if (globalref.genOptions['OpenNewWindow'] or 737 self.activeControl.checkSaveChanges()): 738 searchPaths = self.findResourcePaths('templates', templatePath) 739 if searchPaths: 740 dialog = miscdialogs.TemplateFileDialog(_('New File'), 741 _('&Select Template'), 742 searchPaths) 743 if dialog.exec_() == QDialog.Accepted: 744 self.createLocalControl(dialog.selectedPath()) 745 self.activeControl.filePathObj = None 746 self.activeControl.updateWindowCaptions() 747 self.activeControl.expandRootNodes() 748 else: 749 self.createLocalControl() 750 self.activeControl.selectRootSpot() 751 752 def fileOpen(self): 753 """Prompt for a filename and open it. 754 """ 755 if (globalref.genOptions['OpenNewWindow'] or 756 self.activeControl.checkSaveChanges()): 757 filters = ';;'.join((globalref.fileFilters['trlnopen'], 758 globalref.fileFilters['all'])) 759 fileName, selFilter = QFileDialog.getOpenFileName(QApplication. 760 activeWindow(), 761 _('TreeLine - Open File'), 762 str(self.defaultPathObj(True)), 763 filters) 764 if fileName: 765 self.openFile(pathlib.Path(fileName)) 766 767 def fileOpenSample(self): 768 """Open a sample file from the doc directories. 769 """ 770 if (globalref.genOptions['OpenNewWindow'] or 771 self.activeControl.checkSaveChanges()): 772 searchPaths = self.findResourcePaths('samples', samplePath) 773 dialog = miscdialogs.TemplateFileDialog(_('Open Sample File'), 774 _('&Select Sample'), 775 searchPaths, False) 776 if dialog.exec_() == QDialog.Accepted: 777 self.createLocalControl(dialog.selectedPath()) 778 name = dialog.selectedName() + '.trln' 779 self.activeControl.filePathObj = pathlib.Path(name) 780 self.activeControl.updateWindowCaptions() 781 self.activeControl.expandRootNodes() 782 self.activeControl.imported = True 783 784 def fileImport(self): 785 """Prompt for an import type, then a file to import. 786 """ 787 importControl = imports.ImportControl() 788 structure = importControl.interactiveImport() 789 if structure: 790 self.createLocalControl(importControl.pathObj, structure) 791 if importControl.treeLineRootAttrib: 792 self.activeControl.printData.readData(importControl. 793 treeLineRootAttrib) 794 self.activeControl.imported = True 795 796 def fileQuit(self): 797 """Close all windows to exit the applications. 798 """ 799 for control in self.localControls[:]: 800 control.closeWindows() 801 802 def dataConfigDialog(self, show): 803 """Show or hide the non-modal data config dialog. 804 805 Arguments: 806 show -- true if dialog should be shown, false to hide it 807 """ 808 if show: 809 if not self.configDialog: 810 self.configDialog = configdialog.ConfigDialog() 811 dataConfigAct = self.allActions['DataConfigType'] 812 self.configDialog.dialogShown.connect(dataConfigAct.setChecked) 813 self.configDialog.setRefs(self.activeControl, True) 814 self.configDialog.show() 815 else: 816 self.configDialog.close() 817 818 def dataVisualConfig(self): 819 """Show a TreeLine file to visualize the config structure. 820 """ 821 structure = (self.activeControl.structure.treeFormats. 822 visualConfigStructure(str(self.activeControl. 823 filePathObj))) 824 self.createLocalControl(treeStruct=structure, forceNewWindow=True) 825 self.activeControl.filePathObj = pathlib.Path('structure.trln') 826 self.activeControl.updateWindowCaptions() 827 self.activeControl.expandRootNodes() 828 self.activeControl.imported = True 829 win = self.activeControl.activeWindow 830 win.rightTabs.setCurrentWidget(win.outputSplitter) 831 832 def dataSortDialog(self, show): 833 """Show or hide the non-modal data sort nodes dialog. 834 835 Arguments: 836 show -- true if dialog should be shown, false to hide it 837 """ 838 if show: 839 if not self.sortDialog: 840 self.sortDialog = miscdialogs.SortDialog() 841 dataSortAct = self.allActions['DataSortNodes'] 842 self.sortDialog.dialogShown.connect(dataSortAct.setChecked) 843 self.sortDialog.show() 844 else: 845 self.sortDialog.close() 846 847 def dataNumberingDialog(self, show): 848 """Show or hide the non-modal update node numbering dialog. 849 850 Arguments: 851 show -- true if dialog should be shown, false to hide it 852 """ 853 if show: 854 if not self.numberingDialog: 855 self.numberingDialog = miscdialogs.NumberingDialog() 856 dataNumberingAct = self.allActions['DataNumbering'] 857 self.numberingDialog.dialogShown.connect(dataNumberingAct. 858 setChecked) 859 self.numberingDialog.show() 860 if not self.numberingDialog.checkForNumberingFields(): 861 self.numberingDialog.close() 862 else: 863 self.numberingDialog.close() 864 865 def toolsFindTextDialog(self, show): 866 """Show or hide the non-modal find text dialog. 867 868 Arguments: 869 show -- true if dialog should be shown 870 """ 871 if show: 872 if not self.findTextDialog: 873 self.findTextDialog = miscdialogs.FindFilterDialog() 874 toolsFindTextAct = self.allActions['ToolsFindText'] 875 self.findTextDialog.dialogShown.connect(toolsFindTextAct. 876 setChecked) 877 self.findTextDialog.selectAllText() 878 self.findTextDialog.show() 879 else: 880 self.findTextDialog.close() 881 882 def toolsFindConditionDialog(self, show): 883 """Show or hide the non-modal conditional find dialog. 884 885 Arguments: 886 show -- true if dialog should be shown 887 """ 888 if show: 889 if not self.findConditionDialog: 890 dialogType = conditional.FindDialogType.findDialog 891 self.findConditionDialog = (conditional. 892 ConditionDialog(dialogType, 893 _('Conditional Find'))) 894 toolsFindConditionAct = self.allActions['ToolsFindCondition'] 895 (self.findConditionDialog.dialogShown. 896 connect(toolsFindConditionAct.setChecked)) 897 else: 898 self.findConditionDialog.loadTypeNames() 899 self.findConditionDialog.show() 900 else: 901 self.findConditionDialog.close() 902 903 def toolsFindReplaceDialog(self, show): 904 """Show or hide the non-modal find and replace text dialog. 905 906 Arguments: 907 show -- true if dialog should be shown 908 """ 909 if show: 910 if not self.findReplaceDialog: 911 self.findReplaceDialog = miscdialogs.FindReplaceDialog() 912 toolsFindReplaceAct = self.allActions['ToolsFindReplace'] 913 self.findReplaceDialog.dialogShown.connect(toolsFindReplaceAct. 914 setChecked) 915 else: 916 self.findReplaceDialog.loadTypeNames() 917 self.findReplaceDialog.show() 918 else: 919 self.findReplaceDialog.close() 920 921 def toolsFilterTextDialog(self, show): 922 """Show or hide the non-modal filter text dialog. 923 924 Arguments: 925 show -- true if dialog should be shown 926 """ 927 if show: 928 if not self.filterTextDialog: 929 self.filterTextDialog = miscdialogs.FindFilterDialog(True) 930 toolsFilterTextAct = self.allActions['ToolsFilterText'] 931 self.filterTextDialog.dialogShown.connect(toolsFilterTextAct. 932 setChecked) 933 self.filterTextDialog.selectAllText() 934 self.filterTextDialog.show() 935 else: 936 self.filterTextDialog.close() 937 938 def toolsFilterConditionDialog(self, show): 939 """Show or hide the non-modal conditional filter dialog. 940 941 Arguments: 942 show -- true if dialog should be shown 943 """ 944 if show: 945 if not self.filterConditionDialog: 946 dialogType = conditional.FindDialogType.filterDialog 947 self.filterConditionDialog = (conditional. 948 ConditionDialog(dialogType, 949 _('Conditional Filter'))) 950 toolsFilterConditionAct = (self. 951 allActions['ToolsFilterCondition']) 952 (self.filterConditionDialog.dialogShown. 953 connect(toolsFilterConditionAct.setChecked)) 954 else: 955 self.filterConditionDialog.loadTypeNames() 956 self.filterConditionDialog.show() 957 else: 958 self.filterConditionDialog.close() 959 960 def toolsGenOptions(self): 961 """Set general user preferences for all files. 962 """ 963 oldAutoSaveMinutes = globalref.genOptions['AutoSaveMinutes'] 964 dialog = options.OptionDialog(globalref.genOptions, 965 QApplication.activeWindow()) 966 dialog.setWindowTitle(_('General Options')) 967 if (dialog.exec_() == QDialog.Accepted and 968 globalref.genOptions.modified): 969 globalref.genOptions.writeFile() 970 self.recentFiles.updateOptions() 971 if globalref.genOptions['MinToSysTray']: 972 self.createTrayIcon() 973 elif self.trayIcon: 974 self.trayIcon.hide() 975 autoSaveMinutes = globalref.genOptions['AutoSaveMinutes'] 976 for control in self.localControls: 977 for window in control.windowList: 978 window.updateWinGenOptions() 979 control.structure.undoList.setNumLevels() 980 control.updateAll(False) 981 if autoSaveMinutes != oldAutoSaveMinutes: 982 control.resetAutoSave() 983 984 def toolsCustomShortcuts(self): 985 """Show dialog to customize keyboard commands. 986 """ 987 actions = self.activeControl.activeWindow.allActions 988 dialog = miscdialogs.CustomShortcutsDialog(actions, QApplication. 989 activeWindow()) 990 dialog.exec_() 991 992 def toolsCustomToolbars(self): 993 """Show dialog to customize toolbar buttons. 994 """ 995 actions = self.activeControl.activeWindow.allActions 996 dialog = miscdialogs.CustomToolbarDialog(actions, self.updateToolbars, 997 QApplication. 998 activeWindow()) 999 dialog.exec_() 1000 1001 def updateToolbars(self): 1002 """Update toolbars after changes in custom toolbar dialog. 1003 """ 1004 for control in self.localControls: 1005 for window in control.windowList: 1006 window.setupToolbars() 1007 1008 def toolsCustomFonts(self): 1009 """Show dialog to customize fonts in various views. 1010 """ 1011 dialog = miscdialogs.CustomFontDialog(QApplication. 1012 activeWindow()) 1013 dialog.updateRequired.connect(self.updateCustomFonts) 1014 dialog.exec_() 1015 1016 def toolsCustomColors(self): 1017 """Show dialog to customize GUI colors ans themes. 1018 """ 1019 self.colorSet.showDialog(QApplication.activeWindow()) 1020 1021 def updateCustomFonts(self): 1022 """Update fonts in all windows based on a dialog signal. 1023 """ 1024 self.updateAppFont() 1025 for control in self.localControls: 1026 for window in control.windowList: 1027 window.updateFonts() 1028 control.printData.setDefaultFont() 1029 for control in self.localControls: 1030 control.updateAll(False) 1031 1032 def updateAppFont(self): 1033 """Update application default font from settings. 1034 """ 1035 appFont = QFont(self.systemFont) 1036 appFontName = globalref.miscOptions['AppFont'] 1037 if appFontName: 1038 appFont.fromString(appFontName) 1039 QApplication.setFont(appFont) 1040 1041 def formatSelectAll(self): 1042 """Select all text in any currently focused editor. 1043 """ 1044 try: 1045 QApplication.focusWidget().selectAll() 1046 except AttributeError: 1047 pass 1048 1049 def helpViewBasic(self): 1050 """Display basic usage instructions. 1051 """ 1052 if not self.basicHelpView: 1053 path = self.findResourceFile('basichelp.html', 'doc', docPath) 1054 if not path: 1055 QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', 1056 _('Error - basic help file not found')) 1057 return 1058 self.basicHelpView = helpview.HelpView(path, 1059 _('TreeLine Basic Usage'), 1060 globalref.toolIcons) 1061 self.basicHelpView.show() 1062 1063 def helpViewFull(self): 1064 """Open a TreeLine file with full documentation. 1065 """ 1066 path = self.findResourceFile('documentation.trln', 'doc', docPath) 1067 if not path: 1068 QMessageBox.warning(QApplication.activeWindow(), 'TreeLine', 1069 _('Error - documentation file not found')) 1070 return 1071 self.createLocalControl(path, forceNewWindow=True) 1072 self.activeControl.filePathObj = pathlib.Path('documentation.trln') 1073 self.activeControl.updateWindowCaptions() 1074 self.activeControl.expandRootNodes() 1075 self.activeControl.imported = True 1076 win = self.activeControl.activeWindow 1077 win.rightTabs.setCurrentWidget(win.outputSplitter) 1078 1079 def helpAbout(self): 1080 """ Display version info about this program. 1081 """ 1082 pyVersion = '.'.join([repr(num) for num in sys.version_info[:3]]) 1083 textLines = [_('TreeLine version {0}').format(__version__), 1084 _('written by {0}').format(__author__), '', 1085 _('Library versions:'), 1086 ' Python: {0}'.format(pyVersion), 1087 ' Qt: {0}'.format(qVersion()), 1088 ' PyQt: {0}'.format(PYQT_VERSION_STR), 1089 ' OS: {0}'.format(platform.platform())] 1090 dialog = miscdialogs.AboutDialog('TreeLine', textLines, 1091 QApplication.windowIcon(), 1092 QApplication.activeWindow()) 1093 dialog.exec_() 1094 1095 1096def setThemeColors(): 1097 """Set the app colors based on options setting. 1098 """ 1099 if globalref.genOptions['ColorTheme'] == optiondefaults.colorThemes[1]: 1100 # dark theme 1101 myDarkGray = QColor(53, 53, 53) 1102 myVeryDarkGray = QColor(25, 25, 25) 1103 myBlue = QColor(42, 130, 218) 1104 palette = QPalette() 1105 palette.setColor(QPalette.Window, myDarkGray) 1106 palette.setColor(QPalette.WindowText, Qt.white) 1107 palette.setColor(QPalette.Base, myVeryDarkGray) 1108 palette.setColor(QPalette.AlternateBase, myDarkGray) 1109 palette.setColor(QPalette.ToolTipBase, Qt.darkBlue) 1110 palette.setColor(QPalette.ToolTipText, Qt.lightGray) 1111 palette.setColor(QPalette.Text, Qt.white) 1112 palette.setColor(QPalette.Button, myDarkGray) 1113 palette.setColor(QPalette.ButtonText, Qt.white) 1114 palette.setColor(QPalette.BrightText, Qt.red) 1115 palette.setColor(QPalette.Link, myBlue) 1116 palette.setColor(QPalette.Highlight, myBlue) 1117 palette.setColor(QPalette.HighlightedText, Qt.black) 1118 palette.setColor(QPalette.Disabled, QPalette.Text, Qt.darkGray) 1119 palette.setColor(QPalette.Disabled, QPalette.ButtonText, Qt.darkGray) 1120 qApp.setPalette(palette) 1121