From df31aed6c553d6250373029f611391beac0b3e76 Mon Sep 17 00:00:00 2001 From: Sayak Mukhopadhyay Date: Mon, 26 Apr 2021 01:22:01 +0530 Subject: [PATCH 1/8] Added appearance config option and implementation for minimize to tray on close functionality --- EDMarketConnector.py | 31 +++++++++++++++++++++++++++++++ L10n/en.template | 3 +++ prefs.py | 10 ++++++++++ requirements.txt | 1 + 4 files changed, 45 insertions(+) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index f019f8f9..b733a66f 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -9,6 +9,7 @@ import locale import pathlib import re import sys +import threading import webbrowser from builtins import object, str from os import chdir, environ @@ -16,6 +17,7 @@ from os.path import dirname, join from sys import platform from time import localtime, strftime, time from typing import TYPE_CHECKING, Optional, Tuple +from infi.systray import SysTrayIcon # Have this as early as possible for people running EDMarketConnector.exe # from cmd.exe or a bat file or similar. Else they might not be in the correct @@ -330,6 +332,18 @@ class AppWindow(object): self.prefsdialog = None + self.only_tray_close = 0 + + def open_window(systray): + self.only_tray_close = 2 + shutdown_thread = threading.Thread(target=systray.shutdown) + shutdown_thread.setDaemon(True) + shutdown_thread.start() + self.w.deiconify() + + menu_options = (("Open", None, open_window),) + self.systray = SysTrayIcon("EDMarketConnector.ico", applongname, menu_options, on_quit=self.exit_tray) + plug.load_plugins(master) if platform != 'darwin': @@ -1373,6 +1387,23 @@ class AppWindow(object): def onexit(self, event=None) -> None: """Application shutdown procedure.""" + value = bool(config.get_int('close_system_tray')) + + if value: + self.w.withdraw() + self.systray.start() + else: + self.exit() + + def exit_tray(self, systray): + if self.only_tray_close > 0: + self.only_tray_close -= 1 + else: + exit_thread = threading.Thread(target=self.exit) + exit_thread.setDaemon(True) + exit_thread.start() + + def exit(self): config.set_shutdown() # Signal we're in shutdown now. # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 diff --git a/L10n/en.template b/L10n/en.template index dd54a762..3bb2865d 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -19,6 +19,9 @@ /* Appearance setting. [EDMarketConnector.py] */ "Always on top" = "Always on top"; +/* Appearance setting. [EDMarketConnector.py] */ +"Close to system tray" = "Close to system tray"; + /* CQC rank. [stats.py] */ "Amateur" = "Amateur"; diff --git a/prefs.py b/prefs.py index 81c850c8..f62af8c2 100644 --- a/prefs.py +++ b/prefs.py @@ -636,6 +636,7 @@ class PreferencesDialog(tk.Toplevel): # Appearance theme and language setting self.lang = tk.StringVar(value=self.languages.get(config.get_str('language'), _('Default'))) self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop'))) + self.close_system_tray = tk.BooleanVar(value=bool(config.get_int('close_system_tray'))) self.theme = tk.IntVar(value=config.get_int('theme')) self.theme_colors = [config.get_str('dark_text'), config.get_str('dark_highlight')] self.theme_prompts = [ @@ -791,6 +792,14 @@ class PreferencesDialog(tk.Toplevel): ) self.ontop_button.grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance setting + self.system_tray_button = nb.Checkbutton( + appearance_frame, + text=_('Close to system tray'), + variable=self.close_system_tray, + command=self.themevarchanged + ) + self.system_tray_button.grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance setting + nb.Label(appearance_frame).grid(sticky=tk.W) # big spacer notebook.add(appearance_frame, text=_('Appearance')) # Tab heading in settings @@ -1163,6 +1172,7 @@ class PreferencesDialog(tk.Toplevel): config.set('ui_scale', self.ui_scale.get()) config.set('ui_transparency', self.transparency.get()) config.set('always_ontop', self.always_ontop.get()) + config.set('close_system_tray', self.close_system_tray.get()) config.set('theme', self.theme.get()) config.set('dark_text', self.theme_colors[0]) config.set('dark_highlight', self.theme_colors[1]) diff --git a/requirements.txt b/requirements.txt index f7c83c73..b157c90b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ certifi==2020.12.5 requests==2.25.1 watchdog==2.0.3 +infi.systray==0.1.12 # argh==0.26.2 watchdog dep # pyyaml==5.3.1 watchdog dep semantic-version==2.8.5 From ecf16762d0764054a9d6ec316ef418406dcedfc9 Mon Sep 17 00:00:00 2001 From: Sayak Mukhopadhyay Date: Mon, 26 Apr 2021 13:37:27 +0530 Subject: [PATCH 2/8] Fixed linting issues with EDMarketConnector.py --- EDMarketConnector.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index b733a66f..095e5ed4 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -17,6 +17,7 @@ from os.path import dirname, join from sys import platform from time import localtime, strftime, time from typing import TYPE_CHECKING, Optional, Tuple + from infi.systray import SysTrayIcon # Have this as early as possible for people running EDMarketConnector.exe @@ -1395,7 +1396,8 @@ class AppWindow(object): else: self.exit() - def exit_tray(self, systray): + def exit_tray(self, systray) -> None: + """Tray icon is shutting down.""" if self.only_tray_close > 0: self.only_tray_close -= 1 else: @@ -1403,7 +1405,8 @@ class AppWindow(object): exit_thread.setDaemon(True) exit_thread.start() - def exit(self): + def exit(self) -> None: + """Actual application shutdown.""" config.set_shutdown() # Signal we're in shutdown now. # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 From 6c0437642e983c470bb02082b092d4e2a4564d36 Mon Sep 17 00:00:00 2001 From: Sayak Mukhopadhyay Date: Mon, 26 Apr 2021 14:03:22 +0530 Subject: [PATCH 3/8] Formatting fixes --- EDMarketConnector.py | 4 +++- prefs.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 095e5ed4..67f7f706 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -335,7 +335,7 @@ class AppWindow(object): self.only_tray_close = 0 - def open_window(systray): + def open_window(systray) -> None: self.only_tray_close = 2 shutdown_thread = threading.Thread(target=systray.shutdown) shutdown_thread.setDaemon(True) @@ -1393,6 +1393,7 @@ class AppWindow(object): if value: self.w.withdraw() self.systray.start() + else: self.exit() @@ -1400,6 +1401,7 @@ class AppWindow(object): """Tray icon is shutting down.""" if self.only_tray_close > 0: self.only_tray_close -= 1 + else: exit_thread = threading.Thread(target=self.exit) exit_thread.setDaemon(True) diff --git a/prefs.py b/prefs.py index f62af8c2..19289478 100644 --- a/prefs.py +++ b/prefs.py @@ -636,7 +636,7 @@ class PreferencesDialog(tk.Toplevel): # Appearance theme and language setting self.lang = tk.StringVar(value=self.languages.get(config.get_str('language'), _('Default'))) self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop'))) - self.close_system_tray = tk.BooleanVar(value=bool(config.get_int('close_system_tray'))) + self.close_system_tray = tk.BooleanVar(value=config.get_bool('close_system_tray')) self.theme = tk.IntVar(value=config.get_int('theme')) self.theme_colors = [config.get_str('dark_text'), config.get_str('dark_highlight')] self.theme_prompts = [ From fa58d2f0c2bd561f6fdaf67a48c341bb02242f49 Mon Sep 17 00:00:00 2001 From: Sayak Mukhopadhyay Date: Mon, 26 Apr 2021 14:23:08 +0530 Subject: [PATCH 4/8] Added windows OS checks --- EDMarketConnector.py | 38 ++++++++++++++++++++++---------------- prefs.py | 14 +++++++------- requirements.txt | 2 +- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 67f7f706..528584ca 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -9,7 +9,6 @@ import locale import pathlib import re import sys -import threading import webbrowser from builtins import object, str from os import chdir, environ @@ -18,8 +17,6 @@ from sys import platform from time import localtime, strftime, time from typing import TYPE_CHECKING, Optional, Tuple -from infi.systray import SysTrayIcon - # Have this as early as possible for people running EDMarketConnector.exe # from cmd.exe or a bat file or similar. Else they might not be in the correct # place for things like config.py reading .gitversion @@ -333,17 +330,21 @@ class AppWindow(object): self.prefsdialog = None - self.only_tray_close = 0 + if platform == 'win32': + import threading + from infi.systray import SysTrayIcon - def open_window(systray) -> None: - self.only_tray_close = 2 - shutdown_thread = threading.Thread(target=systray.shutdown) - shutdown_thread.setDaemon(True) - shutdown_thread.start() - self.w.deiconify() + self.only_tray_close = 0 - menu_options = (("Open", None, open_window),) - self.systray = SysTrayIcon("EDMarketConnector.ico", applongname, menu_options, on_quit=self.exit_tray) + def open_window(systray) -> None: + self.only_tray_close = 2 + shutdown_thread = threading.Thread(target=systray.shutdown) + shutdown_thread.setDaemon(True) + shutdown_thread.start() + self.w.deiconify() + + menu_options = (("Open", None, open_window),) + self.systray = SysTrayIcon("EDMarketConnector.ico", applongname, menu_options, on_quit=self.exit_tray) plug.load_plugins(master) @@ -1388,17 +1389,22 @@ class AppWindow(object): def onexit(self, event=None) -> None: """Application shutdown procedure.""" - value = bool(config.get_int('close_system_tray')) + if platform == 'win32': + value = bool(config.get_int('close_system_tray')) - if value: - self.w.withdraw() - self.systray.start() + if value: + self.w.withdraw() + self.systray.start() + + else: + self.exit() else: self.exit() def exit_tray(self, systray) -> None: """Tray icon is shutting down.""" + import threading if self.only_tray_close > 0: self.only_tray_close -= 1 diff --git a/prefs.py b/prefs.py index 19289478..929ddc0c 100644 --- a/prefs.py +++ b/prefs.py @@ -792,13 +792,13 @@ class PreferencesDialog(tk.Toplevel): ) self.ontop_button.grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance setting - self.system_tray_button = nb.Checkbutton( - appearance_frame, - text=_('Close to system tray'), - variable=self.close_system_tray, - command=self.themevarchanged - ) - self.system_tray_button.grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance setting + if platform == 'win32': + nb.Checkbutton( + appearance_frame, + text=_('Close to system tray'), + variable=self.close_system_tray, + command=self.themevarchanged + ).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance setting nb.Label(appearance_frame).grid(sticky=tk.W) # big spacer diff --git a/requirements.txt b/requirements.txt index b157c90b..cc3d8c4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ certifi==2020.12.5 requests==2.25.1 watchdog==2.0.3 -infi.systray==0.1.12 +infi.systray==0.1.12; sys_platform == 'win32' # argh==0.26.2 watchdog dep # pyyaml==5.3.1 watchdog dep semantic-version==2.8.5 From 2dddf02f9b4023d36e8f17ffce4587de49c8b8b4 Mon Sep 17 00:00:00 2001 From: Sayak Mukhopadhyay Date: Mon, 26 Apr 2021 14:40:11 +0530 Subject: [PATCH 5/8] Comments to describe the hack --- EDMarketConnector.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 528584ca..2ea6f372 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -334,16 +334,24 @@ class AppWindow(object): import threading from infi.systray import SysTrayIcon - self.only_tray_close = 0 + self.only_tray_close = 0 # This is kind of a hack. + # When the tray icon is double click to reopen the EDMC window, I want the tray icon to disappear. To do + # that I have to call the `shutdown()` method of the systray object. Calling the shutdown method triggers + # the method associated with `on_quit` twice, once for WM_DESTROY and again for WM_CLOSE. This is not the + # case when the application is exited by clicking on `Quit` in the tray menu. So, to handle this, we are + # creating this class variable and setting it to 2 when `Open` is called from the tray menu (either by + # clicking open or by double clicking the icon) and decrementing it by 1 when the tray shutdown is triggered def open_window(systray) -> None: self.only_tray_close = 2 + # Shutdown needs to happen in a separate thread to prevent joining with itself shutdown_thread = threading.Thread(target=systray.shutdown) shutdown_thread.setDaemon(True) shutdown_thread.start() self.w.deiconify() menu_options = (("Open", None, open_window),) + # Method associated with on_quit is called whenever the systray is closing self.systray = SysTrayIcon("EDMarketConnector.ico", applongname, menu_options, on_quit=self.exit_tray) plug.load_plugins(master) @@ -1391,7 +1399,7 @@ class AppWindow(object): """Application shutdown procedure.""" if platform == 'win32': value = bool(config.get_int('close_system_tray')) - + # When exit is called, either exit application or minimize to system tray if value: self.w.withdraw() self.systray.start() @@ -1405,6 +1413,7 @@ class AppWindow(object): def exit_tray(self, systray) -> None: """Tray icon is shutting down.""" import threading + # Hack to see if the tray shutdown has been called by calling quit or calling shutdown if self.only_tray_close > 0: self.only_tray_close -= 1 From b377199119e339921b1b995638809b5ee1ce1f48 Mon Sep 17 00:00:00 2001 From: Sayak Mukhopadhyay Date: Mon, 26 Apr 2021 15:36:43 +0530 Subject: [PATCH 6/8] Changed logic to handle minimize too system tray --- EDMarketConnector.py | 68 +++++++++++++++----------------------------- L10n/en.template | 2 +- prefs.py | 8 +++--- 3 files changed, 28 insertions(+), 50 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 2ea6f372..6d90e03f 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -9,6 +9,7 @@ import locale import pathlib import re import sys +import threading import webbrowser from builtins import object, str from os import chdir, environ @@ -331,28 +332,15 @@ class AppWindow(object): self.prefsdialog = None if platform == 'win32': - import threading from infi.systray import SysTrayIcon - self.only_tray_close = 0 # This is kind of a hack. - # When the tray icon is double click to reopen the EDMC window, I want the tray icon to disappear. To do - # that I have to call the `shutdown()` method of the systray object. Calling the shutdown method triggers - # the method associated with `on_quit` twice, once for WM_DESTROY and again for WM_CLOSE. This is not the - # case when the application is exited by clicking on `Quit` in the tray menu. So, to handle this, we are - # creating this class variable and setting it to 2 when `Open` is called from the tray menu (either by - # clicking open or by double clicking the icon) and decrementing it by 1 when the tray shutdown is triggered - def open_window(systray) -> None: - self.only_tray_close = 2 - # Shutdown needs to happen in a separate thread to prevent joining with itself - shutdown_thread = threading.Thread(target=systray.shutdown) - shutdown_thread.setDaemon(True) - shutdown_thread.start() self.w.deiconify() menu_options = (("Open", None, open_window),) # Method associated with on_quit is called whenever the systray is closing self.systray = SysTrayIcon("EDMarketConnector.ico", applongname, menu_options, on_quit=self.exit_tray) + self.systray.start() plug.load_plugins(master) @@ -1395,35 +1383,20 @@ class AppWindow(object): logger.debug('"other" exception', exc_info=e) self.status['text'] = str(e) - def onexit(self, event=None) -> None: - """Application shutdown procedure.""" - if platform == 'win32': - value = bool(config.get_int('close_system_tray')) - # When exit is called, either exit application or minimize to system tray - if value: - self.w.withdraw() - self.systray.start() - - else: - self.exit() - - else: - self.exit() - def exit_tray(self, systray) -> None: """Tray icon is shutting down.""" - import threading - # Hack to see if the tray shutdown has been called by calling quit or calling shutdown - if self.only_tray_close > 0: - self.only_tray_close -= 1 + exit_thread = threading.Thread(target=self.onexit) + exit_thread.setDaemon(True) + exit_thread.start() - else: - exit_thread = threading.Thread(target=self.exit) - exit_thread.setDaemon(True) - exit_thread.start() + def onexit(self, event=None) -> None: + """Application shutdown procedure.""" + value = config.get_bool('minimize_system_tray') + if platform == 'win32' and value is not None and value: + shutdown_thread = threading.Thread(target=self.systray.shutdown) + shutdown_thread.setDaemon(True) + shutdown_thread.start() - def exit(self) -> None: - """Actual application shutdown.""" config.set_shutdown() # Signal we're in shutdown now. # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 @@ -1490,12 +1463,17 @@ class AppWindow(object): self.drag_offset = (None, None) def oniconify(self, event=None) -> None: - """Handle iconification of the application.""" - self.w.overrideredirect(0) # Can't iconize while overrideredirect - self.w.iconify() - self.w.update_idletasks() # Size and windows styles get recalculated here - self.w.wait_visibility() # Need main window to be re-created before returning - theme.active = None # So theme will be re-applied on map + """Handle minimization of the application.""" + value = config.get_bool('minimize_system_tray') + if platform == 'win32' and value is not None and value: + self.w.withdraw() + + else: + self.w.overrideredirect(0) # Can't iconize while overrideredirect + self.w.iconify() + self.w.update_idletasks() # Size and windows styles get recalculated here + self.w.wait_visibility() # Need main window to be re-created before returning + theme.active = None # So theme will be re-applied on map # TODO: Confirm this is unused and remove. def onmap(self, event=None) -> None: diff --git a/L10n/en.template b/L10n/en.template index 3bb2865d..c7379ba7 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -20,7 +20,7 @@ "Always on top" = "Always on top"; /* Appearance setting. [EDMarketConnector.py] */ -"Close to system tray" = "Close to system tray"; +"Minimize to system tray" = "Minimize to system tray"; /* CQC rank. [stats.py] */ "Amateur" = "Amateur"; diff --git a/prefs.py b/prefs.py index 929ddc0c..119b727f 100644 --- a/prefs.py +++ b/prefs.py @@ -636,7 +636,7 @@ class PreferencesDialog(tk.Toplevel): # Appearance theme and language setting self.lang = tk.StringVar(value=self.languages.get(config.get_str('language'), _('Default'))) self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop'))) - self.close_system_tray = tk.BooleanVar(value=config.get_bool('close_system_tray')) + self.minimize_system_tray = tk.BooleanVar(value=config.get_bool('minimize_system_tray')) self.theme = tk.IntVar(value=config.get_int('theme')) self.theme_colors = [config.get_str('dark_text'), config.get_str('dark_highlight')] self.theme_prompts = [ @@ -795,8 +795,8 @@ class PreferencesDialog(tk.Toplevel): if platform == 'win32': nb.Checkbutton( appearance_frame, - text=_('Close to system tray'), - variable=self.close_system_tray, + text=_('Minimize to system tray'), + variable=self.minimize_system_tray, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, sticky=tk.W, row=row.get()) # Appearance setting @@ -1172,7 +1172,7 @@ class PreferencesDialog(tk.Toplevel): config.set('ui_scale', self.ui_scale.get()) config.set('ui_transparency', self.transparency.get()) config.set('always_ontop', self.always_ontop.get()) - config.set('close_system_tray', self.close_system_tray.get()) + config.set('minimize_system_tray', self.minimize_system_tray.get()) config.set('theme', self.theme.get()) config.set('dark_text', self.theme_colors[0]) config.set('dark_highlight', self.theme_colors[1]) From fc83dcf0903c938e7159eb84e4569210e1d771fb Mon Sep 17 00:00:00 2001 From: Sayak Mukhopadhyay Date: Mon, 26 Apr 2021 16:05:40 +0530 Subject: [PATCH 7/8] Added annotations for systray --- EDMarketConnector.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 6d90e03f..1eedb0f3 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -264,6 +264,7 @@ if __name__ == '__main__': # noqa: C901 if TYPE_CHECKING: from logging import trace, TRACE # type: ignore # noqa: F401 import update + from infi.systray import SysTrayIcon # isort: on def _(x: str) -> str: @@ -334,7 +335,7 @@ class AppWindow(object): if platform == 'win32': from infi.systray import SysTrayIcon - def open_window(systray) -> None: + def open_window(systray: 'SysTrayIcon') -> None: self.w.deiconify() menu_options = (("Open", None, open_window),) @@ -1383,7 +1384,7 @@ class AppWindow(object): logger.debug('"other" exception', exc_info=e) self.status['text'] = str(e) - def exit_tray(self, systray) -> None: + def exit_tray(self, systray: 'SysTrayIcon') -> None: """Tray icon is shutting down.""" exit_thread = threading.Thread(target=self.onexit) exit_thread.setDaemon(True) From a52472d6e03955aa224048b35be2c326a9d7bbe0 Mon Sep 17 00:00:00 2001 From: Sayak Mukhopadhyay Date: Mon, 26 Apr 2021 19:18:47 +0530 Subject: [PATCH 8/8] Fix for tray icon not stopping on app stop when minimize to tray option is disabled --- EDMarketConnector.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 1eedb0f3..30ddc857 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -1392,8 +1392,7 @@ class AppWindow(object): def onexit(self, event=None) -> None: """Application shutdown procedure.""" - value = config.get_bool('minimize_system_tray') - if platform == 'win32' and value is not None and value: + if platform == 'win32': shutdown_thread = threading.Thread(target=self.systray.shutdown) shutdown_thread.setDaemon(True) shutdown_thread.start()