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