mirror of
https://github.com/EDCD/EDMarketConnector.git
synced 2025-04-12 15:27:14 +03:00
Add hotkey/shortcut support.
This commit is contained in:
parent
edcda94e0f
commit
9273e1e3d7
@ -26,6 +26,7 @@ import coriolis
|
|||||||
import flightlog
|
import flightlog
|
||||||
import prefs
|
import prefs
|
||||||
from config import appname, applongname, config
|
from config import appname, applongname, config
|
||||||
|
from hotkey import hotkeymgr
|
||||||
|
|
||||||
l10n.Translations().install()
|
l10n.Translations().install()
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ class AppWindow:
|
|||||||
apple_menu.add_command(label=_("Check for Updates..."), command=lambda:self.updater.checkForUpdates())
|
apple_menu.add_command(label=_("Check for Updates..."), command=lambda:self.updater.checkForUpdates())
|
||||||
menubar.add_cascade(menu=apple_menu)
|
menubar.add_cascade(menu=apple_menu)
|
||||||
window_menu = tk.Menu(menubar, name='window')
|
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
|
# https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm
|
||||||
self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0')
|
self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0')
|
||||||
self.w.createcommand('tkAboutDialog', lambda:self.w.call('tk::mac::standardAboutPanel'))
|
self.w.createcommand('tkAboutDialog', lambda:self.w.call('tk::mac::standardAboutPanel'))
|
||||||
@ -109,10 +110,10 @@ class AppWindow:
|
|||||||
else:
|
else:
|
||||||
file_menu = tk.Menu(menubar, tearoff=tk.FALSE)
|
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=_("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_separator()
|
||||||
file_menu.add_command(label=_("Exit"), command=self.onexit) # Menu item
|
file_menu.add_command(label=_("Exit"), command=self.onexit) # Item in the File menu on Windows
|
||||||
menubar.add_cascade(label=_("File"), menu=file_menu) # Top-level menu on Windows
|
menubar.add_cascade(label=_("File"), menu=file_menu) # Menu title on Windows
|
||||||
self.w.protocol("WM_DELETE_WINDOW", self.onexit)
|
self.w.protocol("WM_DELETE_WINDOW", self.onexit)
|
||||||
if platform == 'linux2':
|
if platform == 'linux2':
|
||||||
# Fix up menu to use same styling as everything else
|
# Fix up menu to use same styling as everything else
|
||||||
@ -147,6 +148,10 @@ class AppWindow:
|
|||||||
self.updater = update.Updater(self.w)
|
self.updater = update.Updater(self.w)
|
||||||
self.w.bind_all('<<Quit>>', self.onexit) # user-generated
|
self.w.bind_all('<<Quit>>', self.onexit) # user-generated
|
||||||
|
|
||||||
|
# Install hotkey monitoring
|
||||||
|
self.w.bind_all('<<Invoke>>', 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
|
# call after credentials have changed
|
||||||
def login(self):
|
def login(self):
|
||||||
@ -187,8 +192,13 @@ class AppWindow:
|
|||||||
|
|
||||||
def getandsend(self, event=None, retrying=False):
|
def getandsend(self, event=None, retrying=False):
|
||||||
|
|
||||||
|
play_sound = event and event.type=='35' and not config.getint('hotkey_mute')
|
||||||
|
|
||||||
if not retrying:
|
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.cmdr['text'] = self.system['text'] = self.station['text'] = ''
|
||||||
self.status['text'] = _('Fetching station data...')
|
self.status['text'] = _('Fetching station data...')
|
||||||
self.button['state'] = tk.DISABLED
|
self.button['state'] = tk.DISABLED
|
||||||
@ -209,14 +219,17 @@ class AppWindow:
|
|||||||
# Validation
|
# Validation
|
||||||
if not data.get('commander') or not data['commander'].get('name','').strip():
|
if not data.get('commander') or not data['commander'].get('name','').strip():
|
||||||
self.status['text'] = _("Who are you?!") # Shouldn't happen
|
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():
|
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
|
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():
|
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
|
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:
|
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).
|
# 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
|
# Stuff we can do while waiting for retry
|
||||||
if config.getint('output') & config.OUT_LOG:
|
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)):
|
if not (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)):
|
||||||
# no further output requested
|
# 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')
|
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'):
|
elif not data['commander'].get('docked'):
|
||||||
self.status['text'] = _("You're not docked at a station!")
|
self.status['text'] = _("You're not docked at a station!")
|
||||||
|
if play_sound: hotkeymgr.play_bad()
|
||||||
else:
|
else:
|
||||||
if data['lastStarport'].get('commodities'):
|
if data['lastStarport'].get('commodities'):
|
||||||
# Fixup anomalies in the commodity data
|
# Fixup anomalies in the commodity data
|
||||||
@ -267,12 +282,16 @@ class AppWindow:
|
|||||||
self.w.update_idletasks()
|
self.w.update_idletasks()
|
||||||
eddn.export(data)
|
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')
|
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:
|
else:
|
||||||
self.status['text'] = _("Station doesn't have anything!")
|
self.status['text'] = _("Station doesn't have anything!")
|
||||||
|
if play_sound: hotkeymgr.play_good() # not really an error
|
||||||
elif not data['lastStarport'].get('commodities'):
|
elif not data['lastStarport'].get('commodities'):
|
||||||
self.status['text'] = _("Station doesn't have a market!")
|
self.status['text'] = _("Station doesn't have a market!")
|
||||||
|
if play_sound: hotkeymgr.play_good() # not really an error
|
||||||
else:
|
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')
|
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:
|
except companion.VerificationRequired:
|
||||||
return prefs.AuthenticationDialog(self.w, self.verify)
|
return prefs.AuthenticationDialog(self.w, self.verify)
|
||||||
@ -280,18 +299,22 @@ class AppWindow:
|
|||||||
# Companion API problem
|
# Companion API problem
|
||||||
except companion.ServerError as e:
|
except companion.ServerError as e:
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
|
if play_sound: hotkeymgr.play_bad()
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError as e:
|
except requests.exceptions.ConnectionError as e:
|
||||||
if __debug__: print_exc()
|
if __debug__: print_exc()
|
||||||
self.status['text'] = _("Error: Can't connect to EDDN")
|
self.status['text'] = _("Error: Can't connect to EDDN")
|
||||||
|
if play_sound: hotkeymgr.play_bad()
|
||||||
|
|
||||||
except requests.exceptions.Timeout as e:
|
except requests.exceptions.Timeout as e:
|
||||||
if __debug__: print_exc()
|
if __debug__: print_exc()
|
||||||
self.status['text'] = _("Error: Connection to EDDN timed out")
|
self.status['text'] = _("Error: Connection to EDDN timed out")
|
||||||
|
if play_sound: hotkeymgr.play_bad()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if __debug__: print_exc()
|
if __debug__: print_exc()
|
||||||
self.status['text'] = str(e)
|
self.status['text'] = str(e)
|
||||||
|
if play_sound: hotkeymgr.play_bad()
|
||||||
|
|
||||||
self.cooldown()
|
self.cooldown()
|
||||||
|
|
||||||
|
@ -98,6 +98,12 @@
|
|||||||
<Component Guid="{9DBAB544-E815-40A5-866A-391B68919344}">
|
<Component Guid="{9DBAB544-E815-40A5-866A-391B68919344}">
|
||||||
<File KeyPath="yes" Source="SourceDir\select.pyd" />
|
<File KeyPath="yes" Source="SourceDir\select.pyd" />
|
||||||
</Component>
|
</Component>
|
||||||
|
<Component Guid="*">
|
||||||
|
<File KeyPath="yes" Source="SourceDir\snd_good.wav" />
|
||||||
|
</Component>
|
||||||
|
<Component Guid="*">
|
||||||
|
<File KeyPath="yes" Source="SourceDir\snd_bad.wav" />
|
||||||
|
</Component>
|
||||||
<Component Guid="{30EEAD30-A43B-4A31-A209-450A8AD17AC2}">
|
<Component Guid="{30EEAD30-A43B-4A31-A209-450A8AD17AC2}">
|
||||||
<File KeyPath="yes" Source="SourceDir\tcl85.dll" />
|
<File KeyPath="yes" Source="SourceDir\tcl85.dll" />
|
||||||
</Component>
|
</Component>
|
||||||
@ -107,6 +113,9 @@
|
|||||||
<Component Guid="{E8E3701A-8AA1-4D46-A56D-7AF08D6AFCD4}">
|
<Component Guid="{E8E3701A-8AA1-4D46-A56D-7AF08D6AFCD4}">
|
||||||
<File KeyPath="yes" Source="SourceDir\unicodedata.pyd" />
|
<File KeyPath="yes" Source="SourceDir\unicodedata.pyd" />
|
||||||
</Component>
|
</Component>
|
||||||
|
<Component Guid="*">
|
||||||
|
<File KeyPath="yes" Source="SourceDir\winsound.pyd" />
|
||||||
|
</Component>
|
||||||
<Component Guid="{3117D2CF-1D87-4B99-BE44-7BDDFE8C8E60}">
|
<Component Guid="{3117D2CF-1D87-4B99-BE44-7BDDFE8C8E60}">
|
||||||
<File KeyPath="yes" Source="SourceDir\WinSparkle.dll" />
|
<File KeyPath="yes" Source="SourceDir\WinSparkle.dll" />
|
||||||
</Component>
|
</Component>
|
||||||
@ -344,9 +353,12 @@
|
|||||||
<ComponentRef Id="pyexpat.pyd" />
|
<ComponentRef Id="pyexpat.pyd" />
|
||||||
<ComponentRef Id="python27.dll" />
|
<ComponentRef Id="python27.dll" />
|
||||||
<ComponentRef Id="select.pyd" />
|
<ComponentRef Id="select.pyd" />
|
||||||
|
<ComponentRef Id="snd_good.wav" />
|
||||||
|
<ComponentRef Id="snd_bad.wav" />
|
||||||
<ComponentRef Id="tcl85.dll" />
|
<ComponentRef Id="tcl85.dll" />
|
||||||
<ComponentRef Id="tk85.dll" />
|
<ComponentRef Id="tk85.dll" />
|
||||||
<ComponentRef Id="unicodedata.pyd" />
|
<ComponentRef Id="unicodedata.pyd" />
|
||||||
|
<ComponentRef Id="winsound.pyd" />
|
||||||
<ComponentRef Id="WinSparkle.dll" />
|
<ComponentRef Id="WinSparkle.dll" />
|
||||||
<ComponentRef Id="auto.tcl" />
|
<ComponentRef Id="auto.tcl" />
|
||||||
<ComponentRef Id="clock.tcl" />
|
<ComponentRef Id="clock.tcl" />
|
||||||
|
153
L10n/en.template
153
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.";
|
"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}";
|
"About {APP}" = "About {APP}";
|
||||||
|
|
||||||
/* Folder selection button on Windows. [prefs.py:101] */
|
/* Folder selection button on Windows. [prefs.py:113] */
|
||||||
"Browse..." = "Browse...";
|
"Browse..." = "Browse...";
|
||||||
|
|
||||||
/* Folder selection button on OSX. [prefs.py:100] */
|
/* Folder selection button on OSX. [prefs.py:112] */
|
||||||
"Change..." = "Change...";
|
"Change..." = "Change...";
|
||||||
|
|
||||||
/* [EDMarketConnector.py:98] */
|
/* [EDMarketConnector.py:99] */
|
||||||
"Check for Updates..." = "Check for Updates...";
|
"Check for Updates..." = "Check for Updates...";
|
||||||
|
|
||||||
/* Privacy setting. [prefs.py:114] */
|
/* Privacy setting. [prefs.py:149] */
|
||||||
"Cmdr name" = "Cmdr name";
|
"Cmdr name" = "Cmdr name";
|
||||||
|
|
||||||
/* Main window. [EDMarketConnector.py:71] */
|
/* Main window. [EDMarketConnector.py:72] */
|
||||||
"Cmdr:" = "Cmdr:";
|
"Cmdr:" = "Cmdr:";
|
||||||
|
|
||||||
/* Update button in main window. [EDMarketConnector.py:300] */
|
/* Update button in main window. [EDMarketConnector.py:323] */
|
||||||
"cooldown {SS}s" = "cooldown {SS}s";
|
"cooldown {SS}s" = "cooldown {SS}s";
|
||||||
|
|
||||||
/* Section heading in settings. [prefs.py:58] */
|
/* Section heading in settings. [prefs.py:70] */
|
||||||
"Credentials" = "Credentials";
|
"Credentials" = "Credentials";
|
||||||
|
|
||||||
/* [EDMarketConnector.py:286] */
|
/* [EDMarketConnector.py:306] */
|
||||||
"Error: Can't connect to EDDN" = "Error: Can't connect to EDDN";
|
"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";
|
"Error: Connection to EDDN timed out" = "Error: Connection to EDDN timed out";
|
||||||
|
|
||||||
/* [companion.py:107] */
|
/* [companion.py:109] */
|
||||||
"Error: Invalid Credentials" = "Error: Invalid Credentials";
|
"Error: Invalid Credentials" = "Error: Invalid Credentials";
|
||||||
|
|
||||||
/* [companion.py:103] */
|
/* [companion.py:103] */
|
||||||
"Error: Server is down" = "Error: Server is down";
|
"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";
|
"Exit" = "Exit";
|
||||||
|
|
||||||
/* [EDMarketConnector.py:193] */
|
/* [EDMarketConnector.py:203] */
|
||||||
"Fetching station data..." = "Fetching station data...";
|
"Fetching station data..." = "Fetching station data...";
|
||||||
|
|
||||||
/* Top-level menu on Windows. [EDMarketConnector.py:115] */
|
/* Menu title on Windows. [EDMarketConnector.py:116] */
|
||||||
"File" = "File";
|
"File" = "File";
|
||||||
|
|
||||||
/* Output folder prompt on Windows. [prefs.py:99] */
|
/* Output folder prompt on Windows. [prefs.py:111] */
|
||||||
"File location:" = "File location:";
|
"File location:" = "File location:";
|
||||||
|
|
||||||
/* [prefs.py:96] */
|
/* [prefs.py:108] */
|
||||||
"Flight log" = "Flight log";
|
"Flight log" = "Flight log";
|
||||||
|
|
||||||
/* Shouldn't happen. [companion.py:176] */
|
/* Section heading in settings on Windows. [prefs.py:125] */
|
||||||
"General error" = "General error";
|
"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";
|
"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}";
|
"Last updated at {HH}:{MM}:{SS}" = "Last updated at {HH}:{MM}:{SS}";
|
||||||
|
|
||||||
/* [EDMarketConnector.py:153] */
|
/* [EDMarketConnector.py:158] */
|
||||||
"Logging in..." = "Logging in...";
|
"Logging in..." = "Logging in...";
|
||||||
|
|
||||||
/* [prefs.py:90] */
|
/* [prefs.py:102] */
|
||||||
"Market data in CSV format" = "Market data in CSV format";
|
"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";
|
"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";
|
"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";
|
"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";
|
"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";
|
"Password" = "Password";
|
||||||
|
|
||||||
/* [prefs.py:82] */
|
/* Hotkey/Shortcut setting. [prefs.py:140] */
|
||||||
"Please choose what data to save" = "Please choose what data to save";
|
"Play sound" = "Play sound";
|
||||||
|
|
||||||
/* 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";
|
|
||||||
|
|
||||||
/* [prefs.py:94] */
|
/* [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";
|
"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";
|
"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!";
|
"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!";
|
"Station doesn't have anything!" = "Station doesn't have anything!";
|
||||||
|
|
||||||
/* Main window. [EDMarketConnector.py:73] */
|
/* Main window. [EDMarketConnector.py:74] */
|
||||||
"Station:" = "Station:";
|
"Station:" = "Station:";
|
||||||
|
|
||||||
/* Main window. [EDMarketConnector.py:72] */
|
/* Main window. [EDMarketConnector.py:73] */
|
||||||
"System:" = "System:";
|
"System:" = "System:";
|
||||||
|
|
||||||
/* Update button in main window. [EDMarketConnector.py:78] */
|
/* Update button in main window. [EDMarketConnector.py:79] */
|
||||||
"Update" = "Update";
|
"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)";
|
"Username (Email)" = "Username (Email)";
|
||||||
|
|
||||||
/* Shouldn't happen. [EDMarketConnector.py:215] */
|
/* Shouldn't happen. [EDMarketConnector.py:227] */
|
||||||
"What are you flying?!" = "What are you flying?!";
|
"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?!";
|
"Where are you?!" = "Where are you?!";
|
||||||
|
|
||||||
/* Output folder prompt on OSX. [prefs.py:98] */
|
/* Output folder prompt on OSX. [prefs.py:110] */
|
||||||
"Where:" = "Where:";
|
"Where:" = "Where:";
|
||||||
|
|
||||||
/* Shouldn't happen. [EDMarketConnector.py:211] */
|
/* Shouldn't happen. [EDMarketConnector.py:221] */
|
||||||
"Who are you?!" = "Who are you?!";
|
"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!";
|
"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";
|
||||||
|
|
||||||
|
@ -37,13 +37,13 @@
|
|||||||
/* [companion.py:103] */
|
/* [companion.py:103] */
|
||||||
"Error: Server is down" = "Erreur : Le serveur est indisponible";
|
"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";
|
"Exit" = "Quitter";
|
||||||
|
|
||||||
/* [EDMarketConnector.py:193] */
|
/* [EDMarketConnector.py:193] */
|
||||||
"Fetching station data..." = "Récupération des données de la station...";
|
"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";
|
"File" = "Fichier";
|
||||||
|
|
||||||
/* Output folder prompt on Windows. [prefs.py:99] */
|
/* Output folder prompt on Windows. [prefs.py:99] */
|
||||||
@ -103,7 +103,7 @@
|
|||||||
/* [EDMarketConnector.py:266] */
|
/* [EDMarketConnector.py:266] */
|
||||||
"Sending data to EDDN..." = "Envoi des données à EDDN...";
|
"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";
|
"Settings" = "Paramètres";
|
||||||
|
|
||||||
/* [prefs.py:94] */
|
/* [prefs.py:94] */
|
||||||
@ -143,4 +143,28 @@
|
|||||||
"Who are you?!" = "Qui êtes-vous ?";
|
"Who are you?!" = "Qui êtes-vous ?";
|
||||||
|
|
||||||
/* [EDMarketConnector.py:251] */
|
/* [EDMarketConnector.py:251] */
|
||||||
"You're not docked at a station!" = "Vous n'êtes pas amarré à une station !";
|
"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" = "";
|
||||||
|
@ -37,13 +37,13 @@
|
|||||||
/* [companion.py:103] */
|
/* [companion.py:103] */
|
||||||
"Error: Server is down" = "Errore: Il Server è offline";
|
"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";
|
"Exit" = "Esci";
|
||||||
|
|
||||||
/* [EDMarketConnector.py:193] */
|
/* [EDMarketConnector.py:193] */
|
||||||
"Fetching station data..." = "Raccogliendo i dati della Stazione...";
|
"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";
|
"File" = "File";
|
||||||
|
|
||||||
/* Output folder prompt on Windows. [prefs.py:99] */
|
/* Output folder prompt on Windows. [prefs.py:99] */
|
||||||
@ -104,7 +104,7 @@
|
|||||||
/* [EDMarketConnector.py:266] */
|
/* [EDMarketConnector.py:266] */
|
||||||
"Sending data to EDDN..." = "Sto inviando i dati a EDDN...";
|
"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";
|
"Settings" = "Impostazioni";
|
||||||
|
|
||||||
/* [prefs.py:94] */
|
/* [prefs.py:94] */
|
||||||
@ -145,3 +145,27 @@
|
|||||||
|
|
||||||
/* [EDMarketConnector.py:251] */
|
/* [EDMarketConnector.py:251] */
|
||||||
"You're not docked at a station!" = "Non sei parcheggiato in nessuna stazione !";
|
"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" = "";
|
||||||
|
@ -37,13 +37,13 @@
|
|||||||
/* [companion.py:103] */
|
/* [companion.py:103] */
|
||||||
"Error: Server is down" = "Błąd: Serwer padł";
|
"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";
|
"Exit" = "Zakończ";
|
||||||
|
|
||||||
/* [EDMarketConnector.py:193] */
|
/* [EDMarketConnector.py:193] */
|
||||||
"Fetching station data..." = "Pobieranie danych o stacji...";
|
"Fetching station data..." = "Pobieranie danych o stacji...";
|
||||||
|
|
||||||
/* Top-level menu on Windows. [EDMarketConnector.py:115] */
|
/* Menu title on Windows. [EDMarketConnector.py:116] */
|
||||||
"File" = "Plik";
|
"File" = "Plik";
|
||||||
|
|
||||||
/* Output folder prompt on Windows. [prefs.py:99] */
|
/* Output folder prompt on Windows. [prefs.py:99] */
|
||||||
@ -103,7 +103,7 @@
|
|||||||
/* [EDMarketConnector.py:266] */
|
/* [EDMarketConnector.py:266] */
|
||||||
"Sending data to EDDN..." = "Wysłanie danych do EDDN...";
|
"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";
|
"Settings" = "Ustawienia";
|
||||||
|
|
||||||
/* [prefs.py:94] */
|
/* [prefs.py:94] */
|
||||||
@ -144,3 +144,27 @@
|
|||||||
|
|
||||||
/* [EDMarketConnector.py:251] */
|
/* [EDMarketConnector.py:251] */
|
||||||
"You're not docked at a station!" = "Nie jesteś zadokowany do stacji!";
|
"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" = "";
|
||||||
|
@ -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
|
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.
|
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.
|
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
|
Note that each “verification code” is one-time only - if you enter the code incorrectly or quit the app before
|
||||||
|
381
hotkey.py
Normal file
381
hotkey.py
Normal file
@ -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('<<Invoke>>', 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('<KeyPress>', 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('<<Invoke>>', 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()
|
111
prefs.py
111
prefs.py
@ -8,10 +8,22 @@ import Tkinter as tk
|
|||||||
import ttk
|
import ttk
|
||||||
import tkFileDialog
|
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
|
# sigh tkFileDialog.askdirectory doesn't support unicode on Windows
|
||||||
import ctypes
|
import ctypes
|
||||||
from ctypes.wintypes import *
|
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
|
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
|
_('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.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.insert(0, config.get('outdir'))
|
||||||
self.outdir.grid(row=9, columnspan=2, padx=5, pady=5, sticky=tk.EW)
|
self.outdir.grid(row=9, columnspan=2, padx=5, pady=5, sticky=tk.EW)
|
||||||
self.outvarchanged()
|
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('<FocusIn>', self.hotkeystart)
|
||||||
|
self.hotkey_text.bind('<FocusOut>', 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 = ttk.LabelFrame(frame, text=_('Privacy')) # Section heading in settings
|
||||||
privacyframe.grid(padx=10, pady=10, sticky=tk.NSEW)
|
privacyframe.grid(padx=10, pady=10, sticky=tk.NSEW)
|
||||||
privacyframe.columnconfigure(0, weight=1)
|
privacyframe.columnconfigure(0, weight=1)
|
||||||
@ -122,6 +157,10 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
buttonframe.columnconfigure(0, weight=1)
|
buttonframe.columnconfigure(0, weight=1)
|
||||||
ttk.Label(buttonframe).grid(row=0, column=0) # spacer
|
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)
|
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
|
# wait for window to appear on screen before calling grab_set
|
||||||
self.wait_visibility()
|
self.wait_visibility()
|
||||||
@ -164,17 +203,81 @@ class PreferencesDialog(tk.Toplevel):
|
|||||||
self.outdir.insert(0, d.replace('/', sep))
|
self.outdir.insert(0, d.replace('/', sep))
|
||||||
self.outdir['state'] = 'readonly'
|
self.outdir['state'] = 'readonly'
|
||||||
|
|
||||||
|
def hotkeystart(self, event):
|
||||||
|
event.widget.bind('<KeyPress>', self.hotkeylisten)
|
||||||
|
event.widget.bind('<KeyRelease>', self.hotkeylisten)
|
||||||
|
event.widget.delete(0, tk.END)
|
||||||
|
hotkeymgr.acquire_start()
|
||||||
|
|
||||||
|
def hotkeyend(self, event):
|
||||||
|
event.widget.unbind('<KeyPress>')
|
||||||
|
event.widget.unbind('<KeyRelease>')
|
||||||
|
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):
|
def apply(self):
|
||||||
credentials = (config.get('username'), config.get('password'))
|
credentials = (config.get('username'), config.get('password'))
|
||||||
config.set('username', self.username.get().strip())
|
config.set('username', self.username.get().strip())
|
||||||
config.set('password', self.password.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('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('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())
|
config.set('anonymous', self.out_anon.get())
|
||||||
self.destroy()
|
self._destroy()
|
||||||
if credentials != (config.get('username'), config.get('password')) and self.callback:
|
if credentials != (config.get('username'), config.get('password')) and self.callback:
|
||||||
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('<<Quit>>', when="tail")
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationDialog(tk.Toplevel):
|
class AuthenticationDialog(tk.Toplevel):
|
||||||
|
|
||||||
|
7
setup.py
7
setup.py
@ -57,13 +57,14 @@ VERSION = re.search(r"^appversion\s*=\s*'(.+)'", file('config.py').read(), re.MU
|
|||||||
SHORTVERSION = ''.join(VERSION.split('.')[:3])
|
SHORTVERSION = ''.join(VERSION.split('.')[:3])
|
||||||
|
|
||||||
if sys.platform=='darwin':
|
if sys.platform=='darwin':
|
||||||
OPTIONS = { 'py2app':
|
OPTIONS = { 'py2app':
|
||||||
{'dist_dir': dist_dir,
|
{'dist_dir': dist_dir,
|
||||||
'optimize': 2,
|
'optimize': 2,
|
||||||
'packages': [ 'requests' ],
|
'packages': [ 'requests' ],
|
||||||
'frameworks': [ 'Sparkle.framework' ],
|
'frameworks': [ 'Sparkle.framework' ],
|
||||||
'excludes': [ 'PIL', 'simplejson' ],
|
'excludes': [ 'PIL', 'simplejson' ],
|
||||||
'iconfile': '%s.icns' % APPNAME,
|
'iconfile': '%s.icns' % APPNAME,
|
||||||
|
'resources': ['snd_good.wav', 'snd_bad.wav'],
|
||||||
'semi_standalone': True,
|
'semi_standalone': True,
|
||||||
'site_packages': False,
|
'site_packages': False,
|
||||||
'plist': {
|
'plist': {
|
||||||
@ -98,6 +99,8 @@ elif sys.platform=='win32':
|
|||||||
DATA_FILES = [ ('', [requests.certs.where(),
|
DATA_FILES = [ ('', [requests.certs.where(),
|
||||||
'WinSparkle.dll',
|
'WinSparkle.dll',
|
||||||
'WinSparkle.pdb', # For debugging - don't include in package
|
'WinSparkle.pdb', # For debugging - don't include in package
|
||||||
|
'snd_good.wav',
|
||||||
|
'snd_bad.wav',
|
||||||
'%s.VisualElementsManifest.xml' % APPNAME,
|
'%s.VisualElementsManifest.xml' % APPNAME,
|
||||||
'%s.ico' % APPNAME ] +
|
'%s.ico' % APPNAME ] +
|
||||||
[join('L10n',x) for x in os.listdir('L10n') if x.endswith('.strings')] ) ]
|
[join('L10n',x) for x in os.listdir('L10n') if x.endswith('.strings')] ) ]
|
||||||
|
BIN
snd_bad.wav
Normal file
BIN
snd_bad.wav
Normal file
Binary file not shown.
BIN
snd_good.wav
Normal file
BIN
snd_good.wav
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user