diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 95f7aac7..a9f5491b 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -26,6 +26,7 @@ import coriolis import flightlog import prefs from config import appname, applongname, config +from hotkey import hotkeymgr l10n.Translations().install() @@ -98,7 +99,7 @@ class AppWindow: apple_menu.add_command(label=_("Check for Updates..."), command=lambda:self.updater.checkForUpdates()) menubar.add_cascade(menu=apple_menu) window_menu = tk.Menu(menubar, name='window') - menubar.add_cascade(menu=window_menu) + menubar.add_cascade(label=_('Window'), menu=window_menu) # Menu title on OSX # https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0') self.w.createcommand('tkAboutDialog', lambda:self.w.call('tk::mac::standardAboutPanel')) @@ -109,10 +110,10 @@ class AppWindow: else: file_menu = tk.Menu(menubar, tearoff=tk.FALSE) file_menu.add_command(label=_("Check for Updates..."), command=lambda:self.updater.checkForUpdates()) - file_menu.add_command(label=_("Settings"), command=lambda:prefs.PreferencesDialog(self.w, self.login)) # Menu item + file_menu.add_command(label=_("Settings"), command=lambda:prefs.PreferencesDialog(self.w, self.login)) # Item in the File menu on Windows file_menu.add_separator() - file_menu.add_command(label=_("Exit"), command=self.onexit) # Menu item - menubar.add_cascade(label=_("File"), menu=file_menu) # Top-level menu on Windows + file_menu.add_command(label=_("Exit"), command=self.onexit) # Item in the File menu on Windows + menubar.add_cascade(label=_("File"), menu=file_menu) # Menu title on Windows self.w.protocol("WM_DELETE_WINDOW", self.onexit) if platform == 'linux2': # Fix up menu to use same styling as everything else @@ -147,6 +148,10 @@ class AppWindow: self.updater = update.Updater(self.w) self.w.bind_all('<>', self.onexit) # user-generated + # Install hotkey monitoring + self.w.bind_all('<>', self.getandsend) # user-generated + print config.getint('hotkey_code'), config.getint('hotkey_mods') + hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # call after credentials have changed def login(self): @@ -187,8 +192,13 @@ class AppWindow: def getandsend(self, event=None, retrying=False): + play_sound = event and event.type=='35' and not config.getint('hotkey_mute') + if not retrying: - if time() < self.holdofftime: return # Was invoked by Return key while in cooldown + if time() < self.holdofftime: # Was invoked by key while in cooldown + if play_sound and (self.holdofftime-time()) < companion.holdoff*0.75: + hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats + return self.cmdr['text'] = self.system['text'] = self.station['text'] = '' self.status['text'] = _('Fetching station data...') self.button['state'] = tk.DISABLED @@ -209,14 +219,17 @@ class AppWindow: # Validation if not data.get('commander') or not data['commander'].get('name','').strip(): self.status['text'] = _("Who are you?!") # Shouldn't happen + if play_sound: hotkeymgr.play_bad() elif not data.get('lastSystem') or not data['lastSystem'].get('name','').strip() or not data.get('lastStarport') or not data['lastStarport'].get('name','').strip(): self.status['text'] = _("Where are you?!") # Shouldn't happen + if play_sound: hotkeymgr.play_bad() elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name','').strip(): self.status['text'] = _("What are you flying?!") # Shouldn't happen + if play_sound: hotkeymgr.play_bad() elif (config.getint('output') & config.OUT_EDDN) and data['commander'].get('docked') and not data['lastStarport'].get('ships') and not retrying: # API is flakey about shipyard info - retry if missing (<1s is usually sufficient - 5s for margin). - self.w.after(SHIPYARD_RETRY * 1000, lambda:self.getandsend(retrying=True)) + self.w.after(SHIPYARD_RETRY * 1000, lambda:self.getandsend(event, retrying=True)) # Stuff we can do while waiting for retry if config.getint('output') & config.OUT_LOG: @@ -246,9 +259,11 @@ class AppWindow: if not (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)): # no further output requested self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(querytime)).decode('utf-8') + if play_sound: hotkeymgr.play_good() elif not data['commander'].get('docked'): self.status['text'] = _("You're not docked at a station!") + if play_sound: hotkeymgr.play_bad() else: if data['lastStarport'].get('commodities'): # Fixup anomalies in the commodity data @@ -267,12 +282,16 @@ class AppWindow: self.w.update_idletasks() eddn.export(data) self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(querytime)).decode('utf-8') + if play_sound: hotkeymgr.play_good() else: self.status['text'] = _("Station doesn't have anything!") + if play_sound: hotkeymgr.play_good() # not really an error elif not data['lastStarport'].get('commodities'): self.status['text'] = _("Station doesn't have a market!") + if play_sound: hotkeymgr.play_good() # not really an error else: self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(querytime)).decode('utf-8') + if play_sound: hotkeymgr.play_good() except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, self.verify) @@ -280,18 +299,22 @@ class AppWindow: # Companion API problem except companion.ServerError as e: self.status['text'] = str(e) + if play_sound: hotkeymgr.play_bad() except requests.exceptions.ConnectionError as e: if __debug__: print_exc() self.status['text'] = _("Error: Can't connect to EDDN") + if play_sound: hotkeymgr.play_bad() except requests.exceptions.Timeout as e: if __debug__: print_exc() self.status['text'] = _("Error: Connection to EDDN timed out") + if play_sound: hotkeymgr.play_bad() except Exception as e: if __debug__: print_exc() self.status['text'] = str(e) + if play_sound: hotkeymgr.play_bad() self.cooldown() diff --git a/EDMarketConnector.wxs b/EDMarketConnector.wxs index f1863f62..8405691b 100644 --- a/EDMarketConnector.wxs +++ b/EDMarketConnector.wxs @@ -98,6 +98,12 @@ + + + + + + @@ -107,6 +113,9 @@ + + + @@ -344,9 +353,12 @@ + + + diff --git a/L10n/en.template b/L10n/en.template index 4561c53c..fcb3d4c6 100644 --- a/L10n/en.template +++ b/L10n/en.template @@ -1,150 +1,171 @@ -/* Use same text as E:D Launcher's verification dialog. [prefs.py:208] */ +/* Use same text as E:D Launcher's verification dialog. [prefs.py:311] */ "A verification code has now been sent to the{CR}email address associated with your Elite account." = "A verification code has now been sent to the{CR}email address associated with your Elite account."; -/* App menu entry on OSX. [EDMarketConnector.py:97] */ +/* App menu entry on OSX. [EDMarketConnector.py:98] */ "About {APP}" = "About {APP}"; -/* Folder selection button on Windows. [prefs.py:101] */ +/* Folder selection button on Windows. [prefs.py:113] */ "Browse..." = "Browse..."; -/* Folder selection button on OSX. [prefs.py:100] */ +/* Folder selection button on OSX. [prefs.py:112] */ "Change..." = "Change..."; -/* [EDMarketConnector.py:98] */ +/* [EDMarketConnector.py:99] */ "Check for Updates..." = "Check for Updates..."; -/* Privacy setting. [prefs.py:114] */ +/* Privacy setting. [prefs.py:149] */ "Cmdr name" = "Cmdr name"; -/* Main window. [EDMarketConnector.py:71] */ +/* Main window. [EDMarketConnector.py:72] */ "Cmdr:" = "Cmdr:"; -/* Update button in main window. [EDMarketConnector.py:300] */ +/* Update button in main window. [EDMarketConnector.py:323] */ "cooldown {SS}s" = "cooldown {SS}s"; -/* Section heading in settings. [prefs.py:58] */ +/* Section heading in settings. [prefs.py:70] */ "Credentials" = "Credentials"; -/* [EDMarketConnector.py:286] */ +/* [EDMarketConnector.py:306] */ "Error: Can't connect to EDDN" = "Error: Can't connect to EDDN"; -/* [EDMarketConnector.py:290] */ +/* [EDMarketConnector.py:311] */ "Error: Connection to EDDN timed out" = "Error: Connection to EDDN timed out"; -/* [companion.py:107] */ +/* [companion.py:109] */ "Error: Invalid Credentials" = "Error: Invalid Credentials"; /* [companion.py:103] */ "Error: Server is down" = "Error: Server is down"; -/* Menu item. [EDMarketConnector.py:114] */ +/* Item in the File menu on Windows. [EDMarketConnector.py:115] */ "Exit" = "Exit"; -/* [EDMarketConnector.py:193] */ +/* [EDMarketConnector.py:203] */ "Fetching station data..." = "Fetching station data..."; -/* Top-level menu on Windows. [EDMarketConnector.py:115] */ +/* Menu title on Windows. [EDMarketConnector.py:116] */ "File" = "File"; -/* Output folder prompt on Windows. [prefs.py:99] */ +/* Output folder prompt on Windows. [prefs.py:111] */ "File location:" = "File location:"; -/* [prefs.py:96] */ +/* [prefs.py:108] */ "Flight log" = "Flight log"; -/* Shouldn't happen. [companion.py:176] */ -"General error" = "General error"; +/* Section heading in settings on Windows. [prefs.py:125] */ +"Hotkey" = "Hotkey"; -/* [prefs.py:113] */ +/* [prefs.py:148] */ "How do you want to be identified in the saved data" = "How do you want to be identified in the saved data"; -/* [EDMarketConnector.py:248] */ +/* Section heading in settings on OSX. [prefs.py:124] */ +"Keyboard shortcut" = "Keyboard shortcut"; + +/* [EDMarketConnector.py:261] */ "Last updated at {HH}:{MM}:{SS}" = "Last updated at {HH}:{MM}:{SS}"; -/* [EDMarketConnector.py:153] */ +/* [EDMarketConnector.py:158] */ "Logging in..." = "Logging in..."; -/* [prefs.py:90] */ +/* [prefs.py:102] */ "Market data in CSV format" = "Market data in CSV format"; -/* [prefs.py:86] */ +/* [prefs.py:98] */ "Market data in Slopey's BPC format" = "Market data in Slopey's BPC format"; -/* [prefs.py:88] */ +/* [prefs.py:100] */ "Market data in Trade Dangerous format" = "Market data in Trade Dangerous format"; -/* [prefs.py:124] */ +/* No hotkey/shortcut currently defined. [prefs.py:136] */ +"none" = "none"; + +/* [prefs.py:159] */ "OK" = "OK"; -/* Section heading in settings. [prefs.py:77] */ +/* Shortcut settings button on OSX. [prefs.py:133] */ +"Open System Preferences" = "Open System Preferences"; + +/* Section heading in settings. [prefs.py:89] */ "Output" = "Output"; -/* Use same text as E:D Launcher's login dialog. [prefs.py:64] */ +/* Use same text as E:D Launcher's login dialog. [prefs.py:76] */ "Password" = "Password"; -/* [prefs.py:82] */ -"Please choose what data to save" = "Please choose what data to save"; - -/* Use same text as E:D Launcher's verification dialog. [prefs.py:211] */ -"Please enter the code into the box below." = "Please enter the code into the box below."; - -/* Use same text as E:D Launcher's login dialog. [prefs.py:62] */ -"Please log in with your Elite: Dangerous account details" = "Please log in with your Elite: Dangerous account details"; - -/* [prefs.py:37] */ -"Preferences" = "Preferences"; - -/* Section heading in settings. [prefs.py:108] */ -"Privacy" = "Privacy"; - -/* Privacy setting. [prefs.py:115] */ -"Pseudo-anonymized ID" = "Pseudo-anonymized ID"; - -/* [prefs.py:84] */ -"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network"; - -/* [EDMarketConnector.py:266] */ -"Sending data to EDDN..." = "Sending data to EDDN..."; - -/* Menu item. [EDMarketConnector.py:112] */ -"Settings" = "Settings"; +/* Hotkey/Shortcut setting. [prefs.py:140] */ +"Play sound" = "Play sound"; /* [prefs.py:94] */ +"Please choose what data to save" = "Please choose what data to save"; + +/* Use same text as E:D Launcher's verification dialog. [prefs.py:314] */ +"Please enter the code into the box below." = "Please enter the code into the box below."; + +/* Use same text as E:D Launcher's login dialog. [prefs.py:74] */ +"Please log in with your Elite: Dangerous account details" = "Please log in with your Elite: Dangerous account details"; + +/* [prefs.py:49] */ +"Preferences" = "Preferences"; + +/* Section heading in settings. [prefs.py:143] */ +"Privacy" = "Privacy"; + +/* Privacy setting. [prefs.py:150] */ +"Pseudo-anonymized ID" = "Pseudo-anonymized ID"; + +/* Shortcut settings prompt on OSX. [prefs.py:130] */ +"Re-start {APP} to use shortcuts" = "Re-start {APP} to use shortcuts"; + +/* [prefs.py:96] */ +"Send station data to the Elite Dangerous Data Network" = "Send station data to the Elite Dangerous Data Network"; + +/* [EDMarketConnector.py:281] */ +"Sending data to EDDN..." = "Sending data to EDDN..."; + +/* Item in the File menu on Windows. [EDMarketConnector.py:113] */ +"Settings" = "Settings"; + +/* [prefs.py:106] */ "Ship loadout in Coriolis format" = "Ship loadout in Coriolis format"; -/* [prefs.py:92] */ +/* [prefs.py:104] */ "Ship loadout in E:D Shipyard format" = "Ship loadout in E:D Shipyard format"; -/* [EDMarketConnector.py:273] */ +/* [EDMarketConnector.py:290] */ "Station doesn't have a market!" = "Station doesn't have a market!"; -/* [EDMarketConnector.py:271] */ +/* [EDMarketConnector.py:287] */ "Station doesn't have anything!" = "Station doesn't have anything!"; -/* Main window. [EDMarketConnector.py:73] */ +/* Main window. [EDMarketConnector.py:74] */ "Station:" = "Station:"; -/* Main window. [EDMarketConnector.py:72] */ +/* Main window. [EDMarketConnector.py:73] */ "System:" = "System:"; -/* Update button in main window. [EDMarketConnector.py:78] */ +/* Update button in main window. [EDMarketConnector.py:79] */ "Update" = "Update"; -/* Use same text as E:D Launcher's login dialog. [prefs.py:63] */ +/* Use same text as E:D Launcher's login dialog. [prefs.py:75] */ "Username (Email)" = "Username (Email)"; -/* Shouldn't happen. [EDMarketConnector.py:215] */ +/* Shouldn't happen. [EDMarketConnector.py:227] */ "What are you flying?!" = "What are you flying?!"; -/* Shouldn't happen. [EDMarketConnector.py:213] */ +/* Shouldn't happen. [EDMarketConnector.py:224] */ "Where are you?!" = "Where are you?!"; -/* Output folder prompt on OSX. [prefs.py:98] */ +/* Output folder prompt on OSX. [prefs.py:110] */ "Where:" = "Where:"; -/* Shouldn't happen. [EDMarketConnector.py:211] */ +/* Shouldn't happen. [EDMarketConnector.py:221] */ "Who are you?!" = "Who are you?!"; -/* [EDMarketConnector.py:251] */ +/* Menu title on OSX. [EDMarketConnector.py:102] */ +"Window" = "Window"; + +/* [EDMarketConnector.py:265] */ "You're not docked at a station!" = "You're not docked at a station!"; +/* Shortcut settings prompt on OSX. [prefs.py:132] */ +"{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts"; + diff --git a/L10n/fr.strings b/L10n/fr.strings index 56ee1351..28cb39e0 100644 --- a/L10n/fr.strings +++ b/L10n/fr.strings @@ -37,13 +37,13 @@ /* [companion.py:103] */ "Error: Server is down" = "Erreur : Le serveur est indisponible"; -/* Menu item. [EDMarketConnector.py:114] */ +/* Item in the File menu on Windows. [EDMarketConnector.py:115] */ "Exit" = "Quitter"; /* [EDMarketConnector.py:193] */ "Fetching station data..." = "Récupération des données de la station..."; -/* Top-level menu on Windows. [EDMarketConnector.py:115] */ +/* Menu title on Windows. [EDMarketConnector.py:116] */ "File" = "Fichier"; /* Output folder prompt on Windows. [prefs.py:99] */ @@ -103,7 +103,7 @@ /* [EDMarketConnector.py:266] */ "Sending data to EDDN..." = "Envoi des données à EDDN..."; -/* Menu item. [EDMarketConnector.py:112] */ +/* Item in the File menu on Windows. [EDMarketConnector.py:113] */ "Settings" = "Paramètres"; /* [prefs.py:94] */ @@ -143,4 +143,28 @@ "Who are you?!" = "Qui êtes-vous ?"; /* [EDMarketConnector.py:251] */ -"You're not docked at a station!" = "Vous n'êtes pas amarré à une station !"; \ No newline at end of file +"You're not docked at a station!" = "Vous n'êtes pas amarré à une station !"; + +/* Section heading in settings on Windows. [prefs.py:125] */ +"Hotkey" = ""; + +/* No hotkey/shortcut currently defined. [prefs.py:136] */ +"none" = "aucun"; + +/* Shortcut settings button on OSX. [prefs.py:133] */ +"Open System Preferences" = "Ouvrir Préférences Système"; + +/* Hotkey/Shortcut setting. [prefs.py:140] */ +"Play sound" = ""; + +/* Shortcut settings prompt on OSX. [prefs.py:130] */ +"Re-start {APP} to use shortcuts" = ""; + +/* Section heading in settings on OSX. [prefs.py:124] */ +"Keyboard shortcut" = "Raccourci clavier"; + +/* Menu title on OSX. [EDMarketConnector.py:102] */ +"Window" = "Fenêtre"; + +/* Shortcut settings prompt on OSX. [prefs.py:132] */ +"{APP} needs permission to use shortcuts" = ""; diff --git a/L10n/it.strings b/L10n/it.strings index 53e71dd3..d176d53f 100644 --- a/L10n/it.strings +++ b/L10n/it.strings @@ -37,13 +37,13 @@ /* [companion.py:103] */ "Error: Server is down" = "Errore: Il Server è offline"; -/* Menu item. [EDMarketConnector.py:114] */ +/* Item in the File menu on Windows. [EDMarketConnector.py:115] */ "Exit" = "Esci"; /* [EDMarketConnector.py:193] */ "Fetching station data..." = "Raccogliendo i dati della Stazione..."; -/* Top-level menu on Windows. [EDMarketConnector.py:115] */ +/* Menu title on Windows. [EDMarketConnector.py:116] */ "File" = "File"; /* Output folder prompt on Windows. [prefs.py:99] */ @@ -104,7 +104,7 @@ /* [EDMarketConnector.py:266] */ "Sending data to EDDN..." = "Sto inviando i dati a EDDN..."; -/* Menu item. [EDMarketConnector.py:112] */ +/* Item in the File menu on Windows. [EDMarketConnector.py:113] */ "Settings" = "Impostazioni"; /* [prefs.py:94] */ @@ -145,3 +145,27 @@ /* [EDMarketConnector.py:251] */ "You're not docked at a station!" = "Non sei parcheggiato in nessuna stazione !"; + +/* Section heading in settings on Windows. [prefs.py:125] */ +"Hotkey" = ""; + +/* No hotkey/shortcut currently defined. [prefs.py:136] */ +"none" = "nessuna"; + +/* Shortcut settings button on OSX. [prefs.py:133] */ +"Open System Preferences" = "Apri Preferenze di Systema"; + +/* Hotkey/Shortcut setting. [prefs.py:140] */ +"Play sound" = ""; + +/* Shortcut settings prompt on OSX. [prefs.py:130] */ +"Re-start {APP} to use shortcuts" = ""; + +/* Section heading in settings on OSX. [prefs.py:124] */ +"Keyboard shortcut" = "Abbreviazione da tastiera"; + +/* Menu title on OSX. [EDMarketConnector.py:102] */ +"Window" = "Finestra"; + +/* Shortcut settings prompt on OSX. [prefs.py:132] */ +"{APP} needs permission to use shortcuts" = ""; diff --git a/L10n/pl.strings b/L10n/pl.strings index d2270777..1ee123f3 100644 --- a/L10n/pl.strings +++ b/L10n/pl.strings @@ -37,13 +37,13 @@ /* [companion.py:103] */ "Error: Server is down" = "Błąd: Serwer padł"; -/* Menu item. [EDMarketConnector.py:114] */ +/* Item in the File menu on Windows. [EDMarketConnector.py:115] */ "Exit" = "Zakończ"; /* [EDMarketConnector.py:193] */ "Fetching station data..." = "Pobieranie danych o stacji..."; -/* Top-level menu on Windows. [EDMarketConnector.py:115] */ +/* Menu title on Windows. [EDMarketConnector.py:116] */ "File" = "Plik"; /* Output folder prompt on Windows. [prefs.py:99] */ @@ -103,7 +103,7 @@ /* [EDMarketConnector.py:266] */ "Sending data to EDDN..." = "Wysłanie danych do EDDN..."; -/* Menu item. [EDMarketConnector.py:112] */ +/* Item in the File menu on Windows. [EDMarketConnector.py:113] */ "Settings" = "Ustawienia"; /* [prefs.py:94] */ @@ -144,3 +144,27 @@ /* [EDMarketConnector.py:251] */ "You're not docked at a station!" = "Nie jesteś zadokowany do stacji!"; + +/* Section heading in settings on Windows. [prefs.py:125] */ +"Hotkey" = ""; + +/* No hotkey/shortcut currently defined. [prefs.py:136] */ +"none" = "brak"; + +/* Shortcut settings button on OSX. [prefs.py:133] */ +"Open System Preferences" = "Otwórz Preferencje systemowe"; + +/* Hotkey/Shortcut setting. [prefs.py:140] */ +"Play sound" = ""; + +/* Shortcut settings prompt on OSX. [prefs.py:130] */ +"Re-start {APP} to use shortcuts" = ""; + +/* Section heading in settings on OSX. [prefs.py:124] */ +"Keyboard shortcut" = "Skrót klawiaturowy"; + +/* Menu title on OSX. [EDMarketConnector.py:102] */ +"Window" = "Okno"; + +/* Shortcut settings prompt on OSX. [prefs.py:132] */ +"{APP} needs permission to use shortcuts" = ""; diff --git a/README.md b/README.md index e252314a..fdfca336 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Setup The first time that you run the app you are prompted for your username and password. This is the same username and password combination that you use to log into the Elite: Dangerous launcher, and is required so that the Frontier servers can send the app *your* data and the market data for the station that *you* are docked at. -You can also choose here what data to save (refer to the next section for details) and whether to attach your Cmdr name or a [pseudo-anonymized](http://en.wikipedia.org/wiki/Pseudonymity) ID to the data. +You can also choose here what data to save (refer to the next section for details), whether to set up a hotkey so you don't have to switch to the app in order to “Update”, and whether to attach your Cmdr name or a [pseudo-anonymized](http://en.wikipedia.org/wiki/Pseudonymity) ID to the data. You are next prompted to authenticate with a “verification code”, which you will shortly receive by email from Frontier. Note that each “verification code” is one-time only - if you enter the code incorrectly or quit the app before diff --git a/hotkey.py b/hotkey.py new file mode 100644 index 00000000..a3220b75 --- /dev/null +++ b/hotkey.py @@ -0,0 +1,381 @@ +# -*- coding: utf-8 -*- + +from os.path import dirname, join, normpath +import sys +from sys import platform + +if platform == 'darwin': + + import threading + import objc + + from AppKit import NSApplication, NSWorkspace, NSBeep, NSSound, NSEvent, NSKeyDown, NSKeyUp, NSFlagsChanged, NSKeyDownMask, NSFlagsChangedMask, NSShiftKeyMask, NSControlKeyMask, NSAlternateKeyMask, NSCommandKeyMask, NSNumericPadKeyMask, NSDeviceIndependentModifierFlagsMask, NSF1FunctionKey, NSF35FunctionKey, NSDeleteFunctionKey, NSClearLineFunctionKey + + class HotkeyMgr: + + MODIFIERMASK = NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask|NSNumericPadKeyMask + POLL = 250 + # https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/#//apple_ref/doc/constant_group/Function_Key_Unicodes + DISPLAY = { 0x03: u'⌅', 0x09: u'⇥', 0xd: u'↩', 0x19: u'⇤', 0x1b: u'esc', 0x20: u'⏘', 0x7f: u'⌫', + 0xf700: u'↑', 0xf701: u'↓', 0xf702: u'←', 0xf703: u'→', + 0xf727: u'Ins', + 0xf728: u'⌦', 0xf729: u'↖', 0xf72a: u'Fn', 0xf72b: u'↘', + 0xf72c: u'⇞', 0xf72d: u'⇟', 0xf72e: u'PrtScr', 0xf72f: u'ScrollLock', + 0xf730: u'Pause', 0xf731: u'SysReq', 0xf732: u'Break', 0xf733: u'Reset', + 0xf739: u'⌧', + } + (ACQUIRE_INACTIVE, ACQUIRE_ACTIVE, ACQUIRE_NEW) = range(3) + + def __init__(self): + self.root = None + + self.keycode = 0 + self.modifiers = 0 + self.activated = False + self.observer = None + + self.acquire_key = 0 + self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE + + self.tkProcessKeyEvent_old = None + + if getattr(sys, 'frozen', False): + respath = normpath(join(dirname(sys.executable), os.pardir, 'Resources')) + elif __file__: + respath = dirname(__file__) + else: + respath = '.' + self.snd_good = NSSound.alloc().initWithContentsOfFile_byReference_(join(respath, 'snd_good.wav'), False) + self.snd_bad = NSSound.alloc().initWithContentsOfFile_byReference_(join(respath, 'snd_bad.wav'), False) + + def register(self, root, keycode, modifiers): + self.root = root + self.keycode = keycode + self.modifiers = modifiers + self.activated = False + + if keycode: + if not self.observer: + self.root.after_idle(self._observe) + self.root.after(HotkeyMgr.POLL, self._poll) + + # Monkey-patch tk (tkMacOSXKeyEvent.c) + if not self.tkProcessKeyEvent_old: + sel = 'tkProcessKeyEvent:' + cls = NSApplication.sharedApplication().class__() + self.tkProcessKeyEvent_old = NSApplication.sharedApplication().methodForSelector_(sel) + newmethod = objc.selector(self.tkProcessKeyEvent, selector = self.tkProcessKeyEvent_old.selector, signature = self.tkProcessKeyEvent_old.signature) + objc.classAddMethod(cls, sel, newmethod) + + # Monkey-patch tk (tkMacOSXKeyEvent.c) to: + # - workaround crash on OSX 10.9 & 10.10 on seeing a composing character + # - notice when modifier key state changes + # - keep a copy of NSEvent.charactersIgnoringModifiers, which is what we need for the hotkey + # (Would like to use a decorator but need to ensure the application is created before this is installed) + def tkProcessKeyEvent(self, cls, theEvent): + if self.acquire_state: + if theEvent.type() == NSFlagsChanged: + self.acquire_key = theEvent.modifierFlags() & NSDeviceIndependentModifierFlagsMask + self.acquire_state = HotkeyMgr.ACQUIRE_NEW + # suppress the event by not chaining the old function + return theEvent + elif theEvent.type() in (NSKeyDown, NSKeyUp): + c = theEvent.charactersIgnoringModifiers() + self.acquire_key = (c and ord(c[0]) or 0) | (theEvent.modifierFlags() & NSDeviceIndependentModifierFlagsMask) + self.acquire_state = HotkeyMgr.ACQUIRE_NEW + # suppress the event by not chaining the old function + return theEvent + + # replace empty characters with charactersIgnoringModifiers to avoid crash + elif theEvent.type() in (NSKeyDown, NSKeyUp) and not theEvent.characters(): + theEvent = NSEvent.keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode_(theEvent.type(), theEvent.locationInWindow(), theEvent.modifierFlags(), theEvent.timestamp(), theEvent.windowNumber(), theEvent.context(), theEvent.charactersIgnoringModifiers(), theEvent.charactersIgnoringModifiers(), theEvent.isARepeat(), theEvent.keyCode()) + return self.tkProcessKeyEvent_old(cls, theEvent) + + def _observe(self): + # Must be called after root.mainloop() so that the app's message loop has been created + self.observer = NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSKeyDownMask, self._handler) + + def _poll(self): + # No way of signalling to Tkinter from within the callback handler block that doesn't + # cause Python to crash, so poll. + if self.activated: + self.activated = False + self.root.event_generate('<>', when="tail") + if self.keycode or self.modifiers: + self.root.after(HotkeyMgr.POLL, self._poll) + + def unregister(self): + self.keycode = None + self.modifiers = None + + @objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_) + def _handler(self, event): + # use event.charactersIgnoringModifiers to handle composing characters like Alt-e + if (event.modifierFlags() & HotkeyMgr.MODIFIERMASK) == self.modifiers and ord(event.charactersIgnoringModifiers()[0]) == self.keycode: + # Only trigger if game client is front process + active = [x for x in NSWorkspace.sharedWorkspace().runningApplications() if x.isActive()] + if active and active[0] and active[0].bundleIdentifier() == 'uk.co.frontier.EliteDangerous': + self.activated = True + + def acquire_start(self): + self.acquire_state = HotkeyMgr.ACQUIRE_ACTIVE + self.root.after_idle(self._acquire_poll) + + def acquire_stop(self): + self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE + + def _acquire_poll(self): + # No way of signalling to Tkinter from within the monkey-patched event handler that doesn't + # cause Python to crash, so poll. + if self.acquire_state: + if self.acquire_state == HotkeyMgr.ACQUIRE_NEW: + # Abuse tkEvent's keycode field to hold our acquired key & modifier + self.root.event_generate('', keycode = self.acquire_key) + self.acquire_state = HotkeyMgr.ACQUIRE_ACTIVE + self.root.after(50, self._acquire_poll) + + def fromevent(self, event): + # Return configuration (keycode, modifiers) or None=clear or False=retain previous + (keycode, modifiers) = (event.keycode & 0xffff, event.keycode & 0xffff0000) # Set by _acquire_poll() + if keycode and not (modifiers & (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask)): + if keycode == 0x1b: # Esc = retain previous + self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE + return False + elif keycode in [0x7f, NSDeleteFunctionKey, NSClearLineFunctionKey]: # BkSp, Del, Clear = clear hotkey + self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE + return None + elif keycode in [0x13, 0x20, 0x2d] or 0x61 <= keycode <= 0x7a: # don't allow keys needed for typing in System Map + NSBeep() + self.acquire_state = HotkeyMgr.ACQUIRE_INACTIVE + return None + return (keycode, modifiers) + + def display(self, keycode, modifiers): + # Return displayable form + text = '' + if modifiers & NSControlKeyMask: text += u'⌃' + if modifiers & NSAlternateKeyMask: text += u'⌥' + if modifiers & NSShiftKeyMask: text += u'⇧' + if modifiers & NSCommandKeyMask: text += u'⌘' + if (modifiers & NSNumericPadKeyMask) and keycode <= 0x7f: text += u'№' + if not keycode: + pass + elif NSF1FunctionKey <= keycode <= NSF35FunctionKey: + text += 'F%d' % (keycode + 1 - NSF1FunctionKey) + elif keycode in HotkeyMgr.DISPLAY: # specials + text += HotkeyMgr.DISPLAY[keycode] + elif keycode < 0x20: # control keys + text += unichr(keycode+0x40) + elif keycode < 0xf700: # key char + text += unichr(keycode).upper() + else: + text += u'⁈' + return text + + def play_good(self): + self.snd_good.play() + + def play_bad(self): + self.snd_bad.play() + + +elif platform == 'win32': + + import ctypes + from ctypes.wintypes import * + import threading + import winsound + + RegisterHotKey = ctypes.windll.user32.RegisterHotKey + UnregisterHotKey = ctypes.windll.user32.UnregisterHotKey + MOD_ALT = 0x0001 + MOD_CONTROL = 0x0002 + MOD_SHIFT = 0x0004 + MOD_WIN = 0x0008 + MOD_NOREPEAT = 0x4000 + + GetMessage = ctypes.windll.user32.GetMessageW + TranslateMessage = ctypes.windll.user32.TranslateMessage + DispatchMessage = ctypes.windll.user32.DispatchMessageW + PostThreadMessage = ctypes.windll.user32.PostThreadMessageW + WM_QUIT = 0x0012 + WM_HOTKEY = 0x0312 + WM_APP = 0x8000 + WM_SND_GOOD = WM_APP + 1 + WM_SND_BAD = WM_APP + 2 + + GetKeyState = ctypes.windll.user32.GetKeyState + MapVirtualKey = ctypes.windll.user32.MapVirtualKeyW + VK_BACK = 0x08 + VK_CLEAR = 0x0c + VK_RETURN = 0x0d + VK_SHIFT = 0x10 + VK_CONTROL = 0x11 + VK_MENU = 0x12 + VK_CAPITAL = 0x14 + VK_MODECHANGE= 0x1f + VK_ESCAPE = 0x1b + VK_SPACE = 0x20 + VK_DELETE = 0x2e + VK_LWIN = 0x5b + VK_RWIN = 0x5c + VK_NUMPAD0 = 0x60 + VK_DIVIDE = 0x6f + VK_F1 = 0x70 + VK_F24 = 0x87 + VK_OEM_MINUS = 0xbd + VK_NUMLOCK = 0x90 + VK_SCROLL = 0x91 + VK_PROCESSKEY= 0xe5 + VK_OEM_CLEAR = 0xfe + + GetForegroundWindow = ctypes.windll.user32.GetForegroundWindow + GetWindowText = ctypes.windll.user32.GetWindowTextW + GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] + GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW + + class HotkeyMgr: + + # https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx + # Limit ourselves to symbols in Windows 7 Segoe UI + DISPLAY = { 0x03: 'Break', 0x08: 'Bksp', 0x09: u'↹', 0x0c: 'Clear', 0x0d: u'↵', 0x13: 'Pause', + 0x14: u'Ⓐ', 0x1b: 'Esc', + 0x20: u'⏘', 0x21: 'PgUp', 0x22: 'PgDn', 0x23: 'End', 0x24: 'Home', + 0x25: u'←', 0x26: u'↑', 0x27: u'→', 0x28: u'↓', + 0x2c: 'PrtScn', 0x2d: 'Ins', 0x2e: 'Del', 0x2f: 'Help', + 0x5d: u'▤', 0x5f: u'☾', + 0x90: u'➀', 0x91: 'ScrLk', + 0xa6: u'⇦', 0xa7: u'⇨', 0xa9: u'⊗', 0xab: u'☆', 0xac: u'⌂', 0xb4: u'✉', + } + + def __init__(self): + self.root = None + self.thread = None + + if getattr(sys, 'frozen', False): + respath = dirname(sys.executable) + elif __file__: + respath = dirname(__file__) + else: + respath = '.' + self.snd_good = open(join(respath, 'snd_good.wav'), 'rb').read() + self.snd_bad = open(join(respath, 'snd_bad.wav'), 'rb').read() + + def register(self, root, keycode, modifiers): + self.root = root + if self.thread: + self.unregister() + if keycode or modifiers: + self.thread = threading.Thread(target = self.worker, name = 'Hotkey "%x:%x"' % (keycode,modifiers), args = (keycode,modifiers)) + self.thread.daemon = True + self.thread.start() + + def unregister(self): + if self.thread: + PostThreadMessage(self.thread.ident, WM_QUIT, 0, 0) + self.thread = None + + def worker(self, keycode, modifiers): + + # Hotkey must be registered by the thread that handles it + if not RegisterHotKey(None, 1, modifiers|MOD_NOREPEAT, keycode): + self.thread = None + return + + msg = MSG() + while GetMessage(ctypes.byref(msg), None, 0, 0) != 0: + if msg.message == WM_HOTKEY: + h = GetForegroundWindow() + if h: + l = GetWindowTextLength(h) + 1 + buf = ctypes.create_unicode_buffer(l) + if GetWindowText(h, buf, l) and buf.value.startswith('Elite - Dangerous'): + self.root.event_generate('<>', when="tail") + elif msg.message == WM_SND_GOOD: + winsound.PlaySound(self.snd_good, winsound.SND_MEMORY) # synchronous + elif msg.message == WM_SND_BAD: + winsound.PlaySound(self.snd_bad, winsound.SND_MEMORY) # synchronous + else: + TranslateMessage(ctypes.byref(msg)) + DispatchMessage(ctypes.byref(msg)) + + UnregisterHotKey(None, 1) + self.thread = None + + def acquire_start(self): + pass + + def acquire_stop(self): + pass + + def fromevent(self, event): + # event.state is a pain - it shows the state of the modifiers *before* a modifier key was pressed + modifiers = ((GetKeyState(VK_MENU) & 0x8000) and MOD_ALT) | ((GetKeyState(VK_CONTROL) & 0x8000) and MOD_CONTROL) | ((GetKeyState(VK_SHIFT) & 0x8000) and MOD_SHIFT) | ((GetKeyState(VK_LWIN) & 0x8000) and MOD_WIN) | ((GetKeyState(VK_RWIN) & 0x8000) and MOD_WIN) + keycode = event.keycode + + if keycode in [ VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN ]: + return (0, modifiers) + if not modifiers: + if keycode == VK_ESCAPE: # Esc = retain previous + return False + elif keycode in [ VK_BACK, VK_DELETE, VK_CLEAR, VK_OEM_CLEAR ]: # BkSp, Del, Clear = clear hotkey + return None + elif keycode in [ VK_RETURN, VK_SPACE, VK_OEM_MINUS] or 0x41 <= keycode <= 0x5a: # don't allow keys needed for typing in System Map + winsound.MessageBeep() + return None + elif keycode in [ VK_NUMLOCK, VK_SCROLL, VK_PROCESSKEY ] or VK_CAPITAL <= keycode <= VK_MODECHANGE: # ignore unmodified mode switch keys + return (0, modifiers) + + # See if the keycode is usable and available + if RegisterHotKey(None, 2, modifiers|MOD_NOREPEAT, keycode): + UnregisterHotKey(None, 2) + return (keycode, modifiers) + else: + winsound.MessageBeep() + return None + + def display(self, keycode, modifiers): + text = '' + if modifiers & MOD_WIN: text += u'❖+' + if modifiers & MOD_CONTROL: text += u'Ctrl+' + if modifiers & MOD_ALT: text += u'Alt+' + if modifiers & MOD_SHIFT: text += u'⇧+' + if VK_NUMPAD0 <= keycode <= VK_DIVIDE: text += u'№' + + if not keycode: + pass + elif VK_F1 <= keycode <= VK_F24: + text += 'F%d' % (keycode + 1 - VK_F1) + elif keycode in HotkeyMgr.DISPLAY: # specials + text += HotkeyMgr.DISPLAY[keycode] + else: + c = MapVirtualKey(keycode, 2) # printable ? + if not c: # oops not printable + text += u'⁈' + elif c < 0x20: # control keys + text += unichr(c+0x40) + else: + text += unichr(c).upper() + return text + + def play_good(self): + if self.thread: + PostThreadMessage(self.thread.ident, WM_SND_GOOD, 0, 0) + + def play_bad(self): + if self.thread: + PostThreadMessage(self.thread.ident, WM_SND_BAD, 0, 0) + +else: # Linux + + class HotkeyMgr: + + def register(self, keycode, modifiers): + pass + + def unregister(self): + pass + + +# singleton +hotkeymgr = HotkeyMgr() diff --git a/prefs.py b/prefs.py index fa13d48a..0a4dbf21 100644 --- a/prefs.py +++ b/prefs.py @@ -8,10 +8,22 @@ import Tkinter as tk import ttk import tkFileDialog -from config import config +from config import applongname, config +from hotkey import hotkeymgr -if platform=='win32': +if platform == 'darwin': + import objc + try: + from ApplicationServices import AXIsProcessTrusted, AXIsProcessTrustedWithOptions, kAXTrustedCheckOptionPrompt + except: + HIServices = objc.loadBundle('HIServices', globals(), '/System/Library/Frameworks/ApplicationServices.framework/Frameworks/HIServices.framework') + objc.loadBundleFunctions(HIServices, globals(), [('AXIsProcessTrusted', 'B'), + ('AXIsProcessTrustedWithOptions', 'B@')]) + objc.loadBundleVariables(HIServices, globals(), [('kAXTrustedCheckOptionPrompt', '@^{__CFString=}')]) + was_accessible_at_launch = AXIsProcessTrusted() + +elif platform=='win32': # sigh tkFileDialog.askdirectory doesn't support unicode on Windows import ctypes from ctypes.wintypes import * @@ -100,11 +112,34 @@ class PreferencesDialog(tk.Toplevel): self.outbutton = ttk.Button(outframe, text=(platform=='darwin' and _('Change...') or # Folder selection button on OSX _('Browse...')), command=self.outbrowse) # Folder selection button on Windows self.outbutton.grid(row=8, column=1, padx=5, pady=(5,0), sticky=tk.NSEW) - self.outdir = ttk.Entry(outframe) + self.outdir = ttk.Entry(outframe, takefocus=False) self.outdir.insert(0, config.get('outdir')) self.outdir.grid(row=9, columnspan=2, padx=5, pady=5, sticky=tk.EW) self.outvarchanged() + if platform in ['darwin','win32']: + self.hotkey_code = config.getint('hotkey_code') + self.hotkey_mods = config.getint('hotkey_mods') + self.hotkey_play = tk.IntVar(value = not config.getint('hotkey_mute')) + hotkeyframe = ttk.LabelFrame(frame, text=platform == 'darwin' and _('Keyboard shortcut') or # Section heading in settings on OSX + _('Hotkey')) # Section heading in settings on Windows + hotkeyframe.grid(padx=10, pady=10, sticky=tk.NSEW) + hotkeyframe.columnconfigure(1, weight=1) + if platform == 'darwin' and not was_accessible_at_launch: + if AXIsProcessTrusted(): + ttk.Label(hotkeyframe, text = _('Re-start {APP} to use shortcuts').format(APP=applongname)).grid(row=0, padx=5, pady=5, sticky=tk.NSEW) # Shortcut settings prompt on OSX + else: + ttk.Label(hotkeyframe, text = _('{APP} needs permission to use shortcuts').format(APP=applongname)).grid(row=0, columnspan=2, padx=5, pady=5, sticky=tk.W) # Shortcut settings prompt on OSX + ttk.Button(hotkeyframe, text = _('Open System Preferences'), command = self.enableshortcuts).grid(row=1, column=1, padx=5, pady=(0,5), sticky=tk.E) # Shortcut settings button on OSX + else: + self.hotkey_text = ttk.Entry(hotkeyframe, width=30, justify=tk.CENTER) + self.hotkey_text.insert(0, self.hotkey_code and hotkeymgr.display(self.hotkey_code, self.hotkey_mods) or _('none')) # No hotkey/shortcut currently defined + self.hotkey_text.bind('', self.hotkeystart) + self.hotkey_text.bind('', self.hotkeyend) + self.hotkey_text.grid(row=0, padx=5, pady=5, sticky=tk.NSEW) + self.hotkey_play_btn = ttk.Checkbutton(hotkeyframe, text=_('Play sound'), variable=self.hotkey_play, state = self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting + self.hotkey_play_btn.grid(row=0, column=1, padx=(10,0), pady=5, sticky=tk.NSEW) + privacyframe = ttk.LabelFrame(frame, text=_('Privacy')) # Section heading in settings privacyframe.grid(padx=10, pady=10, sticky=tk.NSEW) privacyframe.columnconfigure(0, weight=1) @@ -122,6 +157,10 @@ class PreferencesDialog(tk.Toplevel): buttonframe.columnconfigure(0, weight=1) ttk.Label(buttonframe).grid(row=0, column=0) # spacer ttk.Button(buttonframe, text=_('OK'), command=self.apply).grid(row=0, column=1, sticky=tk.E) + self.protocol("WM_DELETE_WINDOW", self._destroy) + + # disable hotkey for the duration + hotkeymgr.unregister() # wait for window to appear on screen before calling grab_set self.wait_visibility() @@ -164,17 +203,81 @@ class PreferencesDialog(tk.Toplevel): self.outdir.insert(0, d.replace('/', sep)) self.outdir['state'] = 'readonly' + def hotkeystart(self, event): + event.widget.bind('', self.hotkeylisten) + event.widget.bind('', self.hotkeylisten) + event.widget.delete(0, tk.END) + hotkeymgr.acquire_start() + + def hotkeyend(self, event): + event.widget.unbind('') + event.widget.unbind('') + hotkeymgr.acquire_stop() # in case focus was lost while in the middle of acquiring + event.widget.delete(0, tk.END) + self.hotkey_text.insert(0, self.hotkey_code and hotkeymgr.display(self.hotkey_code, self.hotkey_mods) or _('none')) # No hotkey/shortcut currently defined + + def hotkeylisten(self, event): + good = hotkeymgr.fromevent(event) + if good: + (hotkey_code, hotkey_mods) = good + event.widget.delete(0, tk.END) + event.widget.insert(0, hotkeymgr.display(hotkey_code, hotkey_mods)) + if hotkey_code: + # done + (self.hotkey_code, self.hotkey_mods) = (hotkey_code, hotkey_mods) + self.hotkey_play_btn['state'] = tk.NORMAL + self.hotkey_play_btn.focus() # move to next widget - calls hotkeyend() implicitly + else: + if good is None: # clear + (self.hotkey_code, self.hotkey_mods) = (0, 0) + event.widget.delete(0, tk.END) + if self.hotkey_code: + event.widget.insert(0, hotkeymgr.display(self.hotkey_code, self.hotkey_mods)) + self.hotkey_play_btn['state'] = tk.NORMAL + else: + event.widget.insert(0, _('none')) # No hotkey/shortcut currently defined + self.hotkey_play_btn['state'] = tk.DISABLED + self.hotkey_play_btn.focus() # move to next widget - calls hotkeyend() implicitly + return('break') # stops further processing - insertion, Tab traversal etc + + def apply(self): credentials = (config.get('username'), config.get('password')) config.set('username', self.username.get().strip()) config.set('password', self.password.get().strip()) config.set('output', (self.out_eddn.get() and config.OUT_EDDN or 0) + (self.out_bpc.get() and config.OUT_BPC or 0) + (self.out_td.get() and config.OUT_TD or 0) + (self.out_csv.get() and config.OUT_CSV or 0) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS or 0) + (self.out_log.get() and config.OUT_LOG or 0) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS or 0)) config.set('outdir', self.outdir.get().strip()) + config.set('hotkey_code', self.hotkey_code) + config.set('hotkey_mods', self.hotkey_mods) + config.set('hotkey_mute', int(not self.hotkey_play.get())) config.set('anonymous', self.out_anon.get()) - self.destroy() + self._destroy() if credentials != (config.get('username'), config.get('password')) and self.callback: self.callback() + def _destroy(self): + # Re-enable hotkey monitoring before exit + hotkeymgr.register(self.parent, config.getint('hotkey_code'), config.getint('hotkey_mods')) + self.destroy() + + if platform == 'darwin': + def enableshortcuts(self): + self.apply() + # popup System Preferences dialog + try: + # http://stackoverflow.com/questions/6652598/cocoa-button-opens-a-system-preference-page/6658201 + from ScriptingBridge import SBApplication + sysprefs = 'com.apple.systempreferences' + prefs = SBApplication.applicationWithBundleIdentifier_(sysprefs) + pane = [x for x in prefs.panes() if x.id() == 'com.apple.preference.security'][0] + prefs.setCurrentPane_(pane) + anchor = [x for x in pane.anchors() if x.name() == 'Privacy_Accessibility'][0] + anchor.reveal() + prefs.activate() + except: + AXIsProcessTrustedWithOptions({kAXTrustedCheckOptionPrompt: True}) + self.parent.event_generate('<>', when="tail") + class AuthenticationDialog(tk.Toplevel): diff --git a/setup.py b/setup.py index a3eda7cc..90cae6ff 100755 --- a/setup.py +++ b/setup.py @@ -57,13 +57,14 @@ VERSION = re.search(r"^appversion\s*=\s*'(.+)'", file('config.py').read(), re.MU SHORTVERSION = ''.join(VERSION.split('.')[:3]) if sys.platform=='darwin': - OPTIONS = { 'py2app': - {'dist_dir': dist_dir, + OPTIONS = { 'py2app': + {'dist_dir': dist_dir, 'optimize': 2, 'packages': [ 'requests' ], 'frameworks': [ 'Sparkle.framework' ], 'excludes': [ 'PIL', 'simplejson' ], 'iconfile': '%s.icns' % APPNAME, + 'resources': ['snd_good.wav', 'snd_bad.wav'], 'semi_standalone': True, 'site_packages': False, 'plist': { @@ -98,6 +99,8 @@ elif sys.platform=='win32': DATA_FILES = [ ('', [requests.certs.where(), 'WinSparkle.dll', 'WinSparkle.pdb', # For debugging - don't include in package + 'snd_good.wav', + 'snd_bad.wav', '%s.VisualElementsManifest.xml' % APPNAME, '%s.ico' % APPNAME ] + [join('L10n',x) for x in os.listdir('L10n') if x.endswith('.strings')] ) ] diff --git a/snd_bad.wav b/snd_bad.wav new file mode 100644 index 00000000..56818536 Binary files /dev/null and b/snd_bad.wav differ diff --git a/snd_good.wav b/snd_good.wav new file mode 100644 index 00000000..fbeb2548 Binary files /dev/null and b/snd_good.wav differ