def plugin_app(parent) -> tk.Frame: global system_label, system_status, body_label, body_status frame = tk.Frame(parent) system_label = tk.Label(frame, text=f'{_("System")}:') system_label.grid(row=0, column=0, sticky=tk.W) system_status = HyperlinkLabel(frame, text='', url=None) system_status.grid(row=0, column=2, sticky=tk.E) body_label = tk.Label(frame, text=f'{_("Body")}:') body_label.grid(row=1, column=0, sticky=tk.W) body_status = HyperlinkLabel(frame, text='', url=None) body_status.grid(row=1, column=2, sticky=tk.E) frame.columnconfigure(2, weight=1) update_status() return frame
class PreferencesDialog(tk.Toplevel): def __init__(self, parent, callback): tk.Toplevel.__init__(self, parent) self.parent = parent self.callback = callback self.title(platform=='darwin' and _('Preferences') or _('Settings')) if parent.winfo_viewable(): self.transient(parent) # position over parent if platform!='darwin' or parent.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.geometry("+%d+%d" % (parent.winfo_rootx(), parent.winfo_rooty())) # remove decoration self.resizable(tk.FALSE, tk.FALSE) if platform=='win32': self.attributes('-toolwindow', tk.TRUE) elif platform=='darwin': # http://wiki.tcl.tk/13428 parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') style = ttk.Style() frame = ttk.Frame(self) frame.grid(sticky=tk.NSEW) notebook = nb.Notebook(frame) PADX = 10 BUTTONX = 12 # indent Checkbuttons and Radiobuttons PADY = 2 # close spacing credframe = nb.Frame(notebook) credframe.columnconfigure(1, weight=1) nb.Label(credframe, text=_('Credentials')).grid(padx=PADX, sticky=tk.W) # Section heading in settings ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) nb.Label(credframe, text=_('Please log in with your Elite: Dangerous account details')).grid(padx=PADX, columnspan=2, sticky=tk.W) # Use same text as E:D Launcher's login dialog nb.Label(credframe, text=_('Username (Email)')).grid(row=10, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog nb.Label(credframe, text=_('Password')).grid(row=11, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog self.username = nb.Entry(credframe) self.username.insert(0, config.get('username') or '') self.username.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW) self.username.focus_set() self.password = nb.Entry(credframe, show=u'•') self.password.insert(0, config.get('password') or '') self.password.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) nb.Label(credframe).grid(sticky=tk.W) # big spacer nb.Label(credframe, text=_('Privacy')).grid(padx=PADX, sticky=tk.W) # Section heading in settings ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) self.out_anon= tk.IntVar(value = config.getint('anonymous') and 1) nb.Label(credframe, text=_('How do you want to be identified in the saved data')).grid(columnspan=2, padx=PADX, sticky=tk.W) nb.Radiobutton(credframe, text=_('Cmdr name'), variable=self.out_anon, value=0).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # EDSM & privacy setting nb.Radiobutton(credframe, text=_('Pseudo-anonymized ID'), variable=self.out_anon, value=1).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Privacy setting notebook.add(credframe, text=_('Identity')) # Tab heading in settings outframe = nb.Frame(notebook) outframe.columnconfigure(0, weight=1) output = config.getint('output') or (config.OUT_EDDN | config.OUT_SHIP_EDS) # default settings nb.Label(outframe, text=_('Please choose what data to save')).grid(columnspan=2, padx=PADX, sticky=tk.W) self.out_eddn= tk.IntVar(value = (output & config.OUT_EDDN) and 1) nb.Checkbutton(outframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.out_eddn, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_csv = tk.IntVar(value = (output & config.OUT_CSV ) and 1) nb.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_bpc = tk.IntVar(value = (output & config.OUT_BPC ) and 1) nb.Checkbutton(outframe, text=_("Market data in Slopey's BPC format file"), variable=self.out_bpc, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_td = tk.IntVar(value = (output & config.OUT_TD ) and 1) nb.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_ship_eds= tk.IntVar(value = (output & config.OUT_SHIP_EDS) and 1) nb.Checkbutton(outframe, text=_('Ship loadout in E:D Shipyard format file'), variable=self.out_ship_eds, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) self.out_ship_coriolis= tk.IntVar(value = (output & config.OUT_SHIP_CORIOLIS) and 1) nb.Checkbutton(outframe, text=_('Ship loadout in Coriolis format file'), variable=self.out_ship_coriolis, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_log_file = tk.IntVar(value = (output & config.OUT_LOG_FILE) and 1) nb.Checkbutton(outframe, text=_('Flight log in CSV format file'), variable=self.out_log_file, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) self.out_log_auto = tk.IntVar(value = output & config.OUT_LOG_AUTO and 1 or 0) self.out_log_auto_button = nb.Checkbutton(outframe, text=_('Automatically make a log entry on entering a system'), variable=self.out_log_auto, command=self.outvarchanged) # Output setting self.out_log_auto_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_log_auto_text = nb.Label(outframe, foreground='firebrick') self.out_log_auto_text.grid(columnspan=2, padx=(30,0), sticky=tk.W) self.outdir_label = nb.Label(outframe, text=_('File location')) # Section heading in settings self.outdir_label.grid(padx=BUTTONX, sticky=tk.W) self.outdir = nb.Entry(outframe, takefocus=False) if config.get('outdir').startswith(config.home): self.outdir.insert(0, '~' + config.get('outdir')[len(config.home):]) else: self.outdir.insert(0, config.get('outdir')) self.outdir.grid(row=20, padx=(PADX,0), sticky=tk.EW) self.outbutton = nb.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=20, column=1, padx=PADX) nb.Frame(outframe).grid(pady=5) # bottom spacer notebook.add(outframe, text=_('Output')) # Tab heading in settings edsmframe = nb.Frame(notebook) edsmframe.columnconfigure(1, weight=1) HyperlinkLabel(edsmframe, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='http://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate ttk.Separator(edsmframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) self.out_log_edsm = tk.IntVar(value = (output & config.OUT_LOG_EDSM) and 1) nb.Checkbutton(edsmframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.out_log_edsm, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.edsm_autoopen = tk.BooleanVar(value = config.getint('edsm_autoopen')) self.edsm_autoopen_button = nb.Checkbutton(edsmframe, text=_(u"Automatically open uncharted systems’ EDSM pages"), variable=self.edsm_autoopen) self.edsm_autoopen_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.edsm_log_auto_button = nb.Checkbutton(edsmframe, text=_('Automatically make a log entry on entering a system'), variable=self.out_log_auto, command=self.outvarchanged) # Output setting self.edsm_log_auto_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.edsm_log_auto_text = nb.Label(edsmframe, foreground='firebrick') self.edsm_log_auto_text.grid(columnspan=2, padx=(30,0), sticky=tk.W) self.edsm_label = HyperlinkLabel(edsmframe, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='http://www.edsm.net/settings/api', underline=True) # Section heading in settings self.edsm_label.grid(columnspan=2, padx=PADX, sticky=tk.W) self.edsm_cmdr_label = nb.Label(edsmframe, text=_('Cmdr name')) # EDSM & privacy setting self.edsm_cmdr_label.grid(row=10, padx=PADX, sticky=tk.W) self.edsm_cmdr = nb.Entry(edsmframe) self.edsm_cmdr.insert(0, config.get('edsm_cmdrname') or '') self.edsm_cmdr.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW) self.edsm_apikey_label = nb.Label(edsmframe, text=_('API Key')) # EDSM setting self.edsm_apikey_label.grid(row=11, padx=PADX, sticky=tk.W) self.edsm_apikey = nb.Entry(edsmframe) self.edsm_apikey.insert(0, config.get('edsm_apikey') or '') self.edsm_apikey.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) notebook.add(edsmframe, text='EDSM') # Not translated if platform in ['darwin','win32']: self.hotkey_code = config.getint('hotkey_code') self.hotkey_mods = config.getint('hotkey_mods') self.hotkey_only = tk.IntVar(value = not config.getint('hotkey_always')) self.hotkey_play = tk.IntVar(value = not config.getint('hotkey_mute')) hotkeyframe = nb.Frame(notebook) hotkeyframe.columnconfigure(1, weight=1) nb.Label(hotkeyframe).grid(sticky=tk.W) # big spacer if platform == 'darwin' and not was_accessible_at_launch: if AXIsProcessTrusted(): nb.Label(hotkeyframe, text = _('Re-start {APP} to use shortcuts').format(APP=applongname), foreground='firebrick').grid(padx=PADX, sticky=tk.NSEW) # Shortcut settings prompt on OSX else: nb.Label(hotkeyframe, text = _('{APP} needs permission to use shortcuts').format(APP=applongname), foreground='firebrick').grid(columnspan=2, padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX nb.Button(hotkeyframe, text = _('Open System Preferences'), command = self.enableshortcuts).grid(column=1, padx=PADX, sticky=tk.E) # Shortcut settings button on OSX else: self.hotkey_text = nb.Entry(hotkeyframe, width = (platform == 'darwin' and 20 or 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) nb.Label(hotkeyframe, text = platform=='darwin' and _('Keyboard shortcut') or # Tab heading in settings on OSX _('Hotkey') # Tab heading in settings on Windows ).grid(row=10, column=0, padx=PADX, sticky=tk.NSEW) self.hotkey_text.grid(row=10, column=1, padx=PADX, sticky=tk.NSEW) nb.Label(hotkeyframe).grid(sticky=tk.W) # big spacer self.hotkey_only_btn = nb.Checkbutton(hotkeyframe, text=_('Only when Elite: Dangerous is the active app'), variable=self.hotkey_only, state = self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting self.hotkey_only_btn.grid(columnspan=2, padx=PADX, sticky=tk.W) self.hotkey_play_btn = nb.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(columnspan=2, padx=PADX, sticky=tk.W) notebook.add(hotkeyframe, text = platform=='darwin' and _('Keyboard shortcut') or # Tab heading in settings on OSX _('Hotkey')) # Tab heading in settings on Windows # build plugin prefs tabs for plugname in plug.PLUGINS: plugframe = plug.get_plugin_pref(plugname, notebook) if plugframe: notebook.add(plugframe, text=plugname) if platform=='darwin': self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes else: buttonframe = ttk.Frame(frame) buttonframe.grid(padx=PADX, pady=PADX, sticky=tk.NSEW) buttonframe.columnconfigure(0, weight=1) ttk.Label(buttonframe).grid(row=0, column=0) # spacer button = ttk.Button(buttonframe, text=_('OK'), command=self.apply) button.grid(row=0, column=1, sticky=tk.E) button.bind("<Return>", lambda event:self.apply()) self.protocol("WM_DELETE_WINDOW", self._destroy) # Selectively disable buttons depending on output settings self.proxypoll() # disable hotkey for the duration hotkeymgr.unregister() # wait for window to appear on screen before calling grab_set self.wait_visibility() self.grab_set() #self.wait_window(self) # causes duplicate events on OSX def proxypoll(self): self.outvarchanged() self.after(1000, self.proxypoll) def outvarchanged(self): local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship_eds.get() or self.out_ship_coriolis.get() or self.out_log_file.get() self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED edsm_state = self.out_log_edsm.get() and tk.NORMAL or tk.DISABLED self.edsm_autoopen_button['state'] = edsm_state self.edsm_label['state'] = edsm_state self.edsm_cmdr_label['state'] = edsm_state self.edsm_apikey_label['state'] = edsm_state self.edsm_cmdr['state'] = edsm_state self.edsm_apikey['state'] = edsm_state proxyaddr = edproxy.status() self.out_log_auto_text['text'] = '' self.edsm_log_auto_text['text'] = '' if monitor.logdir or proxyaddr: log = self.out_log_file.get() self.out_log_auto_button['state'] = log and tk.NORMAL or tk.DISABLED if log and self.out_log_auto.get(): if proxyaddr: self.out_log_auto_text['text'] = _('Connected to {EDPROXY} at {ADDR}').format(EDPROXY = 'edproxy', ADDR = proxyaddr) # Output settings elif not monitor.enable_logging(): self.out_log_auto_text['text'] = "Can't enable automatic logging!" # Shouldn't happen - don't translate elif monitor.restart_required(): self.out_log_auto_text['text'] = _('Re-start Elite: Dangerous to use this feature') # Output settings prompt self.edsm_log_auto_button['state'] = edsm_state if self.out_log_edsm.get() and self.out_log_auto.get(): if proxyaddr: self.edsm_log_auto_text['text'] = _('Connected to {EDPROXY} at {ADDR}').format(EDPROXY = 'edproxy', ADDR = proxyaddr) # Output settings elif not monitor.enable_logging(): self.edsm_log_auto_text['text'] = "Can't enable automatic logging!" # Shouldn't happen - don't translate elif monitor.restart_required(): self.edsm_log_auto_text['text'] = _('Re-start Elite: Dangerous to use this feature') # Output settings prompt else: self.out_log_auto_button['state'] = tk.DISABLED self.edsm_log_auto_button['state'] = tk.DISABLED def outbrowse(self): if platform != 'win32': d = tkFileDialog.askdirectory(parent=self, initialdir=expanduser(self.outdir.get()), title=_('File location'), mustexist=tk.TRUE) else: def browsecallback(hwnd, uMsg, lParam, lpData): # set initial folder if uMsg==BFFM_INITIALIZED and lpData: ctypes.windll.user32.SendMessageW(hwnd, BFFM_SETSELECTION, 1, lpData); return 0 browseInfo = BROWSEINFO() browseInfo.lpszTitle = _('File location') browseInfo.ulFlags = BIF_RETURNONLYFSDIRS|BIF_USENEWUI browseInfo.lpfn = BrowseCallbackProc(browsecallback) browseInfo.lParam = self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[1:]) or self.outdir.get() ctypes.windll.ole32.CoInitialize(None) pidl = ctypes.windll.shell32.SHBrowseForFolderW(ctypes.byref(browseInfo)) if pidl: path = ctypes.create_unicode_buffer(MAX_PATH) ctypes.windll.shell32.SHGetPathFromIDListW(pidl, path) ctypes.windll.ole32.CoTaskMemFree(pidl) d = path.value else: d = None if d: self.outdir['state'] = tk.NORMAL # must be writable to update self.outdir.delete(0, tk.END) if d.startswith(config.home): self.outdir.insert(0, '~' + d[len(config.home):]) else: self.outdir.insert(0, d) 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_only_btn['state'] = tk.NORMAL self.hotkey_play_btn['state'] = tk.NORMAL self.hotkey_only_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_only_btn['state'] = tk.NORMAL self.hotkey_play_btn['state'] = tk.NORMAL else: event.widget.insert(0, _('None')) # No hotkey/shortcut currently defined self.hotkey_only_btn['state'] = tk.DISABLED self.hotkey_play_btn['state'] = tk.DISABLED self.hotkey_only_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) + (self.out_bpc.get() and config.OUT_BPC) + (self.out_td.get() and config.OUT_TD) + (self.out_csv.get() and config.OUT_CSV) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS) + (self.out_log_file.get() and config.OUT_LOG_FILE) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) + (self.out_log_edsm.get() and config.OUT_LOG_EDSM) + (self.out_log_auto.get() and config.OUT_LOG_AUTO)) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) config.set('edsm_autoopen', self.edsm_autoopen.get()) config.set('edsm_cmdrname', self.edsm_cmdr.get().strip()) config.set('edsm_apikey', self.edsm_apikey.get().strip()) if platform in ['darwin','win32']: config.set('hotkey_code', self.hotkey_code) config.set('hotkey_mods', self.hotkey_mods) config.set('hotkey_always', int(not self.hotkey_only.get())) config.set('hotkey_mute', int(not self.hotkey_play.get())) config.set('anonymous', self.out_anon.get()) self._destroy() if credentials != (config.get('username'), config.get('password')) and self.callback: self.callback() def _destroy(self): # Re-enable hotkey and log monitoring before exit hotkeymgr.register(self.parent, config.getint('hotkey_code'), config.getint('hotkey_mods')) if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_FILE|config.OUT_LOG_EDSM)): monitor.enable_logging() monitor.start(self.parent) edproxy.start(self.parent) else: monitor.stop() edproxy.stop() 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 AppWindow: STATION_UNDOCKED = u'×' # "Station" name to display when not docked = U+00D7 def __init__(self, master): self.holdofftime = config.getint('querytime') + companion.holdoff self.session = companion.Session() self.edsm = edsm.EDSM() self.w = master self.w.title(applongname) self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') elif platform == 'linux2': from PIL import Image, ImageTk icon = ImageTk.PhotoImage(Image.open("EDMarketConnector.png")) self.w.tk.call('wm', 'iconphoto', self.w, '-default', icon) style = ttk.Style() style.theme_use('clam') elif platform == 'darwin': # Default ttk font choice looks bad on El Capitan font = tkFont.Font(family='TkDefaultFont', size=13, weight=tkFont.NORMAL) style = ttk.Style() style.configure('TLabel', font=font) style.configure('TButton', font=font) style.configure('TLabelframe.Label', font=font) style.configure('TCheckbutton', font=font) style.configure('TRadiobutton', font=font) style.configure('TEntry', font=font) frame = ttk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) frame.rowconfigure(4, weight=1) ttk.Label(frame, text=_('Cmdr') + ':').grid(row=0, column=0, sticky=tk.W) # Main window ttk.Label(frame, text=_('System') + ':').grid(row=1, column=0, sticky=tk.W) # Main window ttk.Label(frame, text=_('Station') + ':').grid(row=2, column=0, sticky=tk.W) # Main window self.cmdr = ttk.Label(frame, width=-21) self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.system_url, popup_copy=True) self.station = HyperlinkLabel( frame, url=self.station_url, popup_copy=lambda x: x != self.STATION_UNDOCKED) self.button = ttk.Button( frame, name='update', text=_('Update'), command=self.getandsend, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.status = ttk.Label(frame, name='status', width=-25) self.w.bind('<Return>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend) self.cmdr.grid(row=0, column=1, sticky=tk.EW) self.system.grid(row=1, column=1, sticky=tk.EW) self.station.grid(row=2, column=1, sticky=tk.EW) self.button.grid(row=3, column=0, columnspan=2, sticky=tk.NSEW) self.status.grid(row=4, column=0, columnspan=2, sticky=tk.EW) for child in frame.winfo_children(): child.grid_configure(padx=5, pady=(platform == 'darwin' and 3 or 2)) menubar = tk.Menu() if platform == 'darwin': from Foundation import NSBundle # https://www.tcl.tk/man/tcl/TkCmd/menu.htm apple_menu = tk.Menu(menubar, name='apple') apple_menu.add_command( label=_("About {APP}").format(APP=applongname), command=lambda: self.w.call('tk::mac::standardAboutPanel' )) # App menu entry on OSX apple_menu.add_command( label=_("Check for Updates..."), command=lambda: self.updater.checkForUpdates()) menubar.add_cascade(menu=apple_menu) self.edit_menu = tk.Menu(menubar, name='edit') self.edit_menu.add_command( label=_('Copy'), accelerator='Command-c', state=tk.DISABLED, command=self.copy) # As in Copy and Paste menubar.add_cascade(label=_('Edit'), menu=self.edit_menu) # Menu title self.w.bind('<Command-c>', self.copy) self.view_menu = tk.Menu(menubar, name='view') self.view_menu.add_command(label=_('Status'), state=tk.DISABLED, command=lambda: stats.StatsDialog( self.w, self.session)) # Menu item menubar.add_cascade(label=_('View'), menu=self.view_menu) # Menu title on OSX window_menu = tk.Menu(menubar, name='window') 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')) self.w.createcommand("::tk::mac::Quit", self.onexit) self.w.createcommand( "::tk::mac::ShowPreferences", lambda: prefs.PreferencesDialog(self.w, self.login)) self.w.createcommand( "::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app else: file_menu = self.view_menu = tk.Menu(menubar, tearoff=tk.FALSE) file_menu.add_command(label=_('Status'), state=tk.DISABLED, command=lambda: stats.StatsDialog( self.w, self.session)) # Menu item 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)) # Item in the File menu on Windows file_menu.add_separator() 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.edit_menu = tk.Menu(menubar, tearoff=tk.FALSE) self.edit_menu.add_command( label=_('Copy'), accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) # As in Copy and Paste menubar.add_cascade(label=_('Edit'), menu=self.edit_menu) # Menu title self.w.bind('<Control-c>', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) if platform == 'linux2': # Fix up menu to use same styling as everything else (fg, bg, afg, abg) = (style.lookup('TLabel.label', 'foreground'), style.lookup('TLabel.label', 'background'), style.lookup('TButton.label', 'foreground', ['active']), style.lookup('TButton.label', 'background', ['active'])) menubar.configure(fg=fg, bg=bg, activeforeground=afg, activebackground=abg) file_menu.configure(fg=fg, bg=bg, activeforeground=afg, activebackground=abg) self.edit_menu.configure(fg=fg, bg=bg, activeforeground=afg, activebackground=abg) self.w['menu'] = menubar # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match and ( platform != 'darwin' or int(match.group(2)) > 0 ): # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) self.w.update_idletasks() self.w.wait_visibility() (w, h) = (self.w.winfo_width(), self.w.winfo_height()) self.w.minsize(w, h) # Minimum size = initial size if platform != 'linux2': # update_idletasks() doesn't allow for the menubar on Linux self.w.maxsize(-1, h) # Maximum height = initial height # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) self.w.bind_all('<<Quit>>', self.onexit) # user-generated # Install hotkey monitoring self.w.bind_all('<<Invoke>>', self.getandsend) # user-generated hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring self.w.bind_all('<<Jump>>', self.system_change) # user-generated if (config.getint('output') & config.OUT_LOG_AUTO) and ( config.getint('output') & (config.OUT_LOG_AUTO | config.OUT_LOG_EDSM)): monitor.enable_logging() monitor.start(self.w) # First run if not config.get('username') or not config.get('password'): prefs.PreferencesDialog(self.w, self.login) else: self.login() # call after credentials have changed def login(self): self.status['text'] = _('Logging in...') self.button['state'] = tk.DISABLED self.w.update_idletasks() try: self.session.login(config.get('username'), config.get('password')) self.view_menu.entryconfigure(_('Status'), state=tk.NORMAL) self.status['text'] = '' except companion.VerificationRequired: # don't worry about authentication now - prompt on query self.status['text'] = '' except companion.ServerError as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) # Try to obtain exclusive lock on flight log ASAP if config.getint('output') & config.OUT_LOG_FILE: try: flightlog.openlog() except Exception as e: if __debug__: print_exc() if not self.status['text']: self.status['text'] = unicode(e) if not self.status['text'] and monitor.restart_required(): self.status['text'] = _( 'Re-start Elite: Dangerous for automatic log entries' ) # Status bar message on launch elif not getattr(sys, 'frozen', False): self.updater.checkForUpdates( ) # Sparkle / WinSparkle does this automatically for packaged apps self.cooldown() # callback after verification code def verify(self, code): try: self.session.verify(code) except Exception as e: if __debug__: print_exc() self.button['state'] = tk.NORMAL self.status['text'] = unicode(e) else: return self.getandsend() # try again 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: # Was invoked by key while in cooldown self.status['text'] = '' 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 elif play_sound: hotkeymgr.play_good() self.cmdr['text'] = self.system['text'] = self.station['text'] = '' self.system['image'] = '' self.status['text'] = _('Fetching data...') self.button['state'] = tk.DISABLED self.edit_menu.entryconfigure(_('Copy'), state=tk.DISABLED) self.w.update_idletasks() try: querytime = int(time()) data = self.session.query() config.set('querytime', querytime) # Validation if not data.get('commander') or not data['commander'].get( 'name', '').strip(): self.status['text'] = _("Who are you?!") # Shouldn't happen 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 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 else: if __debug__: # Recording with open( '%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.' + data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h: h.write( json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True).encode('utf-8')) self.cmdr['text'] = data.get('commander') and data.get( 'commander').get('name') or '' self.system['text'] = data.get('lastSystem') and data.get( 'lastSystem').get('name') or '' self.station['text'] = data.get('commander') and data.get( 'commander').get('docked') and data.get( 'lastStarport') and data.get('lastStarport').get( 'name') or (EDDB.system(self.system['text']) and self.STATION_UNDOCKED or '') self.status['text'] = '' self.edit_menu.entryconfigure(_('Copy'), state=tk.NORMAL) self.view_menu.entryconfigure(_('Status'), state=tk.NORMAL) # stuff we can do when not docked if config.getint('output') & config.OUT_SHIP_EDS: loadout.export(data) if config.getint('output') & config.OUT_SHIP_CORIOLIS: coriolis.export(data) if config.getint('output') & config.OUT_LOG_FILE: flightlog.export(data) if config.getint('output') & config.OUT_LOG_EDSM: # Catch any EDSM errors here so that they don't prevent station update try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.export(data, lambda: self.edsm.lookup( self.system['text'], EDDB.system(self.system['text'])) ) # Do EDSM lookup during EDSM export self.status['text'] = '' except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) else: self.edsm.link(self.system['text']) self.edsmpoll() if not (config.getint('output') & (config.OUT_CSV | config.OUT_TD | config.OUT_BPC | config.OUT_EDDN)): # no station data requested - we're done pass elif not data['commander'].get('docked'): # signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up if not self.status['text']: self.status['text'] = _( "You're not docked at a station!") else: # Finally - the data looks sane and we're docked at a station (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) # No EDDN output at known station? if (config.getint('output') & config.OUT_EDDN) and station_id and not ( has_market or has_outfitting or has_shipyard): if not self.status['text']: self.status['text'] = _( "Station doesn't have anything!") # No EDDN output at unknown station? elif ( config.getint('output') & config.OUT_EDDN ) and not station_id and not ( data['lastStarport'].get('commodities') or data['lastStarport'].get('modules') ): # Ignore usually spurious shipyard at unknown stations if not self.status['text']: self.status['text'] = _( "Station doesn't have anything!") # No market output at known station? elif not (config.getint('output') & config.OUT_EDDN ) and station_id and not has_market: if not self.status['text']: self.status['text'] = _( "Station doesn't have a market!") # No market output at unknown station? elif not (config.getint('output') & config.OUT_EDDN) and not station_id and not data[ 'lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _( "Station doesn't have a market!") else: if data['lastStarport'].get('commodities'): # Fixup anomalies in the commodity data self.session.fixup( data['lastStarport']['commodities']) if config.getint('output') & config.OUT_CSV: bpc.export(data, True) if config.getint('output') & config.OUT_TD: td.export(data) if config.getint('output') & config.OUT_BPC: bpc.export(data, False) elif has_market and ( config.getint('output') & (config.OUT_CSV | config.OUT_TD | config.OUT_BPC | config.OUT_EDDN)): # Overwrite any previous error message self.status['text'] = _( "Error: Can't get market data!") if config.getint('output') & config.OUT_EDDN: old_status = self.status['text'] if not old_status: self.status['text'] = _( 'Sending data to EDDN...') self.w.update_idletasks() eddn.export_commodities(data) if has_outfitting or not station_id: # Only send if eddb says that the station provides outfitting, or unknown station eddn.export_outfitting(data) elif __debug__ and data['lastStarport'].get( 'modules'): print 'Spurious outfitting!' if has_shipyard: # Only send if eddb says that the station has a shipyard - # https://github.com/Marginal/EDMarketConnector/issues/16 if data['lastStarport'].get('ships'): eddn.export_shipyard(data) else: # API is flakey about shipyard info - silently retry if missing (<1s is usually sufficient - 5s for margin). self.w.after(int(SERVER_RETRY * 1000), self.retry_for_shipyard) elif __debug__ and data['lastStarport'].get( 'ships'): print 'Spurious shipyard!' if not old_status: self.status['text'] = '' except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, self.verify) # Companion API problem except companion.ServerError as e: if retrying: self.status['text'] = unicode(e) else: # Retry once if Companion server is unresponsive self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True)) return # early exit to avoid starting cooldown count except requests.exceptions.ConnectionError as e: if __debug__: print_exc() self.status['text'] = _("Error: Can't connect to EDDN") except requests.exceptions.Timeout as e: if __debug__: print_exc() self.status['text'] = _("Error: Connection to EDDN timed out") except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not self.status['text']: # no errors 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') elif play_sound: hotkeymgr.play_bad() self.holdofftime = querytime + companion.holdoff self.cooldown() def retry_for_shipyard(self): # Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data. try: data = self.session.query() if __debug__: print 'Retry for shipyard - ' + ( data['commander'].get('docked') and (data['lastStarport'].get('ships') and 'Success' or 'Failure') or 'Undocked!') if data['commander'].get( 'docked' ): # might have undocked while we were waiting for retry in which case station data is unreliable eddn.export_shipyard(data) except: pass def system_change(self, event): if not monitor.last_event: if __debug__: print 'spurious system_change', event # eh? return timestamp, system = monitor.last_event # would like to use event user_data to carry this, but not accessible in Tkinter if self.system['text'] != system: self.system['text'] = system self.system['image'] = '' self.station['text'] = EDDB.system( system) and self.STATION_UNDOCKED or '' if config.getint('output') & config.OUT_LOG_FILE: flightlog.writelog(timestamp, system) if config.getint('output') & config.OUT_LOG_EDSM: try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.writelog( timestamp, system, lambda: self.edsm.lookup(system, EDDB.system( system))) # Do EDSM lookup during EDSM export self.status['text'] = strftime( _('Last updated at {HH}:{MM}:{SS}').format( HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not config.getint('hotkey_mute'): hotkeymgr.play_bad() else: self.edsm.link(system) self.status['text'] = strftime( _('Last updated at {HH}:{MM}:{SS}').format( HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') self.edsmpoll() def edsmpoll(self): result = self.edsm.result if result['done']: self.system['image'] = result['img'] if result['uncharted'] and (config.getint('output') & config.EDSM_AUTOOPEN): webbrowser.open(result['url']) else: self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) def system_url(self, text): return text and self.edsm.result['url'] def station_url(self, text): if text: (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) if station_id: return 'http://eddb.io/station/%d' % station_id system_id = EDDB.system(self.system['text']) if system_id: return 'http://eddb.io/system/%d' % system_id return None def cooldown(self): if time() < self.holdofftime: self.button['text'] = _('cooldown {SS}s').format( SS=int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: self.button['text'] = _('Update') # Update button in main window self.button['state'] = tk.NORMAL def copy(self, event=None): if self.system['text']: self.w.clipboard_clear() self.w.clipboard_append( self.station['text'] == self.STATION_UNDOCKED and self.system['text'] or '%s,%s' % (self.system['text'], self.station['text'])) def onexit(self, event=None): hotkeymgr.unregister() flightlog.close() if platform != 'darwin' or self.w.winfo_rooty( ) > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) config.close() self.session.close() self.w.destroy()
class AppWindow: STATION_UNDOCKED = u'×' # "Station" name to display when not docked = U+00D7 def __init__(self, master): self.holdofftime = config.getint('querytime') + companion.holdoff self.session = companion.Session() self.edsm = edsm.EDSM() self.w = master self.w.title(applongname) self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') elif platform == 'linux2': from PIL import Image, ImageTk icon = ImageTk.PhotoImage(Image.open("EDMarketConnector.png")) self.w.tk.call('wm', 'iconphoto', self.w, '-default', icon) style = ttk.Style() style.theme_use('clam') elif platform=='darwin': # Default ttk font choice looks bad on El Capitan font = tkFont.Font(family='TkDefaultFont', size=13, weight=tkFont.NORMAL) style = ttk.Style() style.configure('TLabel', font=font) style.configure('TButton', font=font) style.configure('TLabelframe.Label', font=font) style.configure('TCheckbutton', font=font) style.configure('TRadiobutton', font=font) style.configure('TEntry', font=font) frame = ttk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) frame.rowconfigure(4, weight=1) ttk.Label(frame, text=_('Cmdr')+':').grid(row=0, column=0, sticky=tk.W) # Main window ttk.Label(frame, text=_('System')+':').grid(row=1, column=0, sticky=tk.W) # Main window ttk.Label(frame, text=_('Station')+':').grid(row=2, column=0, sticky=tk.W) # Main window self.cmdr = ttk.Label(frame, width=-21) self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.system_url, popup_copy = True) self.station = HyperlinkLabel(frame, url = self.station_url, popup_copy = lambda x: x!=self.STATION_UNDOCKED) self.button = ttk.Button(frame, name='update', text=_('Update'), command=self.getandsend, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.status = ttk.Label(frame, name='status', width=-25) self.w.bind('<Return>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend) self.cmdr.grid(row=0, column=1, sticky=tk.EW) self.system.grid(row=1, column=1, sticky=tk.EW) self.station.grid(row=2, column=1, sticky=tk.EW) self.button.grid(row=3, column=0, columnspan=2, sticky=tk.NSEW) self.status.grid(row=4, column=0, columnspan=2, sticky=tk.EW) for child in frame.winfo_children(): child.grid_configure(padx=5, pady=(platform=='darwin' and 3 or 2)) menubar = tk.Menu() if platform=='darwin': from Foundation import NSBundle # https://www.tcl.tk/man/tcl/TkCmd/menu.htm apple_menu = tk.Menu(menubar, name='apple') apple_menu.add_command(label=_("About {APP}").format(APP=applongname), command=lambda:self.w.call('tk::mac::standardAboutPanel')) # App menu entry on OSX apple_menu.add_command(label=_("Check for Updates..."), command=lambda:self.updater.checkForUpdates()) menubar.add_cascade(menu=apple_menu) self.edit_menu = tk.Menu(menubar, name='edit') self.edit_menu.add_command(label=_('Copy'), accelerator='Command-c', state=tk.DISABLED, command=self.copy) # As in Copy and Paste menubar.add_cascade(label=_('Edit'), menu=self.edit_menu) # Menu title self.w.bind('<Command-c>', self.copy) self.view_menu = tk.Menu(menubar, name='view') self.view_menu.add_command(label=_('Status'), state=tk.DISABLED, command=lambda:stats.StatsDialog(self.w, self.session)) # Menu item menubar.add_cascade(label=_('View'), menu=self.view_menu) # Menu title on OSX window_menu = tk.Menu(menubar, name='window') 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')) self.w.createcommand("::tk::mac::Quit", self.onexit) self.w.createcommand("::tk::mac::ShowPreferences", lambda:prefs.PreferencesDialog(self.w, self.login)) self.w.createcommand("::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app else: file_menu = self.view_menu = tk.Menu(menubar, tearoff=tk.FALSE) file_menu.add_command(label=_('Status'), state=tk.DISABLED, command=lambda:stats.StatsDialog(self.w, self.session)) # Menu item 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)) # Item in the File menu on Windows file_menu.add_separator() 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.edit_menu = tk.Menu(menubar, tearoff=tk.FALSE) self.edit_menu.add_command(label=_('Copy'), accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) # As in Copy and Paste menubar.add_cascade(label=_('Edit'), menu=self.edit_menu) # Menu title self.w.bind('<Control-c>', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) if platform == 'linux2': # Fix up menu to use same styling as everything else (fg, bg, afg, abg) = (style.lookup('TLabel.label', 'foreground'), style.lookup('TLabel.label', 'background'), style.lookup('TButton.label', 'foreground', ['active']), style.lookup('TButton.label', 'background', ['active'])) menubar.configure( fg = fg, bg = bg, activeforeground = afg, activebackground = abg) file_menu.configure(fg = fg, bg = bg, activeforeground = afg, activebackground = abg) self.edit_menu.configure(fg = fg, bg = bg, activeforeground = afg, activebackground = abg) self.w['menu'] = menubar # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match and (platform!='darwin' or int(match.group(2))>0): # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) self.w.update_idletasks() self.w.wait_visibility() (w, h) = (self.w.winfo_width(), self.w.winfo_height()) self.w.minsize(w, h) # Minimum size = initial size if platform != 'linux2': # update_idletasks() doesn't allow for the menubar on Linux self.w.maxsize(-1, h) # Maximum height = initial height # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) self.w.bind_all('<<Quit>>', self.onexit) # user-generated # Install hotkey monitoring self.w.bind_all('<<Invoke>>', self.getandsend) # user-generated hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring self.w.bind_all('<<Jump>>', self.system_change) # user-generated if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_AUTO|config.OUT_LOG_EDSM)): monitor.enable_logging() monitor.start(self.w) # First run if not config.get('username') or not config.get('password'): prefs.PreferencesDialog(self.w, self.login) else: self.login() # call after credentials have changed def login(self): self.status['text'] = _('Logging in...') self.button['state'] = tk.DISABLED self.w.update_idletasks() try: self.session.login(config.get('username'), config.get('password')) self.view_menu.entryconfigure(_('Status'), state=tk.NORMAL) self.status['text'] = '' except companion.VerificationRequired: # don't worry about authentication now - prompt on query self.status['text'] = '' except companion.ServerError as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) # Try to obtain exclusive lock on flight log ASAP if config.getint('output') & config.OUT_LOG_FILE: try: flightlog.openlog() except Exception as e: if __debug__: print_exc() if not self.status['text']: self.status['text'] = unicode(e) if not self.status['text'] and monitor.restart_required(): self.status['text'] = _('Re-start Elite: Dangerous for automatic log entries') # Status bar message on launch elif not getattr(sys, 'frozen', False): self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps self.cooldown() # callback after verification code def verify(self, code): try: self.session.verify(code) except Exception as e: if __debug__: print_exc() self.button['state'] = tk.NORMAL self.status['text'] = unicode(e) else: return self.getandsend() # try again 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: # Was invoked by key while in cooldown self.status['text'] = '' 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 elif play_sound: hotkeymgr.play_good() self.cmdr['text'] = self.system['text'] = self.station['text'] = '' self.system['image'] = '' self.status['text'] = _('Fetching data...') self.button['state'] = tk.DISABLED self.edit_menu.entryconfigure(_('Copy'), state=tk.DISABLED) self.w.update_idletasks() try: querytime = int(time()) data = self.session.query() config.set('querytime', querytime) # Validation if not data.get('commander') or not data['commander'].get('name','').strip(): self.status['text'] = _("Who are you?!") # Shouldn't happen 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 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 else: if __debug__: # Recording with open('%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h: h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True).encode('utf-8')) self.cmdr['text'] = data.get('commander') and data.get('commander').get('name') or '' self.system['text'] = data.get('lastSystem') and data.get('lastSystem').get('name') or '' self.station['text'] = data.get('commander') and data.get('commander').get('docked') and data.get('lastStarport') and data.get('lastStarport').get('name') or (EDDB.system(self.system['text']) and self.STATION_UNDOCKED or '') self.status['text'] = '' self.edit_menu.entryconfigure(_('Copy'), state=tk.NORMAL) self.view_menu.entryconfigure(_('Status'), state=tk.NORMAL) # stuff we can do when not docked if config.getint('output') & config.OUT_SHIP_EDS: loadout.export(data) if config.getint('output') & config.OUT_SHIP_CORIOLIS: coriolis.export(data) if config.getint('output') & config.OUT_LOG_FILE: flightlog.export(data) if config.getint('output') & config.OUT_LOG_EDSM: # Catch any EDSM errors here so that they don't prevent station update try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.export(data, lambda:self.edsm.lookup(self.system['text'], EDDB.system(self.system['text']))) # Do EDSM lookup during EDSM export self.status['text'] = '' except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) else: self.edsm.link(self.system['text']) self.edsmpoll() if not (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)): # no station data requested - we're done pass elif not data['commander'].get('docked'): # signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up if not self.status['text']: self.status['text'] = _("You're not docked at a station!") else: # Finally - the data looks sane and we're docked at a station (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) # No EDDN output at known station? if (config.getint('output') & config.OUT_EDDN) and station_id and not (has_market or has_outfitting or has_shipyard): if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") # No EDDN output at unknown station? elif (config.getint('output') & config.OUT_EDDN) and not station_id and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): # Ignore usually spurious shipyard at unknown stations if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") # No market output at known station? elif not (config.getint('output') & config.OUT_EDDN) and station_id and not has_market: if not self.status['text']: self.status['text'] = _("Station doesn't have a market!") # No market output at unknown station? elif not (config.getint('output') & config.OUT_EDDN) and not station_id and not data['lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _("Station doesn't have a market!") else: if data['lastStarport'].get('commodities'): # Fixup anomalies in the commodity data self.session.fixup(data['lastStarport']['commodities']) if config.getint('output') & config.OUT_CSV: bpc.export(data, True) if config.getint('output') & config.OUT_TD: td.export(data) if config.getint('output') & config.OUT_BPC: bpc.export(data, False) elif has_market and (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)): # Overwrite any previous error message self.status['text'] = _("Error: Can't get market data!") if config.getint('output') & config.OUT_EDDN: old_status = self.status['text'] if not old_status: self.status['text'] = _('Sending data to EDDN...') self.w.update_idletasks() eddn.export_commodities(data) if has_outfitting or not station_id: # Only send if eddb says that the station provides outfitting, or unknown station eddn.export_outfitting(data) elif __debug__ and data['lastStarport'].get('modules'): print 'Spurious outfitting!' if has_shipyard: # Only send if eddb says that the station has a shipyard - # https://github.com/Marginal/EDMarketConnector/issues/16 if data['lastStarport'].get('ships'): eddn.export_shipyard(data) else: # API is flakey about shipyard info - silently retry if missing (<1s is usually sufficient - 5s for margin). self.w.after(int(SERVER_RETRY * 1000), self.retry_for_shipyard) elif __debug__ and data['lastStarport'].get('ships'): print 'Spurious shipyard!' if not old_status: self.status['text'] = '' except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, self.verify) # Companion API problem except companion.ServerError as e: if retrying: self.status['text'] = unicode(e) else: # Retry once if Companion server is unresponsive self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True)) return # early exit to avoid starting cooldown count except requests.exceptions.ConnectionError as e: if __debug__: print_exc() self.status['text'] = _("Error: Can't connect to EDDN") except requests.exceptions.Timeout as e: if __debug__: print_exc() self.status['text'] = _("Error: Connection to EDDN timed out") except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not self.status['text']: # no errors 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') elif play_sound: hotkeymgr.play_bad() self.holdofftime = querytime + companion.holdoff self.cooldown() def retry_for_shipyard(self): # Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data. try: data = self.session.query() if __debug__: print 'Retry for shipyard - ' + (data['commander'].get('docked') and (data['lastStarport'].get('ships') and 'Success' or 'Failure') or 'Undocked!') if data['commander'].get('docked'): # might have undocked while we were waiting for retry in which case station data is unreliable eddn.export_shipyard(data) except: pass def system_change(self, event): if not monitor.last_event: if __debug__: print 'spurious system_change', event # eh? return timestamp, system = monitor.last_event # would like to use event user_data to carry this, but not accessible in Tkinter if self.system['text'] != system: self.system['text'] = system self.system['image'] = '' self.station['text'] = EDDB.system(system) and self.STATION_UNDOCKED or '' if config.getint('output') & config.OUT_LOG_FILE: flightlog.writelog(timestamp, system) if config.getint('output') & config.OUT_LOG_EDSM: try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.writelog(timestamp, system, lambda:self.edsm.lookup(system, EDDB.system(system))) # Do EDSM lookup during EDSM export self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not config.getint('hotkey_mute'): hotkeymgr.play_bad() else: self.edsm.link(system) self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') self.edsmpoll() def edsmpoll(self): result = self.edsm.result if result['done']: self.system['image'] = result['img'] else: self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) def system_url(self, text): return text and self.edsm.result['url'] def station_url(self, text): if text: (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) if station_id: return 'http://eddb.io/station/%d' % station_id system_id = EDDB.system(self.system['text']) if system_id: return 'http://eddb.io/system/%d' % system_id return None def cooldown(self): if time() < self.holdofftime: self.button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: self.button['text'] = _('Update') # Update button in main window self.button['state'] = tk.NORMAL def copy(self, event=None): if self.system['text']: self.w.clipboard_clear() self.w.clipboard_append(self.station['text'] == self.STATION_UNDOCKED and self.system['text'] or '%s,%s' % (self.system['text'], self.station['text'])) def onexit(self, event=None): flightlog.close() if platform!='darwin' or self.w.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) config.close() self.session.close() self.w.destroy()
class AppWindow: STATION_UNDOCKED = u'×' # "Station" name to display when not docked = U+00D7 # Tkinter Event types EVENT_KEYPRESS = 2 EVENT_BUTTON = 4 EVENT_VIRTUAL = 35 def __init__(self, master): self.ser = serial.Serial('/dev/cu.wchusbserial1490') sleep(3) self.holdofftime = config.getint('querytime') + companion.holdoff self.session = companion.Session() self.edsm = edsm.EDSM() self.w = master self.w.title(applongname) self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) plug.load_plugins() if platform != 'darwin': if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') else: from PIL import Image, ImageTk self.w.tk.call( 'wm', 'iconphoto', self.w, '-default', ImageTk.PhotoImage(Image.open("EDMarketConnector.png"))) self.theme_icon = tk.PhotoImage( data= 'R0lGODlhFAAQAMZVAAAAAAEAAAIBAAMBAAQCAAYDAAcDAAkEAAoEAAwGAQ8IARAIAREJARYKABkLARsMASMQASgSAiUUAy0UAjAVAioXBDIWAy4YBC4ZBS8ZBTkZA0EdBDsgBkUfA0MkB00iA1AjA1IlBFQmBE4qCFgoBVkoBFArCF0qBVQtCGUrBGMtBWYtBWA0Cm8xBW8xBm8yBXMzBXU1Bms5C3s1BXs2BXw2BX02BXw4B4A5B3Q/DIJGDYNGDYJHDoNHDYdJDppGCItLD4xLDo5MDo5MD5hSD59VEKdaEbJgErtlE7tlFLxlE8BpFMJpFMNpFMZrFdFxFtl1F995GOB6GOF6GP+LG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAejgACCgiODhoeGBABPPgACj48DA4gAk00cSRUYGZycEogBAE4LCUM8Oj2pOzlQBAKHSBeKlABKBq+DHkS0g0wJiCZFvABHJBuHBSxADFRTUs/PUUsiKhaIKEZBKTM13TU0Nj8IIRqThjJCK8MnFIgKMMMAJRGGAQUvvAIPLocBAjgdPggcKMLAgRi0GjxYyNBBCwjwQoEKQLEiABA3HMU7NOFQIAA7' ) self.theme_minimize = tk.BitmapImage( data= '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n' ) self.theme_close = tk.BitmapImage( data= '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n' ) frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) self.cmdr_label = tk.Label(frame) self.ship_label = tk.Label(frame) self.system_label = tk.Label(frame) self.station_label = tk.Label(frame) self.cmdr_label.grid(row=1, column=0, sticky=tk.W) self.ship_label.grid(row=2, column=0, sticky=tk.W) self.system_label.grid(row=3, column=0, sticky=tk.W) self.station_label.grid(row=4, column=0, sticky=tk.W) self.cmdr = tk.Label(frame, anchor=tk.W) self.ship = HyperlinkLabel(frame, url=self.shipyard_url) self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.system_url, popup_copy=True) self.station = HyperlinkLabel( frame, url=self.station_url, popup_copy=lambda x: x != self.STATION_UNDOCKED) self.cmdr.grid(row=1, column=1, sticky=tk.EW) self.ship.grid(row=2, column=1, sticky=tk.EW) self.system.grid(row=3, column=1, sticky=tk.EW) self.station.grid(row=4, column=1, sticky=tk.EW) for plugname in plug.PLUGINS: appitem = plug.get_plugin_app(plugname, frame) if appitem: if isinstance(appitem, tuple) and len(appitem) == 2: row = frame.grid_size()[1] appitem[0].grid(row=row, column=0, sticky=tk.W) appitem[1].grid(row=row, column=1, sticky=tk.EW) else: appitem.grid(columnspan=2, sticky=tk.W) self.button = ttk.Button( frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.theme_button = tk.Label(frame, width=platform == 'darwin' and 32 or 28, state=tk.DISABLED) self.status = tk.Label(frame, name='status', anchor=tk.W) row = frame.grid_size()[1] self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) theme.register_alternate((self.button, self.theme_button), { 'row': row, 'columnspan': 2, 'sticky': tk.NSEW }) self.status.grid(columnspan=2, sticky=tk.EW) self.button.bind('<Button-1>', self.getandsend) theme.button_bind(self.theme_button, self.getandsend) for child in frame.winfo_children(): child.grid_configure(padx=5, pady=(platform != 'win32' and 2 or 0)) self.menubar = tk.Menu() if platform == 'darwin': # Can't handle (de)iconify if topmost is set, so suppress iconify button # http://wiki.tcl.tk/13428 and p15 of https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox horizontalZoom resizable') # https://www.tcl.tk/man/tcl/TkCmd/menu.htm self.system_menu = tk.Menu(self.menubar, name='apple') self.system_menu.add_command( command=lambda: self.w.call('tk::mac::standardAboutPanel')) self.system_menu.add_command( command=lambda: self.updater.checkForUpdates()) self.menubar.add_cascade(menu=self.system_menu) self.file_menu = tk.Menu(self.menubar, name='file') self.file_menu.add_command(command=self.save_raw) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, name='edit') self.edit_menu.add_command(accelerator='Command-c', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) self.w.bind('<Command-c>', self.copy) self.view_menu = tk.Menu(self.menubar, name='view') self.view_menu.add_command(command=lambda: stats.StatsDialog(self)) self.menubar.add_cascade(menu=self.view_menu) window_menu = tk.Menu(self.menubar, name='window') self.menubar.add_cascade(menu=window_menu) self.w['menu'] = self.menubar # 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')) self.w.createcommand("::tk::mac::Quit", self.onexit) self.w.createcommand( "::tk::mac::ShowPreferences", lambda: prefs.PreferencesDialog(self.w, self.postprefs)) self.w.createcommand( "::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app else: self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.file_menu.add_command(command=lambda: stats.StatsDialog(self)) self.file_menu.add_command(command=self.save_raw) self.file_menu.add_command( command=lambda: self.updater.checkForUpdates()) self.file_menu.add_command(command=lambda: prefs.PreferencesDialog( self.w, self.postprefs)) self.file_menu.add_separator() self.file_menu.add_command(command=self.onexit) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) if platform == 'win32': # Must be added after at least one "real" menu entry self.always_ontop = tk.BooleanVar( value=config.getint('always_ontop')) self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) self.system_menu.add_separator() self.system_menu.add_checkbutton( label=_('Always on top'), variable=self.always_ontop, command=self.ontop_changed) # Appearance setting self.menubar.add_cascade(menu=self.system_menu) self.w.bind('<Control-c>', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) theme.register( self.menubar ) # menus and children aren't automatically registered theme.register(self.file_menu) theme.register(self.edit_menu) # Alternate title bar and menu for dark theme self.theme_menubar = tk.Frame(frame) self.theme_menubar.columnconfigure(2, weight=1) theme_titlebar = tk.Label(self.theme_menubar, text=applongname, image=self.theme_icon, anchor=tk.W, compound=tk.LEFT) theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW) self.drag_offset = None theme_titlebar.bind('<Button-1>', self.drag_start) theme_titlebar.bind('<B1-Motion>', self.drag_continue) theme_titlebar.bind('<ButtonRelease-1>', self.drag_end) if platform == 'win32': # Can't work out how to deiconify on Linux theme_minimize = tk.Label(self.theme_menubar, image=self.theme_minimize) theme_minimize.grid(row=0, column=3, padx=2) theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) theme_close = tk.Label(self.theme_menubar, image=self.theme_close) theme_close.grid(row=0, column=4, padx=2) theme.button_bind(theme_close, self.onexit, image=self.theme_close) self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W) theme.button_bind( self.theme_file_menu, lambda e: self.file_menu.tk_popup( e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) theme.button_bind( self.theme_edit_menu, lambda e: self.edit_menu.tk_popup( e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) theme.register_highlight(theme_titlebar) theme.register( self.theme_minimize) # images aren't automatically registered theme.register(self.theme_close) theme.register_alternate((self.menubar, self.theme_menubar), { 'row': 0, 'columnspan': 2, 'sticky': tk.NSEW }) self.set_labels() # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match: if platform == 'darwin': if int( match.group(2) ) >= 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) elif platform == 'win32': # Check that the titlebar will be at least partly on screen import ctypes from ctypes.wintypes import POINT # https://msdn.microsoft.com/en-us/library/dd145064 MONITOR_DEFAULTTONULL = 0 if ctypes.windll.user32.MonitorFromPoint( POINT( int(match.group(1)) + 16, int(match.group(2)) + 16), MONITOR_DEFAULTTONULL): self.w.geometry(config.get('geometry')) else: self.w.geometry(config.get('geometry')) self.w.attributes('-topmost', config.getint('always_ontop') and 1 or 0) self.w.resizable(tk.TRUE, tk.FALSE) theme.register(frame) theme.register_highlight(self.ship) theme.register_highlight(self.system) theme.register_highlight(self.station) theme.apply(self.w) self.w.bind("<Map>", self.onmap) # Special handling for overrideredict self.w.bind('<Return>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend) self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring self.w.bind_all('<<Quit>>', self.onexit) # Updater # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) # First run if not config.get('username') or not config.get('password'): prefs.PreferencesDialog(self.w, self.postprefs) else: self.login() self.ser.write(b'!0Initialising...\n') self.ser.flush() # callback after the Preferences dialog is applied def postprefs(self): self.set_labels() # in case language has changed self.login() # in case credentials gave changed # set main window labels, e.g. after language change def set_labels(self): self.cmdr_label['text'] = _('Cmdr') + ':' # Main window self.ship_label['text'] = _('Ship') + ':' # Main window self.system_label['text'] = _('System') + ':' # Main window self.station_label['text'] = _('Station') + ':' # Main window self.button['text'] = self.theme_button['text'] = _( 'Update') # Update button in main window if platform == 'darwin': self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX self.system_menu.entryconfigure( 0, label=_("About {APP}").format( APP=applongname)) # App menu entry on OSX self.system_menu.entryconfigure( 1, label=_("Check for Updates...")) # Menu item self.file_menu.entryconfigure( 0, label=_('Save Raw Data...')) # Menu item self.view_menu.entryconfigure(0, label=_('Status')) # Menu item else: self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.theme_file_menu['text'] = _('File') # Menu title self.theme_edit_menu['text'] = _('Edit') # Menu title self.file_menu.entryconfigure(0, label=_('Status')) # Menu item self.file_menu.entryconfigure( 1, label=_('Save Raw Data...')) # Menu item self.file_menu.entryconfigure( 2, label=_("Check for Updates...")) # Menu item self.file_menu.entryconfigure( 3, label=_("Settings")) # Item in the File menu on Windows self.file_menu.entryconfigure( 5, label=_("Exit")) # Item in the File menu on Windows self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste def login(self): self.status['text'] = _('Logging in...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() try: self.session.login(config.get('username'), config.get('password')) self.status['text'] = '' except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, partial(self.verify, self.login)) except companion.ServerError as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not getattr(sys, 'frozen', False): self.updater.checkForUpdates( ) # Sparkle / WinSparkle does this automatically for packaged apps # Try to obtain exclusive lock on journal cache, even if we don't need it yet if not eddn.load(): self.status[ 'text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing # (Re-)install hotkey monitoring hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # (Re-)install log monitoring if not monitor.start(self.w): self.status['text'] = 'Error: Check %s' % _( 'E:D journal file location' ) # Location of the new Journal file in E:D 2.2 self.cooldown() # callback after verification code def verify(self, callback, code): try: self.session.verify(code) config.save() # Save settings now for use by command-line app except Exception as e: if __debug__: print_exc() self.button['state'] = self.theme_button['state'] = tk.NORMAL self.status['text'] = unicode(e) else: return callback() # try again def getandsend(self, event=None, retrying=False): auto_update = not event play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL ) and not config.getint('hotkey_mute') if (monitor.cmdr and not monitor.mode) or monitor.is_beta: return # In CQC - do nothing if not retrying: if time( ) < self.holdofftime: # Was invoked by key while in cooldown self.status['text'] = '' 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 elif play_sound: hotkeymgr.play_good() self.status['text'] = _('Fetching data...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.edit_menu.entryconfigure(0, state=tk.DISABLED) # Copy self.w.update_idletasks() try: querytime = int(time()) data = self.session.query() config.set('querytime', querytime) # Validation if not data.get('commander') or not data['commander'].get( 'name', '').strip(): self.status['text'] = _("Who are you?!") # Shouldn't happen 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 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 elif monitor.cmdr and data['commander']['name'] != monitor.cmdr: raise companion.CredentialsError( ) # Companion API credentials don't match Journal elif (auto_update and not data['commander'].get('docked')) or ( monitor.system and data['lastSystem']['name'] != monitor.system ) or (monitor.shipid and data['ship']['id'] != monitor.shipid) or ( monitor.shiptype and data['ship']['name'].lower() != monitor.shiptype): raise companion.ServerLagging() else: if __debug__: # Recording if not isdir('dump'): mkdir('dump') with open( 'dump/%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.' + data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h: h.write( json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) self.cmdr['text'] = data['commander']['name'] self.ship['text'] = companion.ship_map.get( data['ship']['name'].lower(), data['ship']['name']) if not monitor.system: self.system['text'] = data['lastSystem']['name'] self.system['image'] = '' self.station['text'] = data['commander'].get( 'docked') and data.get('lastStarport') and data[ 'lastStarport'].get('name') or (EDDB.system( self.system['text']) and self.STATION_UNDOCKED or '') self.status['text'] = '' self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy # stuff we can do when not docked plug.notify_newdata(data) if config.getint('output') & config.OUT_SHIP_EDS: loadout.export(data) if config.getint('output') & config.OUT_SHIP_CORIOLIS: coriolis.export(data) if not (config.getint('output') & (config.OUT_MKT_CSV | config.OUT_MKT_TD | config.OUT_MKT_BPC | config.OUT_MKT_EDDN)): # no station data requested - we're done pass elif not data['commander'].get('docked'): if not event and not retrying: # Silently retry if we got here by 'Automatically update on docking' and the server hasn't caught up self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True)) return # early exit to avoid starting cooldown count else: # Signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up if not self.status['text']: self.status['text'] = _( "You're not docked at a station!") else: # Finally - the data looks sane and we're docked at a station (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) # No EDDN output? if (config.getint('output') & config.OUT_MKT_EDDN ) and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules') ): # Ignore possibly missing shipyard info if not self.status['text']: self.status['text'] = _( "Station doesn't have anything!") # No market output? elif not ( config.getint('output') & config.OUT_MKT_EDDN ) and not data['lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _( "Station doesn't have a market!") else: if data['lastStarport'].get( 'commodities') and config.getint('output') & ( config.OUT_MKT_CSV | config.OUT_MKT_TD | config.OUT_MKT_BPC): # Fixup anomalies in the commodity data fixed = companion.fixup(data) if config.getint('output') & config.OUT_MKT_CSV: commodity.export(fixed, COMMODITY_CSV) if config.getint('output') & config.OUT_MKT_TD: td.export(fixed) if config.getint('output') & config.OUT_MKT_BPC: commodity.export(fixed, COMMODITY_BPC) if config.getint('output') & config.OUT_MKT_EDDN: old_status = self.status['text'] if not old_status: self.status['text'] = _( 'Sending data to EDDN...') self.w.update_idletasks() eddn.export_commodities(data) eddn.export_outfitting(data) if has_shipyard and not data['lastStarport'].get( 'ships'): # API is flakey about shipyard info - silently retry if missing (<1s is usually sufficient - 5s for margin). self.w.after(int(SERVER_RETRY * 1000), self.retry_for_shipyard) else: eddn.export_shipyard(data) if not old_status: self.status['text'] = '' # Update credits and ship info and send to EDSM if config.getint( 'output' ) & config.OUT_SYS_EDSM and not monitor.is_beta: try: if data['commander'].get('credits') is not None: monitor.credits = ( data['commander']['credits'], data['commander'].get('debt', 0)) self.edsm.setcredits(monitor.credits) ship = companion.ship(data) if ship == self.edsm.lastship: props = [] else: props = [ ('cargoCapacity', ship['cargo']['capacity']), ('fuelMainCapacity', ship['fuel']['main']['capacity']), ('linkToCoriolis', coriolis.url(data)), ('linkToEDShipyard', edshipyard.url(data)), ] if monitor.shippaint is None: # Companion API server can lag, so prefer Journal. But paintjob only reported in Journal on change. monitor.shipid = data['ship']['id'] monitor.shiptype = data['ship']['name'].lower() monitor.shippaint = data['ship']['modules'][ 'PaintJob'] and data['ship']['modules'][ 'PaintJob']['module']['name'].lower( ) or '' props.append(('paintJob', monitor.shippaint)) if props: self.edsm.updateship(monitor.shipid, monitor.shiptype, props) self.edsm.lastship = ship except Exception as e: # Not particularly important so silent on failure if __debug__: print_exc() except companion.VerificationRequired: return prefs.AuthenticationDialog( self.w, partial(self.verify, self.getandsend)) # Companion API problem except (companion.ServerError, companion.ServerLagging) as e: if retrying: self.status['text'] = unicode(e) else: # Retry once if Companion server is unresponsive self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True)) return # early exit to avoid starting cooldown count except requests.RequestException as e: if __debug__: print_exc() self.status['text'] = _("Error: Can't connect to EDDN") except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not self.status['text']: # no errors 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') elif play_sound: hotkeymgr.play_bad() self.holdofftime = querytime + companion.holdoff self.cooldown() def retry_for_shipyard(self): # Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data. try: data = self.session.query() if __debug__: print 'Retry for shipyard - ' + ( data['commander'].get('docked') and (data['lastStarport'].get('ships') and 'Success' or 'Failure') or 'Undocked!') if data['commander'].get( 'docked' ): # might have undocked while we were waiting for retry in which case station data is unreliable eddn.export_shipyard(data) except: pass # Handle event(s) from the journal def journal_event(self, event): while True: entry = monitor.get_entry() if not entry: return system_changed = monitor.system and self.system[ 'text'] != monitor.system station_changed = monitor.station and self.station[ 'text'] != monitor.station # Update main window self.cmdr['text'] = monitor.cmdr or '' self.ship['text'] = monitor.shiptype and companion.ship_map.get( monitor.shiptype, monitor.shiptype) or '' self.station['text'] = monitor.station or ( EDDB.system(monitor.system) and self.STATION_UNDOCKED or '') if system_changed or station_changed: self.status['text'] = '' if self.station['text'] == self.STATION_UNDOCKED: self.ser.write('!0 \n') else: self.ser.write('!0' + self.station['text'].encode('utf-8') + ' \n') self.ser.flush() if system_changed: self.system['text'] = monitor.system or '' self.system['image'] = '' self.edsm.link(monitor.system) self.ser.write('!1' + self.system['text'].encode('utf-8') + ' \n') self.ser.flush() self.w.update_idletasks() # Send interesting events to EDSM if config.getint( 'output') & config.OUT_SYS_EDSM and not monitor.is_beta: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() try: # Update system status on startup if monitor.mode and not entry['event']: self.edsm.lookup(monitor.system) # Send credits to EDSM on new game (but not on startup - data might be old) if entry['event'] == 'LoadGame': self.edsm.setcredits(monitor.credits) # Send rank info to EDSM on startup or change if entry['event'] in [None, 'Progress', 'Promotion']: self.edsm.setranks(monitor.ranks) # Send ship info to EDSM on startup or change if entry['event'] in [ None, 'LoadGame', 'ShipyardNew', 'ShipyardSwap' ]: self.edsm.setshipid(monitor.shipid) self.edsm.updateship( monitor.shipid, monitor.shiptype, monitor.shippaint and [('paintJob', monitor.shippaint)] or []) elif entry['event'] in ['ShipyardBuy', 'ShipyardSell']: self.edsm.sellship(entry.get('SellShipID')) # Send paintjob info to EDSM on change if entry['event'] in ['ModuleBuy', 'ModuleSell' ] and entry['Slot'] == 'PaintJob': self.edsm.updateship(monitor.shipid, monitor.shiptype, [('paintJob', monitor.shippaint)]) # Write EDSM log on change if monitor.mode and entry['event'] in [ 'Location', 'FSDJump' ]: self.edsm.writelog( timegm( strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), monitor.system, monitor.coordinates, monitor.shipid) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not config.getint('hotkey_mute'): hotkeymgr.play_bad() else: self.status['text'] = '' self.edsmpoll() if not entry['event'] or not monitor.mode: return # Startup or in CQC # Plugins plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry) if system_changed: # Backwards compatibility plug.notify_system_changed( timegm(strptime(entry['timestamp'], '%Y-%m-%dT%H:%M:%SZ')), monitor.system, monitor.coordinates) # Auto-Update after docking if station_changed and not monitor.is_beta and not config.getint( 'output') & config.OUT_MKT_MANUAL and config.getint( 'output') & config.OUT_STATION_ANY: self.w.after(int(SERVER_RETRY * 1000), self.getandsend) # Send interesting events to EDDN try: if (config.getint('output') & config.OUT_SYS_EDDN and monitor.cmdr and (entry['event'] == 'FSDJump' or entry['event'] == 'Docked' or entry['event'] == 'Scan' and monitor.system and monitor.coordinates)): # strip out properties disallowed by the schema for thing in [ 'CockpitBreach', 'BoostUsed', 'FuelLevel', 'FuelUsed', 'JumpDist' ]: entry.pop(thing, None) for thing in entry.keys(): if thing.endswith('_Localised'): entry.pop(thing, None) # add planet to Docked event for planetary stations if known if entry['event'] == 'Docked' and monitor.body: entry['BodyName'] = monitor.body # add mandatory StarSystem and StarPos properties to Scan events if 'StarSystem' not in entry: entry['StarSystem'] = monitor.system if 'StarPos' not in entry: entry['StarPos'] = list(monitor.coordinates) self.status['text'] = _('Sending data to EDDN...') self.w.update_idletasks() eddn.export_journal_entry(monitor.cmdr, monitor.is_beta, entry) self.status['text'] = '' elif (config.getint('output') & config.OUT_MKT_EDDN and monitor.cmdr and entry['event'] == 'MarketSell' and entry.get('BlackMarket')): # Construct blackmarket message msg = OrderedDict([ ('systemName', monitor.system), ('stationName', monitor.station), ('timestamp', entry['timestamp']), ('name', entry['Type']), ('sellPrice', entry['SellPrice']), ('prohibited', entry.get('IllegalGoods', False)), ]) self.status['text'] = _('Sending data to EDDN...') self.w.update_idletasks() eddn.export_blackmarket(monitor.cmdr, monitor.is_beta, msg) self.status['text'] = '' except requests.exceptions.RequestException as e: if __debug__: print_exc() self.status['text'] = _("Error: Can't connect to EDDN") if not config.getint('hotkey_mute'): hotkeymgr.play_bad() except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not config.getint('hotkey_mute'): hotkeymgr.play_bad() def edsmpoll(self): result = self.edsm.result if result['done']: self.system['image'] = result['img'] else: self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) def shipyard_url(self, shipname=None): self.status['text'] = _('Fetching data...') self.w.update_idletasks() try: data = self.session.query() except companion.VerificationRequired: return prefs.AuthenticationDialog( self.parent, partial(self.verify, self.shipyard_url)) except companion.ServerError as e: self.status['text'] = str(e) return except Exception as e: if __debug__: print_exc() self.status['text'] = str(e) return if not data.get('commander') or not data['commander'].get('name', '').strip(): self.status['text'] = _("Who are you?!") # Shouldn't happen 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 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 elif shipname and shipname != companion.ship_map.get( data['ship']['name'].lower(), data['ship']['name']): self.status['text'] = _( 'Error: Server is lagging' ) # Raised when Companion API server is returning old data, e.g. when the servers are too busy else: self.status['text'] = '' if config.getint('shipyard') == config.SHIPYARD_EDSHIPYARD: return edshipyard.url(data) elif config.getint('shipyard') == config.SHIPYARD_CORIOLIS: return coriolis.url(data) else: assert False, config.getint('shipyard') return False def system_url(self, text): return text and self.edsm.result['url'] def station_url(self, text): if text: (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) if station_id: return 'https://eddb.io/station/%d' % station_id system_id = EDDB.system(self.system['text']) if system_id: return 'https://eddb.io/system/%d' % system_id return None def cooldown(self): if time() < self.holdofftime: self.button['text'] = self.theme_button['text'] = _( 'cooldown {SS}s').format( SS=int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: self.button['text'] = self.theme_button['text'] = _( 'Update') # Update button in main window self.button['state'] = self.theme_button['state'] = tk.NORMAL def ontop_changed(self, event=None): config.set('always_ontop', self.always_ontop.get()) self.w.wm_attributes('-topmost', self.always_ontop.get()) def copy(self, event=None): if self.system['text']: self.w.clipboard_clear() self.w.clipboard_append( self.station['text'] == self.STATION_UNDOCKED and self.system['text'] or '%s,%s' % (self.system['text'], self.station['text'])) def save_raw(self): self.status['text'] = _('Fetching data...') self.w.update_idletasks() try: data = self.session.query() self.cmdr['text'] = data.get('commander') and data.get( 'commander').get('name') or '' self.status['text'] = '' f = tkFileDialog.asksaveasfilename( parent=self.w, defaultextension=platform == 'darwin' and '.json' or '', filetypes=[('JSON', '.json'), ('All Files', '*')], initialdir=config.get('outdir'), initialfile='%s%s.%s.json' % (data['lastSystem'].get( 'name', 'Unknown'), data['commander'].get('docked') and '.' + data['lastStarport'].get('name', 'Unknown') or '', strftime('%Y-%m-%dT%H.%M.%S', localtime()))) if f: with open(f, 'wt') as h: h.write( json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) except companion.VerificationRequired: prefs.AuthenticationDialog(self.w, partial(self.verify, self.save_raw)) except companion.ServerError as e: self.status['text'] = str(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) def onexit(self, event=None): if platform != 'darwin' or self.w.winfo_rooty( ) > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) self.w.withdraw( ) # Following items can take a few seconds, so hide the main window while they happen hotkeymgr.unregister() monitor.close() eddn.close() self.updater.close() self.session.close() config.close() self.w.destroy() def drag_start(self, event): self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) def drag_continue(self, event): if self.drag_offset: self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1])) def drag_end(self, event): self.drag_offset = None def oniconify(self, event=None): self.w.overrideredirect(0) # Can't iconize while overrideredirect self.w.iconify() self.w.update_idletasks( ) # Size and windows styles get recalculated here self.w.wait_visibility( ) # Need main window to be re-created before returning theme.active = None # So theme will be re-applied on map def onmap(self, event=None): if event.widget == self.w: theme.apply(self.w)
class AppWindow: # Tkinter Event types EVENT_KEYPRESS = 2 EVENT_BUTTON = 4 EVENT_VIRTUAL = 35 def __init__(self, master): # Start a protocol handler to handle cAPI registration. Requires main window to exist. protocolhandler.start(master) self.holdofftime = config.getint('querytime') + companion.holdoff self.w = master self.w.title(applongname) self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) self.prefsdialog = None plug.load_plugins(master) if platform != 'darwin': if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') else: self.w.tk.call( 'wm', 'iconphoto', self.w, '-default', tk.PhotoImage( file=join(config.respath, 'EDMarketConnector.png'))) self.theme_icon = tk.PhotoImage( data= 'R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==' ) self.theme_minimize = tk.BitmapImage( data= '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n' ) self.theme_close = tk.BitmapImage( data= '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n' ) frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) self.cmdr_label = tk.Label(frame) self.ship_label = tk.Label(frame) self.system_label = tk.Label(frame) self.station_label = tk.Label(frame) self.cmdr_label.grid(row=1, column=0, sticky=tk.W) self.ship_label.grid(row=2, column=0, sticky=tk.W) self.system_label.grid(row=3, column=0, sticky=tk.W) self.station_label.grid(row=4, column=0, sticky=tk.W) self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='cmdr') self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.shipyard_url, name='ship') self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.system_url, popup_copy=True, name='system') self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.station_url, name='station') self.cmdr.grid(row=1, column=1, sticky=tk.EW) self.ship.grid(row=2, column=1, sticky=tk.EW) self.system.grid(row=3, column=1, sticky=tk.EW) self.station.grid(row=4, column=1, sticky=tk.EW) for plugin in plug.PLUGINS: appitem = plugin.get_app(frame) if appitem: tk.Frame(frame, highlightthickness=1).grid(columnspan=2, sticky=tk.EW) # separator if isinstance(appitem, tuple) and len(appitem) == 2: row = frame.grid_size()[1] appitem[0].grid(row=row, column=0, sticky=tk.W) appitem[1].grid(row=row, column=1, sticky=tk.EW) else: appitem.grid(columnspan=2, sticky=tk.EW) self.button = ttk.Button( frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.theme_button = tk.Label(frame, width=platform == 'darwin' and 32 or 28, state=tk.DISABLED) self.status = tk.Label(frame, name='status', anchor=tk.W) row = frame.grid_size()[1] self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) theme.register_alternate( (self.button, self.theme_button, self.theme_button), { 'row': row, 'columnspan': 2, 'sticky': tk.NSEW }) self.status.grid(columnspan=2, sticky=tk.EW) self.button.bind('<Button-1>', self.getandsend) theme.button_bind(self.theme_button, self.getandsend) for child in frame.winfo_children(): child.grid_configure( padx=5, pady=(platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0) self.menubar = tk.Menu() if platform == 'darwin': # Can't handle (de)iconify if topmost is set, so suppress iconify button # http://wiki.tcl.tk/13428 and p15 of https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox resizable') # https://www.tcl.tk/man/tcl/TkCmd/menu.htm self.system_menu = tk.Menu(self.menubar, name='apple') self.system_menu.add_command( command=lambda: self.w.call('tk::mac::standardAboutPanel')) self.system_menu.add_command( command=lambda: self.updater.checkForUpdates()) self.menubar.add_cascade(menu=self.system_menu) self.file_menu = tk.Menu(self.menubar, name='file') self.file_menu.add_command(command=self.save_raw) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, name='edit') self.edit_menu.add_command(accelerator='Command-c', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) self.w.bind('<Command-c>', self.copy) self.view_menu = tk.Menu(self.menubar, name='view') self.view_menu.add_command(command=lambda: stats.StatsDialog(self)) self.menubar.add_cascade(menu=self.view_menu) window_menu = tk.Menu(self.menubar, name='window') self.menubar.add_cascade(menu=window_menu) self.help_menu = tk.Menu(self.menubar, name='help') self.w.createcommand("::tk::mac::ShowHelp", self.help_general) self.help_menu.add_command(command=self.help_privacy) self.help_menu.add_command(command=self.help_releases) self.menubar.add_cascade(menu=self.help_menu) self.w['menu'] = self.menubar # 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')) self.w.createcommand("::tk::mac::Quit", self.onexit) self.w.createcommand( "::tk::mac::ShowPreferences", lambda: prefs.PreferencesDialog(self.w, self.postprefs)) self.w.createcommand( "::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app self.w.resizable(tk.FALSE, tk.FALSE) # Can't be only resizable on one axis else: self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.file_menu.add_command(command=lambda: stats.StatsDialog(self)) self.file_menu.add_command(command=self.save_raw) self.file_menu.add_command(command=lambda: prefs.PreferencesDialog( self.w, self.postprefs)) self.file_menu.add_separator() self.file_menu.add_command(command=self.onexit) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) self.help_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.help_menu.add_command(command=self.help_general) self.help_menu.add_command(command=self.help_privacy) self.help_menu.add_command(command=self.help_releases) self.help_menu.add_command( command=lambda: self.updater.checkForUpdates()) self.menubar.add_cascade(menu=self.help_menu) if platform == 'win32': # Must be added after at least one "real" menu entry self.always_ontop = tk.BooleanVar( value=config.getint('always_ontop')) self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) self.system_menu.add_separator() self.system_menu.add_checkbutton( label=_('Always on top'), variable=self.always_ontop, command=self.ontop_changed) # Appearance setting self.menubar.add_cascade(menu=self.system_menu) self.w.bind('<Control-c>', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) theme.register( self.menubar ) # menus and children aren't automatically registered theme.register(self.file_menu) theme.register(self.edit_menu) theme.register(self.help_menu) # Alternate title bar and menu for dark theme self.theme_menubar = tk.Frame(frame) self.theme_menubar.columnconfigure(2, weight=1) theme_titlebar = tk.Label(self.theme_menubar, text=applongname, image=self.theme_icon, cursor='fleur', anchor=tk.W, compound=tk.LEFT) theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW) self.drag_offset = None theme_titlebar.bind('<Button-1>', self.drag_start) theme_titlebar.bind('<B1-Motion>', self.drag_continue) theme_titlebar.bind('<ButtonRelease-1>', self.drag_end) theme_minimize = tk.Label(self.theme_menubar, image=self.theme_minimize) theme_minimize.grid(row=0, column=3, padx=2) theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) theme_close = tk.Label(self.theme_menubar, image=self.theme_close) theme_close.grid(row=0, column=4, padx=2) theme.button_bind(theme_close, self.onexit, image=self.theme_close) self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W) theme.button_bind( self.theme_file_menu, lambda e: self.file_menu.tk_popup( e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) theme.button_bind( self.theme_edit_menu, lambda e: self.edit_menu.tk_popup( e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) self.theme_help_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_help_menu.grid(row=1, column=2, sticky=tk.W) theme.button_bind( self.theme_help_menu, lambda e: self.help_menu.tk_popup( e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) tk.Frame(self.theme_menubar, highlightthickness=1).grid(columnspan=5, padx=5, sticky=tk.EW) theme.register( self.theme_minimize) # images aren't automatically registered theme.register(self.theme_close) self.blank_menubar = tk.Frame(frame) tk.Label(self.blank_menubar).grid() tk.Label(self.blank_menubar).grid() tk.Frame(self.blank_menubar, height=2).grid() theme.register_alternate( (self.menubar, self.theme_menubar, self.blank_menubar), { 'row': 0, 'columnspan': 2, 'sticky': tk.NSEW }) self.w.resizable(tk.TRUE, tk.FALSE) # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match: if platform == 'darwin': if int( match.group(2) ) >= 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) elif platform == 'win32': # Check that the titlebar will be at least partly on screen import ctypes from ctypes.wintypes import POINT # https://msdn.microsoft.com/en-us/library/dd145064 MONITOR_DEFAULTTONULL = 0 if ctypes.windll.user32.MonitorFromPoint( POINT( int(match.group(1)) + 16, int(match.group(2)) + 16), MONITOR_DEFAULTTONULL): self.w.geometry(config.get('geometry')) else: self.w.geometry(config.get('geometry')) self.w.attributes('-topmost', config.getint('always_ontop') and 1 or 0) theme.register(frame) theme.apply(self.w) self.w.bind('<Map>', self.onmap) # Special handling for overrideredict self.w.bind('<Enter>', self.onenter) # Special handling for transparency self.w.bind('<FocusIn>', self.onenter) # " self.w.bind('<Leave>', self.onleave) # " self.w.bind('<FocusOut>', self.onleave) # " self.w.bind('<Return>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend) self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring self.w.bind_all('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar self.w.bind_all('<<CompanionAuthEvent>>', self.auth) # cAPI auth self.w.bind_all('<<Quit>>', self.onexit) # Updater # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) if not getattr(sys, 'frozen', False): self.updater.checkForUpdates( ) # Sparkle / WinSparkle does this automatically for packaged apps try: config.get_password( '') # Prod SecureStorage on Linux to initialise except RuntimeError: pass # Migration from <= 3.30 for username in config.get('fdev_usernames') or []: config.delete_password(username) config.delete('fdev_usernames') config.delete('username') config.delete('password') config.delete('logdir') self.postprefs( False) # Companion login happens in callback from monitor plugins_not_py3_last = config.getint('plugins_not_py3_last') or int( time()) if (plugins_not_py3_last + 86400) < int(time()) and len( plug.PLUGINS_not_py3): import tkMessageBox tkMessageBox.showinfo( 'Plugins Without Python 3.x Support', "One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the 'Plugins' tab of 'File' > 'Settings'. You should check if there is an updated version available, else alert the developer that they will need to update the code when EDMC moves to Python 3.x" ) config.set('plugins_not_py3_last', int(time())) # callback after the Preferences dialog is applied def postprefs(self, dologin=True): self.prefsdialog = None self.set_labels() # in case language has changed # Reset links in case plugins changed them self.ship.configure(url=self.shipyard_url) self.system.configure(url=self.system_url) self.station.configure(url=self.station_url) # (Re-)install hotkey monitoring hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # (Re-)install log monitoring if not monitor.start(self.w): self.status['text'] = 'Error: Check %s' % _( 'E:D journal file location' ) # Location of the new Journal file in E:D 2.2 if dologin and monitor.cmdr: self.login() # Login if not already logged in with this Cmdr # set main window labels, e.g. after language change def set_labels(self): self.cmdr_label['text'] = _('Cmdr') + ':' # Main window self.ship_label['text'] = ( monitor.state['Captain'] and _('Role') or # Multicrew role label in main window _('Ship')) + ':' # Main window self.system_label['text'] = _('System') + ':' # Main window self.station_label['text'] = _('Station') + ':' # Main window self.button['text'] = self.theme_button['text'] = _( 'Update') # Update button in main window if platform == 'darwin': self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX self.menubar.entryconfigure(5, label=_('Help')) # Menu title self.system_menu.entryconfigure( 0, label=_("About {APP}").format( APP=applongname)) # App menu entry on OSX self.system_menu.entryconfigure( 1, label=_("Check for Updates...")) # Menu item self.file_menu.entryconfigure( 0, label=_('Save Raw Data...')) # Menu item self.view_menu.entryconfigure(0, label=_('Status')) # Menu item self.help_menu.entryconfigure( 1, label=_('Privacy Policy')) # Help menu item self.help_menu.entryconfigure( 2, label=_('Release Notes')) # Help menu item else: self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.menubar.entryconfigure(3, label=_('Help')) # Menu title self.theme_file_menu['text'] = _('File') # Menu title self.theme_edit_menu['text'] = _('Edit') # Menu title self.theme_help_menu['text'] = _('Help') # Menu title self.file_menu.entryconfigure(0, label=_('Status')) # Menu item self.file_menu.entryconfigure( 1, label=_('Save Raw Data...')) # Menu item self.file_menu.entryconfigure( 2, label=_('Settings')) # Item in the File menu on Windows self.file_menu.entryconfigure( 4, label=_('Exit')) # Item in the File menu on Windows self.help_menu.entryconfigure( 0, label=_('Documentation')) # Help menu item self.help_menu.entryconfigure( 1, label=_('Privacy Policy')) # Help menu item self.help_menu.entryconfigure( 2, label=_('Release Notes')) # Help menu item self.help_menu.entryconfigure( 3, label=_('Check for Updates...')) # Menu item self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste def login(self): if not self.status['text']: self.status['text'] = _('Logging in...') self.button['state'] = self.theme_button['state'] = tk.DISABLED if platform == 'darwin': self.view_menu.entryconfigure(0, state=tk.DISABLED) # Status self.file_menu.entryconfigure(0, state=tk.DISABLED) # Save Raw Data else: self.file_menu.entryconfigure(0, state=tk.DISABLED) # Status self.file_menu.entryconfigure(1, state=tk.DISABLED) # Save Raw Data self.w.update_idletasks() try: if companion.session.login(monitor.cmdr, monitor.is_beta): self.status['text'] = _( 'Authentication successful' ) # Successfully authenticated with the Frontier website if platform == 'darwin': self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure( 0, state=tk.NORMAL) # Save Raw Data else: self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure( 1, state=tk.NORMAL) # Save Raw Data except (companion.CredentialsError, companion.ServerError, companion.ServerLagging) as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) self.cooldown() def getandsend(self, event=None, retrying=False): auto_update = not event play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL ) and not config.getint('hotkey_mute') play_bad = False if not monitor.cmdr or not monitor.mode or monitor.state[ 'Captain'] or not monitor.system: return # In CQC or on crew - do nothing if companion.session.state == companion.Session.STATE_AUTH: # Attempt another Auth self.login() return if not retrying: if time( ) < self.holdofftime: # Was invoked by key while in cooldown self.status['text'] = '' 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 elif play_sound: hotkeymgr.play_good() self.status['text'] = _('Fetching data...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() try: querytime = int(time()) data = companion.session.station() config.set('querytime', querytime) # Validation if not data.get('commander', {}).get('name'): self.status['text'] = _("Who are you?!") # Shouldn't happen elif (not data.get('lastSystem', {}).get('name') or (data['commander'].get('docked') and not data.get( 'lastStarport', {}).get('name'))): # Only care if docked self.status['text'] = _("Where are you?!") # Shouldn't happen elif not data.get('ship', {}).get('name') or not data.get( 'ship', {}).get('modules'): self.status['text'] = _( "What are you flying?!") # Shouldn't happen elif monitor.cmdr and data['commander']['name'] != monitor.cmdr: raise companion.CmdrError( ) # Companion API return doesn't match Journal elif ( (auto_update and not data['commander'].get('docked')) or (data['lastSystem']['name'] != monitor.system) or ((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or (data['ship']['id'] != monitor.state['ShipID']) or (data['ship']['name'].lower() != monitor.state['ShipType'])): raise companion.ServerLagging() else: if __debug__: # Recording if isdir('dump'): with open( 'dump/%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.' + data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h: h.write( json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) if not monitor.state[ 'ShipType']: # Started game in SRV or fighter self.ship['text'] = companion.ship_map.get( data['ship']['name'].lower(), data['ship']['name']) monitor.state['ShipID'] = data['ship']['id'] monitor.state['ShipType'] = data['ship']['name'].lower() if data['commander'].get('credits') is not None: monitor.state['Credits'] = data['commander']['credits'] monitor.state['Loan'] = data['commander'].get('debt', 0) # stuff we can do when not docked err = plug.notify_newdata(data, monitor.is_beta) self.status['text'] = err and err or '' if err: play_bad = True # Export market data if config.getint('output') & (config.OUT_STATION_ANY): if not data['commander'].get('docked'): if not self.status['text']: # Signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up self.status['text'] = _( "You're not docked at a station!") play_bad = True elif (config.getint('output') & config.OUT_MKT_EDDN) and not ( data['lastStarport'].get('commodities') or data['lastStarport'].get('modules') ): # Ignore possibly missing shipyard info if not self.status['text']: self.status['text'] = _( "Station doesn't have anything!") elif not data['lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _( "Station doesn't have a market!") elif config.getint('output') & (config.OUT_MKT_CSV | config.OUT_MKT_TD): # Fixup anomalies in the commodity data fixed = companion.fixup(data) if config.getint('output') & config.OUT_MKT_CSV: commodity.export(fixed, COMMODITY_CSV) if config.getint('output') & config.OUT_MKT_TD: td.export(fixed) self.holdofftime = querytime + companion.holdoff # Companion API problem except companion.ServerLagging as e: if retrying: self.status['text'] = unicode(e) play_bad = True else: # Retry once if Companion server is unresponsive self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True)) return # early exit to avoid starting cooldown count except companion.CmdrError as e: # Companion API return doesn't match Journal self.status['text'] = unicode(e) play_bad = True companion.session.invalidate() self.login() except Exception as e: # Including CredentialsError, ServerError if __debug__: print_exc() self.status['text'] = unicode(e) play_bad = True if not self.status['text']: # no errors 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 and play_bad: hotkeymgr.play_bad() self.cooldown() def retry_for_shipyard(self, tries): # Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data. try: data = companion.session.station() if __debug__: print 'Retry for shipyard - ' + ( data['commander'].get('docked') and (data.get('lastStarport', {}).get('ships') and 'Success' or 'Failure') or 'Undocked!') if not data['commander'].get('docked'): pass # might have undocked while we were waiting for retry in which case station data is unreliable elif (data.get('lastSystem', {}).get('name') == monitor.system and data.get('lastStarport', {}).get('name') == monitor.station and data.get('lastStarport', {}).get( 'ships', {}).get('shipyard_list')): self.eddn.export_shipyard(data, monitor.is_beta) elif tries > 1: # bogus data - retry self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries - 1)) except: pass # Handle event(s) from the journal def journal_event(self, event): def crewroletext(role): # Return translated crew role. Needs to be dynamic to allow for changing language. return { None: '', 'Idle': '', 'FighterCon': _('Fighter'), # Multicrew role 'FireCon': _('Gunner'), # Multicrew role 'FlightCon': _('Helm'), # Multicrew role }.get(role, role) while True: entry = monitor.get_entry() if not entry: return # Update main window self.cooldown() if monitor.cmdr and monitor.state['Captain']: self.cmdr['text'] = '%s / %s' % (monitor.cmdr, monitor.state['Captain']) self.ship_label['text'] = _( 'Role') + ':' # Multicrew role label in main window self.ship.configure(state=tk.NORMAL, text=crewroletext(monitor.state['Role']), url=None) elif monitor.cmdr: if monitor.group: self.cmdr['text'] = '%s / %s' % (monitor.cmdr, monitor.group) else: self.cmdr['text'] = monitor.cmdr self.ship_label['text'] = _('Ship') + ':' # Main window self.ship.configure( text=monitor.state['ShipName'] or companion.ship_map.get( monitor.state['ShipType'], monitor.state['ShipType']) or '', url=self.shipyard_url) else: self.cmdr['text'] = '' self.ship_label['text'] = _('Ship') + ':' # Main window self.ship['text'] = '' self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy if entry['event'] in [ 'Undocked', 'StartJump', 'SetUserShipName', 'ShipyardBuy', 'ShipyardSell', 'ShipyardSwap', 'ModuleBuy', 'ModuleSell', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis', 'JoinACrew' ]: self.status['text'] = '' # Periodically clear any old error self.w.update_idletasks() # Companion login if entry['event'] in [None, 'StartUp', 'NewCommander', 'LoadGame' ] and monitor.cmdr: if not config.get('cmdrs') or monitor.cmdr not in config.get( 'cmdrs'): config.set('cmdrs', (config.get('cmdrs') or []) + [monitor.cmdr]) self.login() if not entry['event'] or not monitor.mode: return # Startup or in CQC if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: # Disable WinSparkle automatic update checks, IFF configured to do so when in-game if config.getint('disable_autoappupdatecheckingame') and 1: self.updater.setAutomaticUpdatesCheck(False) print 'Monitor: Disable WinSparkle automatic update checks' # Can start dashboard monitoring if not dashboard.start(self.w, monitor.started): print "Can't start Status monitoring" # Export loadout if entry['event'] == 'Loadout' and not monitor.state[ 'Captain'] and config.getint('output') & config.OUT_SHIP: monitor.export_ship() # Plugins err = plug.notify_journal_entry(monitor.cmdr, monitor.is_beta, monitor.system, monitor.station, entry, monitor.state) if err: self.status['text'] = err if not config.getint('hotkey_mute'): hotkeymgr.play_bad() # Auto-Update after docking, but not if auth callback is pending if entry['event'] in [ 'StartUp', 'Location', 'Docked' ] and monitor.station and not config.getint( 'output' ) & config.OUT_MKT_MANUAL and config.getint( 'output' ) & config.OUT_STATION_ANY and companion.session.state != companion.Session.STATE_AUTH: self.w.after(int(SERVER_RETRY * 1000), self.getandsend) if entry['event'] == 'ShutDown': # Enable WinSparkle automatic update checks # NB: Do this blindly, in case option got changed whilst in-game self.updater.setAutomaticUpdatesCheck(True) print 'Monitor: Enable WinSparkle automatic update checks' # cAPI auth def auth(self, event=None): try: companion.session.auth_callback() self.status['text'] = _( 'Authentication successful' ) # Successfully authenticated with the Frontier website if platform == 'darwin': self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data else: self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data except companion.ServerError as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) self.cooldown() # Handle Status event def dashboard_event(self, event): entry = dashboard.status if entry: # Currently we don't do anything with these events err = plug.notify_dashboard_entry(monitor.cmdr, monitor.is_beta, entry) if err: self.status['text'] = err if not config.getint('hotkey_mute'): hotkeymgr.play_bad() # Display asynchronous error from plugin def plugin_error(self, event=None): if plug.last_error.get('msg'): self.status['text'] = plug.last_error['msg'] self.w.update_idletasks() if not config.getint('hotkey_mute'): hotkeymgr.play_bad() def shipyard_url(self, shipname): return plug.invoke(config.get('shipyard_provider'), 'EDSY', 'shipyard_url', monitor.ship(), monitor.is_beta) def system_url(self, system): return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system) def station_url(self, station): return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station) def cooldown(self): if time() < self.holdofftime: self.button['text'] = self.theme_button['text'] = _( 'cooldown {SS}s').format( SS=int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: self.button['text'] = self.theme_button['text'] = _( 'Update') # Update button in main window self.button['state'] = self.theme_button['state'] = ( monitor.cmdr and monitor.mode and not monitor.state['Captain'] and monitor.system and tk.NORMAL or tk.DISABLED) def ontop_changed(self, event=None): config.set('always_ontop', self.always_ontop.get()) self.w.wm_attributes('-topmost', self.always_ontop.get()) def copy(self, event=None): if monitor.system: self.w.clipboard_clear() self.w.clipboard_append(monitor.station and '%s,%s' % (monitor.system, monitor.station) or monitor.system) def help_general(self, event=None): webbrowser.open('https://github.com/EDCD/EDMarketConnector/wiki') def help_privacy(self, event=None): webbrowser.open( 'https://github.com/EDCD/EDMarketConnector/wiki/Privacy-Policy') def help_releases(self, event=None): webbrowser.open('https://github.com/EDCD/EDMarketConnector/releases') def save_raw(self): self.status['text'] = _('Fetching data...') self.w.update_idletasks() try: data = companion.session.station() self.status['text'] = '' f = tkFileDialog.asksaveasfilename( parent=self.w, defaultextension=platform == 'darwin' and '.json' or '', filetypes=[('JSON', '.json'), ('All Files', '*')], initialdir=config.get('outdir'), initialfile='%s%s.%s.json' % (data.get('lastSystem', {}).get( 'name', 'Unknown'), data['commander'].get('docked') and '.' + data.get('lastStarport', {}).get('name', 'Unknown') or '', strftime('%Y-%m-%dT%H.%M.%S', localtime()))) if f: with open(f, 'wt') as h: h.write( json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) except companion.ServerError as e: self.status['text'] = str(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) def onexit(self, event=None): if platform != 'darwin' or self.w.winfo_rooty( ) > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) self.w.withdraw( ) # Following items can take a few seconds, so hide the main window while they happen protocolhandler.close() hotkeymgr.unregister() dashboard.close() monitor.close() plug.notify_stop() self.updater.close() companion.session.close() config.close() self.w.destroy() def drag_start(self, event): self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) def drag_continue(self, event): if self.drag_offset: self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1])) def drag_end(self, event): self.drag_offset = None def oniconify(self, event=None): self.w.overrideredirect(0) # Can't iconize while overrideredirect self.w.iconify() self.w.update_idletasks( ) # Size and windows styles get recalculated here self.w.wait_visibility( ) # Need main window to be re-created before returning theme.active = None # So theme will be re-applied on map def onmap(self, event=None): if event.widget == self.w: theme.apply(self.w) def onenter(self, event=None): if config.getint('theme') > 1: self.w.attributes("-transparentcolor", '') self.blank_menubar.grid_remove() self.theme_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW) def onleave(self, event=None): if config.getint('theme') > 1 and event.widget == self.w: self.w.attributes("-transparentcolor", 'grey4') self.theme_menubar.grid_remove() self.blank_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW)
class AppWindow: STATION_UNDOCKED = u'×' # "Station" name to display when not docked = U+00D7 def __init__(self, master): self.holdofftime = config.getint('querytime') + companion.holdoff self.session = companion.Session() self.edsm = edsm.EDSM() self.w = master self.w.title(applongname) self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) # Special handling for overrideredict self.w.bind("<Map>", self.onmap) plug.load_plugins() if platform != 'darwin': if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') else: from PIL import Image, ImageTk self.w.tk.call('wm', 'iconphoto', self.w, '-default', ImageTk.PhotoImage(Image.open("EDMarketConnector.png"))) self.theme_icon = tk.PhotoImage(data = 'R0lGODlhEAAQAMYAAAAAAAEAAAEBAQICAgQEAwYFBAsHBAoIBgwIBAwIBQ0IBA8JBBAJBBAKBRMMBRkPBhoQBykWCSoWCCoXCTsfCzwfCkAhDEIjDD8kC0AlDEEmC0EmDEcoDk4oDU8pDU4qEFMrD1ktDlotD1ouD1g0EWAyEWU0EV03EmA4EWo2EW03EWQ6Emw4EWo9FGo+E3Y8FH5AFH1IFoBJFo1RGo1SGY1SGpBTGZFTGZJTGZhYG6piHa1kHa5kHbBkHr9uIMt0IgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEKAEAALAAAAAAQABAAAAd7gACCg4SFhoeHGCiIhRs5JwMCkpKGGTIFODaaNjc/D4QaMQMAk5MuEIQOO6OFAiscCIQNPQk8NTO4NDofLwayPi0mIMPDLAcqvoIBDiQWkaUCAykKhAsXAoYCHRKEDDAjIyIiIyEEHhHYhAPr7BQlE+mMABXo8oTx9oWBADs=') self.theme_minimize = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') self.theme_close = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) self.cmdr_label = tk.Label(frame) self.system_label = tk.Label(frame) self.station_label = tk.Label(frame) self.cmdr_label.grid(row=1, column=0, sticky=tk.W) self.system_label.grid(row=2, column=0, sticky=tk.W) self.station_label.grid(row=3, column=0, sticky=tk.W) self.cmdr = tk.Label(frame, anchor=tk.W) self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.system_url, popup_copy = True) self.station = HyperlinkLabel(frame, url = self.station_url, popup_copy = lambda x: x!=self.STATION_UNDOCKED) self.cmdr.grid(row=1, column=1, sticky=tk.EW) self.system.grid(row=2, column=1, sticky=tk.EW) self.station.grid(row=3, column=1, sticky=tk.EW) for plugname in plug.PLUGINS: appitem = plug.get_plugin_app(plugname, frame) if appitem: appitem.grid(columnspan=2, sticky=tk.W) self.button = ttk.Button(frame, text=_('Update'), width=28, command=self.getandsend, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.theme_button = tk.Label(frame, width = platform == 'darwin' and 32 or 28, state=tk.DISABLED) self.status = tk.Label(frame, name='status', anchor=tk.W) row = frame.grid_size()[1] self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) theme.register_alternate((self.button, self.theme_button), {'row':row, 'columnspan':2, 'sticky':tk.NSEW}) self.status.grid(columnspan=2, sticky=tk.EW) theme.button_bind(self.theme_button, self.getandsend) self.w.bind('<Return>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend) for child in frame.winfo_children(): child.grid_configure(padx=5, pady=(platform=='win32' and 1 or 3)) self.menubar = tk.Menu() if platform=='darwin': # Can't handle (de)iconify if topmost is set, so suppress iconify button # http://wiki.tcl.tk/13428 and p15 of https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox horizontalZoom resizable') # https://www.tcl.tk/man/tcl/TkCmd/menu.htm self.file_menu = tk.Menu(self.menubar, name='apple') self.file_menu.add_command(command=lambda:self.w.call('tk::mac::standardAboutPanel')) self.file_menu.add_command(command=lambda:self.updater.checkForUpdates()) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, name='edit') self.edit_menu.add_command(accelerator='Command-c', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) self.w.bind('<Command-c>', self.copy) self.view_menu = tk.Menu(self.menubar, name='view') self.view_menu.add_command(state=tk.DISABLED, command=lambda:stats.StatsDialog(self.w, self.session)) self.menubar.add_cascade(menu=self.view_menu) window_menu = tk.Menu(self.menubar, name='window') self.menubar.add_cascade(menu=window_menu) self.w['menu'] = self.menubar # 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')) self.w.createcommand("::tk::mac::Quit", self.onexit) self.w.createcommand("::tk::mac::ShowPreferences", lambda:prefs.PreferencesDialog(self.w, self.postprefs)) self.w.createcommand("::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app else: self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.file_menu.add_command(state=tk.DISABLED, command=lambda:stats.StatsDialog(self.w, self.session)) self.file_menu.add_command(command=lambda:self.updater.checkForUpdates()) self.file_menu.add_command(command=lambda:prefs.PreferencesDialog(self.w, self.postprefs)) self.file_menu.add_separator() self.file_menu.add_command(command=self.onexit) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) if platform == 'win32': # Must be added after at least one "real" menu entry self.always_ontop = tk.BooleanVar(value = config.getint('always_ontop')) system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) system_menu.add_separator() system_menu.add_checkbutton(label=_('Always on top'), variable = self.always_ontop, command=self.ontop_changed) # Appearance setting self.menubar.add_cascade(menu=system_menu) # Gets index 0 self.w.bind('<Control-c>', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) theme.register(self.menubar) # menus and children aren't automatically registered theme.register(self.file_menu) theme.register(self.edit_menu) # Alternate title bar and menu for dark theme self.theme_menubar = tk.Frame(frame) self.theme_menubar.columnconfigure(2, weight=1) theme_titlebar = tk.Label(self.theme_menubar, text=applongname, image=self.theme_icon, anchor=tk.W, compound=tk.LEFT) theme_titlebar.grid(columnspan=3, sticky=tk.NSEW) self.drag_offset = None theme_titlebar.bind('<Button-1>', self.drag_start) theme_titlebar.bind('<B1-Motion>', self.drag_continue) theme_titlebar.bind('<ButtonRelease-1>', self.drag_end) if platform == 'win32': # Can't work out how to deiconify on Linux theme_minimize = tk.Label(self.theme_menubar, image=self.theme_minimize) theme_minimize.grid(row=0, column=3) theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) theme_close = tk.Label(self.theme_menubar, image=self.theme_close) theme_close.grid(row=0, column=4) theme.button_bind(theme_close, self.onexit, image=self.theme_close) self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W) theme.button_bind(self.theme_file_menu, lambda e: self.file_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W) # Menu title self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) theme.button_bind(self.theme_edit_menu, lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) theme.register_highlight(theme_titlebar) theme.register(self.theme_minimize) # images aren't automatically registered theme.register(self.theme_close) theme.register_alternate((self.menubar, self.theme_menubar), {'row':0, 'columnspan':2, 'sticky':tk.NSEW}) self.set_labels() # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match: if platform == 'darwin': if int(match.group(2)) >= 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) elif platform == 'win32': # Check that the titlebar will be at least partly on screen import ctypes from ctypes.wintypes import POINT # https://msdn.microsoft.com/en-us/library/dd145064 MONITOR_DEFAULTTONULL = 0 if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16), MONITOR_DEFAULTTONULL): self.w.geometry(config.get('geometry')) else: self.w.geometry(config.get('geometry')) self.w.attributes('-topmost', config.getint('always_ontop') and 1 or 0) self.w.resizable(tk.TRUE, tk.FALSE) theme.register(frame) theme.register_highlight(self.system) theme.register_highlight(self.station) theme.apply(self.w) # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) self.w.bind_all('<<Quit>>', self.onexit) # user-generated # Install hotkey monitoring self.w.bind_all('<<Invoke>>', self.getandsend) # user-generated hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring monitor.set_callback(self.system_change) edproxy.set_callback(self.system_change) if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_FILE|config.OUT_LOG_EDSM)): monitor.enable_logging() monitor.start(self.w) edproxy.start(self.w) # First run if not config.get('username') or not config.get('password'): prefs.PreferencesDialog(self.w, self.postprefs) else: self.login() # callback after the Preferences dialog is applied def postprefs(self): self.set_labels() # in case language has changed self.login() # in case credentials gave changed # set main window labels, e.g. after language change def set_labels(self): self.cmdr_label['text'] = _('Cmdr') + ':' # Main window self.system_label['text'] = _('System') + ':' # Main window self.station_label['text'] = _('Station') + ':' # Main window self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste self.view_menu.entryconfigure(0, label=_('Status')) # Menu item self.file_menu.entryconfigure(1, label=_("Check for Updates...")) # Menu item if platform == 'darwin': self.file_menu.entryconfigure(0, label=_("About {APP}").format(APP=applongname)) # App menu entry on OSX self.menubar.entryconfigure(1, label=_('Edit')) # Menu title self.menubar.entryconfigure(2, label=_('View')) # Menu title on OSX self.menubar.entryconfigure(3, label=_('Window')) # Menu title on OSX else: self.file_menu.entryconfigure(2, label=_("Settings")) # Item in the File menu on Windows self.file_menu.entryconfigure(4, label=_("Exit")) # Item in the File menu on Windows self.menubar.entryconfigure(self.menubar.index('end')-2, label=_('File')) # Menu title on Windows self.menubar.entryconfigure(self.menubar.index('end')-1, label=_('Edit')) # Menu title self.theme_file_menu['text'] = _('File') # Menu title on Windows self.theme_edit_menu['text'] = _('Edit') # Menu title def login(self): self.status['text'] = _('Logging in...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() try: self.session.login(config.get('username'), config.get('password')) self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status self.status['text'] = '' except companion.VerificationRequired: # don't worry about authentication now - prompt on query self.status['text'] = '' except companion.ServerError as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) # Try to obtain exclusive lock on flight log ASAP if config.getint('output') & config.OUT_LOG_FILE: try: flightlog.openlog() except Exception as e: if __debug__: print_exc() if not self.status['text']: self.status['text'] = unicode(e) if not self.status['text'] and monitor.restart_required(): self.status['text'] = _('Re-start Elite: Dangerous for automatic log entries') # Status bar message on launch elif not getattr(sys, 'frozen', False): self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps self.cooldown() # callback after verification code def verify(self, code): try: self.session.verify(code) config.save() # Save settings now for use by command-line app except Exception as e: if __debug__: print_exc() self.button['state'] = self.theme_button['state'] = tk.NORMAL self.status['text'] = unicode(e) else: return self.getandsend() # try again 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: # Was invoked by key while in cooldown self.status['text'] = '' 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 elif play_sound: hotkeymgr.play_good() self.cmdr['text'] = self.system['text'] = self.station['text'] = '' self.system['image'] = '' self.status['text'] = _('Fetching data...') self.theme_button['state'] = tk.DISABLED self.edit_menu.entryconfigure(0, state=tk.DISABLED) # Copy self.w.update_idletasks() try: querytime = int(time()) data = self.session.query() config.set('querytime', querytime) # Validation if not data.get('commander') or not data['commander'].get('name','').strip(): self.status['text'] = _("Who are you?!") # Shouldn't happen 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 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 else: if __debug__: # Recording with open('%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h: h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True).encode('utf-8')) self.cmdr['text'] = data.get('commander') and data.get('commander').get('name') or '' self.system['text'] = data.get('lastSystem') and data.get('lastSystem').get('name') or '' self.station['text'] = data.get('commander') and data.get('commander').get('docked') and data.get('lastStarport') and data.get('lastStarport').get('name') or (EDDB.system(self.system['text']) and self.STATION_UNDOCKED or '') self.status['text'] = '' self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status if data['lastStarport'].get('commodities'): # Fixup anomalies in the commodity data self.session.fixup(data['lastStarport']['commodities']) # stuff we can do when not docked plug.notify_newdata(data) if config.getint('output') & config.OUT_SHIP_EDS: loadout.export(data) if config.getint('output') & config.OUT_SHIP_CORIOLIS: coriolis.export(data) if config.getint('output') & config.OUT_LOG_FILE: flightlog.export(data) if config.getint('output') & config.OUT_LOG_EDSM: # Catch any EDSM errors here so that they don't prevent station update try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.export(data, lambda:self.edsm.lookup(self.system['text'], EDDB.system(self.system['text']))) # Do EDSM lookup during EDSM export self.status['text'] = '' except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) else: self.edsm.link(self.system['text']) self.edsmpoll() if not (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)): # no station data requested - we're done pass elif not data['commander'].get('docked'): # signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up if not self.status['text']: self.status['text'] = _("You're not docked at a station!") else: # Finally - the data looks sane and we're docked at a station (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) # No EDDN output at known station? if (config.getint('output') & config.OUT_EDDN) and station_id and not (has_market or has_outfitting or has_shipyard): if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") # No EDDN output at unknown station? elif (config.getint('output') & config.OUT_EDDN) and not station_id and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): # Ignore usually spurious shipyard at unknown stations if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") # No market output at known station? elif not (config.getint('output') & config.OUT_EDDN) and station_id and not has_market: if not self.status['text']: self.status['text'] = _("Station doesn't have a market!") # No market output at unknown station? elif not (config.getint('output') & config.OUT_EDDN) and not station_id and not data['lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _("Station doesn't have a market!") else: if data['lastStarport'].get('commodities'): if config.getint('output') & config.OUT_CSV: bpc.export(data, True) if config.getint('output') & config.OUT_TD: td.export(data) if config.getint('output') & config.OUT_BPC: bpc.export(data, False) elif has_market and (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)): # Overwrite any previous error message self.status['text'] = _("Error: Can't get market data!") if config.getint('output') & config.OUT_EDDN: old_status = self.status['text'] if not old_status: self.status['text'] = _('Sending data to EDDN...') self.w.update_idletasks() eddn.export_commodities(data) if has_outfitting or not station_id: # Only send if eddb says that the station provides outfitting, or unknown station eddn.export_outfitting(data) elif __debug__ and data['lastStarport'].get('modules'): print 'Spurious outfitting!' if has_shipyard: # Only send if eddb says that the station has a shipyard - # https://github.com/Marginal/EDMarketConnector/issues/16 if data['lastStarport'].get('ships'): eddn.export_shipyard(data) else: # API is flakey about shipyard info - silently retry if missing (<1s is usually sufficient - 5s for margin). self.w.after(int(SERVER_RETRY * 1000), self.retry_for_shipyard) elif __debug__ and data['lastStarport'].get('ships'): print 'Spurious shipyard!' if not old_status: self.status['text'] = '' except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, self.verify) # Companion API problem except companion.ServerError as e: if retrying: self.status['text'] = unicode(e) else: # Retry once if Companion server is unresponsive self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True)) return # early exit to avoid starting cooldown count except requests.exceptions.ConnectionError as e: if __debug__: print_exc() self.status['text'] = _("Error: Can't connect to EDDN") except requests.exceptions.Timeout as e: if __debug__: print_exc() self.status['text'] = _("Error: Connection to EDDN timed out") except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not self.status['text']: # no errors 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') elif play_sound: hotkeymgr.play_bad() self.holdofftime = querytime + companion.holdoff self.cooldown() def retry_for_shipyard(self): # Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data. try: data = self.session.query() if __debug__: print 'Retry for shipyard - ' + (data['commander'].get('docked') and (data['lastStarport'].get('ships') and 'Success' or 'Failure') or 'Undocked!') if data['commander'].get('docked'): # might have undocked while we were waiting for retry in which case station data is unreliable eddn.export_shipyard(data) except: pass def system_change(self, timestamp, system): if self.system['text'] != system: self.system['text'] = system self.system['image'] = '' self.station['text'] = EDDB.system(system) and self.STATION_UNDOCKED or '' plug.notify_system_changed(timestamp, system) if config.getint('output') & config.OUT_LOG_FILE: flightlog.writelog(timestamp, system) if config.getint('output') & config.OUT_LOG_EDSM: try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.writelog(timestamp, system, lambda:self.edsm.lookup(system, EDDB.system(system))) # Do EDSM lookup during EDSM export self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not config.getint('hotkey_mute'): hotkeymgr.play_bad() else: self.edsm.link(system) self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') self.edsmpoll() def edsmpoll(self): result = self.edsm.result if result['done']: self.system['image'] = result['img'] if result['uncharted'] and config.getint('edsm_autoopen'): webbrowser.open(result['url']) else: self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) def system_url(self, text): return text and self.edsm.result['url'] def station_url(self, text): if text: (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) if station_id: return 'http://eddb.io/station/%d' % station_id system_id = EDDB.system(self.system['text']) if system_id: return 'http://eddb.io/system/%d' % system_id return None def cooldown(self): if time() < self.holdofftime: self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window self.button['state'] = self.theme_button['state'] = tk.NORMAL def ontop_changed(self, event=None): config.set('always_ontop', self.always_ontop.get()) self.w.wm_attributes('-topmost', self.always_ontop.get()) def copy(self, event=None): if self.system['text']: self.w.clipboard_clear() self.w.clipboard_append(self.station['text'] == self.STATION_UNDOCKED and self.system['text'] or '%s,%s' % (self.system['text'], self.station['text'])) def onexit(self, event=None): hotkeymgr.unregister() flightlog.close() if platform!='darwin' or self.w.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) config.close() self.updater.close() self.session.close() self.w.destroy() def drag_start(self, event): self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) def drag_continue(self, event): if self.drag_offset: self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1])) def drag_end(self, event): self.drag_offset = None def oniconify(self, event=None): self.w.overrideredirect(0) # Can't iconize while overrideredirect self.w.iconify() self.w.update_idletasks() # Size and windows styles get recalculated here self.w.wait_visibility() # Need main window to be re-created before returning theme.active = None # So theme will be re-applied on map def onmap(self, event=None): if event.widget == self.w: theme.apply(self.w)
class PreferencesDialog(tk.Toplevel): def __init__(self, parent, callback): tk.Toplevel.__init__(self, parent) self.parent = parent self.callback = callback self.title(platform=='darwin' and _('Preferences') or _('Settings')) if parent.winfo_viewable(): self.transient(parent) # position over parent if platform!='darwin' or parent.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.geometry("+%d+%d" % (parent.winfo_rootx(), parent.winfo_rooty())) # remove decoration self.resizable(tk.FALSE, tk.FALSE) if platform=='win32': self.attributes('-toolwindow', tk.TRUE) elif platform=='darwin': # http://wiki.tcl.tk/13428 parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') style = ttk.Style() frame = ttk.Frame(self) frame.grid(sticky=tk.NSEW) notebook = nb.Notebook(frame) PADX = 10 BUTTONX = 12 # indent Checkbuttons and Radiobuttons PADY = 2 # close spacing credframe = nb.Frame(notebook) credframe.columnconfigure(1, weight=1) nb.Label(credframe, text=_('Credentials')).grid(padx=PADX, sticky=tk.W) # Section heading in settings ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) nb.Label(credframe, text=_('Please log in with your Elite: Dangerous account details')).grid(padx=PADX, columnspan=2, sticky=tk.W) # Use same text as E:D Launcher's login dialog nb.Label(credframe, text=_('Username (Email)')).grid(row=10, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog nb.Label(credframe, text=_('Password')).grid(row=11, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog self.username = nb.Entry(credframe) self.username.insert(0, config.get('username') or '') self.username.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW) self.username.focus_set() self.password = nb.Entry(credframe, show=u'•') self.password.insert(0, config.get('password') or '') self.password.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) nb.Label(credframe).grid(sticky=tk.W) # big spacer nb.Label(credframe, text=_('Privacy')).grid(padx=PADX, sticky=tk.W) # Section heading in settings ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) self.out_anon= tk.IntVar(value = config.getint('anonymous') and 1) nb.Label(credframe, text=_('How do you want to be identified in the saved data')).grid(columnspan=2, padx=PADX, sticky=tk.W) nb.Radiobutton(credframe, text=_('Cmdr name'), variable=self.out_anon, value=0).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # EDSM & privacy setting nb.Radiobutton(credframe, text=_('Pseudo-anonymized ID'), variable=self.out_anon, value=1).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Privacy setting notebook.add(credframe, text=_('Identity')) # Tab heading in settings outframe = nb.Frame(notebook) outframe.columnconfigure(0, weight=1) output = config.getint('output') or (config.OUT_EDDN | config.OUT_SHIP_EDS) # default settings nb.Label(outframe, text=_('Please choose what data to save')).grid(columnspan=2, padx=PADX, sticky=tk.W) self.out_eddn= tk.IntVar(value = (output & config.OUT_EDDN) and 1) nb.Checkbutton(outframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.out_eddn, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_csv = tk.IntVar(value = (output & config.OUT_CSV ) and 1) nb.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_bpc = tk.IntVar(value = (output & config.OUT_BPC ) and 1) nb.Checkbutton(outframe, text=_("Market data in Slopey's BPC format file"), variable=self.out_bpc, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_td = tk.IntVar(value = (output & config.OUT_TD ) and 1) nb.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_ship_eds= tk.IntVar(value = (output & config.OUT_SHIP_EDS) and 1) nb.Checkbutton(outframe, text=_('Ship loadout in E:D Shipyard format file'), variable=self.out_ship_eds, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) self.out_ship_coriolis= tk.IntVar(value = (output & config.OUT_SHIP_CORIOLIS) and 1) nb.Checkbutton(outframe, text=_('Ship loadout in Coriolis format file'), variable=self.out_ship_coriolis, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_log_file = tk.IntVar(value = (output & config.OUT_LOG_FILE) and 1) nb.Checkbutton(outframe, text=_('Flight log in CSV format file'), variable=self.out_log_file, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) self.out_log_auto = tk.IntVar(value = monitor.logdir and (output & config.OUT_LOG_AUTO) and 1 or 0) if monitor.logdir: self.out_log_auto_button = nb.Checkbutton(outframe, text=_('Automatically make a log entry on entering a system'), variable=self.out_log_auto, command=self.outvarchanged) # Output setting self.out_log_auto_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_log_auto_text = nb.Label(outframe, foreground='firebrick') self.out_log_auto_text.grid(columnspan=2, padx=(30,0), sticky=tk.W) self.outdir_label = nb.Label(outframe, text=_('File location')) # Section heading in settings self.outdir_label.grid(padx=BUTTONX, sticky=tk.W) self.outdir = nb.Entry(outframe, takefocus=False) if config.get('outdir').startswith(config.home): self.outdir.insert(0, '~' + config.get('outdir')[len(config.home):]) else: self.outdir.insert(0, config.get('outdir')) self.outdir.grid(row=20, padx=(PADX,0), sticky=tk.EW) self.outbutton = nb.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=20, column=1, padx=PADX) nb.Frame(outframe).grid(pady=5) # bottom spacer notebook.add(outframe, text=_('Output')) # Tab heading in settings edsmframe = nb.Frame(notebook) edsmframe.columnconfigure(1, weight=1) HyperlinkLabel(edsmframe, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='http://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate ttk.Separator(edsmframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) self.out_log_edsm = tk.IntVar(value = (output & config.OUT_LOG_EDSM) and 1) nb.Checkbutton(edsmframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.out_log_edsm, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.edsm_autoopen = tk.BooleanVar(value = config.getint('edsm_autoopen')) self.edsm_autoopen_button = nb.Checkbutton(edsmframe, text=_(u"Automatically open uncharted systems’ EDSM pages"), variable=self.edsm_autoopen) self.edsm_autoopen_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) if monitor.logdir: self.edsm_log_auto_button = nb.Checkbutton(edsmframe, text=_('Automatically make a log entry on entering a system'), variable=self.out_log_auto, command=self.outvarchanged) # Output setting self.edsm_log_auto_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.edsm_log_auto_text = nb.Label(edsmframe, foreground='firebrick') self.edsm_log_auto_text.grid(columnspan=2, padx=(30,0), sticky=tk.W) self.edsm_label = HyperlinkLabel(edsmframe, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='http://www.edsm.net/settings/api', underline=True) # Section heading in settings self.edsm_label.grid(columnspan=2, padx=PADX, sticky=tk.W) self.edsm_cmdr_label = nb.Label(edsmframe, text=_('Cmdr name')) # EDSM & privacy setting self.edsm_cmdr_label.grid(row=10, padx=PADX, sticky=tk.W) self.edsm_cmdr = nb.Entry(edsmframe) self.edsm_cmdr.insert(0, config.get('edsm_cmdrname') or '') self.edsm_cmdr.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW) self.edsm_apikey_label = nb.Label(edsmframe, text=_('API Key')) # EDSM setting self.edsm_apikey_label.grid(row=11, padx=PADX, sticky=tk.W) self.edsm_apikey = nb.Entry(edsmframe) self.edsm_apikey.insert(0, config.get('edsm_apikey') or '') self.edsm_apikey.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) notebook.add(edsmframe, text='EDSM') # Not translated if platform in ['darwin','win32']: self.hotkey_code = config.getint('hotkey_code') self.hotkey_mods = config.getint('hotkey_mods') self.hotkey_only = tk.IntVar(value = not config.getint('hotkey_always')) self.hotkey_play = tk.IntVar(value = not config.getint('hotkey_mute')) hotkeyframe = nb.Frame(notebook) hotkeyframe.columnconfigure(1, weight=1) nb.Label(hotkeyframe).grid(sticky=tk.W) # big spacer if platform == 'darwin' and not was_accessible_at_launch: if AXIsProcessTrusted(): nb.Label(hotkeyframe, text = _('Re-start {APP} to use shortcuts').format(APP=applongname), foreground='firebrick').grid(padx=PADX, sticky=tk.NSEW) # Shortcut settings prompt on OSX else: nb.Label(hotkeyframe, text = _('{APP} needs permission to use shortcuts').format(APP=applongname), foreground='firebrick').grid(columnspan=2, padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX nb.Button(hotkeyframe, text = _('Open System Preferences'), command = self.enableshortcuts).grid(column=1, padx=PADX, sticky=tk.E) # Shortcut settings button on OSX else: self.hotkey_text = nb.Entry(hotkeyframe, width = (platform == 'darwin' and 20 or 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) nb.Label(hotkeyframe, text = platform=='darwin' and _('Keyboard shortcut') or # Tab heading in settings on OSX _('Hotkey') # Tab heading in settings on Windows ).grid(row=10, column=0, padx=PADX, sticky=tk.NSEW) self.hotkey_text.grid(row=10, column=1, padx=PADX, sticky=tk.NSEW) nb.Label(hotkeyframe).grid(sticky=tk.W) # big spacer self.hotkey_only_btn = nb.Checkbutton(hotkeyframe, text=_('Only when Elite: Dangerous is the active app'), variable=self.hotkey_only, state = self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting self.hotkey_only_btn.grid(columnspan=2, padx=PADX, sticky=tk.W) self.hotkey_play_btn = nb.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(columnspan=2, padx=PADX, sticky=tk.W) notebook.add(hotkeyframe, text = platform=='darwin' and _('Keyboard shortcut') or # Tab heading in settings on OSX _('Hotkey')) # Tab heading in settings on Windows # build plugin prefs tabs for plugname in plug.PLUGINS: plugframe = plug.get_plugin_pref(plugname, notebook) if plugframe: notebook.add(plugframe, text=plugname) if platform=='darwin': self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes else: buttonframe = ttk.Frame(frame) buttonframe.grid(padx=PADX, pady=PADX, sticky=tk.NSEW) buttonframe.columnconfigure(0, weight=1) ttk.Label(buttonframe).grid(row=0, column=0) # spacer button = ttk.Button(buttonframe, text=_('OK'), command=self.apply) button.grid(row=0, column=1, sticky=tk.E) button.bind("<Return>", lambda event:self.apply()) self.protocol("WM_DELETE_WINDOW", self._destroy) # Selectively disable buttons depending on output settings self.outvarchanged() # disable hotkey for the duration hotkeymgr.unregister() # wait for window to appear on screen before calling grab_set self.wait_visibility() self.grab_set() #self.wait_window(self) # causes duplicate events on OSX def outvarchanged(self): local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship_eds.get() or self.out_ship_coriolis.get() or self.out_log_file.get() self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED edsm_state = self.out_log_edsm.get() and tk.NORMAL or tk.DISABLED self.edsm_autoopen_button['state'] = edsm_state self.edsm_label['state'] = edsm_state self.edsm_cmdr_label['state'] = edsm_state self.edsm_apikey_label['state'] = edsm_state self.edsm_cmdr['state'] = edsm_state self.edsm_apikey['state'] = edsm_state if monitor.logdir: log = self.out_log_file.get() self.out_log_auto_button['state'] = log and tk.NORMAL or tk.DISABLED self.out_log_auto_text['text'] = '' if log and self.out_log_auto.get(): if not monitor.enable_logging(): self.out_log_auto_text['text'] = "Can't enable automatic logging!" # Shouldn't happen - don't translate elif monitor.restart_required(): self.out_log_auto_text['text'] = _('Re-start Elite: Dangerous to use this feature') # Output settings prompt self.edsm_log_auto_button['state'] = edsm_state self.edsm_log_auto_text['text'] = '' if self.out_log_edsm.get() and self.out_log_auto.get(): if not monitor.enable_logging(): self.edsm_log_auto_text['text'] = "Can't enable automatic logging!" # Shouldn't happen - don't translate elif monitor.restart_required(): self.edsm_log_auto_text['text'] = _('Re-start Elite: Dangerous to use this feature') # Output settings prompt def outbrowse(self): if platform != 'win32': d = tkFileDialog.askdirectory(parent=self, initialdir=expanduser(self.outdir.get()), title=_('File location'), mustexist=tk.TRUE) else: def browsecallback(hwnd, uMsg, lParam, lpData): # set initial folder if uMsg==BFFM_INITIALIZED and lpData: ctypes.windll.user32.SendMessageW(hwnd, BFFM_SETSELECTION, 1, lpData); return 0 browseInfo = BROWSEINFO() browseInfo.lpszTitle = _('File location') browseInfo.ulFlags = BIF_RETURNONLYFSDIRS|BIF_USENEWUI browseInfo.lpfn = BrowseCallbackProc(browsecallback) browseInfo.lParam = self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[1:]) or self.outdir.get() ctypes.windll.ole32.CoInitialize(None) pidl = ctypes.windll.shell32.SHBrowseForFolderW(ctypes.byref(browseInfo)) if pidl: path = ctypes.create_unicode_buffer(MAX_PATH) ctypes.windll.shell32.SHGetPathFromIDListW(pidl, path) ctypes.windll.ole32.CoTaskMemFree(pidl) d = path.value else: d = None if d: self.outdir['state'] = tk.NORMAL # must be writable to update self.outdir.delete(0, tk.END) if d.startswith(config.home): self.outdir.insert(0, '~' + d[len(config.home):]) else: self.outdir.insert(0, d) 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_only_btn['state'] = tk.NORMAL self.hotkey_play_btn['state'] = tk.NORMAL self.hotkey_only_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_only_btn['state'] = tk.NORMAL self.hotkey_play_btn['state'] = tk.NORMAL else: event.widget.insert(0, _('None')) # No hotkey/shortcut currently defined self.hotkey_only_btn['state'] = tk.DISABLED self.hotkey_play_btn['state'] = tk.DISABLED self.hotkey_only_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) + (self.out_bpc.get() and config.OUT_BPC) + (self.out_td.get() and config.OUT_TD) + (self.out_csv.get() and config.OUT_CSV) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS) + (self.out_log_file.get() and config.OUT_LOG_FILE) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) + (self.out_log_edsm.get() and config.OUT_LOG_EDSM) + (self.out_log_auto.get() and config.OUT_LOG_AUTO)) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) config.set('edsm_autoopen', self.edsm_autoopen.get()) config.set('edsm_cmdrname', self.edsm_cmdr.get().strip()) config.set('edsm_apikey', self.edsm_apikey.get().strip()) if platform in ['darwin','win32']: config.set('hotkey_code', self.hotkey_code) config.set('hotkey_mods', self.hotkey_mods) config.set('hotkey_always', int(not self.hotkey_only.get())) config.set('hotkey_mute', int(not self.hotkey_play.get())) config.set('anonymous', self.out_anon.get()) self._destroy() if credentials != (config.get('username'), config.get('password')) and self.callback: self.callback() def _destroy(self): # Re-enable hotkey and log monitoring before exit hotkeymgr.register(self.parent, config.getint('hotkey_code'), config.getint('hotkey_mods')) if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_AUTO|config.OUT_LOG_EDSM)): monitor.enable_logging() monitor.start(self.parent) else: monitor.stop() 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 PreferencesDialog(tk.Toplevel): def __init__(self, parent, callback): tk.Toplevel.__init__(self, parent) self.parent = parent self.callback = callback self.title(platform == 'darwin' and _('Preferences') or _('Settings')) if parent.winfo_viewable(): self.transient(parent) # position over parent if platform != 'darwin' or parent.winfo_rooty( ) > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.geometry("+%d+%d" % (parent.winfo_rootx(), parent.winfo_rooty())) # remove decoration if platform == 'win32': self.attributes('-toolwindow', tk.TRUE) elif platform == 'darwin': # http://wiki.tcl.tk/13428 parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') self.resizable(tk.FALSE, tk.FALSE) style = ttk.Style() frame = ttk.Frame(self) frame.grid(sticky=tk.NSEW) notebook = nb.Notebook(frame) PADX = 10 BUTTONX = 12 # indent Checkbuttons and Radiobuttons PADY = 2 # close spacing credframe = nb.Frame(notebook) credframe.columnconfigure(1, weight=1) nb.Label(credframe, text=_('Credentials')).grid( padx=PADX, sticky=tk.W) # Section heading in settings ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) nb.Label( credframe, text=_('Please log in with your Elite: Dangerous account details') ).grid(padx=PADX, columnspan=2, sticky=tk.W) # Use same text as E:D Launcher's login dialog nb.Label(credframe, text=_('Username (Email)')).grid( row=10, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog nb.Label(credframe, text=_('Password')).grid( row=11, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog self.username = nb.Entry(credframe) self.username.insert(0, config.get('username') or '') self.username.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW) self.username.focus_set() self.password = nb.Entry(credframe, show=u'•') self.password.insert(0, config.get('password') or '') self.password.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) nb.Label(credframe).grid(sticky=tk.W) # big spacer nb.Label(credframe, text=_('Privacy')).grid( padx=PADX, sticky=tk.W) # Section heading in settings ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) self.out_anon = tk.IntVar(value=config.getint('anonymous') and 1) nb.Label( credframe, text=_('How do you want to be identified in the saved data')).grid( columnspan=2, padx=PADX, sticky=tk.W) nb.Radiobutton(credframe, text=_('Cmdr name'), variable=self.out_anon, value=0).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Privacy setting nb.Radiobutton(credframe, text=_('Pseudo-anonymized ID'), variable=self.out_anon, value=1).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Privacy setting notebook.add(credframe, text=_('Identity')) # Tab heading in settings outframe = nb.Frame(notebook) outframe.columnconfigure(0, weight=1) output = config.getint('output') or ( config.OUT_MKT_EDDN | config.OUT_SYS_EDDN | config.OUT_SHIP ) # default settings nb.Label(outframe, text=_('Please choose what data to save')).grid(columnspan=2, padx=PADX, sticky=tk.W) self.out_csv = tk.IntVar(value=(output & config.OUT_MKT_CSV) and 1) nb.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_bpc = tk.IntVar(value=(output & config.OUT_MKT_BPC) and 1) nb.Checkbutton(outframe, text=_("Market data in Slopey's BPC format file"), variable=self.out_bpc, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_td = tk.IntVar(value=(output & config.OUT_MKT_TD) and 1) nb.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_ship = tk.IntVar( value=(output & (config.OUT_SHIP | config.OUT_SHIP_EDS | config.OUT_SHIP_CORIOLIS) and 1)) nb.Checkbutton(outframe, text=_('Ship loadout'), variable=self.out_ship, command=self.outvarchanged).grid( columnspan=2, padx=BUTTONX, pady=(5, 0), sticky=tk.W) # Output setting self.out_auto = tk.IntVar(value=0 if output & config.OUT_MKT_MANUAL else 1) # inverted self.out_auto_button = nb.Checkbutton( outframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting self.out_auto_button.grid(columnspan=2, padx=BUTTONX, pady=(5, 0), sticky=tk.W) self.outdir = tk.StringVar() self.outdir.set(config.get('outdir')) self.outdir_label = nb.Label(outframe, text=_('File location') + ':') # Section heading in settings self.outdir_label.grid(padx=BUTTONX, pady=(5, 0), sticky=tk.W) self.outdir_entry = nb.Entry(outframe, takefocus=False) self.outdir_entry.grid(row=20, padx=(PADX, 0), sticky=tk.EW) self.outbutton = nb.Button( outframe, text=( platform == 'darwin' and _('Change...') or # Folder selection button on OSX _('Browse...')), # Folder selection button on Windows command=lambda: self.filebrowse(_('File location'), self.outdir)) self.outbutton.grid(row=20, column=1, padx=PADX, sticky=tk.NSEW) nb.Frame(outframe).grid(pady=5) # bottom spacer notebook.add(outframe, text=_('Output')) # Tab heading in settings eddnframe = nb.Frame(notebook) HyperlinkLabel(eddnframe, text='Elite Dangerous Data Network', background=nb.Label().cget('background'), url='https://github.com/jamesremuscat/EDDN/wiki', underline=True).grid(padx=PADX, sticky=tk.W) # Don't translate self.eddn_station = tk.IntVar( value=(output & config.OUT_MKT_EDDN) and 1) nb.Checkbutton( eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.eddn_station, command=self.outvarchanged).grid(padx=BUTTONX, pady=(5, 0), sticky=tk.W) # Output setting self.eddn_auto_button = nb.Checkbutton( eddnframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting self.eddn_auto_button.grid(padx=BUTTONX, sticky=tk.W) self.eddn_system = tk.IntVar( value=(output & config.OUT_SYS_EDDN) and 1) self.eddn_system_button = nb.Checkbutton( eddnframe, text=_( 'Send system and scan data to the Elite Dangerous Data Network' ), variable=self.eddn_system, command=self.outvarchanged) # Output setting new in E:D 2.2 self.eddn_system_button.grid(padx=BUTTONX, pady=(5, 0), sticky=tk.W) self.eddn_delay = tk.IntVar( value=(output & config.OUT_SYS_DELAY) and 1) self.eddn_delay_button = nb.Checkbutton( eddnframe, text=_('Delay sending until docked'), variable=self.eddn_delay ) # Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2 self.eddn_delay_button.grid(padx=BUTTONX, sticky=tk.W) notebook.add(eddnframe, text='EDDN') # Not translated edsmframe = nb.Frame(notebook) edsmframe.columnconfigure(1, weight=1) HyperlinkLabel(edsmframe, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate self.edsm_log = tk.IntVar(value=(output & config.OUT_SYS_EDSM) and 1) self.edsm_log_button = nb.Checkbutton( edsmframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.edsm_log, command=self.outvarchanged) self.edsm_log_button.grid(columnspan=2, padx=BUTTONX, pady=(5, 0), sticky=tk.W) nb.Label(edsmframe).grid(sticky=tk.W) # big spacer self.edsm_label = HyperlinkLabel( edsmframe, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings self.edsm_label.grid(columnspan=2, padx=PADX, sticky=tk.W) self.edsm_cmdr_label = nb.Label( edsmframe, text=_('Commander Name')) # EDSM setting self.edsm_cmdr_label.grid(row=10, padx=PADX, sticky=tk.W) self.edsm_cmdr = nb.Entry(edsmframe) self.edsm_cmdr.insert(0, config.get('edsm_cmdrname') or '') self.edsm_cmdr.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW) self.edsm_apikey_label = nb.Label(edsmframe, text=_('API Key')) # EDSM setting self.edsm_apikey_label.grid(row=11, padx=PADX, sticky=tk.W) self.edsm_apikey = nb.Entry(edsmframe) self.edsm_apikey.insert(0, config.get('edsm_apikey') or '') self.edsm_apikey.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) notebook.add(edsmframe, text='EDSM') # Not translated # build plugin prefs tabs for plugname in plug.PLUGINS: plugframe = plug.get_plugin_prefs(plugname, notebook) if plugframe: notebook.add(plugframe, text=plugname) configframe = nb.Frame(notebook) configframe.columnconfigure(1, weight=1) self.logdir = tk.StringVar() self.logdir.set( config.get('journaldir') or config.default_journal_dir or '') self.logdir_entry = nb.Entry(configframe, takefocus=False) if platform != 'darwin': # Apple's SMB implementation is way too flaky - no filesystem events and bogus NULLs nb.Label( configframe, text=_('E:D journal file location') + ':').grid( columnspan=3, padx=PADX, sticky=tk.W) # Location of the new Journal file in E:D 2.2 self.logdir_entry.grid(row=10, columnspan=2, padx=(PADX, 0), sticky=tk.EW) self.logbutton = nb.Button( configframe, text=( platform == 'darwin' and _('Change...') or # Folder selection button on OSX _('Browse...')), # Folder selection button on Windows command=lambda: self.filebrowse(_('E:D journal file location'), self.logdir)) self.logbutton.grid(row=10, column=2, padx=PADX, sticky=tk.EW) if config.default_journal_dir: nb.Button( configframe, text=_('Default'), command=self.logdir_reset, state=config.get('journaldir') and tk.NORMAL or tk.DISABLED).grid( column=2, padx=PADX, pady=(5, 0), sticky=tk.EW) # Appearance theme and language setting if platform == 'win32': ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY * 8, sticky=tk.EW) if platform in ['darwin', 'win32']: self.hotkey_code = config.getint('hotkey_code') self.hotkey_mods = config.getint('hotkey_mods') self.hotkey_only = tk.IntVar( value=not config.getint('hotkey_always')) self.hotkey_play = tk.IntVar( value=not config.getint('hotkey_mute')) nb.Label( configframe, text=platform == 'darwin' and _('Keyboard shortcut') or # Hotkey/Shortcut settings prompt on OSX _('Hotkey') # Hotkey/Shortcut settings prompt on Windows ).grid(row=20, padx=PADX, sticky=tk.W) if platform == 'darwin' and not was_accessible_at_launch: if AXIsProcessTrusted(): nb.Label( configframe, text=_('Re-start {APP} to use shortcuts').format( APP=applongname), foreground='firebrick').grid( padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX else: nb.Label( configframe, text=_('{APP} needs permission to use shortcuts' ).format(APP=applongname), foreground='firebrick').grid( columnspan=3, padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX nb.Button( configframe, text=_('Open System Preferences'), command=self.enableshortcuts).grid( column=2, padx=PADX, sticky=tk.E) # Shortcut settings button on OSX else: self.hotkey_text = nb.Entry(configframe, width=(platform == 'darwin' and 20 or 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=20, column=1, columnspan=2, padx=PADX, pady=(5, 0), sticky=tk.W) self.hotkey_only_btn = nb.Checkbutton( configframe, text=_('Only when Elite: Dangerous is the active app'), variable=self.hotkey_only, state=self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting self.hotkey_only_btn.grid(columnspan=3, padx=PADX, pady=(5, 0), sticky=tk.W) self.hotkey_play_btn = nb.Checkbutton( configframe, 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(columnspan=3, padx=PADX, sticky=tk.W) ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY * 8, sticky=tk.EW) nb.Label(configframe, text=_('Preferred Shipyard')).grid( columnspan=3, padx=PADX, sticky=tk.W ) # Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis. self.shipyard = tk.IntVar(value=config.getint('shipyard')) nb.Radiobutton(configframe, text='E:D Shipyard', variable=self.shipyard, value=config.SHIPYARD_EDSHIPYARD).grid(columnspan=3, padx=BUTTONX, pady=(5, 0), sticky=tk.W) nb.Radiobutton(configframe, text='Coriolis', variable=self.shipyard, value=config.SHIPYARD_CORIOLIS).grid(columnspan=3, padx=BUTTONX, sticky=tk.W) nb.Label(configframe).grid(sticky=tk.W) # big spacer notebook.add(configframe, text=_('Configuration')) # Tab heading in settings self.languages = Translations().available_names() self.lang = tk.StringVar(value=self.languages.get( config.get('language'), _('Default'))) # Appearance theme and language setting self.always_ontop = tk.BooleanVar(value=config.getint('always_ontop')) self.theme = tk.IntVar(value=config.getint('theme') and 1 or 0) self.theme_colors = [ config.get('dark_text'), config.get('dark_highlight') ] self.theme_prompts = [ _('Normal text'), # Dark theme color setting _('Highlighted text'), # Dark theme color setting ] themeframe = nb.Frame(notebook) themeframe.columnconfigure(2, weight=1) nb.Label(themeframe, text=_('Language')).grid( row=10, padx=PADX, sticky=tk.W) # Appearance setting prompt self.lang_button = nb.OptionMenu(themeframe, self.lang, self.lang.get(), *self.languages.values()) self.lang_button.grid(row=10, column=1, columnspan=2, padx=PADX, sticky=tk.W) ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY * 8, sticky=tk.EW) nb.Label(themeframe, text=_('Theme')).grid(columnspan=3, padx=PADX, sticky=tk.W) # Appearance setting nb.Radiobutton( themeframe, text=_('Default'), variable=self.theme, value=0, command=self.themevarchanged).grid( columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance theme and language setting nb.Radiobutton(themeframe, text=_('Dark'), variable=self.theme, value=1, command=self.themevarchanged).grid( columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance theme setting self.theme_label_0 = nb.Label(themeframe, text=self.theme_prompts[0]) self.theme_label_0.grid(row=20, padx=PADX, sticky=tk.W) self.theme_button_0 = nb.ColoredButton( themeframe, text=_('Station'), background='grey4', command=lambda: self.themecolorbrowse(0)) # Main window self.theme_button_0.grid(row=20, column=1, padx=PADX, pady=PADY, sticky=tk.NSEW) self.theme_label_1 = nb.Label(themeframe, text=self.theme_prompts[1]) self.theme_label_1.grid(row=21, padx=PADX, sticky=tk.W) self.theme_button_1 = nb.ColoredButton( themeframe, text=' Hutton Orbital ', background='grey4', command=lambda: self.themecolorbrowse(1)) # Do not translate self.theme_button_1.grid(row=21, column=1, padx=PADX, pady=PADY, sticky=tk.NSEW) ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY * 8, sticky=tk.EW) self.ontop_button = nb.Checkbutton(themeframe, text=_('Always on top'), variable=self.always_ontop, command=self.themevarchanged) self.ontop_button.grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance setting nb.Label(themeframe).grid(sticky=tk.W) # big spacer notebook.add(themeframe, text=_('Appearance')) # Tab heading in settings if platform == 'darwin': self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes else: buttonframe = ttk.Frame(frame) buttonframe.grid(padx=PADX, pady=PADX, sticky=tk.NSEW) buttonframe.columnconfigure(0, weight=1) ttk.Label(buttonframe).grid(row=0, column=0) # spacer button = ttk.Button(buttonframe, text=_('OK'), command=self.apply) button.grid(row=0, column=1, sticky=tk.E) button.bind("<Return>", lambda event: self.apply()) self.protocol("WM_DELETE_WINDOW", self._destroy) # Selectively disable buttons depending on output settings self.outvarchanged() self.themevarchanged() # disable hotkey for the duration hotkeymgr.unregister() # wait for window to appear on screen before calling grab_set self.parent.wm_attributes( '-topmost', 0) # needed for dialog to appear ontop of parent on OSX & Linux self.wait_visibility() self.grab_set() def outvarchanged(self): self.displaypath(self.outdir, self.outdir_entry) self.displaypath(self.logdir, self.logdir_entry) logdir = self.logdir.get() logvalid = logdir and exists(logdir) local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get( ) or self.out_ship.get() self.out_auto_button[ 'state'] = local and logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir_entry['state'] = local and 'readonly' or tk.DISABLED self.eddn_auto_button['state'] = self.eddn_station.get( ) and logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED self.eddn_system_button[ 'state'] = logvalid and tk.NORMAL or tk.DISABLED self.eddn_delay_button[ 'state'] = logvalid and eddn.replayfile and self.eddn_system.get( ) and tk.NORMAL or tk.DISABLED self.edsm_log_button['state'] = logvalid and tk.NORMAL or tk.DISABLED edsm_state = logvalid and self.edsm_log.get( ) and tk.NORMAL or tk.DISABLED self.edsm_label['state'] = edsm_state self.edsm_cmdr_label['state'] = edsm_state self.edsm_apikey_label['state'] = edsm_state self.edsm_cmdr['state'] = edsm_state self.edsm_apikey['state'] = edsm_state def filebrowse(self, title, pathvar): if platform != 'win32': import tkFileDialog d = tkFileDialog.askdirectory(parent=self, initialdir=expanduser(pathvar.get()), title=title, mustexist=tk.TRUE) else: def browsecallback(hwnd, uMsg, lParam, lpData): # set initial folder if uMsg == BFFM_INITIALIZED and lpData: ctypes.windll.user32.SendMessageW(hwnd, BFFM_SETSELECTION, 1, lpData) return 0 browseInfo = BROWSEINFO() browseInfo.lpszTitle = title browseInfo.ulFlags = BIF_RETURNONLYFSDIRS | BIF_USENEWUI browseInfo.lpfn = BrowseCallbackProc(browsecallback) browseInfo.lParam = pathvar.get().startswith('~') and join( config.home, pathvar.get()[2:]) or pathvar.get() ctypes.windll.ole32.CoInitialize(None) pidl = ctypes.windll.shell32.SHBrowseForFolderW( ctypes.byref(browseInfo)) if pidl: path = ctypes.create_unicode_buffer(MAX_PATH) ctypes.windll.shell32.SHGetPathFromIDListW(pidl, path) ctypes.windll.ole32.CoTaskMemFree(pidl) d = path.value else: d = None if d: pathvar.set(d) self.outvarchanged() def displaypath(self, pathvar, entryfield): entryfield['state'] = tk.NORMAL # must be writable to update entryfield.delete(0, tk.END) if platform == 'win32': start = pathvar.get().lower().startswith( config.home.lower()) and len(config.home.split('\\')) or 0 display = [] components = normpath(pathvar.get()).split('\\') buf = ctypes.create_unicode_buffer(MAX_PATH) pidsRes = ctypes.c_int() for i in range(start, len(components)): try: if (not SHGetLocalizedName( '\\'.join(components[:i + 1]), buf, MAX_PATH, ctypes.byref(pidsRes)) and LoadString( ctypes.WinDLL(expandvars(buf.value))._handle, pidsRes.value, buf, MAX_PATH)): display.append(buf.value) else: display.append(components[i]) except: display.append(components[i]) entryfield.insert(0, '\\'.join(display)) elif platform == 'darwin' and NSFileManager.defaultManager( ).componentsToDisplayForPath_( pathvar.get()): # None if path doesn't exist if pathvar.get().startswith(config.home): display = [ '~' ] + NSFileManager.defaultManager().componentsToDisplayForPath_( pathvar.get())[len(NSFileManager.defaultManager( ).componentsToDisplayForPath_(config.home)):] else: display = NSFileManager.defaultManager( ).componentsToDisplayForPath_(pathvar.get()) entryfield.insert(0, '/'.join(display)) else: if pathvar.get().startswith(config.home): entryfield.insert(0, '~' + pathvar.get()[len(config.home):]) else: entryfield.insert(0, pathvar.get()) entryfield['state'] = 'readonly' def logdir_reset(self): if config.default_journal_dir: self.logdir.set(config.default_journal_dir) self.outvarchanged() def themecolorbrowse(self, index): (rgb, color) = tkColorChooser.askcolor(self.theme_colors[index], title=self.theme_prompts[index], parent=self.parent) if color: self.theme_colors[index] = color self.themevarchanged() def themevarchanged(self): self.theme_button_0['foreground'], self.theme_button_1[ 'foreground'] = self.theme_colors state = self.theme.get() and tk.NORMAL or tk.DISABLED self.theme_label_0['state'] = state self.theme_label_1['state'] = state self.theme_button_0['state'] = state self.theme_button_1['state'] = state if platform == 'linux2': # Unmanaged windows are always on top on X self.ontop_button['state'] = self.theme.get( ) and tk.DISABLED or tk.NORMAL 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_only_btn['state'] = tk.NORMAL self.hotkey_play_btn['state'] = tk.NORMAL self.hotkey_only_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_only_btn['state'] = tk.NORMAL self.hotkey_play_btn['state'] = tk.NORMAL else: event.widget.insert( 0, _('None')) # No hotkey/shortcut currently defined self.hotkey_only_btn['state'] = tk.DISABLED self.hotkey_play_btn['state'] = tk.DISABLED self.hotkey_only_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_bpc.get() and config.OUT_MKT_BPC) + (self.out_td.get() and config.OUT_MKT_TD) + (self.out_csv.get() and config.OUT_MKT_CSV) + (config.OUT_MKT_MANUAL if not self.out_auto.get() else 0) + (self.out_ship.get() and config.OUT_SHIP) + (self.eddn_station.get() and config.OUT_MKT_EDDN) + (self.eddn_system.get() and config.OUT_SYS_EDDN) + (self.eddn_delay.get() and config.OUT_SYS_DELAY) + (self.edsm_log.get() and config.OUT_SYS_EDSM)) config.set( 'outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) config.set('edsm_cmdrname', self.edsm_cmdr.get().strip()) config.set('edsm_apikey', self.edsm_apikey.get().strip()) logdir = self.logdir.get() if config.default_journal_dir and logdir.lower( ) == config.default_journal_dir.lower(): config.set('journaldir', '') # default location else: config.set('journaldir', logdir) if platform in ['darwin', 'win32']: config.set('hotkey_code', self.hotkey_code) config.set('hotkey_mods', self.hotkey_mods) config.set('hotkey_always', int(not self.hotkey_only.get())) config.set('hotkey_mute', int(not self.hotkey_play.get())) config.set('shipyard', self.shipyard.get()) lang_codes = {v: k for k, v in self.languages.iteritems()} # Codes by name config.set('language', lang_codes.get(self.lang.get()) or '') Translations().install(config.get('language') or None) config.set('always_ontop', self.always_ontop.get()) config.set('theme', self.theme.get()) config.set('dark_text', self.theme_colors[0]) config.set('dark_highlight', self.theme_colors[1]) theme.apply(self.parent) config.set('anonymous', self.out_anon.get()) plug.notify_prefs_changed() self._destroy() if self.callback: self.callback() def _destroy(self): self.parent.wm_attributes('-topmost', config.getint('always_ontop') and 1 or 0) 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 AppWindow: STATION_UNDOCKED = u'×' # "Station" name to display when not docked = U+00D7 def __init__(self, master): self.holdofftime = config.getint('querytime') + companion.holdoff self.session = companion.Session() self.edsm = edsm.EDSM() self.w = master self.w.title(applongname) self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) # Special handling for overrideredict self.w.bind("<Map>", self.onmap) plug.load_plugins() if platform != 'darwin': if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') else: from PIL import Image, ImageTk self.w.tk.call( 'wm', 'iconphoto', self.w, '-default', ImageTk.PhotoImage(Image.open("EDMarketConnector.png"))) self.theme_icon = tk.PhotoImage( data= 'R0lGODlhEAAQAMYAAAAAAAEAAAEBAQICAgQEAwYFBAsHBAoIBgwIBAwIBQ0IBA8JBBAJBBAKBRMMBRkPBhoQBykWCSoWCCoXCTsfCzwfCkAhDEIjDD8kC0AlDEEmC0EmDEcoDk4oDU8pDU4qEFMrD1ktDlotD1ouD1g0EWAyEWU0EV03EmA4EWo2EW03EWQ6Emw4EWo9FGo+E3Y8FH5AFH1IFoBJFo1RGo1SGY1SGpBTGZFTGZJTGZhYG6piHa1kHa5kHbBkHr9uIMt0IgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEKAEAALAAAAAAQABAAAAd7gACCg4SFhoeHGCiIhRs5JwMCkpKGGTIFODaaNjc/D4QaMQMAk5MuEIQOO6OFAiscCIQNPQk8NTO4NDofLwayPi0mIMPDLAcqvoIBDiQWkaUCAykKhAsXAoYCHRKEDDAjIyIiIyEEHhHYhAPr7BQlE+mMABXo8oTx9oWBADs=' ) self.theme_minimize = tk.BitmapImage( data= '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n' ) self.theme_close = tk.BitmapImage( data= '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n' ) frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) self.cmdr_label = tk.Label(frame) self.system_label = tk.Label(frame) self.station_label = tk.Label(frame) self.cmdr_label.grid(row=1, column=0, sticky=tk.W) self.system_label.grid(row=2, column=0, sticky=tk.W) self.station_label.grid(row=3, column=0, sticky=tk.W) self.cmdr = tk.Label(frame, anchor=tk.W) self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.system_url, popup_copy=True) self.station = HyperlinkLabel( frame, url=self.station_url, popup_copy=lambda x: x != self.STATION_UNDOCKED) self.cmdr.grid(row=1, column=1, sticky=tk.EW) self.system.grid(row=2, column=1, sticky=tk.EW) self.station.grid(row=3, column=1, sticky=tk.EW) for plugname in plug.PLUGINS: appitem = plug.get_plugin_app(plugname, frame) if appitem: appitem.grid(columnspan=2, sticky=tk.W) self.button = ttk.Button( frame, text=_('Update'), width=28, command=self.getandsend, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.theme_button = tk.Label(frame, width=platform == 'darwin' and 32 or 28, state=tk.DISABLED) self.status = tk.Label(frame, name='status', anchor=tk.W) row = frame.grid_size()[1] self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) theme.register_alternate((self.button, self.theme_button), { 'row': row, 'columnspan': 2, 'sticky': tk.NSEW }) self.status.grid(columnspan=2, sticky=tk.EW) theme.button_bind(self.theme_button, self.getandsend) self.w.bind('<Return>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend) for child in frame.winfo_children(): child.grid_configure(padx=5, pady=(platform == 'win32' and 1 or 3)) self.menubar = tk.Menu() if platform == 'darwin': # Can't handle (de)iconify if topmost is set, so suppress iconify button # http://wiki.tcl.tk/13428 and p15 of https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox horizontalZoom resizable') # https://www.tcl.tk/man/tcl/TkCmd/menu.htm self.file_menu = tk.Menu(self.menubar, name='apple') self.file_menu.add_command( command=lambda: self.w.call('tk::mac::standardAboutPanel')) self.file_menu.add_command( command=lambda: self.updater.checkForUpdates()) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, name='edit') self.edit_menu.add_command(accelerator='Command-c', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) self.w.bind('<Command-c>', self.copy) self.view_menu = tk.Menu(self.menubar, name='view') self.view_menu.add_command( state=tk.DISABLED, command=lambda: stats.StatsDialog(self.w, self.session)) self.menubar.add_cascade(menu=self.view_menu) window_menu = tk.Menu(self.menubar, name='window') self.menubar.add_cascade(menu=window_menu) self.w['menu'] = self.menubar # 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')) self.w.createcommand("::tk::mac::Quit", self.onexit) self.w.createcommand( "::tk::mac::ShowPreferences", lambda: prefs.PreferencesDialog(self.w, self.postprefs)) self.w.createcommand( "::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app else: self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.file_menu.add_command( state=tk.DISABLED, command=lambda: stats.StatsDialog(self.w, self.session)) self.file_menu.add_command( command=lambda: self.updater.checkForUpdates()) self.file_menu.add_command(command=lambda: prefs.PreferencesDialog( self.w, self.postprefs)) self.file_menu.add_separator() self.file_menu.add_command(command=self.onexit) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) if platform == 'win32': # Must be added after at least one "real" menu entry self.always_ontop = tk.BooleanVar( value=config.getint('always_ontop')) system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) system_menu.add_separator() system_menu.add_checkbutton( label=_('Always on top'), variable=self.always_ontop, command=self.ontop_changed) # Appearance setting self.menubar.add_cascade(menu=system_menu) # Gets index 0 self.w.bind('<Control-c>', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) theme.register( self.menubar ) # menus and children aren't automatically registered theme.register(self.file_menu) theme.register(self.edit_menu) # Alternate title bar and menu for dark theme self.theme_menubar = tk.Frame(frame) self.theme_menubar.columnconfigure(2, weight=1) theme_titlebar = tk.Label(self.theme_menubar, text=applongname, image=self.theme_icon, anchor=tk.W, compound=tk.LEFT) theme_titlebar.grid(columnspan=3, sticky=tk.NSEW) self.drag_offset = None theme_titlebar.bind('<Button-1>', self.drag_start) theme_titlebar.bind('<B1-Motion>', self.drag_continue) theme_titlebar.bind('<ButtonRelease-1>', self.drag_end) if platform == 'win32': # Can't work out how to deiconify on Linux theme_minimize = tk.Label(self.theme_menubar, image=self.theme_minimize) theme_minimize.grid(row=0, column=3) theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) theme_close = tk.Label(self.theme_menubar, image=self.theme_close) theme_close.grid(row=0, column=4) theme.button_bind(theme_close, self.onexit, image=self.theme_close) self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W) theme.button_bind( self.theme_file_menu, lambda e: self.file_menu.tk_popup( e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W) # Menu title self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) theme.button_bind( self.theme_edit_menu, lambda e: self.edit_menu.tk_popup( e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) theme.register_highlight(theme_titlebar) theme.register( self.theme_minimize) # images aren't automatically registered theme.register(self.theme_close) theme.register_alternate((self.menubar, self.theme_menubar), { 'row': 0, 'columnspan': 2, 'sticky': tk.NSEW }) self.set_labels() # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match: if platform == 'darwin': if int( match.group(2) ) >= 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) elif platform == 'win32': # Check that the titlebar will be at least partly on screen import ctypes from ctypes.wintypes import POINT # https://msdn.microsoft.com/en-us/library/dd145064 MONITOR_DEFAULTTONULL = 0 if ctypes.windll.user32.MonitorFromPoint( POINT( int(match.group(1)) + 16, int(match.group(2)) + 16), MONITOR_DEFAULTTONULL): self.w.geometry(config.get('geometry')) else: self.w.geometry(config.get('geometry')) self.w.attributes('-topmost', config.getint('always_ontop') and 1 or 0) self.w.resizable(tk.TRUE, tk.FALSE) theme.register(frame) theme.register_highlight(self.system) theme.register_highlight(self.station) theme.apply(self.w) # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) self.w.bind_all('<<Quit>>', self.onexit) # user-generated # Install hotkey monitoring self.w.bind_all('<<Invoke>>', self.getandsend) # user-generated hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring monitor.set_callback(self.system_change) edproxy.set_callback(self.system_change) if (config.getint('output') & config.OUT_LOG_AUTO) and ( config.getint('output') & (config.OUT_LOG_FILE | config.OUT_LOG_EDSM)): monitor.enable_logging() monitor.start(self.w) edproxy.start(self.w) # First run if not config.get('username') or not config.get('password'): prefs.PreferencesDialog(self.w, self.postprefs) else: self.login() # callback after the Preferences dialog is applied def postprefs(self): self.set_labels() # in case language has changed self.login() # in case credentials gave changed # set main window labels, e.g. after language change def set_labels(self): self.cmdr_label['text'] = _('Cmdr') + ':' # Main window self.system_label['text'] = _('System') + ':' # Main window self.station_label['text'] = _('Station') + ':' # Main window self.button['text'] = self.theme_button['text'] = _( 'Update') # Update button in main window self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste self.view_menu.entryconfigure(0, label=_('Status')) # Menu item self.file_menu.entryconfigure( 1, label=_("Check for Updates...")) # Menu item if platform == 'darwin': self.file_menu.entryconfigure( 0, label=_("About {APP}").format( APP=applongname)) # App menu entry on OSX self.menubar.entryconfigure(1, label=_('Edit')) # Menu title self.menubar.entryconfigure(2, label=_('View')) # Menu title on OSX self.menubar.entryconfigure(3, label=_('Window')) # Menu title on OSX else: self.file_menu.entryconfigure( 2, label=_("Settings")) # Item in the File menu on Windows self.file_menu.entryconfigure( 4, label=_("Exit")) # Item in the File menu on Windows self.menubar.entryconfigure( self.menubar.index('end') - 2, label=_('File')) # Menu title on Windows self.menubar.entryconfigure(self.menubar.index('end') - 1, label=_('Edit')) # Menu title self.theme_file_menu['text'] = _('File') # Menu title on Windows self.theme_edit_menu['text'] = _('Edit') # Menu title def login(self): self.status['text'] = _('Logging in...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() try: self.session.login(config.get('username'), config.get('password')) self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status self.status['text'] = '' except companion.VerificationRequired: # don't worry about authentication now - prompt on query self.status['text'] = '' except companion.ServerError as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) # Try to obtain exclusive lock on flight log ASAP if config.getint('output') & config.OUT_LOG_FILE: try: flightlog.openlog() except Exception as e: if __debug__: print_exc() if not self.status['text']: self.status['text'] = unicode(e) if not self.status['text'] and monitor.restart_required(): self.status['text'] = _( 'Re-start Elite: Dangerous for automatic log entries' ) # Status bar message on launch elif not getattr(sys, 'frozen', False): self.updater.checkForUpdates( ) # Sparkle / WinSparkle does this automatically for packaged apps self.cooldown() # callback after verification code def verify(self, code): try: self.session.verify(code) config.save() # Save settings now for use by command-line app except Exception as e: if __debug__: print_exc() self.button['state'] = self.theme_button['state'] = tk.NORMAL self.status['text'] = unicode(e) else: return self.getandsend() # try again 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: # Was invoked by key while in cooldown self.status['text'] = '' 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 elif play_sound: hotkeymgr.play_good() self.cmdr['text'] = self.system['text'] = self.station['text'] = '' self.system['image'] = '' self.status['text'] = _('Fetching data...') self.theme_button['state'] = tk.DISABLED self.edit_menu.entryconfigure(0, state=tk.DISABLED) # Copy self.w.update_idletasks() try: querytime = int(time()) data = self.session.query() config.set('querytime', querytime) # Validation if not data.get('commander') or not data['commander'].get( 'name', '').strip(): self.status['text'] = _("Who are you?!") # Shouldn't happen 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 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 else: if __debug__: # Recording with open( '%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.' + data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h: h.write( json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True).encode('utf-8')) self.cmdr['text'] = data.get('commander') and data.get( 'commander').get('name') or '' self.system['text'] = data.get('lastSystem') and data.get( 'lastSystem').get('name') or '' self.station['text'] = data.get('commander') and data.get( 'commander').get('docked') and data.get( 'lastStarport') and data.get('lastStarport').get( 'name') or (EDDB.system(self.system['text']) and self.STATION_UNDOCKED or '') self.status['text'] = '' self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status if data['lastStarport'].get('commodities'): # Fixup anomalies in the commodity data self.session.fixup(data['lastStarport']['commodities']) # stuff we can do when not docked plug.notify_newdata(data) if config.getint('output') & config.OUT_SHIP_EDS: loadout.export(data) if config.getint('output') & config.OUT_SHIP_CORIOLIS: coriolis.export(data) if config.getint('output') & config.OUT_LOG_FILE: flightlog.export(data) if config.getint('output') & config.OUT_LOG_EDSM: # Catch any EDSM errors here so that they don't prevent station update try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.export(data, lambda: self.edsm.lookup( self.system['text'], EDDB.system(self.system['text'])) ) # Do EDSM lookup during EDSM export self.status['text'] = '' except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) else: self.edsm.link(self.system['text']) self.edsmpoll() if not (config.getint('output') & (config.OUT_CSV | config.OUT_TD | config.OUT_BPC | config.OUT_EDDN)): # no station data requested - we're done pass elif not data['commander'].get('docked'): # signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up if not self.status['text']: self.status['text'] = _( "You're not docked at a station!") else: # Finally - the data looks sane and we're docked at a station (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) # No EDDN output at known station? if (config.getint('output') & config.OUT_EDDN) and station_id and not ( has_market or has_outfitting or has_shipyard): if not self.status['text']: self.status['text'] = _( "Station doesn't have anything!") # No EDDN output at unknown station? elif ( config.getint('output') & config.OUT_EDDN ) and not station_id and not ( data['lastStarport'].get('commodities') or data['lastStarport'].get('modules') ): # Ignore usually spurious shipyard at unknown stations if not self.status['text']: self.status['text'] = _( "Station doesn't have anything!") # No market output at known station? elif not (config.getint('output') & config.OUT_EDDN ) and station_id and not has_market: if not self.status['text']: self.status['text'] = _( "Station doesn't have a market!") # No market output at unknown station? elif not (config.getint('output') & config.OUT_EDDN) and not station_id and not data[ 'lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _( "Station doesn't have a market!") else: if data['lastStarport'].get('commodities'): if config.getint('output') & config.OUT_CSV: bpc.export(data, True) if config.getint('output') & config.OUT_TD: td.export(data) if config.getint('output') & config.OUT_BPC: bpc.export(data, False) elif has_market and ( config.getint('output') & (config.OUT_CSV | config.OUT_TD | config.OUT_BPC | config.OUT_EDDN)): # Overwrite any previous error message self.status['text'] = _( "Error: Can't get market data!") if config.getint('output') & config.OUT_EDDN: old_status = self.status['text'] if not old_status: self.status['text'] = _( 'Sending data to EDDN...') self.w.update_idletasks() eddn.export_commodities(data) if has_outfitting or not station_id: # Only send if eddb says that the station provides outfitting, or unknown station eddn.export_outfitting(data) elif __debug__ and data['lastStarport'].get( 'modules'): print 'Spurious outfitting!' if has_shipyard: # Only send if eddb says that the station has a shipyard - # https://github.com/Marginal/EDMarketConnector/issues/16 if data['lastStarport'].get('ships'): eddn.export_shipyard(data) else: # API is flakey about shipyard info - silently retry if missing (<1s is usually sufficient - 5s for margin). self.w.after(int(SERVER_RETRY * 1000), self.retry_for_shipyard) elif __debug__ and data['lastStarport'].get( 'ships'): print 'Spurious shipyard!' if not old_status: self.status['text'] = '' except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, self.verify) # Companion API problem except companion.ServerError as e: if retrying: self.status['text'] = unicode(e) else: # Retry once if Companion server is unresponsive self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True)) return # early exit to avoid starting cooldown count except requests.exceptions.ConnectionError as e: if __debug__: print_exc() self.status['text'] = _("Error: Can't connect to EDDN") except requests.exceptions.Timeout as e: if __debug__: print_exc() self.status['text'] = _("Error: Connection to EDDN timed out") except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not self.status['text']: # no errors 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') elif play_sound: hotkeymgr.play_bad() self.holdofftime = querytime + companion.holdoff self.cooldown() def retry_for_shipyard(self): # Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data. try: data = self.session.query() if __debug__: print 'Retry for shipyard - ' + ( data['commander'].get('docked') and (data['lastStarport'].get('ships') and 'Success' or 'Failure') or 'Undocked!') if data['commander'].get( 'docked' ): # might have undocked while we were waiting for retry in which case station data is unreliable eddn.export_shipyard(data) except: pass def system_change(self, timestamp, system): if self.system['text'] != system: self.system['text'] = system self.system['image'] = '' self.station['text'] = EDDB.system( system) and self.STATION_UNDOCKED or '' plug.notify_system_changed(timestamp, system) if config.getint('output') & config.OUT_LOG_FILE: flightlog.writelog(timestamp, system) if config.getint('output') & config.OUT_LOG_EDSM: try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.writelog( timestamp, system, lambda: self.edsm.lookup(system, EDDB.system( system))) # Do EDSM lookup during EDSM export self.status['text'] = strftime( _('Last updated at {HH}:{MM}:{SS}').format( HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not config.getint('hotkey_mute'): hotkeymgr.play_bad() else: self.edsm.link(system) self.status['text'] = strftime( _('Last updated at {HH}:{MM}:{SS}').format( HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') self.edsmpoll() def edsmpoll(self): result = self.edsm.result if result['done']: self.system['image'] = result['img'] if result['uncharted'] and config.getint('edsm_autoopen'): webbrowser.open(result['url']) else: self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) def system_url(self, text): return text and self.edsm.result['url'] def station_url(self, text): if text: (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) if station_id: return 'http://eddb.io/station/%d' % station_id system_id = EDDB.system(self.system['text']) if system_id: return 'http://eddb.io/system/%d' % system_id return None def cooldown(self): if time() < self.holdofftime: self.button['text'] = self.theme_button['text'] = _( 'cooldown {SS}s').format( SS=int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: self.button['text'] = self.theme_button['text'] = _( 'Update') # Update button in main window self.button['state'] = self.theme_button['state'] = tk.NORMAL def ontop_changed(self, event=None): config.set('always_ontop', self.always_ontop.get()) self.w.wm_attributes('-topmost', self.always_ontop.get()) def copy(self, event=None): if self.system['text']: self.w.clipboard_clear() self.w.clipboard_append( self.station['text'] == self.STATION_UNDOCKED and self.system['text'] or '%s,%s' % (self.system['text'], self.station['text'])) def onexit(self, event=None): hotkeymgr.unregister() flightlog.close() if platform != 'darwin' or self.w.winfo_rooty( ) > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) config.close() self.updater.close() self.session.close() self.w.destroy() def drag_start(self, event): self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) def drag_continue(self, event): if self.drag_offset: self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1])) def drag_end(self, event): self.drag_offset = None def oniconify(self, event=None): self.w.overrideredirect(0) # Can't iconize while overrideredirect self.w.iconify() self.w.update_idletasks( ) # Size and windows styles get recalculated here self.w.wait_visibility( ) # Need main window to be re-created before returning theme.active = None # So theme will be re-applied on map def onmap(self, event=None): if event.widget == self.w: theme.apply(self.w)
def makeLabelAndHyperLabel(frame, r, label_text): tk.Label(frame, text=label_text).grid(row=r, column=0, sticky=tk.W) hl = HyperlinkLabel(frame, text="", foreground="yellow", popup_copy=True) hl.grid(row=r, column=1, sticky=tk.W) return hl
class AppWindow: # Tkinter Event types EVENT_KEYPRESS = 2 EVENT_BUTTON = 4 EVENT_VIRTUAL = 35 def __init__(self, master): # Start a protocol handler to handle cAPI registration. Requires main window to exist. protocolhandler.start(master) self.holdofftime = config.getint('querytime') + companion.holdoff self.w = master self.w.title(applongname) self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) self.prefsdialog = None plug.load_plugins(master) if platform != 'darwin': if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') else: from PIL import Image, ImageTk self.w.tk.call('wm', 'iconphoto', self.w, '-default', ImageTk.PhotoImage(Image.open("EDMarketConnector.png"))) self.theme_icon = tk.PhotoImage(data = 'R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') self.theme_minimize = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') self.theme_close = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) self.cmdr_label = tk.Label(frame) self.ship_label = tk.Label(frame) self.system_label = tk.Label(frame) self.station_label = tk.Label(frame) self.cmdr_label.grid(row=1, column=0, sticky=tk.W) self.ship_label.grid(row=2, column=0, sticky=tk.W) self.system_label.grid(row=3, column=0, sticky=tk.W) self.station_label.grid(row=4, column=0, sticky=tk.W) self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name = 'cmdr') self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.shipyard_url, name = 'ship') self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.system_url, popup_copy = True, name = 'system') self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.station_url, name = 'station') self.cmdr.grid(row=1, column=1, sticky=tk.EW) self.ship.grid(row=2, column=1, sticky=tk.EW) self.system.grid(row=3, column=1, sticky=tk.EW) self.station.grid(row=4, column=1, sticky=tk.EW) for plugin in plug.PLUGINS: appitem = plugin.get_app(frame) if appitem: tk.Frame(frame, highlightthickness=1).grid(columnspan=2, sticky=tk.EW) # separator if isinstance(appitem, tuple) and len(appitem)==2: row = frame.grid_size()[1] appitem[0].grid(row=row, column=0, sticky=tk.W) appitem[1].grid(row=row, column=1, sticky=tk.EW) else: appitem.grid(columnspan=2, sticky=tk.EW) self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.theme_button = tk.Label(frame, width = platform == 'darwin' and 32 or 28, state=tk.DISABLED) self.status = tk.Label(frame, name='status', anchor=tk.W) row = frame.grid_size()[1] self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row':row, 'columnspan':2, 'sticky':tk.NSEW}) self.status.grid(columnspan=2, sticky=tk.EW) self.button.bind('<Button-1>', self.getandsend) theme.button_bind(self.theme_button, self.getandsend) for child in frame.winfo_children(): child.grid_configure(padx=5, pady=(platform!='win32' or isinstance(child, tk.Frame)) and 2 or 0) self.menubar = tk.Menu() if platform=='darwin': # Can't handle (de)iconify if topmost is set, so suppress iconify button # http://wiki.tcl.tk/13428 and p15 of https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox resizable') # https://www.tcl.tk/man/tcl/TkCmd/menu.htm self.system_menu = tk.Menu(self.menubar, name='apple') self.system_menu.add_command(command=lambda:self.w.call('tk::mac::standardAboutPanel')) self.system_menu.add_command(command=lambda:self.updater.checkForUpdates()) self.menubar.add_cascade(menu=self.system_menu) self.file_menu = tk.Menu(self.menubar, name='file') self.file_menu.add_command(command=self.save_raw) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, name='edit') self.edit_menu.add_command(accelerator='Command-c', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) self.w.bind('<Command-c>', self.copy) self.view_menu = tk.Menu(self.menubar, name='view') self.view_menu.add_command(command=lambda:stats.StatsDialog(self)) self.menubar.add_cascade(menu=self.view_menu) window_menu = tk.Menu(self.menubar, name='window') self.menubar.add_cascade(menu=window_menu) self.help_menu = tk.Menu(self.menubar, name='help') self.w.createcommand("::tk::mac::ShowHelp", self.help_general) self.help_menu.add_command(command=self.help_privacy) self.help_menu.add_command(command=self.help_releases) self.menubar.add_cascade(menu=self.help_menu) self.w['menu'] = self.menubar # 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')) self.w.createcommand("::tk::mac::Quit", self.onexit) self.w.createcommand("::tk::mac::ShowPreferences", lambda:prefs.PreferencesDialog(self.w, self.postprefs)) self.w.createcommand("::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app self.w.resizable(tk.FALSE, tk.FALSE) # Can't be only resizable on one axis else: self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.file_menu.add_command(command=lambda:stats.StatsDialog(self)) self.file_menu.add_command(command=self.save_raw) self.file_menu.add_command(command=lambda:prefs.PreferencesDialog(self.w, self.postprefs)) self.file_menu.add_separator() self.file_menu.add_command(command=self.onexit) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) self.help_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.help_menu.add_command(command=self.help_general) self.help_menu.add_command(command=self.help_privacy) self.help_menu.add_command(command=self.help_releases) self.help_menu.add_command(command=lambda:self.updater.checkForUpdates()) self.menubar.add_cascade(menu=self.help_menu) if platform == 'win32': # Must be added after at least one "real" menu entry self.always_ontop = tk.BooleanVar(value = config.getint('always_ontop')) self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) self.system_menu.add_separator() self.system_menu.add_checkbutton(label=_('Always on top'), variable = self.always_ontop, command=self.ontop_changed) # Appearance setting self.menubar.add_cascade(menu=self.system_menu) self.w.bind('<Control-c>', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) theme.register(self.menubar) # menus and children aren't automatically registered theme.register(self.file_menu) theme.register(self.edit_menu) theme.register(self.help_menu) # Alternate title bar and menu for dark theme self.theme_menubar = tk.Frame(frame) self.theme_menubar.columnconfigure(2, weight=1) theme_titlebar = tk.Label(self.theme_menubar, text=applongname, image=self.theme_icon, cursor='fleur', anchor=tk.W, compound=tk.LEFT) theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW) self.drag_offset = None theme_titlebar.bind('<Button-1>', self.drag_start) theme_titlebar.bind('<B1-Motion>', self.drag_continue) theme_titlebar.bind('<ButtonRelease-1>', self.drag_end) if platform == 'win32': # Can't work out how to deiconify on Linux theme_minimize = tk.Label(self.theme_menubar, image=self.theme_minimize) theme_minimize.grid(row=0, column=3, padx=2) theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) theme_close = tk.Label(self.theme_menubar, image=self.theme_close) theme_close.grid(row=0, column=4, padx=2) theme.button_bind(theme_close, self.onexit, image=self.theme_close) self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W) theme.button_bind(self.theme_file_menu, lambda e: self.file_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) theme.button_bind(self.theme_edit_menu, lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) self.theme_help_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_help_menu.grid(row=1, column=2, sticky=tk.W) theme.button_bind(self.theme_help_menu, lambda e: self.help_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) tk.Frame(self.theme_menubar, highlightthickness=1).grid(columnspan=5, padx=5, sticky=tk.EW) theme.register(self.theme_minimize) # images aren't automatically registered theme.register(self.theme_close) self.blank_menubar = tk.Frame(frame) tk.Label(self.blank_menubar).grid() tk.Label(self.blank_menubar).grid() tk.Frame(self.blank_menubar, height=2).grid() theme.register_alternate((self.menubar, self.theme_menubar, self.blank_menubar), {'row':0, 'columnspan':2, 'sticky':tk.NSEW}) self.w.resizable(tk.TRUE, tk.FALSE) # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match: if platform == 'darwin': if int(match.group(2)) >= 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) elif platform == 'win32': # Check that the titlebar will be at least partly on screen import ctypes from ctypes.wintypes import POINT # https://msdn.microsoft.com/en-us/library/dd145064 MONITOR_DEFAULTTONULL = 0 if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16), MONITOR_DEFAULTTONULL): self.w.geometry(config.get('geometry')) else: self.w.geometry(config.get('geometry')) self.w.attributes('-topmost', config.getint('always_ontop') and 1 or 0) theme.register(frame) theme.apply(self.w) self.w.bind('<Map>', self.onmap) # Special handling for overrideredict self.w.bind('<Enter>', self.onenter) # Special handling for transparency self.w.bind('<FocusIn>', self.onenter) # " self.w.bind('<Leave>', self.onleave) # " self.w.bind('<FocusOut>', self.onleave) # " self.w.bind('<Return>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend) self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal monitoring self.w.bind_all('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar self.w.bind_all('<<CompanionAuthEvent>>', self.auth) # cAPI auth self.w.bind_all('<<Quit>>', self.onexit) # Updater # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) if not getattr(sys, 'frozen', False): self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps try: config.get_password('') # Prod SecureStorage on Linux to initialise except RuntimeError: pass # Migration from <= 3.30 for username in config.get('fdev_usernames') or []: config.delete_password(username) config.delete('fdev_usernames') config.delete('username') config.delete('password') config.delete('logdir') self.postprefs(False) # Companion login happens in callback from monitor if keyring.get_keyring().priority < 1: self.status['text'] = 'Warning: Storing passwords as text' # Shouldn't happen unless no secure storage on Linux # callback after the Preferences dialog is applied def postprefs(self, dologin=True): self.prefsdialog = None self.set_labels() # in case language has changed # Reset links in case plugins changed them self.ship.configure(url = self.shipyard_url) self.system.configure(url = self.system_url) self.station.configure(url = self.station_url) # (Re-)install hotkey monitoring hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # (Re-)install log monitoring if not monitor.start(self.w): self.status['text'] = 'Error: Check %s' % _('E:D journal file location') # Location of the new Journal file in E:D 2.2 if dologin and monitor.cmdr: self.login() # Login if not already logged in with this Cmdr # set main window labels, e.g. after language change def set_labels(self): self.cmdr_label['text'] = _('Cmdr') + ':' # Main window self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or # Multicrew role label in main window _('Ship')) + ':' # Main window self.system_label['text'] = _('System') + ':' # Main window self.station_label['text'] = _('Station') + ':' # Main window self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window if platform == 'darwin': self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX self.menubar.entryconfigure(5, label=_('Help')) # Menu title self.system_menu.entryconfigure(0, label=_("About {APP}").format(APP=applongname)) # App menu entry on OSX self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # Menu item self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # Menu item self.view_menu.entryconfigure(0, label=_('Status')) # Menu item self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item else: self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.menubar.entryconfigure(3, label=_('Help')) # Menu title self.theme_file_menu['text'] = _('File') # Menu title self.theme_edit_menu['text'] = _('Edit') # Menu title self.theme_help_menu['text'] = _('Help') # Menu title self.file_menu.entryconfigure(0, label=_('Status')) # Menu item self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # Menu item self.file_menu.entryconfigure(2, label=_('Settings')) # Item in the File menu on Windows self.file_menu.entryconfigure(4, label=_('Exit')) # Item in the File menu on Windows self.help_menu.entryconfigure(0, label=_('Documentation')) # Help menu item self.help_menu.entryconfigure(1, label=_('Privacy Policy')) # Help menu item self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item self.help_menu.entryconfigure(3, label=_('Check for Updates...')) # Menu item self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste def login(self): if not self.status['text']: self.status['text'] = _('Logging in...') self.button['state'] = self.theme_button['state'] = tk.DISABLED if platform == 'darwin': self.view_menu.entryconfigure(0, state=tk.DISABLED) # Status self.file_menu.entryconfigure(0, state=tk.DISABLED) # Save Raw Data else: self.file_menu.entryconfigure(0, state=tk.DISABLED) # Status self.file_menu.entryconfigure(1, state=tk.DISABLED) # Save Raw Data self.w.update_idletasks() try: if companion.session.login(monitor.cmdr, monitor.is_beta): self.status['text'] = _('Authentication successful') # Successfully authenticated with the Frontier website if platform == 'darwin': self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data else: self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data except (companion.CredentialsError, companion.ServerError, companion.ServerLagging) as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) self.cooldown() def getandsend(self, event=None, retrying=False): auto_update = not event play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL) and not config.getint('hotkey_mute') play_bad = False if not monitor.cmdr or not monitor.mode or monitor.state['Captain'] or not monitor.system: return # In CQC or on crew - do nothing if companion.session.state == companion.Session.STATE_AUTH: # Attempt another Auth self.login() return if not retrying: if time() < self.holdofftime: # Was invoked by key while in cooldown self.status['text'] = '' 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 elif play_sound: hotkeymgr.play_good() self.status['text'] = _('Fetching data...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() try: querytime = int(time()) data = companion.session.station() config.set('querytime', querytime) # Validation if not data.get('commander', {}).get('name'): self.status['text'] = _("Who are you?!") # Shouldn't happen elif (not data.get('lastSystem', {}).get('name') or (data['commander'].get('docked') and not data.get('lastStarport', {}).get('name'))): # Only care if docked self.status['text'] = _("Where are you?!") # Shouldn't happen elif not data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'): self.status['text'] = _("What are you flying?!") # Shouldn't happen elif monitor.cmdr and data['commander']['name'] != monitor.cmdr: raise companion.CmdrError() # Companion API return doesn't match Journal elif ((auto_update and not data['commander'].get('docked')) or (data['lastSystem']['name'] != monitor.system) or ((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or (data['ship']['id'] != monitor.state['ShipID']) or (data['ship']['name'].lower() != monitor.state['ShipType'])): raise companion.ServerLagging() else: if __debug__: # Recording if isdir('dump'): with open('dump/%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h: h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) if not monitor.state['ShipType']: # Started game in SRV or fighter self.ship['text'] = companion.ship_map.get(data['ship']['name'].lower(), data['ship']['name']) monitor.state['ShipID'] = data['ship']['id'] monitor.state['ShipType'] = data['ship']['name'].lower() if data['commander'].get('credits') is not None: monitor.state['Credits'] = data['commander']['credits'] monitor.state['Loan'] = data['commander'].get('debt', 0) # stuff we can do when not docked err = plug.notify_newdata(data, monitor.is_beta) self.status['text'] = err and err or '' if err: play_bad = True # Export market data if config.getint('output') & (config.OUT_STATION_ANY): if not data['commander'].get('docked'): if not self.status['text']: # Signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up self.status['text'] = _("You're not docked at a station!") play_bad = True elif (config.getint('output') & config.OUT_MKT_EDDN) and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): # Ignore possibly missing shipyard info if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") elif not data['lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _("Station doesn't have a market!") elif config.getint('output') & (config.OUT_MKT_CSV|config.OUT_MKT_TD): # Fixup anomalies in the commodity data fixed = companion.fixup(data) if config.getint('output') & config.OUT_MKT_CSV: commodity.export(fixed, COMMODITY_CSV) if config.getint('output') & config.OUT_MKT_TD: td.export(fixed) self.holdofftime = querytime + companion.holdoff # Companion API problem except companion.ServerLagging as e: if retrying: self.status['text'] = unicode(e) play_bad = True else: # Retry once if Companion server is unresponsive self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True)) return # early exit to avoid starting cooldown count except companion.CmdrError as e: # Companion API return doesn't match Journal self.status['text'] = unicode(e) play_bad = True companion.session.invalidate() self.login() except Exception as e: # Including CredentialsError, ServerError if __debug__: print_exc() self.status['text'] = unicode(e) play_bad = True if not self.status['text']: # no errors 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 and play_bad: hotkeymgr.play_bad() self.cooldown() def retry_for_shipyard(self, tries): # Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data. try: data = companion.session.station() if __debug__: print 'Retry for shipyard - ' + (data['commander'].get('docked') and (data.get('lastStarport', {}).get('ships') and 'Success' or 'Failure') or 'Undocked!') if not data['commander'].get('docked'): pass # might have undocked while we were waiting for retry in which case station data is unreliable elif (data.get('lastSystem', {}).get('name') == monitor.system and data.get('lastStarport', {}).get('name') == monitor.station and data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')): self.eddn.export_shipyard(data, monitor.is_beta) elif tries > 1: # bogus data - retry self.w.after(int(SERVER_RETRY * 1000), lambda:self.retry_for_shipyard(tries-1)) except: pass # Handle event(s) from the journal def journal_event(self, event): def crewroletext(role): # Return translated crew role. Needs to be dynamic to allow for changing language. return { None: '', 'Idle': '', 'FighterCon': _('Fighter'), # Multicrew role 'FireCon': _('Gunner'), # Multicrew role 'FlightCon': _('Helm'), # Multicrew role }.get(role, role) while True: entry = monitor.get_entry() if not entry: return # Update main window self.cooldown() if monitor.cmdr and monitor.state['Captain']: self.cmdr['text'] = '%s / %s' % (monitor.cmdr, monitor.state['Captain']) self.ship_label['text'] = _('Role') + ':' # Multicrew role label in main window self.ship.configure(state = tk.NORMAL, text = crewroletext(monitor.state['Role']), url = None) elif monitor.cmdr: if monitor.group: self.cmdr['text'] = '%s / %s' % (monitor.cmdr, monitor.group) else: self.cmdr['text'] = monitor.cmdr self.ship_label['text'] = _('Ship') + ':' # Main window self.ship.configure(text = monitor.state['ShipName'] or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType']) or '', url = self.shipyard_url) else: self.cmdr['text'] = '' self.ship_label['text'] = _('Ship') + ':' # Main window self.ship['text'] = '' self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy if entry['event'] in ['Undocked', 'StartJump', 'SetUserShipName', 'ShipyardBuy', 'ShipyardSell', 'ShipyardSwap', 'ModuleBuy', 'ModuleSell', 'MaterialCollected', 'MaterialDiscarded', 'ScientificResearch', 'EngineerCraft', 'Synthesis', 'JoinACrew']: self.status['text'] = '' # Periodically clear any old error self.w.update_idletasks() # Companion login if entry['event'] in [None, 'StartUp', 'NewCommander', 'LoadGame'] and monitor.cmdr: if not config.get('cmdrs') or monitor.cmdr not in config.get('cmdrs'): config.set('cmdrs', (config.get('cmdrs') or []) + [monitor.cmdr]) self.login() if not entry['event'] or not monitor.mode: return # Startup or in CQC if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: # Can start dashboard monitoring if not dashboard.start(self.w, monitor.started): print "Can't start Status monitoring" # Export loadout if entry['event'] == 'Loadout' and not monitor.state['Captain'] and config.getint('output') & config.OUT_SHIP: monitor.export_ship() # Plugins err = plug.notify_journal_entry(monitor.cmdr, monitor.is_beta, monitor.system, monitor.station, entry, monitor.state) if err: self.status['text'] = err if not config.getint('hotkey_mute'): hotkeymgr.play_bad() # Auto-Update after docking, but not if auth callback is pending if entry['event'] in ['StartUp', 'Location', 'Docked'] and monitor.station and not config.getint('output') & config.OUT_MKT_MANUAL and config.getint('output') & config.OUT_STATION_ANY and companion.session.state != companion.Session.STATE_AUTH: self.w.after(int(SERVER_RETRY * 1000), self.getandsend) # cAPI auth def auth(self, event=None): try: companion.session.auth_callback() self.status['text'] = _('Authentication successful') # Successfully authenticated with the Frontier website if platform == 'darwin': self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data else: self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data except companion.ServerError as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) self.cooldown() # Handle Status event def dashboard_event(self, event): entry = dashboard.status if entry: # Currently we don't do anything with these events err = plug.notify_dashboard_entry(monitor.cmdr, monitor.is_beta, entry) if err: self.status['text'] = err if not config.getint('hotkey_mute'): hotkeymgr.play_bad() # Display asynchronous error from plugin def plugin_error(self, event=None): if plug.last_error.get('msg'): self.status['text'] = plug.last_error['msg'] self.w.update_idletasks() if not config.getint('hotkey_mute'): hotkeymgr.play_bad() def shipyard_url(self, shipname): return plug.invoke(config.get('shipyard_provider'), 'EDSY', 'shipyard_url', monitor.ship(), monitor.is_beta) def system_url(self, system): return plug.invoke(config.get('system_provider'), 'EDSM', 'system_url', monitor.system) def station_url(self, station): return plug.invoke(config.get('station_provider'), 'eddb', 'station_url', monitor.system, monitor.station) def cooldown(self): if time() < self.holdofftime: self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window self.button['state'] = self.theme_button['state'] = (monitor.cmdr and monitor.mode and not monitor.state['Captain'] and monitor.system and tk.NORMAL or tk.DISABLED) def ontop_changed(self, event=None): config.set('always_ontop', self.always_ontop.get()) self.w.wm_attributes('-topmost', self.always_ontop.get()) def copy(self, event=None): if monitor.system: self.w.clipboard_clear() self.w.clipboard_append(monitor.station and '%s,%s' % (monitor.system, monitor.station) or monitor.system) def help_general(self, event=None): webbrowser.open('https://github.com/Marginal/EDMarketConnector/wiki') def help_privacy(self, event=None): webbrowser.open('https://github.com/Marginal/EDMarketConnector/wiki/Privacy-Policy') def help_releases(self, event=None): webbrowser.open('https://github.com/Marginal/EDMarketConnector/releases') def save_raw(self): self.status['text'] = _('Fetching data...') self.w.update_idletasks() try: data = companion.session.station() self.status['text'] = '' f = tkFileDialog.asksaveasfilename(parent = self.w, defaultextension = platform=='darwin' and '.json' or '', filetypes = [('JSON', '.json'), ('All Files', '*')], initialdir = config.get('outdir'), initialfile = '%s%s.%s.json' % (data.get('lastSystem', {}).get('name', 'Unknown'), data['commander'].get('docked') and '.'+data.get('lastStarport', {}).get('name', 'Unknown') or '', strftime('%Y-%m-%dT%H.%M.%S', localtime()))) if f: with open(f, 'wt') as h: h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) except companion.ServerError as e: self.status['text'] = str(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) def onexit(self, event=None): if platform!='darwin' or self.w.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen protocolhandler.close() hotkeymgr.unregister() dashboard.close() monitor.close() plug.notify_stop() self.updater.close() companion.session.close() config.close() self.w.destroy() def drag_start(self, event): self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) def drag_continue(self, event): if self.drag_offset: self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1])) def drag_end(self, event): self.drag_offset = None def oniconify(self, event=None): self.w.overrideredirect(0) # Can't iconize while overrideredirect self.w.iconify() self.w.update_idletasks() # Size and windows styles get recalculated here self.w.wait_visibility() # Need main window to be re-created before returning theme.active = None # So theme will be re-applied on map def onmap(self, event=None): if event.widget == self.w: theme.apply(self.w) def onenter(self, event=None): if config.getint('theme') > 1: self.w.attributes("-transparentcolor", '') self.blank_menubar.grid_remove() self.theme_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW) def onleave(self, event=None): if config.getint('theme') > 1 and event.widget==self.w: self.w.attributes("-transparentcolor", 'grey4') self.theme_menubar.grid_remove() self.blank_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW)
class AppWindow: STATION_UNDOCKED = u'×' # "Station" name to display when not docked = U+00D7 def __init__(self, master): self.holdofftime = config.getint('querytime') + companion.holdoff self.session = companion.Session() self.edsm = edsm.EDSM() self.w = master self.w.title(applongname) self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) # Special handling for overrideredict self.w.bind("<Map>", self.onmap) plug.load_plugins() if platform != 'darwin': if platform == 'win32': self.w.wm_iconbitmap(default='EDMarketConnector.ico') else: from PIL import Image, ImageTk self.w.tk.call('wm', 'iconphoto', self.w, '-default', ImageTk.PhotoImage(Image.open("EDMarketConnector.png"))) self.theme_icon = tk.PhotoImage(data = 'R0lGODlhFAAQAMZVAAAAAAEAAAIBAAMBAAQCAAYDAAcDAAkEAAoEAAwGAQ8IARAIAREJARYKABkLARsMASMQASgSAiUUAy0UAjAVAioXBDIWAy4YBC4ZBS8ZBTkZA0EdBDsgBkUfA0MkB00iA1AjA1IlBFQmBE4qCFgoBVkoBFArCF0qBVQtCGUrBGMtBWYtBWA0Cm8xBW8xBm8yBXMzBXU1Bms5C3s1BXs2BXw2BX02BXw4B4A5B3Q/DIJGDYNGDYJHDoNHDYdJDppGCItLD4xLDo5MDo5MD5hSD59VEKdaEbJgErtlE7tlFLxlE8BpFMJpFMNpFMZrFdFxFtl1F995GOB6GOF6GP+LG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAejgACCgiODhoeGBABPPgACj48DA4gAk00cSRUYGZycEogBAE4LCUM8Oj2pOzlQBAKHSBeKlABKBq+DHkS0g0wJiCZFvABHJBuHBSxADFRTUs/PUUsiKhaIKEZBKTM13TU0Nj8IIRqThjJCK8MnFIgKMMMAJRGGAQUvvAIPLocBAjgdPggcKMLAgRi0GjxYyNBBCwjwQoEKQLEiABA3HMU7NOFQIAA7') self.theme_minimize = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f,\n 0xfc, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };\n') self.theme_close = tk.BitmapImage(data = '#define im_width 16\n#define im_height 16\nstatic unsigned char im_bits[] = {\n 0x00, 0x00, 0x00, 0x00, 0x0c, 0x30, 0x1c, 0x38, 0x38, 0x1c, 0x70, 0x0e,\n 0xe0, 0x07, 0xc0, 0x03, 0xc0, 0x03, 0xe0, 0x07, 0x70, 0x0e, 0x38, 0x1c,\n 0x1c, 0x38, 0x0c, 0x30, 0x00, 0x00, 0x00, 0x00 };\n') frame = tk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) self.cmdr_label = tk.Label(frame) self.system_label = tk.Label(frame) self.station_label = tk.Label(frame) self.cmdr_label.grid(row=1, column=0, sticky=tk.W) self.system_label.grid(row=2, column=0, sticky=tk.W) self.station_label.grid(row=3, column=0, sticky=tk.W) self.cmdr = tk.Label(frame, anchor=tk.W) self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url = self.system_url, popup_copy = True) self.station = HyperlinkLabel(frame, url = self.station_url, popup_copy = lambda x: x!=self.STATION_UNDOCKED) self.cmdr.grid(row=1, column=1, sticky=tk.EW) self.system.grid(row=2, column=1, sticky=tk.EW) self.station.grid(row=3, column=1, sticky=tk.EW) for plugname in plug.PLUGINS: appitem = plug.get_plugin_app(plugname, frame) if appitem: appitem.grid(columnspan=2, sticky=tk.W) self.button = ttk.Button(frame, text=_('Update'), width=28, command=self.getandsend, default=tk.ACTIVE, state=tk.DISABLED) # Update button in main window self.theme_button = tk.Label(frame, width = platform == 'darwin' and 32 or 28, state=tk.DISABLED) self.status = tk.Label(frame, name='status', anchor=tk.W) row = frame.grid_size()[1] self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.theme_button.grid(row=row, columnspan=2, sticky=tk.NSEW) theme.register_alternate((self.button, self.theme_button), {'row':row, 'columnspan':2, 'sticky':tk.NSEW}) self.status.grid(columnspan=2, sticky=tk.EW) theme.button_bind(self.theme_button, self.getandsend) self.w.bind('<Return>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend) for child in frame.winfo_children(): child.grid_configure(padx=5, pady=(platform=='win32' and 1 or 3)) self.menubar = tk.Menu() if platform=='darwin': # Can't handle (de)iconify if topmost is set, so suppress iconify button # http://wiki.tcl.tk/13428 and p15 of https://developer.apple.com/legacy/library/documentation/Carbon/Conceptual/HandlingWindowsControls/windowscontrols.pdf root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox horizontalZoom resizable') # https://www.tcl.tk/man/tcl/TkCmd/menu.htm self.system_menu = tk.Menu(self.menubar, name='apple') self.system_menu.add_command(command=lambda:self.w.call('tk::mac::standardAboutPanel')) self.system_menu.add_command(command=lambda:self.updater.checkForUpdates()) self.menubar.add_cascade(menu=self.system_menu) self.file_menu = tk.Menu(self.menubar, name='file') self.file_menu.add_command(command=self.save_raw) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, name='edit') self.edit_menu.add_command(accelerator='Command-c', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) self.w.bind('<Command-c>', self.copy) self.view_menu = tk.Menu(self.menubar, name='view') self.view_menu.add_command(command=lambda:stats.StatsDialog(self)) self.menubar.add_cascade(menu=self.view_menu) window_menu = tk.Menu(self.menubar, name='window') self.menubar.add_cascade(menu=window_menu) self.w['menu'] = self.menubar # 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')) self.w.createcommand("::tk::mac::Quit", self.onexit) self.w.createcommand("::tk::mac::ShowPreferences", lambda:prefs.PreferencesDialog(self.w, self.postprefs)) self.w.createcommand("::tk::mac::ReopenApplication", self.w.deiconify) # click on app in dock = restore self.w.protocol("WM_DELETE_WINDOW", self.w.withdraw) # close button shouldn't quit app else: self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.file_menu.add_command(command=lambda:stats.StatsDialog(self)) self.file_menu.add_command(command=self.save_raw) self.file_menu.add_command(command=lambda:self.updater.checkForUpdates()) self.file_menu.add_command(command=lambda:prefs.PreferencesDialog(self.w, self.postprefs)) self.file_menu.add_separator() self.file_menu.add_command(command=self.onexit) self.menubar.add_cascade(menu=self.file_menu) self.edit_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) self.edit_menu.add_command(accelerator='Ctrl+C', state=tk.DISABLED, command=self.copy) self.menubar.add_cascade(menu=self.edit_menu) if platform == 'win32': # Must be added after at least one "real" menu entry self.always_ontop = tk.BooleanVar(value = config.getint('always_ontop')) self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE) self.system_menu.add_separator() self.system_menu.add_checkbutton(label=_('Always on top'), variable = self.always_ontop, command=self.ontop_changed) # Appearance setting self.menubar.add_cascade(menu=self.system_menu) self.w.bind('<Control-c>', self.copy) self.w.protocol("WM_DELETE_WINDOW", self.onexit) theme.register(self.menubar) # menus and children aren't automatically registered theme.register(self.file_menu) theme.register(self.edit_menu) # Alternate title bar and menu for dark theme self.theme_menubar = tk.Frame(frame) self.theme_menubar.columnconfigure(2, weight=1) theme_titlebar = tk.Label(self.theme_menubar, text=applongname, image=self.theme_icon, anchor=tk.W, compound=tk.LEFT) theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW) self.drag_offset = None theme_titlebar.bind('<Button-1>', self.drag_start) theme_titlebar.bind('<B1-Motion>', self.drag_continue) theme_titlebar.bind('<ButtonRelease-1>', self.drag_end) if platform == 'win32': # Can't work out how to deiconify on Linux theme_minimize = tk.Label(self.theme_menubar, image=self.theme_minimize) theme_minimize.grid(row=0, column=3, padx=2) theme.button_bind(theme_minimize, self.oniconify, image=self.theme_minimize) theme_close = tk.Label(self.theme_menubar, image=self.theme_close) theme_close.grid(row=0, column=4, padx=2) theme.button_bind(theme_close, self.onexit, image=self.theme_close) self.theme_file_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=tk.W) theme.button_bind(self.theme_file_menu, lambda e: self.file_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) self.theme_edit_menu = tk.Label(self.theme_menubar, anchor=tk.W) self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) theme.button_bind(self.theme_edit_menu, lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) theme.register_highlight(theme_titlebar) theme.register(self.theme_minimize) # images aren't automatically registered theme.register(self.theme_close) theme.register_alternate((self.menubar, self.theme_menubar), {'row':0, 'columnspan':2, 'sticky':tk.NSEW}) self.set_labels() # update geometry if config.get('geometry'): match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) if match: if platform == 'darwin': if int(match.group(2)) >= 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.w.geometry(config.get('geometry')) elif platform == 'win32': # Check that the titlebar will be at least partly on screen import ctypes from ctypes.wintypes import POINT # https://msdn.microsoft.com/en-us/library/dd145064 MONITOR_DEFAULTTONULL = 0 if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16), MONITOR_DEFAULTTONULL): self.w.geometry(config.get('geometry')) else: self.w.geometry(config.get('geometry')) self.w.attributes('-topmost', config.getint('always_ontop') and 1 or 0) self.w.resizable(tk.TRUE, tk.FALSE) theme.register(frame) theme.register_highlight(self.system) theme.register_highlight(self.station) theme.apply(self.w) # Load updater after UI creation (for WinSparkle) import update self.updater = update.Updater(self.w) self.w.bind_all('<<Quit>>', self.onexit) # user-generated # Install hotkey monitoring self.w.bind_all('<<Invoke>>', self.getandsend) # user-generated hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) # Install log monitoring monitor.set_callback(self.system_change) edproxy.set_callback(self.system_change) if (config.getint('output') & config.OUT_LOG_AUTO) and (config.getint('output') & (config.OUT_LOG_FILE|config.OUT_LOG_EDSM)): monitor.start(self.w) edproxy.start(self.w) # First run if not config.get('username') or not config.get('password'): prefs.PreferencesDialog(self.w, self.postprefs) else: self.login() # callback after the Preferences dialog is applied def postprefs(self): self.set_labels() # in case language has changed self.login() # in case credentials gave changed # set main window labels, e.g. after language change def set_labels(self): self.cmdr_label['text'] = _('Cmdr') + ':' # Main window self.system_label['text'] = _('System') + ':' # Main window self.station_label['text'] = _('Station') + ':' # Main window self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window if platform == 'darwin': self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX self.system_menu.entryconfigure(0, label=_("About {APP}").format(APP=applongname)) # App menu entry on OSX self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # Menu item self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # Menu item self.view_menu.entryconfigure(0, label=_('Status')) # Menu item else: self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.theme_file_menu['text'] = _('File') # Menu title self.theme_edit_menu['text'] = _('Edit') # Menu title self.file_menu.entryconfigure(0, label=_('Status')) # Menu item self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # Menu item self.file_menu.entryconfigure(2, label=_("Check for Updates...")) # Menu item self.file_menu.entryconfigure(3, label=_("Settings")) # Item in the File menu on Windows self.file_menu.entryconfigure(5, label=_("Exit")) # Item in the File menu on Windows self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste def login(self): self.status['text'] = _('Logging in...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.w.update_idletasks() try: self.session.login(config.get('username'), config.get('password')) self.status['text'] = '' except companion.VerificationRequired: # don't worry about authentication now - prompt on query self.status['text'] = '' except companion.ServerError as e: self.status['text'] = unicode(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) # Try to obtain exclusive lock on flight log ASAP if config.getint('output') & config.OUT_LOG_FILE: try: flightlog.openlog() except Exception as e: if __debug__: print_exc() if not self.status['text']: self.status['text'] = unicode(e) if not getattr(sys, 'frozen', False): self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps self.cooldown() # callback after verification code def verify(self, callback, code): try: self.session.verify(code) config.save() # Save settings now for use by command-line app except Exception as e: if __debug__: print_exc() self.button['state'] = self.theme_button['state'] = tk.NORMAL self.status['text'] = unicode(e) else: return callback() # try again 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: # Was invoked by key while in cooldown self.status['text'] = '' 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 elif play_sound: hotkeymgr.play_good() self.cmdr['text'] = self.system['text'] = self.station['text'] = '' self.system['image'] = '' self.status['text'] = _('Fetching data...') self.button['state'] = self.theme_button['state'] = tk.DISABLED self.edit_menu.entryconfigure(0, state=tk.DISABLED) # Copy self.w.update_idletasks() try: querytime = int(time()) data = self.session.query() config.set('querytime', querytime) # Validation if not data.get('commander') or not data['commander'].get('name','').strip(): self.status['text'] = _("Who are you?!") # Shouldn't happen 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 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 else: if __debug__: # Recording if not isdir('dump'): mkdir('dump') with open('dump/%s%s.%s.json' % (data['lastSystem']['name'], data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '', strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wt') as h: h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) self.cmdr['text'] = data.get('commander') and data.get('commander').get('name') or '' self.system['text'] = data.get('lastSystem') and data.get('lastSystem').get('name') or '' self.station['text'] = data.get('commander') and data.get('commander').get('docked') and data.get('lastStarport') and data.get('lastStarport').get('name') or (EDDB.system(self.system['text']) and self.STATION_UNDOCKED or '') self.status['text'] = '' self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy if data['lastStarport'].get('commodities'): # Fixup anomalies in the commodity data self.session.fixup(data['lastStarport']['commodities']) # stuff we can do when not docked plug.notify_newdata(data) if config.getint('output') & config.OUT_SHIP_EDS: loadout.export(data) if config.getint('output') & config.OUT_SHIP_CORIOLIS: coriolis.export(data) if config.getint('output') & config.OUT_LOG_FILE: flightlog.export(data) if config.getint('output') & config.OUT_LOG_EDSM: # Catch any EDSM errors here so that they don't prevent station update try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.export(data, lambda:self.edsm.lookup(self.system['text'], EDDB.system(self.system['text']))) # Do EDSM lookup during EDSM export self.status['text'] = '' except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) else: self.edsm.link(self.system['text']) self.edsmpoll() if not (config.getint('output') & (config.OUT_CSV|config.OUT_TD|config.OUT_BPC|config.OUT_EDDN)): # no station data requested - we're done pass elif not data['commander'].get('docked'): # signal as error because the user might actually be docked but the server hosting the Companion API hasn't caught up if not self.status['text']: self.status['text'] = _("You're not docked at a station!") else: # Finally - the data looks sane and we're docked at a station (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) # No EDDN output? if (config.getint('output') & config.OUT_EDDN) and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')): # Ignore possibly missing shipyard info if not self.status['text']: self.status['text'] = _("Station doesn't have anything!") # No market output? elif not (config.getint('output') & config.OUT_EDDN) and not data['lastStarport'].get('commodities'): if not self.status['text']: self.status['text'] = _("Station doesn't have a market!") else: if data['lastStarport'].get('commodities'): if config.getint('output') & config.OUT_CSV: commodity.export(data, COMMODITY_CSV) if config.getint('output') & config.OUT_TD: td.export(data) if config.getint('output') & config.OUT_BPC: commodity.export(data, COMMODITY_BPC) if config.getint('output') & config.OUT_EDDN: old_status = self.status['text'] if not old_status: self.status['text'] = _('Sending data to EDDN...') self.w.update_idletasks() eddn.export_commodities(data) eddn.export_outfitting(data) if has_shipyard and not data['lastStarport'].get('ships'): # API is flakey about shipyard info - silently retry if missing (<1s is usually sufficient - 5s for margin). self.w.after(int(SERVER_RETRY * 1000), self.retry_for_shipyard) else: eddn.export_shipyard(data) if not old_status: self.status['text'] = '' except companion.VerificationRequired: return prefs.AuthenticationDialog(self.w, partial(self.verify, self.getandsend)) # Companion API problem except companion.ServerError as e: if retrying: self.status['text'] = unicode(e) else: # Retry once if Companion server is unresponsive self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True)) return # early exit to avoid starting cooldown count except requests.exceptions.ConnectionError as e: if __debug__: print_exc() self.status['text'] = _("Error: Can't connect to EDDN") except requests.exceptions.Timeout as e: if __debug__: print_exc() self.status['text'] = _("Error: Connection to EDDN timed out") except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not self.status['text']: # no errors 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') elif play_sound: hotkeymgr.play_bad() self.holdofftime = querytime + companion.holdoff self.cooldown() def retry_for_shipyard(self): # Try again to get shipyard data and send to EDDN. Don't report errors if can't get or send the data. try: data = self.session.query() if __debug__: print 'Retry for shipyard - ' + (data['commander'].get('docked') and (data['lastStarport'].get('ships') and 'Success' or 'Failure') or 'Undocked!') if data['commander'].get('docked'): # might have undocked while we were waiting for retry in which case station data is unreliable eddn.export_shipyard(data) except: pass def system_change(self, timestamp, system, coordinates): if self.system['text'] != system: self.system['text'] = system self.system['image'] = '' self.station['text'] = EDDB.system(system) and self.STATION_UNDOCKED or '' plug.notify_system_changed(timestamp, system, coordinates) if config.getint('output') & config.OUT_LOG_FILE: flightlog.writelog(timestamp, system) if config.getint('output') & config.OUT_LOG_EDSM: try: self.status['text'] = _('Sending data to EDSM...') self.w.update_idletasks() edsm.writelog(timestamp, system, lambda:self.edsm.lookup(system, EDDB.system(system)), coordinates) # Do EDSM lookup during EDSM export self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) if not config.getint('hotkey_mute'): hotkeymgr.play_bad() else: self.edsm.link(system) self.status['text'] = strftime(_('Last updated at {HH}:{MM}:{SS}').format(HH='%H', MM='%M', SS='%S').encode('utf-8'), localtime(timestamp)).decode('utf-8') self.edsmpoll() def edsmpoll(self): result = self.edsm.result if result['done']: self.system['image'] = result['img'] else: self.w.after(int(EDSM_POLL * 1000), self.edsmpoll) def system_url(self, text): return text and self.edsm.result['url'] def station_url(self, text): if text: (station_id, has_market, has_outfitting, has_shipyard) = EDDB.station(self.system['text'], self.station['text']) if station_id: return 'https://eddb.io/station/%d' % station_id system_id = EDDB.system(self.system['text']) if system_id: return 'https://eddb.io/system/%d' % system_id return None def cooldown(self): if time() < self.holdofftime: self.button['text'] = self.theme_button['text'] = _('cooldown {SS}s').format(SS = int(self.holdofftime - time())) # Update button in main window self.w.after(1000, self.cooldown) else: self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window self.button['state'] = self.theme_button['state'] = tk.NORMAL def ontop_changed(self, event=None): config.set('always_ontop', self.always_ontop.get()) self.w.wm_attributes('-topmost', self.always_ontop.get()) def copy(self, event=None): if self.system['text']: self.w.clipboard_clear() self.w.clipboard_append(self.station['text'] == self.STATION_UNDOCKED and self.system['text'] or '%s,%s' % (self.system['text'], self.station['text'])) def save_raw(self): self.status['text'] = _('Fetching data...') self.w.update_idletasks() try: data = self.session.query() self.cmdr['text'] = data.get('commander') and data.get('commander').get('name') or '' self.status['text'] = '' f = tkFileDialog.asksaveasfilename(parent = self.w, defaultextension = platform=='darwin' and '.json' or '', filetypes = [('JSON', '.json'), ('All Files', '*')], initialdir = config.get('outdir'), initialfile = '%s%s.%s.json' % (data['lastSystem'].get('name', 'Unknown'), data['commander'].get('docked') and '.'+data['lastStarport'].get('name', 'Unknown') or '', strftime('%Y-%m-%dT%H.%M.%S', localtime()))) if f: with open(f, 'wt') as h: h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) except companion.VerificationRequired: prefs.AuthenticationDialog(self.w, partial(self.verify, self.save_raw)) except companion.ServerError as e: self.status['text'] = str(e) except Exception as e: if __debug__: print_exc() self.status['text'] = unicode(e) def onexit(self, event=None): hotkeymgr.unregister() flightlog.close() if platform!='darwin' or self.w.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) config.close() self.updater.close() self.session.close() self.w.destroy() def drag_start(self, event): self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) def drag_continue(self, event): if self.drag_offset: self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1])) def drag_end(self, event): self.drag_offset = None def oniconify(self, event=None): self.w.overrideredirect(0) # Can't iconize while overrideredirect self.w.iconify() self.w.update_idletasks() # Size and windows styles get recalculated here self.w.wait_visibility() # Need main window to be re-created before returning theme.active = None # So theme will be re-applied on map def onmap(self, event=None): if event.widget == self.w: theme.apply(self.w)
class PreferencesDialog(tk.Toplevel): def __init__(self, parent, callback): tk.Toplevel.__init__(self, parent) self.parent = parent self.callback = callback self.title(platform=='darwin' and _('Preferences') or _('Settings')) if parent.winfo_viewable(): self.transient(parent) # position over parent if platform!='darwin' or parent.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 self.geometry("+%d+%d" % (parent.winfo_rootx(), parent.winfo_rooty())) # remove decoration if platform=='win32': self.attributes('-toolwindow', tk.TRUE) elif platform=='darwin': # http://wiki.tcl.tk/13428 parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility') self.resizable(tk.FALSE, tk.FALSE) style = ttk.Style() frame = ttk.Frame(self) frame.grid(sticky=tk.NSEW) notebook = nb.Notebook(frame) PADX = 10 BUTTONX = 12 # indent Checkbuttons and Radiobuttons PADY = 2 # close spacing credframe = nb.Frame(notebook) credframe.columnconfigure(1, weight=1) nb.Label(credframe, text=_('Credentials')).grid(padx=PADX, sticky=tk.W) # Section heading in settings ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) nb.Label(credframe, text=_('Please log in with your Elite: Dangerous account details')).grid(padx=PADX, columnspan=2, sticky=tk.W) # Use same text as E:D Launcher's login dialog nb.Label(credframe, text=_('Username (Email)')).grid(row=10, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog nb.Label(credframe, text=_('Password')).grid(row=11, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog self.username = nb.Entry(credframe) self.username.insert(0, config.get('username') or '') self.username.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW) self.username.focus_set() self.password = nb.Entry(credframe, show=u'•') self.password.insert(0, config.get('password') or '') self.password.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) nb.Label(credframe).grid(sticky=tk.W) # big spacer nb.Label(credframe, text=_('Privacy')).grid(padx=PADX, sticky=tk.W) # Section heading in settings ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW) self.out_anon= tk.IntVar(value = config.getint('anonymous') and 1) nb.Label(credframe, text=_('How do you want to be identified in the saved data')).grid(columnspan=2, padx=PADX, sticky=tk.W) nb.Radiobutton(credframe, text=_('Cmdr name'), variable=self.out_anon, value=0).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Privacy setting nb.Radiobutton(credframe, text=_('Pseudo-anonymized ID'), variable=self.out_anon, value=1).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) # Privacy setting notebook.add(credframe, text=_('Identity')) # Tab heading in settings outframe = nb.Frame(notebook) outframe.columnconfigure(0, weight=1) output = config.getint('output') or (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN | config.OUT_SHIP_EDS) # default settings nb.Label(outframe, text=_('Please choose what data to save')).grid(columnspan=2, padx=PADX, sticky=tk.W) self.out_csv = tk.IntVar(value = (output & config.OUT_MKT_CSV ) and 1) nb.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_bpc = tk.IntVar(value = (output & config.OUT_MKT_BPC ) and 1) nb.Checkbutton(outframe, text=_("Market data in Slopey's BPC format file"), variable=self.out_bpc, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_td = tk.IntVar(value = (output & config.OUT_MKT_TD ) and 1) nb.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_ship_eds= tk.IntVar(value = (output & config.OUT_SHIP_EDS) and 1) nb.Checkbutton(outframe, text=_('Ship loadout in E:D Shipyard format file'), variable=self.out_ship_eds, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) self.out_ship_coriolis= tk.IntVar(value = (output & config.OUT_SHIP_CORIOLIS) and 1) nb.Checkbutton(outframe, text=_('Ship loadout in Coriolis format file'), variable=self.out_ship_coriolis, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W) self.out_auto = tk.IntVar(value = 0 if output & config.OUT_MKT_MANUAL else 1) # inverted self.out_auto_button = nb.Checkbutton(outframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting self.out_auto_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) self.outdir_label = nb.Label(outframe, text=_('File location')+':') # Section heading in settings self.outdir_label.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) self.outdir = nb.Entry(outframe, takefocus=False) if config.get('outdir').startswith(config.home): self.outdir.insert(0, '~' + config.get('outdir')[len(config.home):]) else: self.outdir.insert(0, config.get('outdir')) self.outdir.grid(row=20, padx=(PADX,0), sticky=tk.EW) self.outbutton = nb.Button(outframe, text=(platform=='darwin' and _('Change...') or # Folder selection button on OSX _('Browse...')), # Folder selection button on Windows command = lambda:self.filebrowse(_('File location'), self.outdir)) self.outbutton.grid(row=20, column=1, padx=PADX, sticky=tk.NSEW) nb.Frame(outframe).grid(pady=5) # bottom spacer notebook.add(outframe, text=_('Output')) # Tab heading in settings eddnframe = nb.Frame(notebook) HyperlinkLabel(eddnframe, text='Elite Dangerous Data Network', background=nb.Label().cget('background'), url='https://github.com/jamesremuscat/EDDN/wiki', underline=True).grid(padx=PADX, sticky=tk.W) # Don't translate self.eddn_station= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1) nb.Checkbutton(eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.eddn_station, command=self.outvarchanged).grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) # Output setting self.eddn_auto_button = nb.Checkbutton(eddnframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting self.eddn_auto_button.grid(padx=BUTTONX, sticky=tk.W) self.eddn_system = tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1) self.eddn_system_button = nb.Checkbutton(eddnframe, text=_('Send system and scan data to the Elite Dangerous Data Network'), variable=self.eddn_system, command=self.outvarchanged) # Output setting new in E:D 2.2 self.eddn_system_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) self.eddn_delay= tk.IntVar(value = (output & config.OUT_SYS_DELAY) and 1) self.eddn_delay_button = nb.Checkbutton(eddnframe, text=_('Delay sending until docked'), variable=self.eddn_delay, command=self.outvarchanged) # Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2 self.eddn_delay_button.grid(padx=BUTTONX, sticky=tk.W) notebook.add(eddnframe, text='EDDN') # Not translated edsmframe = nb.Frame(notebook) edsmframe.columnconfigure(1, weight=1) HyperlinkLabel(edsmframe, text='Elite Dangerous Star Map', background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True).grid(columnspan=2, padx=PADX, sticky=tk.W) # Don't translate self.edsm_log = tk.IntVar(value = (output & config.OUT_SYS_EDSM) and 1) self.edsm_log_button = nb.Checkbutton(edsmframe, text=_('Send flight log to Elite Dangerous Star Map'), variable=self.edsm_log, command=self.outvarchanged) self.edsm_log_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) nb.Label(edsmframe).grid(sticky=tk.W) # big spacer self.edsm_label = HyperlinkLabel(edsmframe, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings self.edsm_label.grid(columnspan=2, padx=PADX, sticky=tk.W) self.edsm_cmdr_label = nb.Label(edsmframe, text=_('Commander Name')) # EDSM setting self.edsm_cmdr_label.grid(row=10, padx=PADX, sticky=tk.W) self.edsm_cmdr = nb.Entry(edsmframe) self.edsm_cmdr.insert(0, config.get('edsm_cmdrname') or '') self.edsm_cmdr.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW) self.edsm_apikey_label = nb.Label(edsmframe, text=_('API Key')) # EDSM setting self.edsm_apikey_label.grid(row=11, padx=PADX, sticky=tk.W) self.edsm_apikey = nb.Entry(edsmframe) self.edsm_apikey.insert(0, config.get('edsm_apikey') or '') self.edsm_apikey.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW) notebook.add(edsmframe, text='EDSM') # Not translated configframe = nb.Frame(notebook) configframe.columnconfigure(1, weight=1) self.logdir = nb.Entry(configframe, takefocus=False) logdir = config.get('journaldir') or config.default_journal_dir if not logdir: pass elif logdir.startswith(config.home): self.logdir.insert(0, '~' + logdir[len(config.home):]) else: self.logdir.insert(0, logdir) self.logdir['state'] = 'readonly' if platform != 'darwin': # Apple's SMB implementation is way too flaky - no filesystem events and bogus NULLs nb.Label(configframe, text = _('E:D journal file location')+':').grid(columnspan=3, padx=PADX, sticky=tk.W) # Location of the new Journal file in E:D 2.2 self.logdir.grid(row=10, columnspan=2, padx=(PADX,0), sticky=tk.EW) self.logbutton = nb.Button(configframe, text=(platform=='darwin' and _('Change...') or # Folder selection button on OSX _('Browse...')), # Folder selection button on Windows command = lambda:self.filebrowse(_('E:D journal file location'), self.logdir)) self.logbutton.grid(row=10, column=2, padx=PADX, sticky=tk.EW) if config.default_journal_dir: nb.Button(configframe, text=_('Default'), command=self.logdir_reset, state = config.get('journaldir') and tk.NORMAL or tk.DISABLED).grid(column=2, padx=PADX, pady=(5,0), sticky=tk.EW) # Appearance theme and language setting if platform == 'win32': ttk.Separator(configframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) if platform in ['darwin','win32']: self.hotkey_code = config.getint('hotkey_code') self.hotkey_mods = config.getint('hotkey_mods') self.hotkey_only = tk.IntVar(value = not config.getint('hotkey_always')) self.hotkey_play = tk.IntVar(value = not config.getint('hotkey_mute')) nb.Label(configframe, text = platform=='darwin' and _('Keyboard shortcut') or # Hotkey/Shortcut settings prompt on OSX _('Hotkey') # Hotkey/Shortcut settings prompt on Windows ).grid(row=20, padx=PADX, sticky=tk.W) if platform == 'darwin' and not was_accessible_at_launch: if AXIsProcessTrusted(): nb.Label(configframe, text = _('Re-start {APP} to use shortcuts').format(APP=applongname), foreground='firebrick').grid(padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX else: nb.Label(configframe, text = _('{APP} needs permission to use shortcuts').format(APP=applongname), foreground='firebrick').grid(columnspan=3, padx=PADX, sticky=tk.W) # Shortcut settings prompt on OSX nb.Button(configframe, text = _('Open System Preferences'), command = self.enableshortcuts).grid(column=2, padx=PADX, sticky=tk.E) # Shortcut settings button on OSX else: self.hotkey_text = nb.Entry(configframe, width = (platform == 'darwin' and 20 or 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=20, column=1, columnspan=2, padx=PADX, pady=(5,0), sticky=tk.W) self.hotkey_only_btn = nb.Checkbutton(configframe, text=_('Only when Elite: Dangerous is the active app'), variable=self.hotkey_only, state = self.hotkey_code and tk.NORMAL or tk.DISABLED) # Hotkey/Shortcut setting self.hotkey_only_btn.grid(columnspan=3, padx=PADX, pady=(5,0), sticky=tk.W) self.hotkey_play_btn = nb.Checkbutton(configframe, 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(columnspan=3, padx=PADX, sticky=tk.W) notebook.add(configframe, text=_('Configuration')) # Tab heading in settings self.languages = Translations().available_names() self.lang = tk.StringVar(value = self.languages.get(config.get('language'), _('Default'))) # Appearance theme and language setting self.always_ontop = tk.BooleanVar(value = config.getint('always_ontop')) self.theme = tk.IntVar(value = config.getint('theme') and 1 or 0) self.theme_colors = [config.get('dark_text'), config.get('dark_highlight')] self.theme_prompts = [ _('Normal text'), # Dark theme color setting _('Highlighted text'), # Dark theme color setting ] themeframe = nb.Frame(notebook) themeframe.columnconfigure(2, weight=1) nb.Label(themeframe, text=_('Language')).grid(row=10, padx=PADX, sticky=tk.W) # Appearance setting prompt self.lang_button = nb.OptionMenu(themeframe, self.lang, self.lang.get(), *self.languages.values()) self.lang_button.grid(row=10, column=1, columnspan=2, padx=PADX, sticky=tk.W) ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) nb.Label(themeframe, text=_('Theme')).grid(columnspan=3, padx=PADX, sticky=tk.W) # Appearance setting nb.Radiobutton(themeframe, text=_('Default'), variable=self.theme, value=0, command=self.themevarchanged).grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance theme and language setting nb.Radiobutton(themeframe, text=_('Dark'), variable=self.theme, value=1, command=self.themevarchanged).grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance theme setting self.theme_label_0 = nb.Label(themeframe, text=self.theme_prompts[0]) self.theme_label_0.grid(row=20, padx=PADX, sticky=tk.W) self.theme_button_0 = nb.ColoredButton(themeframe, text=_('Station'), background='grey4', command=lambda:self.themecolorbrowse(0)) # Main window self.theme_button_0.grid(row=20, column=1, padx=PADX, pady=PADY, sticky=tk.NSEW) self.theme_label_1 = nb.Label(themeframe, text=self.theme_prompts[1]) self.theme_label_1.grid(row=21, padx=PADX, sticky=tk.W) self.theme_button_1 = nb.ColoredButton(themeframe, text=' Hutton Orbital ', background='grey4', command=lambda:self.themecolorbrowse(1)) # Do not translate self.theme_button_1.grid(row=21, column=1, padx=PADX, pady=PADY, sticky=tk.NSEW) ttk.Separator(themeframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY*8, sticky=tk.EW) self.ontop_button = nb.Checkbutton(themeframe, text=_('Always on top'), variable=self.always_ontop, command=self.themevarchanged) self.ontop_button.grid(columnspan=3, padx=BUTTONX, sticky=tk.W) # Appearance setting nb.Label(themeframe).grid(sticky=tk.W) # big spacer notebook.add(themeframe, text=_('Appearance')) # Tab heading in settings # build plugin prefs tabs for plugname in plug.PLUGINS: plugframe = plug.get_plugin_pref(plugname, notebook) if plugframe: notebook.add(plugframe, text=plugname) if platform=='darwin': self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes else: buttonframe = ttk.Frame(frame) buttonframe.grid(padx=PADX, pady=PADX, sticky=tk.NSEW) buttonframe.columnconfigure(0, weight=1) ttk.Label(buttonframe).grid(row=0, column=0) # spacer button = ttk.Button(buttonframe, text=_('OK'), command=self.apply) button.grid(row=0, column=1, sticky=tk.E) button.bind("<Return>", lambda event:self.apply()) self.protocol("WM_DELETE_WINDOW", self._destroy) # Selectively disable buttons depending on output settings self.outvarchanged() self.themevarchanged() # disable hotkey for the duration hotkeymgr.unregister() # wait for window to appear on screen before calling grab_set self.parent.wm_attributes('-topmost', 0) # needed for dialog to appear ontop of parent on OSX & Linux self.wait_visibility() self.grab_set() def outvarchanged(self): logdir = self.logdir.get().startswith('~') and join(config.home, self.logdir.get()[2:]) or self.logdir.get() logvalid = logdir and exists(logdir) local = self.out_bpc.get() or self.out_td.get() or self.out_csv.get() or self.out_ship_eds.get() or self.out_ship_coriolis.get() self.out_auto_button['state'] = local and logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED self.outdir['state'] = local and 'readonly' or tk.DISABLED self.eddn_auto_button['state'] = self.eddn_station.get() and logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED self.eddn_system_button['state']= logvalid and tk.NORMAL or tk.DISABLED self.eddn_delay_button['state'] = logvalid and eddn.replayfile and self.eddn_system.get() and tk.NORMAL or tk.DISABLED self.edsm_log_button['state'] = logvalid and tk.NORMAL or tk.DISABLED edsm_state = logvalid and self.edsm_log.get() and tk.NORMAL or tk.DISABLED self.edsm_label['state'] = edsm_state self.edsm_cmdr_label['state'] = edsm_state self.edsm_apikey_label['state'] = edsm_state self.edsm_cmdr['state'] = edsm_state self.edsm_apikey['state'] = edsm_state def filebrowse(self, title, entryfield): if platform != 'win32': import tkFileDialog d = tkFileDialog.askdirectory(parent=self, initialdir=expanduser(entryfield.get()), title=title, mustexist=tk.TRUE) else: def browsecallback(hwnd, uMsg, lParam, lpData): # set initial folder if uMsg==BFFM_INITIALIZED and lpData: ctypes.windll.user32.SendMessageW(hwnd, BFFM_SETSELECTION, 1, lpData); return 0 browseInfo = BROWSEINFO() browseInfo.lpszTitle = title browseInfo.ulFlags = BIF_RETURNONLYFSDIRS|BIF_USENEWUI browseInfo.lpfn = BrowseCallbackProc(browsecallback) browseInfo.lParam = entryfield.get().startswith('~') and join(config.home, entryfield.get()[2:]) or entryfield.get() ctypes.windll.ole32.CoInitialize(None) pidl = ctypes.windll.shell32.SHBrowseForFolderW(ctypes.byref(browseInfo)) if pidl: path = ctypes.create_unicode_buffer(MAX_PATH) ctypes.windll.shell32.SHGetPathFromIDListW(pidl, path) ctypes.windll.ole32.CoTaskMemFree(pidl) d = path.value else: d = None if d: entryfield['state'] = tk.NORMAL # must be writable to update entryfield.delete(0, tk.END) if d.startswith(config.home): entryfield.insert(0, '~' + d[len(config.home):]) else: entryfield.insert(0, d) entryfield['state'] = 'readonly' self.outvarchanged() def logdir_reset(self): self.logdir['state'] = tk.NORMAL # must be writable to update self.logdir.delete(0, tk.END) if not config.default_journal_dir: pass # Can't reset elif config.default_journal_dir.startswith(config.home): self.logdir.insert(0, '~' + config.default_journal_dir[len(config.home):]) else: self.logdir.insert(0, config.default_journal_dir) self.logdir['state'] = 'readonly' self.outvarchanged() def themecolorbrowse(self, index): (rgb, color) = tkColorChooser.askcolor(self.theme_colors[index], title=self.theme_prompts[index], parent=self.parent) if color: self.theme_colors[index] = color self.themevarchanged() def themevarchanged(self): self.theme_button_0['foreground'], self.theme_button_1['foreground'] = self.theme_colors state = self.theme.get() and tk.NORMAL or tk.DISABLED self.theme_label_0['state'] = state self.theme_label_1['state'] = state self.theme_button_0['state'] = state self.theme_button_1['state'] = state if platform == 'linux2': # Unmanaged windows are always on top on X self.ontop_button['state'] = self.theme.get() and tk.DISABLED or tk.NORMAL 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_only_btn['state'] = tk.NORMAL self.hotkey_play_btn['state'] = tk.NORMAL self.hotkey_only_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_only_btn['state'] = tk.NORMAL self.hotkey_play_btn['state'] = tk.NORMAL else: event.widget.insert(0, _('None')) # No hotkey/shortcut currently defined self.hotkey_only_btn['state'] = tk.DISABLED self.hotkey_play_btn['state'] = tk.DISABLED self.hotkey_only_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_bpc.get() and config.OUT_MKT_BPC) + (self.out_td.get() and config.OUT_MKT_TD) + (self.out_csv.get() and config.OUT_MKT_CSV) + (config.OUT_MKT_MANUAL if not self.out_auto.get() else 0) + (self.out_ship_eds.get() and config.OUT_SHIP_EDS) + (self.out_ship_coriolis.get() and config.OUT_SHIP_CORIOLIS) + (self.eddn_station.get() and config.OUT_MKT_EDDN) + (self.eddn_system.get() and config.OUT_SYS_EDDN) + (self.eddn_delay.get() and config.OUT_SYS_DELAY) + (self.edsm_log.get() and config.OUT_SYS_EDSM)) config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get()) config.set('edsm_cmdrname', self.edsm_cmdr.get().strip()) config.set('edsm_apikey', self.edsm_apikey.get().strip()) logdir = self.logdir.get().startswith('~') and join(config.home, self.logdir.get()[2:]) or self.logdir.get() if config.default_journal_dir and logdir.lower() == config.default_journal_dir.lower(): config.set('journaldir', '') # default location else: config.set('journaldir', logdir) if platform in ['darwin','win32']: config.set('hotkey_code', self.hotkey_code) config.set('hotkey_mods', self.hotkey_mods) config.set('hotkey_always', int(not self.hotkey_only.get())) config.set('hotkey_mute', int(not self.hotkey_play.get())) lang_codes = { v: k for k, v in self.languages.iteritems() } # Codes by name config.set('language', lang_codes.get(self.lang.get()) or '') Translations().install(config.get('language') or None) config.set('always_ontop', self.always_ontop.get()) config.set('theme', self.theme.get()) config.set('dark_text', self.theme_colors[0]) config.set('dark_highlight', self.theme_colors[1]) theme.apply(self.parent) config.set('anonymous', self.out_anon.get()) self._destroy() if self.callback: self.callback() def _destroy(self): self.parent.wm_attributes('-topmost', config.getint('always_ontop') and 1 or 0) 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 ShipFrame(tk.Frame): def __init__(self, parent, ship_data, cmdr, bg=None, fg='black', hl='blue', *args, **options): tk.Frame.__init__(self, parent, *args, **options) self.cmdr = cmdr self.grid_columnconfigure(0, weight=1) self.build_ui() self.update_ship(ship_data, self.cmdr) # if self.theme: for element in [self.ship_link, self.station_link, self.system_link]: element.config(background=bg, foreground=hl) for element in [self.sys_label, self.station_label]: element.config(background=bg, foreground=fg) self.sub_frame.config(background=bg) def update_ship(self, ship_data, cmdr): self.cmdr = cmdr self.ship_data = ship_data if 'shipName' in ship_data: self.ship_lbl_txt = u"{} ({})".format( ship_data['shipName'], ship_map[ship_data['name'].lower()]) else: self.ship_lbl_txt = ship_map[ship_data['name'].lower()] self.sysname = ship_data['starsystem']['name'] self.stationname = ship_data['station']['name'] try: self.market_id = ship_data['station']['id'] except: self.market_id = None self.sys_url = get_system_url(self.sysname) self.station_url = get_station_url(self.sysname, self.stationname, None, self.market_id) self.system_link.set_system(self.sysname) self.station_link.set_station(self.sysname, self.stationname, self.market_id) edID = FLAT_SHIPS[self.ship_data['name'].lower()]['edID'] if config.get_str('L3_shipyard_provider') == 'Inara': self.ship_url = ship_data["shipInaraURL"] else: self.ship_url = "https://www.edsm.net/en/user/fleet/id/{}/cmdr/{}/ship/sId/{}/sType/{}".format( config.get_str('EDSM_id'), urllib.parse.quote_plus(self.cmdr), self.ship_data['id'], edID) # https://inara.cz/cmdr-fleet/ self.ship_link.configure(url=self.ship_url, text=self.ship_lbl_txt) #self.station_link.configure(url = self.station_url, text = self.stationname) def build_ui(self): self.sub_frame = tk.Frame(self) self.ship_link = HyperlinkLabel(self, text='', justify=tk.LEFT, anchor='w', url=None) self.system_link = SystemLinkLabel(self.sub_frame, text='', justify=tk.LEFT, anchor='w', url=None) self.station_link = StationLinkLabel(self.sub_frame, text='', justify=tk.LEFT, anchor='w', url=None) self.ship_link.grid(column=0, row=0, sticky='we') self.sub_frame.grid(sticky='we') self.sys_label = tk.Label(self.sub_frame, text='System: ', justify=tk.LEFT, anchor=tk.W, pady=0) self.sys_label.grid(column=0, row=1, sticky='we') self.system_link.grid(column=1, row=1, sticky='we') self.station_label = tk.Label(self.sub_frame, text='Station: ', justify=tk.LEFT, anchor=tk.W, pady=0) self.station_label.grid(column=0, row=2, sticky='we') self.station_link.grid(column=1, row=2, sticky='we') self.menu = tk.Menu(self, tearoff=0) self.menu.add_command(label="Copy system name", command=self.copySystem) # for i in [self.station_link, self.system_link]: # i.bind("<Button-3>", self.rightclick) def copySystem(self): setclipboard(self.sysname) def rightclick(self, event): self.menu.post(event.x_root, event.y_root)