1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2006 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a starter for the system tray.
8"""
9
10import sys
11import os
12import contextlib
13
14from PyQt5.QtCore import QProcess, QSettings, QFileInfo
15from PyQt5.QtGui import QCursor
16from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QDialog, QApplication
17
18from E5Gui import E5MessageBox
19from E5Gui.E5Application import e5App
20
21import Globals
22import UI.PixmapCache
23from UI.Info import Version, Program
24
25import Utilities
26import Preferences
27
28from eric6config import getConfig
29
30
31class TrayStarter(QSystemTrayIcon):
32    """
33    Class implementing a starter for the system tray.
34    """
35    def __init__(self, settingsDir):
36        """
37        Constructor
38
39        @param settingsDir directory to be used for the settings files
40        @type str
41        """
42        super().__init__(
43            UI.PixmapCache.getIcon(
44                Preferences.getTrayStarter("TrayStarterIcon")))
45
46        self.settingsDir = settingsDir
47
48        self.maxMenuFilePathLen = 75
49
50        self.rsettings = QSettings(
51            QSettings.Format.IniFormat,
52            QSettings.Scope.UserScope,
53            Globals.settingsNameOrganization,
54            Globals.settingsNameRecent)
55
56        self.recentProjects = []
57        self.__loadRecentProjects()
58        self.recentMultiProjects = []
59        self.__loadRecentMultiProjects()
60        self.recentFiles = []
61        self.__loadRecentFiles()
62
63        self.activated.connect(self.__activated)
64
65        self.__menu = QMenu(self.tr("eric tray starter"))
66
67        self.recentProjectsMenu = QMenu(
68            self.tr('Recent Projects'), self.__menu)
69        self.recentProjectsMenu.aboutToShow.connect(
70            self.__showRecentProjectsMenu)
71        self.recentProjectsMenu.triggered.connect(self.__openRecent)
72
73        self.recentMultiProjectsMenu = QMenu(
74            self.tr('Recent Multiprojects'), self.__menu)
75        self.recentMultiProjectsMenu.aboutToShow.connect(
76            self.__showRecentMultiProjectsMenu)
77        self.recentMultiProjectsMenu.triggered.connect(self.__openRecent)
78
79        self.recentFilesMenu = QMenu(self.tr('Recent Files'), self.__menu)
80        self.recentFilesMenu.aboutToShow.connect(self.__showRecentFilesMenu)
81        self.recentFilesMenu.triggered.connect(self.__openRecent)
82
83        act = self.__menu.addAction(
84            self.tr("eric tray starter"), self.__about)
85        font = act.font()
86        font.setBold(True)
87        act.setFont(font)
88        self.__menu.addSeparator()
89
90        self.__menu.addAction(
91            self.tr("Show Versions"), self.__showVersions)
92        self.__menu.addSeparator()
93
94        self.__menu.addAction(
95            self.tr("QRegularExpression editor"),
96            self.__startQRegularExpression)
97        self.__menu.addAction(
98            self.tr("Python re editor"), self.__startPyRe)
99        self.__menu.addSeparator()
100
101        self.__menu.addAction(
102            UI.PixmapCache.getIcon("uiPreviewer"),
103            self.tr("UI Previewer"), self.__startUIPreviewer)
104        self.__menu.addAction(
105            UI.PixmapCache.getIcon("trPreviewer"),
106            self.tr("Translations Previewer"), self.__startTRPreviewer)
107        self.__menu.addAction(
108            UI.PixmapCache.getIcon("unittest"),
109            self.tr("Unittest"), self.__startUnittest)
110        self.__menu.addSeparator()
111
112        self.__menu.addAction(
113            UI.PixmapCache.getIcon("diffFiles"),
114            self.tr("Compare Files"), self.__startDiff)
115        self.__menu.addAction(
116            UI.PixmapCache.getIcon("compareFiles"),
117            self.tr("Compare Files side by side"), self.__startCompare)
118        self.__menu.addSeparator()
119
120        self.__menu.addAction(
121            UI.PixmapCache.getIcon("sqlBrowser"),
122            self.tr("SQL Browser"), self.__startSqlBrowser)
123        self.__menu.addSeparator()
124
125        self.__menu.addAction(
126            UI.PixmapCache.getIcon("ericSnap"),
127            self.tr("Snapshot"), self.__startSnapshot)
128        self.__menu.addAction(
129            UI.PixmapCache.getIcon("iconEditor"),
130            self.tr("Icon Editor"), self.__startIconEditor)
131        self.__menu.addSeparator()
132
133        self.__menu.addAction(
134            UI.PixmapCache.getIcon("pluginInstall"),
135            self.tr("Install Plugin"), self.__startPluginInstall)
136        self.__menu.addAction(
137            UI.PixmapCache.getIcon("pluginUninstall"),
138            self.tr("Uninstall Plugin"), self.__startPluginUninstall)
139        self.__menu.addAction(
140            UI.PixmapCache.getIcon("pluginRepository"),
141            self.tr("Plugin Repository"), self.__startPluginRepository)
142        self.__menu.addSeparator()
143
144        self.__menu.addAction(
145            UI.PixmapCache.getIcon("configure"),
146            self.tr('Preferences'), self.__startPreferences)
147        self.__menu.addSeparator()
148
149        self.__menu.addAction(
150            UI.PixmapCache.getIcon("editor"),
151            self.tr("eric Mini Editor"), self.__startMiniEditor)
152        self.__menu.addAction(
153            UI.PixmapCache.getIcon("hexEditor"),
154            self.tr("eric Hex Editor"), self.__startHexEditor)
155        self.__menu.addAction(
156            UI.PixmapCache.getIcon("shell"),
157            self.tr("eric Shell Window"), self.__startShell)
158        self.__menu.addSeparator()
159
160        self.__menu.addAction(
161            UI.PixmapCache.getIcon("ericWeb"),
162            self.tr("eric Web Browser"), self.__startWebBrowser)
163        self.__menu.addAction(
164            UI.PixmapCache.getIcon("ericWeb"),
165            self.tr("eric Web Browser (with QtHelp)"),
166            self.__startWebBrowserQtHelp)
167        self.__menu.addAction(
168            UI.PixmapCache.getIcon("ericWeb"),
169            self.tr("eric Web Browser (Private Mode)"),
170            self.__startWebBrowserPrivate)
171        self.__menu.addSeparator()
172
173        # recent files
174        self.menuRecentFilesAct = self.__menu.addMenu(self.recentFilesMenu)
175        # recent multi projects
176        self.menuRecentMultiProjectsAct = self.__menu.addMenu(
177            self.recentMultiProjectsMenu)
178        # recent projects
179        self.menuRecentProjectsAct = self.__menu.addMenu(
180            self.recentProjectsMenu)
181        self.__menu.addSeparator()
182
183        self.__menu.addAction(
184            UI.PixmapCache.getIcon("erict"),
185            self.tr("eric IDE"), self.__startEric)
186        self.__menu.addSeparator()
187
188        self.__menu.addAction(
189            UI.PixmapCache.getIcon("configure"),
190            self.tr('Configure Tray Starter'), self.__showPreferences)
191        self.__menu.addSeparator()
192
193        self.__menu.addAction(
194            UI.PixmapCache.getIcon("exit"),
195            self.tr('Quit'), e5App().quit)
196
197    def __loadRecentProjects(self):
198        """
199        Private method to load the recently opened project filenames.
200        """
201        rp = self.rsettings.value(Globals.recentNameProject)
202        if rp is not None:
203            for f in rp:
204                if QFileInfo(f).exists():
205                    self.recentProjects.append(f)
206
207    def __loadRecentMultiProjects(self):
208        """
209        Private method to load the recently opened multi project filenames.
210        """
211        rmp = self.rsettings.value(Globals.recentNameMultiProject)
212        if rmp is not None:
213            for f in rmp:
214                if QFileInfo(f).exists():
215                    self.recentMultiProjects.append(f)
216
217    def __loadRecentFiles(self):
218        """
219        Private method to load the recently opened filenames.
220        """
221        rf = self.rsettings.value(Globals.recentNameFiles)
222        if rf is not None:
223            for f in rf:
224                if QFileInfo(f).exists():
225                    self.recentFiles.append(f)
226
227    def __activated(self, reason):
228        """
229        Private slot to handle the activated signal.
230
231        @param reason reason code of the signal
232            (QSystemTrayIcon.ActivationReason)
233        """
234        if reason in (
235            QSystemTrayIcon.ActivationReason.Context,
236            QSystemTrayIcon.ActivationReason.MiddleClick
237        ):
238            self.__showContextMenu()
239        elif reason == QSystemTrayIcon.ActivationReason.DoubleClick:
240            self.__startEric()
241
242    def __showContextMenu(self):
243        """
244        Private slot to show the context menu.
245        """
246        self.menuRecentProjectsAct.setEnabled(len(self.recentProjects) > 0)
247        self.menuRecentMultiProjectsAct.setEnabled(
248            len(self.recentMultiProjects) > 0)
249        self.menuRecentFilesAct.setEnabled(len(self.recentFiles) > 0)
250
251        pos = QCursor.pos()
252        x = pos.x() - self.__menu.sizeHint().width()
253        pos.setX(x > 0 and x or 0)
254        y = pos.y() - self.__menu.sizeHint().height()
255        pos.setY(y > 0 and y or 0)
256        self.__menu.popup(pos)
257
258    def __startProc(self, applName, *applArgs):
259        """
260        Private method to start an eric application.
261
262        @param applName name of the eric application script (string)
263        @param *applArgs variable list of application arguments
264        """
265        proc = QProcess()
266        applPath = os.path.join(getConfig("ericDir"), applName)
267
268        args = []
269        args.append(applPath)
270        args.append("--config={0}".format(Utilities.getConfigDir()))
271        if self.settingsDir:
272            args.append("--settings={0}".format(self.settingsDir))
273        for arg in applArgs:
274            args.append(arg)
275
276        if (
277            not os.path.isfile(applPath) or
278            not proc.startDetached(sys.executable, args)
279        ):
280            E5MessageBox.critical(
281                self,
282                self.tr('Process Generation Error'),
283                self.tr(
284                    '<p>Could not start the process.<br>'
285                    'Ensure that it is available as <b>{0}</b>.</p>'
286                ).format(applPath),
287                self.tr('OK'))
288
289    def __startMiniEditor(self):
290        """
291        Private slot to start the eric Mini Editor.
292        """
293        self.__startProc("eric6_editor.py")
294
295    def __startEric(self):
296        """
297        Private slot to start the eric IDE.
298        """
299        self.__startProc("eric6.py")
300
301    def __startPreferences(self):
302        """
303        Private slot to start the eric configuration dialog.
304        """
305        self.__startProc("eric6_configure.py")
306
307    def __startPluginInstall(self):
308        """
309        Private slot to start the eric plugin installation dialog.
310        """
311        self.__startProc("eric6_plugininstall.py")
312
313    def __startPluginUninstall(self):
314        """
315        Private slot to start the eric plugin uninstallation dialog.
316        """
317        self.__startProc("eric6_pluginuninstall.py")
318
319    def __startPluginRepository(self):
320        """
321        Private slot to start the eric plugin repository dialog.
322        """
323        self.__startProc("eric6_pluginrepository.py")
324
325    def __startWebBrowser(self):
326        """
327        Private slot to start the eric web browser.
328        """
329        variant = Globals.getWebBrowserSupport()
330        if variant == "QtWebEngine":
331            self.__startProc("eric6_browser.py")
332
333    def __startWebBrowserQtHelp(self):
334        """
335        Private slot to start the eric web browser with QtHelp support.
336        """
337        variant = Globals.getWebBrowserSupport()
338        if variant == "QtWebEngine":
339            self.__startProc("eric6_browser.py", "--qthelp")
340
341    def __startWebBrowserPrivate(self):
342        """
343        Private slot to start the eric web browser in private mode.
344        """
345        variant = Globals.getWebBrowserSupport()
346        if variant == "QtWebEngine":
347            self.__startProc("eric6_browser.py", "--private")
348
349    def __startUIPreviewer(self):
350        """
351        Private slot to start the eric UI previewer.
352        """
353        self.__startProc("eric6_uipreviewer.py")
354
355    def __startTRPreviewer(self):
356        """
357        Private slot to start the eric translations previewer.
358        """
359        self.__startProc("eric6_trpreviewer.py")
360
361    def __startUnittest(self):
362        """
363        Private slot to start the eric unittest dialog.
364        """
365        self.__startProc("eric6_unittest.py")
366
367    def __startDiff(self):
368        """
369        Private slot to start the eric diff dialog.
370        """
371        self.__startProc("eric6_diff.py")
372
373    def __startCompare(self):
374        """
375        Private slot to start the eric compare dialog.
376        """
377        self.__startProc("eric6_compare.py")
378
379    def __startSqlBrowser(self):
380        """
381        Private slot to start the eric sql browser dialog.
382        """
383        self.__startProc("eric6_sqlbrowser.py")
384
385    def __startIconEditor(self):
386        """
387        Private slot to start the eric icon editor dialog.
388        """
389        self.__startProc("eric6_iconeditor.py")
390
391    def __startSnapshot(self):
392        """
393        Private slot to start the eric snapshot dialog.
394        """
395        self.__startProc("eric6_snap.py")
396
397    def __startQRegularExpression(self):
398        """
399        Private slot to start the eric QRegularExpression editor dialog.
400        """
401        self.__startProc("eric6_qregularexpression.py")
402
403    def __startPyRe(self):
404        """
405        Private slot to start the eric Python re editor dialog.
406        """
407        self.__startProc("eric6_re.py")
408
409    def __startHexEditor(self):
410        """
411        Private slot to start the eric hex editor dialog.
412        """
413        self.__startProc("eric6_hexeditor.py")
414
415    def __startShell(self):
416        """
417        Private slot to start the eric Shell window.
418        """
419        self.__startProc("eric6_shell.py")
420
421    def __showRecentProjectsMenu(self):
422        """
423        Private method to set up the recent projects menu.
424        """
425        self.recentProjects = []
426        self.rsettings.sync()
427        self.__loadRecentProjects()
428
429        self.recentProjectsMenu.clear()
430
431        for idx, rp in enumerate(self.recentProjects, start=1):
432            formatStr = '&{0:d}. {1}' if idx < 10 else '{0:d}. {1}'
433            act = self.recentProjectsMenu.addAction(
434                formatStr.format(
435                    idx, Utilities.compactPath(rp, self.maxMenuFilePathLen)))
436            act.setData(rp)
437
438    def __showRecentMultiProjectsMenu(self):
439        """
440        Private method to set up the recent multi projects menu.
441        """
442        self.recentMultiProjects = []
443        self.rsettings.sync()
444        self.__loadRecentMultiProjects()
445
446        self.recentMultiProjectsMenu.clear()
447
448        for idx, rmp in enumerate(self.recentMultiProjects, start=1):
449            formatStr = '&{0:d}. {1}' if idx < 10 else '{0:d}. {1}'
450            act = self.recentMultiProjectsMenu.addAction(
451                formatStr.format(
452                    idx, Utilities.compactPath(rmp, self.maxMenuFilePathLen)))
453            act.setData(rmp)
454
455    def __showRecentFilesMenu(self):
456        """
457        Private method to set up the recent files menu.
458        """
459        self.recentFiles = []
460        self.rsettings.sync()
461        self.__loadRecentFiles()
462
463        self.recentFilesMenu.clear()
464
465        for idx, rf in enumerate(self.recentFiles, start=1):
466            formatStr = '&{0:d}. {1}' if idx < 10 else '{0:d}. {1}'
467            act = self.recentFilesMenu.addAction(
468                formatStr.format(
469                    idx, Utilities.compactPath(rf, self.maxMenuFilePathLen)))
470            act.setData(rf)
471
472    def __openRecent(self, act):
473        """
474        Private method to open a project or file from the list of recently
475        opened projects or files.
476
477        @param act reference to the action that triggered (QAction)
478        """
479        filename = act.data()
480        if filename:
481            self.__startProc(
482                "eric6.py",
483                filename)
484
485    def __showPreferences(self):
486        """
487        Private slot to set the preferences.
488        """
489        from Preferences.ConfigurationDialog import (
490            ConfigurationDialog, ConfigurationMode
491        )
492        dlg = ConfigurationDialog(
493            None, 'Configuration', True, fromEric=True,
494            displayMode=ConfigurationMode.TRAYSTARTERMODE)
495        dlg.preferencesChanged.connect(self.preferencesChanged)
496        dlg.show()
497        dlg.showConfigurationPageByName("trayStarterPage")
498        dlg.exec()
499        QApplication.processEvents()
500        if dlg.result() == QDialog.DialogCode.Accepted:
501            dlg.setPreferences()
502            Preferences.syncPreferences()
503            self.preferencesChanged()
504
505    def preferencesChanged(self):
506        """
507        Public slot to handle a change of preferences.
508        """
509        self.setIcon(
510            UI.PixmapCache.getIcon(
511                Preferences.getTrayStarter("TrayStarterIcon")))
512
513    def __about(self):
514        """
515        Private slot to handle the About dialog.
516        """
517        from Plugins.AboutPlugin.AboutDialog import AboutDialog
518        dlg = AboutDialog()
519        dlg.exec()
520
521    def __showVersions(self):
522        """
523        Private slot to handle the Versions dialog.
524        """
525        from PyQt5.QtCore import qVersion, PYQT_VERSION_STR
526        from PyQt5.Qsci import QSCINTILLA_VERSION_STR
527
528        try:
529            try:
530                from PyQt5 import sip
531            except ImportError:
532                import sip
533            sip_version_str = sip.SIP_VERSION_STR
534        except (ImportError, AttributeError):
535            sip_version_str = "sip version not available"
536
537        versionText = self.tr(
538            """<h3>Version Numbers</h3>"""
539            """<table>""")
540        versionText += (
541            """<tr><td><b>Python</b></td><td>{0}</td></tr>"""
542            .format(sys.version.split()[0])
543        )
544        versionText += (
545            """<tr><td><b>Qt</b></td><td>{0}</td></tr>"""
546            .format(qVersion())
547        )
548        versionText += (
549            """<tr><td><b>PyQt</b></td><td>{0}</td></tr>"""
550            .format(PYQT_VERSION_STR)
551        )
552        versionText += (
553            """<tr><td><b>sip</b></td><td>{0}</td></tr>"""
554            .format(sip_version_str)
555        )
556        versionText += (
557            """<tr><td><b>QScintilla</b></td><td>{0}</td></tr>"""
558            .format(QSCINTILLA_VERSION_STR)
559        )
560        with contextlib.suppress(ImportError):
561            from WebBrowser.Tools import WebBrowserTools
562            chromeVersion = WebBrowserTools.getWebEngineVersions()[0]
563            versionText += (
564                """<tr><td><b>WebEngine</b></td><td>{0}</td></tr>"""
565                .format(chromeVersion)
566            )
567        versionText += (
568            """<tr><td><b>{0}</b></td><td>{1}</td></tr>"""
569            .format(Program, Version)
570        )
571        versionText += self.tr("""</table>""")
572
573        E5MessageBox.about(None, Program, versionText)
574