1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-06-17 23:51:42 +03:00

Merge pull request #636 from EDCD/cleanup/EDMarketConnector.py

Continuing work on passing flake8 in EDMarketConnector.py
This commit is contained in:
Athanasius 2020-07-27 18:35:11 +01:00 committed by GitHub
commit cbb64a60a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -5,11 +5,9 @@ from builtins import str
from builtins import object from builtins import object
import sys import sys
from sys import platform from sys import platform
from collections import OrderedDict
from functools import partial
import json import json
from os import chdir, environ from os import chdir, environ
from os.path import dirname, expanduser, isdir, join from os.path import dirname, isdir, join
import re import re
import html import html
from time import time, localtime, strftime from time import time, localtime, strftime
@ -34,7 +32,6 @@ import tkinter.messagebox
from ttkHyperlinkLabel import HyperlinkLabel from ttkHyperlinkLabel import HyperlinkLabel
if __debug__: if __debug__:
from traceback import print_exc
if platform != 'win32': if platform != 'win32':
import pdb import pdb
import signal import signal
@ -55,7 +52,7 @@ from dashboard import dashboard
from theme import theme from theme import theme
SERVER_RETRY = 5 # retry pause for Companion servers [s] SERVER_RETRY = 5 # retry pause for Companion servers [s]
SHIPYARD_HTML_TEMPLATE = """ SHIPYARD_HTML_TEMPLATE = """
<!DOCTYPE HTML> <!DOCTYPE HTML>
@ -77,8 +74,8 @@ class AppWindow(object):
# Tkinter Event types # Tkinter Event types
EVENT_KEYPRESS = 2 EVENT_KEYPRESS = 2
EVENT_BUTTON = 4 EVENT_BUTTON = 4
EVENT_VIRTUAL = 35 EVENT_VIRTUAL = 35
def __init__(self, master): def __init__(self, master):
@ -97,10 +94,10 @@ class AppWindow(object):
if platform == 'win32': if platform == 'win32':
self.w.wm_iconbitmap(default='EDMarketConnector.ico') self.w.wm_iconbitmap(default='EDMarketConnector.ico')
else: else:
self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file = join(config.respath, 'EDMarketConnector.png'))) self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=join(config.respath, 'EDMarketConnector.png'))) # noqa: E501
self.theme_icon = tk.PhotoImage(data = 'R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') self.theme_icon = tk.PhotoImage(data='R0lGODlhFAAQAMZQAAoKCQoKCgsKCQwKCQsLCgwLCg4LCQ4LCg0MCg8MCRAMCRANChINCREOChIOChQPChgQChgRCxwTCyYVCSoXCS0YCTkdCTseCT0fCTsjDU0jB0EnDU8lB1ElB1MnCFIoCFMoCEkrDlkqCFwrCGEuCWIuCGQvCFs0D1w1D2wyCG0yCF82D182EHE0CHM0CHQ1CGQ5EHU2CHc3CHs4CH45CIA6CIE7CJdECIdLEolMEohQE5BQE41SFJBTE5lUE5pVE5RXFKNaFKVbFLVjFbZkFrxnFr9oFsNqFsVrF8RsFshtF89xF9NzGNh1GNl2GP+KG////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////yH5BAEKAH8ALAAAAAAUABAAAAeegAGCgiGDhoeIRDiIjIZGKzmNiAQBQxkRTU6am0tPCJSGShuSAUcLoIIbRYMFra4FAUgQAQCGJz6CDQ67vAFJJBi0hjBBD0w9PMnJOkAiJhaIKEI7HRoc19ceNAolwbWDLD8uAQnl5ga1I9CHEjEBAvDxAoMtFIYCBy+kFDKHAgM3ZtgYSLAGgwkp3pEyBOJCC2ELB31QATGioAoVAwEAOw==') # noqa: E501
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_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') # noqa: E501
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') 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') # noqa: E501
frame = tk.Frame(self.w, name=appname.lower()) frame = tk.Frame(self.w, name=appname.lower())
frame.grid(sticky=tk.NSEW) frame.grid(sticky=tk.NSEW)
@ -116,10 +113,10 @@ class AppWindow(object):
self.system_label.grid(row=3, 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.station_label.grid(row=4, column=0, sticky=tk.W)
self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name = 'cmdr') 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.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.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.station = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.station_url, name='station')
self.cmdr.grid(row=1, column=1, sticky=tk.EW) self.cmdr.grid(row=1, column=1, sticky=tk.EW)
self.ship.grid(row=2, column=1, sticky=tk.EW) self.ship.grid(row=2, column=1, sticky=tk.EW)
@ -129,39 +126,41 @@ class AppWindow(object):
for plugin in plug.PLUGINS: for plugin in plug.PLUGINS:
appitem = plugin.get_app(frame) appitem = plugin.get_app(frame)
if appitem: if appitem:
tk.Frame(frame, highlightthickness=1).grid(columnspan=2, sticky=tk.EW) # separator tk.Frame(frame, highlightthickness=1).grid(columnspan=2, sticky=tk.EW) # separator
if isinstance(appitem, tuple) and len(appitem)==2: if isinstance(appitem, tuple) and len(appitem) == 2:
row = frame.grid_size()[1] row = frame.grid_size()[1]
appitem[0].grid(row=row, column=0, sticky=tk.W) appitem[0].grid(row=row, column=0, sticky=tk.W)
appitem[1].grid(row=row, column=1, sticky=tk.EW) appitem[1].grid(row=row, column=1, sticky=tk.EW)
else: else:
appitem.grid(columnspan=2, sticky=tk.EW) 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 # Update button in main window
self.theme_button = tk.Label(frame, width = platform == 'darwin' and 32 or 28, state=tk.DISABLED) self.button = ttk.Button(frame, text=_('Update'), width=28, default=tk.ACTIVE, state=tk.DISABLED)
self.theme_button = tk.Label(frame, width=32 if platform == 'darwin' else 28, state=tk.DISABLED)
self.status = tk.Label(frame, name='status', anchor=tk.W) self.status = tk.Label(frame, name='status', anchor=tk.W)
row = frame.grid_size()[1] row = frame.grid_size()[1]
self.button.grid(row=row, columnspan=2, sticky=tk.NSEW) self.button.grid(row=row, columnspan=2, sticky=tk.NSEW)
self.theme_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}) theme.register_alternate((self.button, self.theme_button, self.theme_button), {'row': row, 'columnspan': 2, 'sticky': tk.NSEW}) # noqa: E501
self.status.grid(columnspan=2, sticky=tk.EW) self.status.grid(columnspan=2, sticky=tk.EW)
self.button.bind('<Button-1>', self.getandsend) self.button.bind('<Button-1>', self.getandsend)
theme.button_bind(self.theme_button, self.getandsend) theme.button_bind(self.theme_button, self.getandsend)
for child in frame.winfo_children(): for child in frame.winfo_children():
child.grid_configure(padx=5, pady=(platform!='win32' or isinstance(child, tk.Frame)) and 2 or 0) child.grid_configure(padx=5, pady=(platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0)
self.menubar = tk.Menu() self.menubar = tk.Menu()
if platform=='darwin': if platform == 'darwin':
# Can't handle (de)iconify if topmost is set, so suppress iconify button # 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 # 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') root.call('tk::unsupported::MacWindowStyle', 'style', root, 'document', 'closeBox resizable')
# https://www.tcl.tk/man/tcl/TkCmd/menu.htm # https://www.tcl.tk/man/tcl/TkCmd/menu.htm
self.system_menu = tk.Menu(self.menubar, name='apple') 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.w.call('tk::mac::standardAboutPanel'))
self.system_menu.add_command(command=lambda:self.updater.checkForUpdates()) self.system_menu.add_command(command=lambda: self.updater.checkForUpdates())
self.menubar.add_cascade(menu=self.system_menu) self.menubar.add_cascade(menu=self.system_menu)
self.file_menu = tk.Menu(self.menubar, name='file') self.file_menu = tk.Menu(self.menubar, name='file')
self.file_menu.add_command(command=self.save_raw) self.file_menu.add_command(command=self.save_raw)
@ -171,7 +170,7 @@ class AppWindow(object):
self.menubar.add_cascade(menu=self.edit_menu) self.menubar.add_cascade(menu=self.edit_menu)
self.w.bind('<Command-c>', self.copy) self.w.bind('<Command-c>', self.copy)
self.view_menu = tk.Menu(self.menubar, name='view') self.view_menu = tk.Menu(self.menubar, name='view')
self.view_menu.add_command(command=lambda:stats.StatsDialog(self)) self.view_menu.add_command(command=lambda: stats.StatsDialog(self))
self.menubar.add_cascade(menu=self.view_menu) self.menubar.add_cascade(menu=self.view_menu)
window_menu = tk.Menu(self.menubar, name='window') window_menu = tk.Menu(self.menubar, name='window')
self.menubar.add_cascade(menu=window_menu) self.menubar.add_cascade(menu=window_menu)
@ -183,17 +182,17 @@ class AppWindow(object):
self.w['menu'] = self.menubar self.w['menu'] = self.menubar
# https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm # https://www.tcl.tk/man/tcl/TkCmd/tk_mac.htm
self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0') self.w.call('set', 'tk::mac::useCompatibilityMetrics', '0')
self.w.createcommand('tkAboutDialog', lambda:self.w.call('tk::mac::standardAboutPanel')) self.w.createcommand('tkAboutDialog', lambda: self.w.call('tk::mac::standardAboutPanel'))
self.w.createcommand("::tk::mac::Quit", self.onexit) 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::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.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.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 self.w.resizable(tk.FALSE, tk.FALSE) # Can't be only resizable on one axis
else: else:
self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) 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=lambda: stats.StatsDialog(self))
self.file_menu.add_command(command=self.save_raw) 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_command(command=lambda: prefs.PreferencesDialog(self.w, self.postprefs))
self.file_menu.add_separator() self.file_menu.add_separator()
self.file_menu.add_command(command=self.onexit) self.file_menu.add_command(command=self.onexit)
self.menubar.add_cascade(menu=self.file_menu) self.menubar.add_cascade(menu=self.file_menu)
@ -213,11 +212,13 @@ class AppWindow(object):
self.always_ontop = tk.BooleanVar(value=config.getint('always_ontop')) 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 = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE)
self.system_menu.add_separator() 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.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.menubar.add_cascade(menu=self.system_menu)
self.w.bind('<Control-c>', self.copy) self.w.bind('<Control-c>', self.copy)
self.w.protocol("WM_DELETE_WINDOW", self.onexit) self.w.protocol("WM_DELETE_WINDOW", self.onexit)
theme.register(self.menubar) # menus and children aren't automatically registered theme.register(self.menubar) # menus and children aren't automatically registered
theme.register(self.file_menu) theme.register(self.file_menu)
theme.register(self.edit_menu) theme.register(self.edit_menu)
theme.register(self.help_menu) theme.register(self.help_menu)
@ -225,7 +226,9 @@ class AppWindow(object):
# Alternate title bar and menu for dark theme # Alternate title bar and menu for dark theme
self.theme_menubar = tk.Frame(frame) self.theme_menubar = tk.Frame(frame)
self.theme_menubar.columnconfigure(2, weight=1) 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 = 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) theme_titlebar.grid(columnspan=3, padx=2, sticky=tk.NSEW)
self.drag_offset = None self.drag_offset = None
theme_titlebar.bind('<Button-1>', self.drag_start) theme_titlebar.bind('<Button-1>', self.drag_start)
@ -239,37 +242,49 @@ class AppWindow(object):
theme.button_bind(theme_close, self.onexit, image=self.theme_close) 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 = tk.Label(self.theme_menubar, anchor=tk.W)
self.theme_file_menu.grid(row=1, column=0, padx=5, sticky=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())) 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 = tk.Label(self.theme_menubar, anchor=tk.W)
self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W) self.theme_edit_menu.grid(row=1, column=1, sticky=tk.W)
theme.button_bind(self.theme_edit_menu, lambda e: self.edit_menu.tk_popup(e.widget.winfo_rootx(), e.widget.winfo_rooty() + e.widget.winfo_height())) theme.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 = tk.Label(self.theme_menubar, anchor=tk.W)
self.theme_help_menu.grid(row=1, column=2, sticky=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())) 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) 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_minimize) # images aren't automatically registered
theme.register(self.theme_close) theme.register(self.theme_close)
self.blank_menubar = tk.Frame(frame) self.blank_menubar = tk.Frame(frame)
tk.Label(self.blank_menubar).grid() tk.Label(self.blank_menubar).grid()
tk.Label(self.blank_menubar).grid() tk.Label(self.blank_menubar).grid()
tk.Frame(self.blank_menubar, height=2).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}) 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) self.w.resizable(tk.TRUE, tk.FALSE)
# update geometry # update geometry
if config.get('geometry'): if config.get('geometry'):
match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry')) # noqa: W605
if match: if match:
if platform == 'darwin': if platform == 'darwin':
if int(match.group(2)) >= 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
if int(match.group(2)) >= 0:
self.w.geometry(config.get('geometry')) self.w.geometry(config.get('geometry'))
elif platform == 'win32': elif platform == 'win32':
# Check that the titlebar will be at least partly on screen # Check that the titlebar will be at least partly on screen
import ctypes import ctypes
from ctypes.wintypes import POINT from ctypes.wintypes import POINT
# https://msdn.microsoft.com/en-us/library/dd145064 # https://msdn.microsoft.com/en-us/library/dd145064
MONITOR_DEFAULTTONULL = 0 MONITOR_DEFAULTTONULL = 0 # noqa: N806
if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16), MONITOR_DEFAULTTONULL): if ctypes.windll.user32.MonitorFromPoint(POINT(int(match.group(1)) + 16, int(match.group(2)) + 16),
MONITOR_DEFAULTTONULL):
self.w.geometry(config.get('geometry')) self.w.geometry(config.get('geometry'))
else: else:
self.w.geometry(config.get('geometry')) self.w.geometry(config.get('geometry'))
@ -278,22 +293,22 @@ class AppWindow(object):
theme.register(frame) theme.register(frame)
theme.apply(self.w) theme.apply(self.w)
self.w.bind('<Map>', self.onmap) # Special handling for overrideredict self.w.bind('<Map>', self.onmap) # Special handling for overrideredict
self.w.bind('<Enter>', self.onenter) # Special handling for transparency self.w.bind('<Enter>', self.onenter) # Special handling for transparency
self.w.bind('<FocusIn>', self.onenter) # " self.w.bind('<FocusIn>', self.onenter) # Special handling for transparency
self.w.bind('<Leave>', self.onleave) # " self.w.bind('<Leave>', self.onleave) # Special handling for transparency
self.w.bind('<FocusOut>', self.onleave) # " self.w.bind('<FocusOut>', self.onleave) # Special handling for transparency
self.w.bind('<Return>', self.getandsend) self.w.bind('<Return>', self.getandsend)
self.w.bind('<KP_Enter>', self.getandsend) self.w.bind('<KP_Enter>', self.getandsend)
self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring self.w.bind_all('<<Invoke>>', self.getandsend) # Hotkey monitoring
self.w.bind_all('<<JournalEvent>>', self.journal_event) # Journal 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('<<DashboardEvent>>', self.dashboard_event) # Dashboard monitoring
self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar self.w.bind_all('<<PluginError>>', self.plugin_error) # Statusbar
self.w.bind_all('<<CompanionAuthEvent>>', self.auth) # cAPI auth self.w.bind_all('<<CompanionAuthEvent>>', self.auth) # cAPI auth
self.w.bind_all('<<Quit>>', self.onexit) # Updater self.w.bind_all('<<Quit>>', self.onexit) # Updater
# Start a protocol handler to handle cAPI registration. Requires main loop to be running. # Start a protocol handler to handle cAPI registration. Requires main loop to be running.
self.w.after_idle(lambda:protocolhandler.start(self.w)) self.w.after_idle(lambda: protocolhandler.start(self.w))
# Load updater after UI creation (for WinSparkle) # Load updater after UI creation (for WinSparkle)
import update import update
@ -305,7 +320,7 @@ class AppWindow(object):
self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps
try: try:
config.get_password('') # Prod SecureStorage on Linux to initialise config.get_password('') # Prod SecureStorage on Linux to initialise
except RuntimeError: except RuntimeError:
pass pass
@ -317,97 +332,97 @@ class AppWindow(object):
config.delete('password') config.delete('password')
config.delete('logdir') config.delete('logdir')
self.postprefs(False) # Companion login happens in callback from monitor self.postprefs(False) # Companion login happens in callback from monitor
# callback after the Preferences dialog is applied # callback after the Preferences dialog is applied
def postprefs(self, dologin=True): def postprefs(self, dologin=True):
self.prefsdialog = None self.prefsdialog = None
self.set_labels() # in case language has changed self.set_labels() # in case language has changed
# Reset links in case plugins changed them # Reset links in case plugins changed them
self.ship.configure(url = self.shipyard_url) self.ship.configure(url=self.shipyard_url)
self.system.configure(url = self.system_url) self.system.configure(url=self.system_url)
self.station.configure(url = self.station_url) self.station.configure(url=self.station_url)
# (Re-)install hotkey monitoring # (Re-)install hotkey monitoring
hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods')) hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods'))
# (Re-)install log monitoring # (Re-)install log monitoring
if not monitor.start(self.w): if not monitor.start(self.w):
self.status['text'] = 'Error: Check %s' % _('E:D journal file location') # Location of the new Journal file in E:D 2.2 self.status['text'] = f'Error: Check {_("E:D journal file location")}'
if dologin and monitor.cmdr: if dologin and monitor.cmdr:
self.login() # Login if not already logged in with this Cmdr self.login() # Login if not already logged in with this Cmdr
# set main window labels, e.g. after language change # set main window labels, e.g. after language change
def set_labels(self): def set_labels(self):
self.cmdr_label['text'] = _('Cmdr') + ':' # Main window self.cmdr_label['text'] = _('Cmdr') + ':' # Main window
self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or # Multicrew role label in main window # Multicrew role label in main window
_('Ship')) + ':' # Main window self.ship_label['text'] = (monitor.state['Captain'] and _('Role') or _('Ship')) + ':' # Main window
self.system_label['text'] = _('System') + ':' # Main window self.system_label['text'] = _('System') + ':' # Main window
self.station_label['text'] = _('Station') + ':' # Main window self.station_label['text'] = _('Station') + ':' # Main window
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
if platform == 'darwin': if platform == 'darwin':
self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(1, label=_('File')) # Menu title
self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title
self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX self.menubar.entryconfigure(3, label=_('View')) # Menu title on OSX
self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX self.menubar.entryconfigure(4, label=_('Window')) # Menu title on OSX
self.menubar.entryconfigure(5, label=_('Help')) # Menu title 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(0, label=_("About {APP}").format(APP=applongname)) # App menu entry on OSX
self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # Menu item self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # Menu item
self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # Menu item self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # Menu item
self.view_menu.entryconfigure(0, label=_('Status')) # 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(1, label=_('Privacy Policy')) # Help menu item
self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item self.help_menu.entryconfigure(2, label=_('Release Notes')) # Help menu item
else: else:
self.menubar.entryconfigure(1, label=_('File')) # Menu title self.menubar.entryconfigure(1, label=_('File')) # Menu title
self.menubar.entryconfigure(2, label=_('Edit')) # Menu title self.menubar.entryconfigure(2, label=_('Edit')) # Menu title
self.menubar.entryconfigure(3, label=_('Help')) # Menu title self.menubar.entryconfigure(3, label=_('Help')) # Menu title
self.theme_file_menu['text'] = _('File') # Menu title self.theme_file_menu['text'] = _('File') # Menu title
self.theme_edit_menu['text'] = _('Edit') # Menu title self.theme_edit_menu['text'] = _('Edit') # Menu title
self.theme_help_menu['text'] = _('Help') # Menu title self.theme_help_menu['text'] = _('Help') # Menu title
## File menu # File menu
self.file_menu.entryconfigure(0, label=_('Status')) # Menu item self.file_menu.entryconfigure(0, label=_('Status')) # Menu item
self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # 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(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.file_menu.entryconfigure(4, label=_('Exit')) # Item in the File menu on Windows
## Help menu # Help menu
self.help_menu.entryconfigure(0, label=_('Documentation')) # Help menu item 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(1, label=_('Privacy Policy')) # Help menu item
self.help_menu.entryconfigure(2, label=_('Release Notes')) # 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.help_menu.entryconfigure(3, label=_('Check for Updates...')) # Menu item
self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # App menu entry self.help_menu.entryconfigure(4, label=_("About {APP}").format(APP=applongname)) # App menu entry
## Edit menu # Edit menu
self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste
def login(self): def login(self):
if not self.status['text']: if not self.status['text']:
self.status['text'] = _('Logging in...') self.status['text'] = _('Logging in...')
self.button['state'] = self.theme_button['state'] = tk.DISABLED self.button['state'] = self.theme_button['state'] = tk.DISABLED
if platform == 'darwin': if platform == 'darwin':
self.view_menu.entryconfigure(0, state=tk.DISABLED) # Status self.view_menu.entryconfigure(0, state=tk.DISABLED) # Status
self.file_menu.entryconfigure(0, state=tk.DISABLED) # Save Raw Data self.file_menu.entryconfigure(0, state=tk.DISABLED) # Save Raw Data
else: else:
self.file_menu.entryconfigure(0, state=tk.DISABLED) # Status self.file_menu.entryconfigure(0, state=tk.DISABLED) # Status
self.file_menu.entryconfigure(1, state=tk.DISABLED) # Save Raw Data self.file_menu.entryconfigure(1, state=tk.DISABLED) # Save Raw Data
self.w.update_idletasks() self.w.update_idletasks()
try: try:
if companion.session.login(monitor.cmdr, monitor.is_beta): if companion.session.login(monitor.cmdr, monitor.is_beta):
self.status['text'] = _('Authentication successful') # Successfully authenticated with the Frontier website # Successfully authenticated with the Frontier website
self.status['text'] = _('Authentication successful')
if platform == 'darwin': if platform == 'darwin':
self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status self.view_menu.entryconfigure(0, state=tk.NORMAL) # Status
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data self.file_menu.entryconfigure(0, state=tk.NORMAL) # Save Raw Data
else: else:
self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status self.file_menu.entryconfigure(0, state=tk.NORMAL) # Status
self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data self.file_menu.entryconfigure(1, state=tk.NORMAL) # Save Raw Data
except (companion.CredentialsError, companion.ServerError, companion.ServerLagging) as e: except (companion.CredentialsError, companion.ServerError, companion.ServerLagging) as e:
self.status['text'] = str(e) self.status['text'] = str(e)
except Exception as e: except Exception as e:
logger.debug(f'Frontier CAPI Auth', exc_info=e) logger.debug('Frontier CAPI Auth', exc_info=e)
self.status['text'] = str(e) self.status['text'] = str(e)
self.cooldown() self.cooldown()
@ -418,7 +433,7 @@ class AppWindow(object):
play_bad = False play_bad = False
if not monitor.cmdr or not monitor.mode or monitor.state['Captain'] or not monitor.system: if not monitor.cmdr or not monitor.mode or monitor.state['Captain'] or not monitor.system:
return # In CQC or on crew - do nothing return # In CQC or on crew - do nothing
if companion.session.state == companion.Session.STATE_AUTH: if companion.session.state == companion.Session.STATE_AUTH:
# Attempt another Auth # Attempt another Auth
@ -426,10 +441,10 @@ class AppWindow(object):
return return
if not retrying: if not retrying:
if time() < self.holdofftime: # Was invoked by key while in cooldown if time() < self.holdofftime: # Was invoked by key while in cooldown
self.status['text'] = '' self.status['text'] = ''
if play_sound and (self.holdofftime-time()) < companion.holdoff*0.75: 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 hotkeymgr.play_bad() # Don't play sound in first few seconds to prevent repeats
return return
elif play_sound: elif play_sound:
hotkeymgr.play_good() hotkeymgr.play_good()
@ -444,31 +459,41 @@ class AppWindow(object):
# Validation # Validation
if not data.get('commander', {}).get('name'): if not data.get('commander', {}).get('name'):
self.status['text'] = _("Who are you?!") # Shouldn't happen self.status['text'] = _("Who are you?!") # Shouldn't happen
elif (not data.get('lastSystem', {}).get('name') or elif (not data.get('lastSystem', {}).get('name')
(data['commander'].get('docked') and not data.get('lastStarport', {}).get('name'))): # Only care if docked or (data['commander'].get('docked')
self.status['text'] = _("Where are you?!") # Shouldn't happen 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'): elif not data.get('ship', {}).get('name') or not data.get('ship', {}).get('modules'):
self.status['text'] = _("What are you flying?!") # Shouldn't happen self.status['text'] = _("What are you flying?!") # Shouldn't happen
elif monitor.cmdr and data['commander']['name'] != monitor.cmdr: elif monitor.cmdr and data['commander']['name'] != monitor.cmdr:
raise companion.CmdrError() # Companion API return doesn't match Journal # Companion API return doesn't match Journal
elif ((auto_update and not data['commander'].get('docked')) or raise companion.CmdrError()
(data['lastSystem']['name'] != monitor.system) or elif ((auto_update and not data['commander'].get('docked'))
((data['commander']['docked'] and data['lastStarport']['name'] or None) != monitor.station) or or (data['lastSystem']['name'] != monitor.system)
(data['ship']['id'] != monitor.state['ShipID']) or or ((data['commander']['docked']
(data['ship']['name'].lower() != monitor.state['ShipType'])): 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() raise companion.ServerLagging()
else: else:
if __debug__: # Recording if __debug__: # Recording
if isdir('dump'): 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())), 'wb') as h: with open('dump/{system}{station}.{timestamp}.json'.format(
h.write(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True, separators=(',', ': ')).encode('utf-8')) system=data['lastSystem']['name'],
station=data['commander'].get('docked') and '.'+data['lastStarport']['name'] or '',
timestamp=strftime('%Y-%m-%dT%H.%M.%S', localtime())), 'wb') 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 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']) self.ship['text'] = companion.ship_map.get(data['ship']['name'].lower(), data['ship']['name'])
monitor.state['ShipID'] = data['ship']['id'] monitor.state['ShipID'] = data['ship']['id']
monitor.state['ShipType'] = data['ship']['name'].lower() monitor.state['ShipType'] = data['ship']['name'].lower()
if data['commander'].get('credits') is not None: if data['commander'].get('credits') is not None:
@ -485,16 +510,19 @@ class AppWindow(object):
if config.getint('output') & (config.OUT_STATION_ANY): if config.getint('output') & (config.OUT_STATION_ANY):
if not data['commander'].get('docked'): if not data['commander'].get('docked'):
if not self.status['text']: 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 # 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!") self.status['text'] = _("You're not docked at a station!")
play_bad = True 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 # Ignore possibly missing shipyard info
elif (config.getint('output') & config.OUT_MKT_EDDN)\
and not (data['lastStarport'].get('commodities') or data['lastStarport'].get('modules')):
if not self.status['text']: if not self.status['text']:
self.status['text'] = _("Station doesn't have anything!") self.status['text'] = _("Station doesn't have anything!")
elif not data['lastStarport'].get('commodities'): elif not data['lastStarport'].get('commodities'):
if not self.status['text']: if not self.status['text']:
self.status['text'] = _("Station doesn't have a market!") self.status['text'] = _("Station doesn't have a market!")
elif config.getint('output') & (config.OUT_MKT_CSV|config.OUT_MKT_TD): elif config.getint('output') & (config.OUT_MKT_CSV | config.OUT_MKT_TD):
# Fixup anomalies in the commodity data # Fixup anomalies in the commodity data
fixed = companion.fixup(data) fixed = companion.fixup(data)
if config.getint('output') & config.OUT_MKT_CSV: if config.getint('output') & config.OUT_MKT_CSV:
@ -511,10 +539,10 @@ class AppWindow(object):
play_bad = True play_bad = True
else: else:
# Retry once if Companion server is unresponsive # Retry once if Companion server is unresponsive
self.w.after(int(SERVER_RETRY * 1000), lambda:self.getandsend(event, True)) self.w.after(int(SERVER_RETRY * 1000), lambda: self.getandsend(event, True))
return # early exit to avoid starting cooldown count return # early exit to avoid starting cooldown count
except companion.CmdrError as e: # Companion API return doesn't match Journal except companion.CmdrError as e: # Companion API return doesn't match Journal
self.status['text'] = str(e) self.status['text'] = str(e)
play_bad = True play_bad = True
companion.session.invalidate() companion.session.invalidate()
@ -552,8 +580,8 @@ class AppWindow(object):
data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')): data.get('lastStarport', {}).get('ships', {}).get('shipyard_list')):
self.eddn.export_shipyard(data, monitor.is_beta) self.eddn.export_shipyard(data, monitor.is_beta)
elif tries > 1: # bogus data - retry elif tries > 1: # bogus data - retry
self.w.after(int(SERVER_RETRY * 1000), lambda:self.retry_for_shipyard(tries-1)) self.w.after(int(SERVER_RETRY * 1000), lambda: self.retry_for_shipyard(tries-1))
except: except Exception:
pass pass
# Handle event(s) from the journal # Handle event(s) from the journal
@ -564,9 +592,9 @@ class AppWindow(object):
return { return {
None: '', None: '',
'Idle': '', 'Idle': '',
'FighterCon': _('Fighter'), # Multicrew role 'FighterCon': _('Fighter'), # Multicrew role
'FireCon': _('Gunner'), # Multicrew role 'FireCon': _('Gunner'), # Multicrew role
'FlightCon': _('Helm'), # Multicrew role 'FlightCon': _('Helm'), # Multicrew role
}.get(role, role) }.get(role, role)
while True: while True:
@ -577,26 +605,43 @@ class AppWindow(object):
# Update main window # Update main window
self.cooldown() self.cooldown()
if monitor.cmdr and monitor.state['Captain']: if monitor.cmdr and monitor.state['Captain']:
self.cmdr['text'] = '%s / %s' % (monitor.cmdr, monitor.state['Captain']) self.cmdr['text'] = f'{monitor.cmdr} / {monitor.state["Captain"]}'
self.ship_label['text'] = _('Role') + ':' # Multicrew role label in main window self.ship_label['text'] = _('Role') + ':' # Multicrew role label in main window
self.ship.configure(state = tk.NORMAL, text = crewroletext(monitor.state['Role']), url = None) self.ship.configure(state=tk.NORMAL, text=crewroletext(monitor.state['Role']), url=None)
elif monitor.cmdr: elif monitor.cmdr:
if monitor.group: if monitor.group:
self.cmdr['text'] = '%s / %s' % (monitor.cmdr, monitor.group) self.cmdr['text'] = f'{monitor.cmdr} / {monitor.group}'
else: else:
self.cmdr['text'] = monitor.cmdr self.cmdr['text'] = monitor.cmdr
self.ship_label['text'] = _('Ship') + ':' # Main window 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 '', self.ship.configure(
url = self.shipyard_url) text=monitor.state['ShipName']
or companion.ship_map.get(monitor.state['ShipType'], monitor.state['ShipType'])
or '',
url=self.shipyard_url)
else: else:
self.cmdr['text'] = '' self.cmdr['text'] = ''
self.ship_label['text'] = _('Ship') + ':' # Main window self.ship_label['text'] = _('Ship') + ':' # Main window
self.ship['text'] = '' self.ship['text'] = ''
self.edit_menu.entryconfigure(0, state=monitor.system and tk.NORMAL or tk.DISABLED) # Copy 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']: if entry['event'] in (
self.status['text'] = '' # Periodically clear any old error '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() self.w.update_idletasks()
# Companion login # Companion login
@ -606,7 +651,7 @@ class AppWindow(object):
self.login() self.login()
if not entry['event'] or not monitor.mode: if not entry['event'] or not monitor.mode:
return # Startup or in CQC return # Startup or in CQC
if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started:
# Disable WinSparkle automatic update checks, IFF configured to do so when in-game # Disable WinSparkle automatic update checks, IFF configured to do so when in-game
@ -618,8 +663,7 @@ class AppWindow(object):
logger.info("Can't start Status monitoring") logger.info("Can't start Status monitoring")
# Export loadout # Export loadout
if entry['event'] == 'Loadout'\ if entry['event'] == 'Loadout' and not monitor.state['Captain']\
and not monitor.state['Captain']\
and config.getint('output') & config.OUT_SHIP: and config.getint('output') & config.OUT_SHIP:
monitor.export_ship() monitor.export_ship()
@ -636,7 +680,11 @@ class AppWindow(object):
hotkeymgr.play_bad() hotkeymgr.play_bad()
# Auto-Update after docking, but not if auth callback is pending # 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: 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) self.w.after(int(SERVER_RETRY * 1000), self.getandsend)
if entry['event'] == 'ShutDown': if entry['event'] == 'ShutDown':
@ -709,10 +757,12 @@ class AppWindow(object):
def cooldown(self): def cooldown(self):
if time() < self.holdofftime: 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 # Update button in main window
self.button['text'] = self.theme_button['text'] \
= _('cooldown {SS}s').format(SS=int(self.holdofftime - time()))
self.w.after(1000, self.cooldown) self.w.after(1000, self.cooldown)
else: else:
self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window self.button['text'] = self.theme_button['text'] = _('Update') # Update button in main window
self.button['state'] = self.theme_button['state'] = (monitor.cmdr and self.button['state'] = self.theme_button['state'] = (monitor.cmdr and
monitor.mode and monitor.mode and
not monitor.state['Captain'] and not monitor.state['Captain'] and
@ -726,7 +776,7 @@ class AppWindow(object):
def copy(self, event=None): def copy(self, event=None):
if monitor.system: if monitor.system:
self.w.clipboard_clear() self.w.clipboard_clear()
self.w.clipboard_append(monitor.station and '%s,%s' % (monitor.system, monitor.station) or monitor.system) self.w.clipboard_append(monitor.station and f'{monitor.system},{monitor.station}' or monitor.system)
def help_general(self, event=None): def help_general(self, event=None):
webbrowser.open('https://github.com/EDCD/EDMarketConnector/wiki') webbrowser.open('https://github.com/EDCD/EDMarketConnector/wiki')
@ -754,11 +804,12 @@ class AppWindow(object):
self.transient(parent) self.transient(parent)
# position over parent # position over parent
if platform!='darwin' or parent.winfo_rooty()>0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
self.geometry("+%d+%d" % (parent.winfo_rootx(), parent.winfo_rooty())) if platform != 'darwin' or parent.winfo_rooty() > 0:
self.geometry(f'+{parent.winfo_rootx():d}+{parent.winfo_rooty():d}')
# remove decoration # remove decoration
if platform=='win32': if platform == 'win32':
self.attributes('-toolwindow', tk.TRUE) self.attributes('-toolwindow', tk.TRUE)
self.resizable(tk.FALSE, tk.FALSE) self.resizable(tk.FALSE, tk.FALSE)
@ -766,10 +817,6 @@ class AppWindow(object):
frame = ttk.Frame(self) frame = ttk.Frame(self)
frame.grid(sticky=tk.NSEW) frame.grid(sticky=tk.NSEW)
PADX = 10
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
PADY = 2 # close spacing
row = 1 row = 1
############################################################ ############################################################
# applongname # applongname
@ -785,8 +832,8 @@ class AppWindow(object):
self.appversion_label = tk.Label(frame, text=appversion) self.appversion_label = tk.Label(frame, text=appversion)
self.appversion_label.grid(row=row, column=0, sticky=tk.E) self.appversion_label.grid(row=row, column=0, sticky=tk.E)
self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=_('Release Notes'), self.appversion = HyperlinkLabel(frame, compound=tk.RIGHT, text=_('Release Notes'),
url='https://github.com/EDCD/EDMarketConnector/releases/tag/Release/{VERSION}'.format( url='https://github.com/EDCD/EDMarketConnector/releases/tag/Release/'
VERSION=appversion_nobuild), f'{appversion_nobuild}',
underline=True) underline=True)
self.appversion.grid(row=row, column=2, sticky=tk.W) self.appversion.grid(row=row, column=2, sticky=tk.W)
row += 1 row += 1
@ -862,7 +909,7 @@ class AppWindow(object):
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7 # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
if platform != 'darwin' or self.w.winfo_rooty() > 0: if platform != 'darwin' or self.w.winfo_rooty() > 0:
config.set('geometry', '+{1}+{2}'.format(*self.w.geometry().split('+'))) 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 self.w.withdraw() # Following items can take a few seconds, so hide the main window while they happen
protocolhandler.close() protocolhandler.close()
hotkeymgr.unregister() hotkeymgr.unregister()
dashboard.close() dashboard.close()
@ -878,7 +925,9 @@ class AppWindow(object):
def drag_continue(self, event): def drag_continue(self, event):
if self.drag_offset: if self.drag_offset:
self.w.geometry('+%d+%d' % (event.x_root - self.drag_offset[0], event.y_root - self.drag_offset[1])) offset_x = event.x_root - self.drag_offset[0]
offset_y = event.y_root - self.drag_offset[1]
self.w.geometry(f'+{offset_x:d}+{offset_y:d}')
def drag_end(self, event): def drag_end(self, event):
self.drag_offset = None self.drag_offset = None
@ -888,7 +937,7 @@ class AppWindow(object):
self.w.iconify() self.w.iconify()
self.w.update_idletasks() # Size and windows styles get recalculated here self.w.update_idletasks() # Size and windows styles get recalculated here
self.w.wait_visibility() # Need main window to be re-created before returning self.w.wait_visibility() # Need main window to be re-created before returning
theme.active = None # So theme will be re-applied on map theme.active = None # So theme will be re-applied on map
def onmap(self, event=None): def onmap(self, event=None):
if event.widget == self.w: if event.widget == self.w:
@ -912,28 +961,28 @@ def enforce_single_instance() -> None:
if platform == 'win32': if platform == 'win32':
import ctypes import ctypes
from ctypes.wintypes import HWND, LPWSTR, LPCWSTR, INT, BOOL, LPARAM from ctypes.wintypes import HWND, LPWSTR, LPCWSTR, INT, BOOL, LPARAM
EnumWindows = ctypes.windll.user32.EnumWindows EnumWindows = ctypes.windll.user32.EnumWindows # noqa: N806
GetClassName = ctypes.windll.user32.GetClassNameW GetClassName = ctypes.windll.user32.GetClassNameW # noqa: N806
GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int] # noqa: N806
GetWindowText = ctypes.windll.user32.GetWindowTextW GetWindowText = ctypes.windll.user32.GetWindowTextW # noqa: N806
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int] # noqa: N806
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW # noqa: N806
GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd # noqa: N806
SW_RESTORE = 9 SW_RESTORE = 9 # noqa: N806
SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow # noqa: N806
ShowWindow = ctypes.windll.user32.ShowWindow ShowWindow = ctypes.windll.user32.ShowWindow # noqa: N806
ShowWindowAsync = ctypes.windll.user32.ShowWindowAsync ShowWindowAsync = ctypes.windll.user32.ShowWindowAsync # noqa: N806
COINIT_MULTITHREADED = 0 COINIT_MULTITHREADED = 0 # noqa: N806,F841
COINIT_APARTMENTTHREADED = 0x2 COINIT_APARTMENTTHREADED = 0x2 # noqa: N806
COINIT_DISABLE_OLE1DDE = 0x4 COINIT_DISABLE_OLE1DDE = 0x4 # noqa: N806
CoInitializeEx = ctypes.windll.ole32.CoInitializeEx CoInitializeEx = ctypes.windll.ole32.CoInitializeEx # noqa: N806
ShellExecute = ctypes.windll.shell32.ShellExecuteW ShellExecute = ctypes.windll.shell32.ShellExecuteW # noqa: N806
ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT] ShellExecute.argtypes = [HWND, LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, INT]
def WindowTitle(h): def window_title(h):
if h: if h:
text_length = GetWindowTextLength(h) + 1 text_length = GetWindowTextLength(h) + 1
buf = ctypes.create_unicode_buffer(text_length) buf = ctypes.create_unicode_buffer(text_length)
@ -942,22 +991,23 @@ def enforce_single_instance() -> None:
return None return None
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM) @ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
def enumwindowsproc(hWnd, lParam): def enumwindowsproc(window_handle, l_param):
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576 # class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
cls = ctypes.create_unicode_buffer(257) cls = ctypes.create_unicode_buffer(257)
if GetClassName(hWnd, cls, 257)\ if GetClassName(window_handle, cls, 257)\
and cls.value == 'TkTopLevel'\ and cls.value == 'TkTopLevel'\
and WindowTitle(hWnd) == applongname\ and window_title(window_handle) == applongname\
and GetProcessHandleFromHwnd(hWnd): and GetProcessHandleFromHwnd(window_handle):
# If GetProcessHandleFromHwnd succeeds then the app is already running as this user # If GetProcessHandleFromHwnd succeeds then the app is already running as this user
if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler.redirect): if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler.redirect):
# Browser invoked us directly with auth response. Forward the response to the other app instance. # Browser invoked us directly with auth response. Forward the response to the other app instance.
CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)
ShowWindow(hWnd, SW_RESTORE) # Wait for it to be responsive to avoid ShellExecute recursing # Wait for it to be responsive to avoid ShellExecute recursing
ShowWindow(window_handle, SW_RESTORE)
ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE) ShellExecute(0, None, sys.argv[1], None, None, SW_RESTORE)
else: else:
ShowWindowAsync(hWnd, SW_RESTORE) ShowWindowAsync(window_handle, SW_RESTORE)
SetForegroundWindow(hWnd) SetForegroundWindow(window_handle)
sys.exit(0) sys.exit(0)
return True return True
@ -985,9 +1035,9 @@ if __name__ == "__main__":
def __init__(self): def __init__(self):
logger.debug('A call from A.B.__init__') logger.debug('A call from A.B.__init__')
#abinit = A.B() # abinit = A.B()
Translations.install(config.get('language') or None) # Can generate errors so wait til log set up Translations.install(config.get('language') or None) # Can generate errors so wait til log set up
root = tk.Tk(className=appname.lower()) root = tk.Tk(className=appname.lower())
app = AppWindow(root) app = AppWindow(root)
@ -995,9 +1045,25 @@ if __name__ == "__main__":
def messagebox_not_py3(): def messagebox_not_py3():
plugins_not_py3_last = config.getint('plugins_not_py3_last') or 0 plugins_not_py3_last = config.getint('plugins_not_py3_last') or 0
if (plugins_not_py3_last + 86400) < int(time()) and len(plug.PLUGINS_not_py3): if (plugins_not_py3_last + 86400) < int(time()) and len(plug.PLUGINS_not_py3):
# Yes, this is horribly hacky so as to be sure we match the key
# that we told Translators to use.
popup_text = "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 need to update the code for "\
"Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on "\
"the end of the name."
popup_text = popup_text.replace('\n', '\\n')
popup_text = popup_text.replace('\r', '\\r')
# Now the string should match, so try translation
popup_text = _(popup_text)
# And substitute in the other words.
popup_text.format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled')
# And now we do need these to be actual \r\n
popup_text = popup_text.replace('\\n', '\n')
popup_text = popup_text.replace('\\r', '\r')
tk.messagebox.showinfo( tk.messagebox.showinfo(
_('EDMC: Plugins Without Python 3.x Support'), _('EDMC: 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 need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name.".format(PLUGINS=_('Plugins'), FILE=_('File'), SETTINGS=_('Settings'), DISABLED='.disabled')) popup_text
) )
config.set('plugins_not_py3_last', int(time())) config.set('plugins_not_py3_last', int(time()))