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)
예제 #2
0
class FSDTarget(WaferModule):
    
    def __init__(self, parent, *args, **options):
        WaferModule.__init__(self, parent, *args, **options)
        #test_stuff()
        self.system_url = None
        self.system_string = ''
        self.system_frm = tk.Frame(self)
        self.system_frm.pack(fill = tk.Y, anchor = 'w')
        self.system_lbl = tk.Label(self.system_frm, text = 'Target system: ')
        self.system_lbl.pack(side = 'left')
        self.system_hyperlink = HyperlinkLabel(self.system_frm, compound=tk.RIGHT, url = self.system_url, name = 'system', text = '')
        self.system_hyperlink.pack(side = 'left')
        self.system_hyperlink.bind("<Button-3>", self.rightclick)
        this.frame = tk.Frame(self)
        this.frame.bind('<<FSDData>>', edsm_handler)
        this.frame.pack(fill=tk.Y, anchor = 'w')
        this.edsm_button = tk.Button(this.frame, text = 'Request data', command = self.get_data)
#        this.edsm_button.pack()
        this.loading = tk.Label(this.frame, text = "Contacting EDSM...")
        self.menu = tk.Menu(self, tearoff=0)
        self.menu.add_command(label="Copy", command = self.copy_system)
        this.system_string = self.system_string
        # more test stuff
        #self.system_hyperlink.config(text = 'Shinrarta Dezhra')
        #self.system_string = 'Shinrarta Dezhra'
        #self.update_link()
        
    def get_data(self):
        this.edsm_button.forget()
        get_edsm_data(self.system_string)

    def journal_entry(self, cmdr, system, station, entry, state):
        if entry['event'] == 'FSDTarget':
            if entry['Name'] != self.system_string:
                try:
                    this.edsm_data_frame.destroy()
                except:
                    pass
                self.system_string = entry['Name']
                this.system_string = self.system_string
                self.system_hyperlink.config(text = self.system_string)
                self.update_link()
                if self.system_string not in this.edsm_cache:
                    try:
                        this.edsm_button.pack()
                    except:
                        pass
                else:
                    try:
                        this.edsm_button.forget()
                    except:
                        pass
                    display_results(self.system_string)
                    

    def update_link(self):
        self.system_url = get_system_url(self.system_string)
        self.system_hyperlink.configure(url = self.system_url)
        
    def rightclick(self, event):
        self.menu.post(event.x_root, event.y_root)
        
    def copy_system(self):
        setclipboard(self.system_string)
예제 #3
0
class FleetInfo():
    def __init__(self, plugin_dir, logger_in):
        global logger
        logger = logger_in
        self.parent = None
        self.is_beta: bool = False
        self.plugin_dir = plugin_dir
        self.filename: str = "edmc-fleet-info-data.json"
        self.label: Optional[HyperlinkLabel] = None
        self.shiplist: Optional[tk.OptionMenu] = None
        self.selectedship: Optional[tk.StringVar] = None
        self.tracevar = None
        self.shipdata_default = {"0":{"Name":"Loading...","loadout":None}}
        self.shipdata: Optional[Dict[str, Dict[str, Any]]] = self.shipdata_default
        self.shipnames: List = ["0: Loading..."]
        self.lastship: str = self.shipnames[0]

    # Build the UI and hook the selection event
    def setup_gui(self, parent) -> Tuple[HyperlinkLabel,tk.OptionMenu]:
        self.parent = parent
        self.selectedship = tk.StringVar(parent)
        self.label = HyperlinkLabel(parent, url="http://coriolis.io", name='shipyardlink', text=f'{_("Shipyard")}:')
        self.shiplist = tk.OptionMenu(parent, self.selectedship, self.shipnames[0], *self.shipnames)
        self.tracevar = self.selectedship.trace_add("write",self.updateshipurl)
        self.loadshipdata()
        return (self.label,self.shiplist)

    # Update the hyperlink underneath the "Shipyard:" label any time the selection changes
    def updateshipurl(self, name=None, *args) -> None:
        self.lastship = self.selectedship.get()
        shipid = ""
        match = re.match(r'^\s*(\d+):',self.selectedship.get())
        if match:
            shipid = match.group(1)
        if shipid and self.shipdata[shipid] and self.shipdata[shipid]["loadout"]:
            url = plug.invoke(config.get('shipyard_provider'), 'EDSY', 'shipyard_url', self.shipdata[shipid]["loadout"], self.is_beta)
            self.label.configure(url=url)
        else:
            logger.debug("No loadout for {ship}".format(ship=self.lastship))
            self.label.configure(url="http://coriolis.io")

    # Load in fleet data from file
    def loadshipdata(self) -> None:
        self.shipdata = self.shipdata_default
        filename = self.filename
        if self.is_beta:
            filename = "beta-"+filename
        filepath = os.path.join(self.plugin_dir, filename)
        try:
            logger.debug(f'Loading ship data from {filepath!r}')
            with open(filepath, mode='r', encoding='utf-8') as shipsfile:
                self.shipdata = json.load(shipsfile)
                close(shipsfile)
        except Exception as e:
            pass
        if not isinstance(self.shipdata,dict):
            self.shipdata = self.shipdata_default
        self.updateships()

    # Write fleet data out to file, done every time it changes
    def saveshipdata(self) -> None:
        filename = self.filename
        if self.is_beta:
            filename = "beta-"+filename
        filepath = os.path.join(self.plugin_dir, filename)
        try:
            logger.trace(f'Saving ship data to {filepath!r}')
            with open(filepath, mode='w', encoding='utf-8') as shipsfile:
                json.dump(self.shipdata, shipsfile, indent=2)
                close(shipsfile)
        except Exception as e:
            pass

    # Update the loadout for the current ship from a Loudout journal entry
    def updateloadout(self, entry: Dict[str, Any], state: Dict[str,Any]) -> None:
        logger.trace("Updating loadout")
        shipid = str(entry["ShipID"])
        n: str = ""
        if shipid in self.shipdata.keys():
            n = self.shipdata[shipid]["Name"]
        if "ShipName" in state.keys() and state["ShipName"]:
            n = state["ShipName"]
        elif "ShipName" in state.keys() and not n:
            n = ship_map.get(state["ShipType"], state["ShipType"])
        self.shipdata[shipid] = {"Name": n, "loadout": entry}
        # Cleanup the placeholder if it's still there, and we've just added a new ship.
        if shipid != "0" and "0" in self.shipdata.keys() and self.shipdata["0"]["Name"] == self.shipdata_default["0"]["Name"]:
            del self.shipdata["0"]
        self.updateships()
        self.saveshipdata()

    # Update the list of all ships, local and remote, preserving loadouts for
    # any that have them. Clears out ships that aren't found in the StoredShips
    # journal entry/current ship state data
    def updateshipyard(self, entry: Dict[str, Any], state: Dict[str,Any]) -> None:
        logger.trace("Updating Shipyard")
        keep: List[Optional[int]] = []

        # Update the current ship
        shipid: str = str(state["ShipID"])
        loadout: Optional[Dict[str, Any]] = None
        n: str = ""
        if shipid in self.shipdata.keys():
            loadout = self.shipdata[shipid]["loadout"]
            n = self.shipdata[shipid]["Name"]
        if "ShipName" in state.keys() and state["ShipName"]:
            n = state["ShipName"]
        elif "ShipType" in state.keys() and (not n or n == state["ShipType"]):
            n = ship_map.get(state["ShipType"].lower(), state["ShipType"])
        elif not n:
            n = "Unknown"
            logger.debug("Unknown Ship Name/Type")
            logger.debug(s)
        self.shipdata[shipid] = {"Name":n,"loadout":loadout}
        keep.append(shipid)

        # Update all local and remote ships from the journal entry
        for s in (entry["ShipsHere"] + entry["ShipsRemote"]):
            loadout = None
            shipid = str(s["ShipID"])
            n = ""
            if shipid in self.shipdata.keys():
                loadout = self.shipdata[shipid]["loadout"]
                n = self.shipdata[shipid]["Name"]
            if "Name" in s.keys() and s["Name"]:
                n = s["Name"]
            elif "ShipType" in s.keys() and (not n or n == s["ShipType"]):
                n = ship_map.get(s["ShipType"].lower(), s["ShipType"])
            elif not n:
                n = "Unknown"
                logger.debug("Unknown Ship Name/Type")
                logger.debug(s)
            self.shipdata[shipid] = {"Name":n,"loadout":loadout}
            keep.append(shipid)

        # Remove any extra entries that the shipyard + state didn't contain
        self.shipdata = dict([(k,v) for k,v in self.shipdata.items() if k in set(keep)])
        self.updateships()
        self.saveshipdata()

    # Update the "id: Name" list used for the menu
    def updateships(self) -> None:
        logger.trace("Updating ship menu")
        self.shipnames.clear()
        self.shiplist['menu'].delete(0,'end')
        width = max(len(k) for k in self.shipdata.keys())
        for k in self.shipdata.keys():
            s: str = "{k: >{width}}: {n}".format(k=k,n=self.shipdata[k]["Name"],width=width)
            self.shipnames.append(s)
        self.shipnames.sort()
        for s in self.shipnames:
            self.shiplist['menu'].add_command(label=s, command=tk._setit(self.selectedship, s))
        if self.lastship in self.shipnames:
            self.selectedship.set(self.lastship)
        else:
            self.selectedship.set(self.shipnames[0])
        self.lastship = " "
        self.updateshipurl()
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)