Пример #1
0
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
Пример #2
0
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()
Пример #5
0
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)
Пример #8
0
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")
Пример #9
0
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")
Пример #10
0
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)
Пример #11
0
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)
Пример #14
0
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")
Пример #15
0
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)