1""" 2Orange Canvas Application 3 4""" 5import atexit 6import sys 7import os 8import argparse 9import logging 10from typing import Optional, List, Sequence 11 12import AnyQt 13from AnyQt.QtWidgets import QApplication 14from AnyQt.QtCore import ( 15 Qt, QUrl, QEvent, QSettings, QLibraryInfo, pyqtSignal as Signal 16) 17 18from orangecanvas.utils.after_exit import run_after_exit 19from orangecanvas.utils.asyncutils import get_event_loop 20 21 22def fix_qt_plugins_path(): 23 """ 24 Attempt to fix qt plugins path if it is invalid. 25 26 https://www.riverbankcomputing.com/pipermail/pyqt/2018-November/041089.html 27 """ 28 # PyQt5 loads a runtime generated qt.conf file into qt's resource system 29 # but does not correctly (INI) encode non-latin1 characters in paths 30 # (https://www.riverbankcomputing.com/pipermail/pyqt/2018-November/041089.html) 31 # Need to be careful not to mess the plugins path when not installed as 32 # a (delocated) wheel. 33 s = QSettings(":qt/etc/qt.conf", QSettings.IniFormat) 34 path = s.value("Paths/Prefix", type=str) 35 # does the ':qt/etc/qt.conf' exist and has prefix path that does not exist 36 if path and os.path.exists(path): 37 return 38 # Use QLibraryInfo.location to resolve the plugins dir 39 pluginspath = QLibraryInfo.location(QLibraryInfo.PluginsPath) 40 41 # Check effective library paths. Someone might already set the search 42 # paths (including via QT_PLUGIN_PATH). QApplication.libraryPaths() returns 43 # existing paths only. 44 paths = QApplication.libraryPaths() 45 if paths: 46 return 47 48 if AnyQt.USED_API == "pyqt5": 49 import PyQt5.QtCore as qc 50 elif AnyQt.USED_API == "pyside2": 51 import PySide2.QtCore as qc 52 else: 53 return 54 55 def normpath(path): 56 return os.path.normcase(os.path.normpath(path)) 57 58 # guess the appropriate path relative to the installation dir based on the 59 # PyQt5 installation dir and the 'recorded' plugins path. I.e. match the 60 # 'PyQt5' directory name in the recorded path and replace the 'invalid' 61 # prefix with the real PyQt5 install dir. 62 def maybe_match_prefix(prefix: str, path: str) -> Optional[str]: 63 """ 64 >>> maybe_match_prefix("aa/bb/cc", "/a/b/cc/a/b") 65 "aa/bb/cc/a/b" 66 >>> maybe_match_prefix("aa/bb/dd", "/a/b/cc/a/b") 67 None 68 """ 69 prefix = normpath(prefix) 70 path = normpath(path) 71 basename = os.path.basename(prefix) 72 path_components = path.split(os.sep) 73 # find the (rightmost) basename in the prefix_components 74 idx = None 75 try: 76 start = 0 77 while True: 78 idx = path_components.index(basename, start) 79 start = idx + 1 80 except ValueError: 81 pass 82 if idx is None: 83 return None 84 return os.path.join(prefix, *path_components[idx + 1:]) 85 86 newpath = maybe_match_prefix( 87 os.path.dirname(qc.__file__), pluginspath 88 ) 89 if newpath is not None and os.path.exists(newpath): 90 QApplication.addLibraryPath(newpath) 91 92 93class CanvasApplication(QApplication): 94 fileOpenRequest = Signal(QUrl) 95 96 __args = None 97 98 def __init__(self, argv): 99 fix_qt_plugins_path() 100 if hasattr(Qt, "AA_EnableHighDpiScaling"): 101 # Turn on HighDPI support when available 102 QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 103 104 CanvasApplication.__args, argv_ = self.parse_style_arguments(argv) 105 if self.__args.style: 106 argv_ = argv_ + ["-style", self.__args.style] 107 super().__init__(argv_) 108 # Make sure there is an asyncio event loop that runs on the 109 # Qt event loop. 110 _ = get_event_loop() 111 argv[:] = argv_ 112 self.setAttribute(Qt.AA_DontShowIconsInMenus, True) 113 if hasattr(self, "styleHints"): 114 sh = self.styleHints() 115 if hasattr(sh, 'setShowShortcutsInContextMenus'): 116 # PyQt5.13 and up 117 sh.setShowShortcutsInContextMenus(True) 118 self.configureStyle() 119 120 def event(self, event): 121 if event.type() == QEvent.FileOpen: 122 self.fileOpenRequest.emit(event.url()) 123 elif event.type() == QEvent.PolishRequest: 124 self.configureStyle() 125 return super().event(event) 126 127 @staticmethod 128 def parse_style_arguments(argv): 129 parser = argparse.ArgumentParser() 130 parser.add_argument("-style", type=str, default=None) 131 parser.add_argument("-colortheme", type=str, default=None) 132 ns, rest = parser.parse_known_args(argv) 133 if ns.style is not None: 134 if ":" in ns.style: 135 ns.style, colortheme = ns.style.split(":", 1) 136 if ns.colortheme is None: 137 ns.colortheme = colortheme 138 return ns, rest 139 140 @staticmethod 141 def configureStyle(): 142 from orangecanvas import styles 143 args = CanvasApplication.__args 144 settings = QSettings() 145 settings.beginGroup("application-style") 146 name = settings.value("style-name", "", type=str) 147 if args is not None and args.style: 148 # command line params take precedence 149 name = args.style 150 151 if name != "": 152 inst = QApplication.instance() 153 if inst is not None: 154 if inst.style().objectName().lower() != name.lower(): 155 QApplication.setStyle(name) 156 157 theme = settings.value("palette", "", type=str) 158 if args is not None and args.colortheme: 159 theme = args.colortheme 160 161 if theme and theme in styles.colorthemes: 162 palette = styles.colorthemes[theme]() 163 QApplication.setPalette(palette) 164 165 166__restart_command: Optional[List[str]] = None 167 168 169def set_restart_command(cmd: Optional[Sequence[str]]): 170 """ 171 Set or unset the restart command. 172 173 This command will be run after this process exits. 174 175 Pass cmd=None to unset the current command. 176 """ 177 global __restart_command 178 log = logging.getLogger(__name__) 179 atexit.unregister(__restart) 180 if cmd is None: 181 __restart_command = None 182 log.info("Disabling application restart") 183 else: 184 __restart_command = list(cmd) 185 atexit.register(__restart) 186 log.info("Enabling application restart with: %r", cmd) 187 188 189def restart_command() -> Optional[List[str]]: 190 """Return the current set restart command.""" 191 return __restart_command 192 193 194def restart_cancel() -> None: 195 set_restart_command(None) 196 197 198def default_restart_command(): 199 """Return the default restart command.""" 200 return [sys.executable, sys.argv[0]] 201 202 203def __restart(): 204 if __restart_command: 205 run_after_exit(__restart_command) 206