1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-13 15:57:14 +03:00

Merge pull request #2187 from HullSeals/enhancement/2186/remove-darwin

[2186] Remove MacOS Support
This commit is contained in:
David Sangrey 2024-04-06 16:53:48 -04:00 committed by GitHub
commit 065e2ae7de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 269 additions and 1366 deletions

View File

@ -7,7 +7,6 @@ exclude =
FDevIDs/
venv/
.venv/
hotkey/darwin.py # FIXME: Check under macOS VM at some point
# Show exactly where in a line the error happened
#show-source = True

View File

@ -6,5 +6,4 @@ scripts_are_modules = True
; `<var> = <value>`
; i.e. no typing info.
check_untyped_defs = True
; platform = darwin
explicit_package_bases = True

View File

@ -679,7 +679,7 @@ the following does not work:
```py
from sys import platform
if platform == 'darwin':
if platform == 'win32':
...
```

View File

@ -496,21 +496,20 @@ class AppWindow:
plug.load_plugins(master)
if sys.platform != 'darwin':
if sys.platform == 'win32':
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
if sys.platform == 'win32':
self.w.wm_iconbitmap(default='EDMarketConnector.ico')
else:
self.w.tk.call('wm', 'iconphoto', self.w, '-default',
tk.PhotoImage(file=path.join(config.respath_path, 'io.edcd.EDMarketConnector.png')))
else:
self.w.tk.call('wm', 'iconphoto', self.w, '-default',
tk.PhotoImage(file=path.join(config.respath_path, 'io.edcd.EDMarketConnector.png')))
# TODO: Export to files and merge from them in future ?
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') # 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') # noqa: E501
# TODO: Export to files and merge from them in future ?
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') # 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') # noqa: E501
frame = tk.Frame(self.w, name=appname.lower())
frame.grid(sticky=tk.NSEW)
@ -599,7 +598,7 @@ class AppWindow:
self.theme_button = tk.Label(
frame,
name='themed_update_button',
width=32 if sys.platform == 'darwin' else 28,
width=28,
state=tk.DISABLED
)
@ -633,148 +632,104 @@ class AppWindow:
self.updater = update.Updater(tkroot=self.w)
self.updater.check_for_updates() # Sparkle / WinSparkle does this automatically for packaged apps
if sys.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')
self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE)
self.file_menu.add_command(command=lambda: stats.StatsDialog(self.w, self.status))
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) # type: ignore
self.help_menu.add_command(command=self.help_general) # Documentation
self.help_menu.add_command(command=self.help_troubleshooting) # Troubleshooting
self.help_menu.add_command(command=self.help_report_a_bug) # Report A Bug
self.help_menu.add_command(command=self.help_privacy) # Privacy Policy
self.help_menu.add_command(command=self.help_releases) # Release Notes
self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) # Check for Updates...
# About E:D Market Connector
self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w))
self.help_menu.add_command(command=prefs.help_open_log_folder) # Open Log Folder
# 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.check_for_updates())
self.menubar.add_cascade(menu=self.help_menu)
if sys.platform == 'win32':
# Must be added after at least one "real" menu entry
self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop')))
self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE)
self.system_menu.add_separator()
# LANG: Appearance - Label for checkbox to select if application always on top
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.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.w, self.status))
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_troubleshooting)
self.help_menu.add_command(command=self.help_report_a_bug)
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.w, self.status))
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) # type: ignore
self.help_menu.add_command(command=self.help_general) # Documentation
self.help_menu.add_command(command=self.help_troubleshooting) # Troubleshooting
self.help_menu.add_command(command=self.help_report_a_bug) # Report A Bug
self.help_menu.add_command(command=self.help_privacy) # Privacy Policy
self.help_menu.add_command(command=self.help_releases) # Release Notes
self.help_menu.add_command(command=lambda: self.updater.check_for_updates()) # Check for Updates...
# About E:D Market Connector
self.help_menu.add_command(command=lambda: not self.HelpAbout.showing and self.HelpAbout(self.w))
self.help_menu.add_command(command=prefs.help_open_log_folder) # Open Log Folder
self.w.bind('<Control-c>', self.copy)
self.menubar.add_cascade(menu=self.help_menu)
if sys.platform == 'win32':
# Must be added after at least one "real" menu entry
self.always_ontop = tk.BooleanVar(value=bool(config.get_int('always_ontop')))
self.system_menu = tk.Menu(self.menubar, name='system', tearoff=tk.FALSE)
self.system_menu.add_separator()
# LANG: Appearance - Label for checkbox to select if application always on top
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)
# Bind to the Default theme minimise button
self.w.bind("<Unmap>", self.default_iconify)
# Bind to the Default theme minimise button
self.w.bind("<Unmap>", self.default_iconify)
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)
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, name="alternate_menubar")
self.theme_menubar.columnconfigure(2, weight=1)
theme_titlebar = tk.Label(
self.theme_menubar,
name="alternate_titlebar",
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: tuple[int | None, int | None] = (None, 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=self.PADX, 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=self.PADX, sticky=tk.EW)
theme.register(self.theme_minimize) # images aren't automatically registered
theme.register(self.theme_close)
self.blank_menubar = tk.Frame(frame, name="blank_menubar")
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)
# Alternate title bar and menu for dark theme
self.theme_menubar = tk.Frame(frame, name="alternate_menubar")
self.theme_menubar.columnconfigure(2, weight=1)
theme_titlebar = tk.Label(
self.theme_menubar,
name="alternate_titlebar",
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: tuple[int | None, int | None] = (None, 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=self.PADX, 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=self.PADX, sticky=tk.EW)
theme.register(self.theme_minimize) # images aren't automatically registered
theme.register(self.theme_close)
self.blank_menubar = tk.Frame(frame, name="blank_menubar")
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_str('geometry'):
match = re.match(r'\+([\-\d]+)\+([\-\d]+)', config.get_str('geometry'))
if match:
if sys.platform == 'darwin':
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
if int(match.group(2)) >= 0:
self.w.geometry(config.get_str('geometry'))
elif sys.platform == 'win32':
if sys.platform == 'win32':
# Check that the titlebar will be at least partly on screen
import ctypes
from ctypes.wintypes import POINT
@ -910,49 +865,28 @@ class AppWindow:
self.system_label['text'] = _('System') + ':' # LANG: Label for 'System' line in main UI
self.station_label['text'] = _('Station') + ':' # LANG: Label for 'Station' line in main UI
self.button['text'] = self.theme_button['text'] = _('Update') # LANG: Update button in main window
if sys.platform == 'darwin':
self.menubar.entryconfigure(1, label=_('File')) # LANG: 'File' menu title on OSX
self.menubar.entryconfigure(2, label=_('Edit')) # LANG: 'Edit' menu title on OSX
self.menubar.entryconfigure(3, label=_('View')) # LANG: 'View' menu title on OSX
self.menubar.entryconfigure(4, label=_('Window')) # LANG: 'Window' menu title on OSX
self.menubar.entryconfigure(5, label=_('Help')) # LANG: Help' menu title on OSX
self.system_menu.entryconfigure(
0,
label=_("About {APP}").format(APP=applongname) # LANG: App menu entry on OSX
)
self.system_menu.entryconfigure(1, label=_("Check for Updates...")) # LANG: Help > Check for Updates...
self.file_menu.entryconfigure(0, label=_('Save Raw Data...')) # LANG: File > Save Raw Data...
self.view_menu.entryconfigure(0, label=_('Status')) # LANG: File > Status
self.help_menu.entryconfigure(1, label=_('Documentation')) # LANG: Help > Documentation
self.help_menu.entryconfigure(2, label=_('Troubleshooting')) # LANG: Help > Troubleshooting
self.help_menu.entryconfigure(3, label=_('Report A Bug')) # LANG: Help > Report A Bug
self.help_menu.entryconfigure(4, label=_('Privacy Policy')) # LANG: Help > Privacy Policy
self.help_menu.entryconfigure(5, label=_('Release Notes')) # LANG: Help > Release Notes
self.help_menu.entryconfigure(6, label=_('Open Log Folder')) # LANG: Help > Open Log Folder
self.menubar.entryconfigure(1, label=_('File')) # LANG: 'File' menu title
self.menubar.entryconfigure(2, label=_('Edit')) # LANG: 'Edit' menu title
self.menubar.entryconfigure(3, label=_('Help')) # LANG: 'Help' menu title
self.theme_file_menu['text'] = _('File') # LANG: 'File' menu title
self.theme_edit_menu['text'] = _('Edit') # LANG: 'Edit' menu title
self.theme_help_menu['text'] = _('Help') # LANG: 'Help' menu title
else:
self.menubar.entryconfigure(1, label=_('File')) # LANG: 'File' menu title
self.menubar.entryconfigure(2, label=_('Edit')) # LANG: 'Edit' menu title
self.menubar.entryconfigure(3, label=_('Help')) # LANG: 'Help' menu title
self.theme_file_menu['text'] = _('File') # LANG: 'File' menu title
self.theme_edit_menu['text'] = _('Edit') # LANG: 'Edit' menu title
self.theme_help_menu['text'] = _('Help') # LANG: 'Help' menu title
# File menu
self.file_menu.entryconfigure(0, label=_('Status')) # LANG: File > Status
self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # LANG: File > Save Raw Data...
self.file_menu.entryconfigure(2, label=_('Settings')) # LANG: File > Settings
self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: File > Exit
# File menu
self.file_menu.entryconfigure(0, label=_('Status')) # LANG: File > Status
self.file_menu.entryconfigure(1, label=_('Save Raw Data...')) # LANG: File > Save Raw Data...
self.file_menu.entryconfigure(2, label=_('Settings')) # LANG: File > Settings
self.file_menu.entryconfigure(4, label=_('Exit')) # LANG: File > Exit
# Help menu
self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help > Documentation
self.help_menu.entryconfigure(1, label=_('Troubleshooting')) # LANG: Help > Troubleshooting
self.help_menu.entryconfigure(2, label=_('Report A Bug')) # LANG: Help > Report A Bug
self.help_menu.entryconfigure(3, label=_('Privacy Policy')) # LANG: Help > Privacy Policy
self.help_menu.entryconfigure(4, label=_('Release Notes')) # LANG: Help > Release Notes
self.help_menu.entryconfigure(5, label=_('Check for Updates...')) # LANG: Help > Check for Updates...
self.help_menu.entryconfigure(6, label=_("About {APP}").format(APP=applongname)) # LANG: Help > About App
self.help_menu.entryconfigure(7, label=_('Open Log Folder')) # LANG: Help > Open Log Folder
# Help menu
self.help_menu.entryconfigure(0, label=_('Documentation')) # LANG: Help > Documentation
self.help_menu.entryconfigure(1, label=_('Troubleshooting')) # LANG: Help > Troubleshooting
self.help_menu.entryconfigure(2, label=_('Report A Bug')) # LANG: Help > Report A Bug
self.help_menu.entryconfigure(3, label=_('Privacy Policy')) # LANG: Help > Privacy Policy
self.help_menu.entryconfigure(4, label=_('Release Notes')) # LANG: Help > Release Notes
self.help_menu.entryconfigure(5, label=_('Check for Updates...')) # LANG: Help > Check for Updates...
self.help_menu.entryconfigure(6, label=_("About {APP}").format(APP=applongname)) # LANG: Help > About App
self.help_menu.entryconfigure(7, label=_('Open Log Folder')) # LANG: Help > Open Log Folder
# Edit menu
self.edit_menu.entryconfigure(0, label=_('Copy')) # LANG: Label for 'Copy' as in 'Copy and Paste'
@ -975,13 +909,8 @@ class AppWindow:
self.button['state'] = self.theme_button['state'] = tk.DISABLED
if sys.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.file_menu.entryconfigure(0, state=tk.DISABLED) # Status
self.file_menu.entryconfigure(1, state=tk.DISABLED) # Save Raw Data
self.w.update_idletasks()
try:
@ -989,13 +918,8 @@ class AppWindow:
# LANG: Successfully authenticated with the Frontier website
self.status['text'] = _('Authentication successful')
if sys.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
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'] = str(e)
@ -1666,13 +1590,8 @@ class AppWindow:
companion.session.auth_callback()
# LANG: Successfully authenticated with the Frontier website
self.status['text'] = _('Authentication successful')
if sys.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
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'] = str(e)
@ -1831,8 +1750,7 @@ class AppWindow:
# position over parent
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
if sys.platform != 'darwin' or parent.winfo_rooty() > 0:
self.geometry(f'+{parent.winfo_rootx():d}+{parent.winfo_rooty():d}')
self.geometry(f'+{parent.winfo_rootx():d}+{parent.winfo_rooty():d}')
# remove decoration
if sys.platform == 'win32':
@ -1916,9 +1834,6 @@ class AppWindow:
"""
default_extension: str = ''
if sys.platform == 'darwin':
default_extension = '.json'
timestamp: str = strftime('%Y-%m-%dT%H.%M.%S', localtime())
f = tkinter.filedialog.asksaveasfilename(
parent=self.w,
@ -1954,9 +1869,8 @@ class AppWindow:
config.set_shutdown() # Signal we're in shutdown now.
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
if sys.platform != 'darwin' or self.w.winfo_rooty() > 0:
x, y = self.w.geometry().split('+')[1:3] # e.g. '212x170+2881+1267'
config.set('geometry', f'+{x}+{y}')
x, y = self.w.geometry().split('+')[1:3] # e.g. '212x170+2881+1267'
config.set('geometry', f'+{x}+{y}')
# Let the user know we're shutting down.
# LANG: The application is shutting down

View File

@ -78,12 +78,6 @@
/* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */
"Edit" = "Edit";
/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */
"View" = "View";
/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */
"Window" = "Window";
/* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */
"Help" = "Help";
@ -351,9 +345,6 @@
/* inara.py: INARA API returned some kind of error (error message will be contained in {MSG}); In files: inara.py:1650; inara.py:1663; */
"Error: Inara {MSG}" = "Error: Inara {MSG}";
/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */
"Preferences" = "Preferences";
/* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */
"Please choose what data to save" = "Please choose what data to save";
@ -372,9 +363,6 @@
/* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */
"File location" = "File location";
/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */
"Change..." = "Change...";
/* prefs.py: NOT-macOS Settings - files location selection button; prefs.py: NOT-macOS Setting - files location selection button; In files: prefs.py:390; prefs.py:440; */
"Browse..." = "Browse...";
@ -390,21 +378,9 @@
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Enable Fleetcarrier CAPI Queries";
/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */
"Keyboard shortcut" = "Keyboard shortcut";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Hotkey";
/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */
"Re-start {APP} to use shortcuts" = "Re-start {APP} to use shortcuts";
/* prefs.py: macOS - Configuration - need to grant the app permission for keyboard shortcuts; In files: prefs.py:510; */
"{APP} needs permission to use shortcuts" = "{APP} needs permission to use shortcuts";
/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */
"Open System Preferences" = "Open System Preferences";
/* prefs.py: Configuration - Act on hotkey only when ED is in foreground; In files: prefs.py:538; */
"Only when Elite: Dangerous is the active app" = "Only when Elite: Dangerous is the active app";

View File

@ -76,10 +76,8 @@ def generate_data_files(
"ChangeLog.md",
"snd_good.wav",
"snd_bad.wav",
"modules.p", # TODO: Remove in 6.0
"modules.json",
"ships.json",
"ships.p", # TODO: Remove in 6.0
f"{app_name}.ico",
f"resources/{appcmdname}.ico",
"EDMarketConnector - TRACE.bat",

View File

@ -7,7 +7,6 @@ See LICENSE file.
Windows uses the Registry to store values in a flat manner.
Linux uses a file, but for commonality it's still a flat data structure.
macOS uses a 'defaults' object.
"""
from __future__ import annotations
@ -54,7 +53,7 @@ appcmdname = 'EDMC'
# <https://semver.org/#semantic-versioning-specification-semver>
# Major.Minor.Patch(-prerelease)(+buildmetadata)
# NB: Do *not* import this, use the functions appversion() and appversion_nobuild()
_static_appversion = '5.10.3'
_static_appversion = '5.11.0-alpha0'
_cached_version: semantic_version.Version | None = None
copyright = '© 2015-2019 Jonathan Harris, 2020-2024 EDCD'
@ -468,10 +467,6 @@ def get_config(*args, **kwargs) -> AbstractConfig:
:param kwargs: Args to be passed through to implementation.
:return: Instance of the implementation.
"""
if sys.platform == "darwin": # pragma: sys-platform-darwin
from .darwin import MacConfig
return MacConfig(*args, **kwargs)
if sys.platform == "win32": # pragma: sys-platform-win32
from .windows import WinConfig
return WinConfig(*args, **kwargs)

View File

@ -1,191 +0,0 @@
"""
darwin.py - Darwin/macOS implementation of AbstractConfig.
Copyright (c) EDCD, All Rights Reserved
Licensed under the GNU General Public License.
See LICENSE file.
"""
from __future__ import annotations
import pathlib
import sys
from typing import Any
from Foundation import ( # type: ignore
NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains, NSUserDefaults,
NSUserDomainMask
)
from config import AbstractConfig, appname, logger
assert sys.platform == 'darwin'
class MacConfig(AbstractConfig):
"""MacConfig is the implementation of AbstractConfig for Darwin based OSes."""
def __init__(self) -> None:
super().__init__()
support_path = pathlib.Path(
NSSearchPathForDirectoriesInDomains(
NSApplicationSupportDirectory, NSUserDomainMask, True
)[0]
)
self.app_dir_path = support_path / appname
self.app_dir_path.mkdir(exist_ok=True)
self.plugin_dir_path = self.app_dir_path / 'plugins'
self.plugin_dir_path.mkdir(exist_ok=True)
# Bundle IDs identify a singled app though out a system
if getattr(sys, 'frozen', False):
exe_dir = pathlib.Path(sys.executable).parent
self.internal_plugin_dir_path = exe_dir.parent / 'Library' / 'plugins'
self.respath_path = exe_dir.parent / 'Resources'
self.identifier = NSBundle.mainBundle().bundleIdentifier()
else:
file_dir = pathlib.Path(__file__).parent.parent
self.internal_plugin_dir_path = file_dir / 'plugins'
self.respath_path = file_dir
self.identifier = f'uk.org.marginal.{appname.lower()}'
NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier
self.default_journal_dir_path = support_path / 'Frontier Developments' / 'Elite Dangerous'
self._defaults: Any = NSUserDefaults.standardUserDefaults()
self._settings: dict[str, int | str | list] = dict(
self._defaults.persistentDomainForName_(self.identifier) or {}
) # make writeable
if (out_dir := self.get_str('out_dir')) is None or not pathlib.Path(out_dir).exists():
self.set('outdir', NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0])
def __raw_get(self, key: str) -> None | list | str | int:
"""
Retrieve the raw data for the given key.
:param str: str - The key data is being requested for.
:return: The requested data.
"""
res = self._settings.get(key)
# On MacOS Catalina, with python.org python 3.9.2 any 'list'
# has type __NSCFArray so a simple `isinstance(res, list)` is
# False. So, check it's not-None, and not the other types.
#
# If we can find where to import the definition of NSCFArray
# then we could possibly test against that.
if res is not None and not isinstance(res, str) and not isinstance(res, int):
return list(res)
return res
def get_str(self, key: str, *, default: str = None) -> str:
"""
Return the string referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_str`.
"""
res = self.__raw_get(key)
if res is None:
return default # Yes it could be None, but we're _assuming_ that people gave us a default
if not isinstance(res, str):
raise ValueError(f'unexpected data returned from __raw_get: {type(res)=} {res}')
return res
def get_list(self, key: str, *, default: list = None) -> list:
"""
Return the list referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_list`.
"""
res = self.__raw_get(key)
if res is None:
return default # Yes it could be None, but we're _assuming_ that people gave us a default
if not isinstance(res, list):
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
return res
def get_int(self, key: str, *, default: int = 0) -> int:
"""
Return the int referred to by key if it exists in the config.
Implements :meth:`AbstractConfig.get_int`.
"""
res = self.__raw_get(key)
if res is None:
return default
if not isinstance(res, (str, int)):
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
try:
return int(res)
except ValueError as e:
logger.error(f'__raw_get returned {res!r} which cannot be parsed to an int: {e}')
return default # Yes it could be None, but we're _assuming_ that people gave us a default
def get_bool(self, key: str, *, default: bool = None) -> bool:
"""
Return the bool referred to by the given key if it exists, or the default.
Implements :meth:`AbstractConfig.get_bool`.
"""
res = self.__raw_get(key)
if res is None:
return default # Yes it could be None, but we're _assuming_ that people gave us a default
if not isinstance(res, bool):
raise ValueError(f'__raw_get returned unexpected type {type(res)=} {res!r}')
return res
def set(self, key: str, val: int | str | list[str] | bool) -> None:
"""
Set the given key's data to the given value.
Implements :meth:`AbstractConfig.set`.
"""
if self._settings is None:
raise ValueError('attempt to use a closed _settings')
if not isinstance(val, (bool, str, int, list)):
raise ValueError(f'Unexpected type for value {type(val)=}')
self._settings[key] = val
def delete(self, key: str, *, suppress=False) -> None:
"""
Delete the given key from the config.
Implements :meth:`AbstractConfig.delete`.
"""
try:
del self._settings[key]
except Exception:
if suppress:
pass
def save(self) -> None:
"""
Save the current configuration.
Implements :meth:`AbstractConfig.save`.
"""
self._defaults.setPersistentDomain_forName_(self._settings, self.identifier)
self._defaults.synchronize()
def close(self) -> None:
"""
Close this config and release any associated resources.
Implements :meth:`AbstractConfig.close`.
"""
self.save()
self._defaults = None

View File

@ -20,13 +20,13 @@ from EDMCLogging import get_main_logger
logger = get_main_logger()
if sys.platform in ('darwin', 'win32'):
if sys.platform == 'win32':
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
else:
# Linux's inotify doesn't work over CIFS or NFS, so poll
class FileSystemEventHandler: # type: ignore
"""Dummy class to represent a file system event handler on platforms other than macOS and Windows."""
"""Dummy class to represent a file system event handler on platforms other than Windows."""
class Dashboard(FileSystemEventHandler):
@ -160,7 +160,7 @@ class Dashboard(FileSystemEventHandler):
def on_modified(self, event) -> None:
"""
Watchdog callback - DirModifiedEvent on macOS, FileModifiedEvent on Windows.
Watchdog callback - FileModifiedEvent on Windows.
:param event: Watchdog event.
"""

View File

@ -7,6 +7,8 @@ from __future__ import annotations
import logging
import tkinter as tk
from tkinter import ttk
import myNotebook as nb # noqa: N813
from config import appname, config
@ -63,7 +65,7 @@ class ClickCounter:
# setup our config in a "Click Count: number"
nb.Label(frame, text='Click Count').grid(row=current_row)
nb.Entry(frame, textvariable=self.click_count).grid(row=current_row, column=1)
ttk.Entry(frame, textvariable=self.click_count).grid(row=current_row, column=1)
current_row += 1 # Always increment our row counter, makes for far easier tkinter design.
return frame

View File

@ -76,10 +76,6 @@ def get_hotkeymgr() -> AbstractHotkeyMgr:
:return: Appropriate class instance.
:raises ValueError: If unsupported platform.
"""
if sys.platform == 'darwin':
from hotkey.darwin import MacHotkeyMgr
return MacHotkeyMgr()
if sys.platform == 'win32':
from hotkey.windows import WindowsHotkeyMgr
return WindowsHotkeyMgr()

View File

@ -1,276 +0,0 @@
"""darwin/macOS implementation of hotkey.AbstractHotkeyMgr."""
from __future__ import annotations
import pathlib
import sys
import tkinter as tk
from typing import Callable
assert sys.platform == 'darwin'
import objc
from AppKit import (
NSAlternateKeyMask, NSApplication, NSBeep, NSClearLineFunctionKey, NSCommandKeyMask, NSControlKeyMask,
NSDeleteFunctionKey, NSDeviceIndependentModifierFlagsMask, NSEvent, NSF1FunctionKey, NSF35FunctionKey,
NSFlagsChanged, NSKeyDown, NSKeyDownMask, NSKeyUp, NSNumericPadKeyMask, NSShiftKeyMask, NSSound, NSWorkspace
)
from config import config
from EDMCLogging import get_main_logger
from hotkey import AbstractHotkeyMgr
logger = get_main_logger()
class MacHotkeyMgr(AbstractHotkeyMgr):
"""Hot key management."""
POLL = 250
# https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSEvent_Class/#//apple_ref/doc/constant_group/Function_Key_Unicodes
DISPLAY = {
0x03: u'', 0x09: u'', 0xd: u'', 0x19: u'', 0x1b: u'esc', 0x20: u'', 0x7f: u'',
0xf700: u'', 0xf701: u'', 0xf702: u'', 0xf703: u'',
0xf727: u'Ins',
0xf728: u'', 0xf729: u'', 0xf72a: u'Fn', 0xf72b: u'',
0xf72c: u'', 0xf72d: u'', 0xf72e: u'PrtScr', 0xf72f: u'ScrollLock',
0xf730: u'Pause', 0xf731: u'SysReq', 0xf732: u'Break', 0xf733: u'Reset',
0xf739: u'',
}
(ACQUIRE_INACTIVE, ACQUIRE_ACTIVE, ACQUIRE_NEW) = range(3)
def __init__(self):
self.MODIFIERMASK = NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask \
| NSNumericPadKeyMask
self.root: tk.Tk
self.keycode = 0
self.modifiers = 0
self.activated = False
self.observer = None
self.acquire_key = 0
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
self.tkProcessKeyEvent_old: Callable
self.snd_good = NSSound.alloc().initWithContentsOfFile_byReference_(
pathlib.Path(config.respath_path) / 'snd_good.wav', False
)
self.snd_bad = NSSound.alloc().initWithContentsOfFile_byReference_(
pathlib.Path(config.respath_path) / 'snd_bad.wav', False
)
def register(self, root: tk.Tk, keycode: int, modifiers: int) -> None:
"""
Register current hotkey for monitoring.
:param root: parent window.
:param keycode: Key to monitor.
:param modifiers: Any modifiers to take into account.
"""
self.root = root
self.keycode = keycode
self.modifiers = modifiers
self.activated = False
if keycode:
if not self.observer:
self.root.after_idle(self._observe)
self.root.after(MacHotkeyMgr.POLL, self._poll)
# Monkey-patch tk (tkMacOSXKeyEvent.c)
if not callable(self.tkProcessKeyEvent_old):
sel = b'tkProcessKeyEvent:'
cls = NSApplication.sharedApplication().class__() # type: ignore
self.tkProcessKeyEvent_old = NSApplication.sharedApplication().methodForSelector_(sel) # type: ignore
newmethod = objc.selector( # type: ignore
self.tkProcessKeyEvent,
selector=self.tkProcessKeyEvent_old.selector,
signature=self.tkProcessKeyEvent_old.signature
)
objc.classAddMethod(cls, sel, newmethod) # type: ignore
def tkProcessKeyEvent(self, cls, the_event): # noqa: N802
"""
Monkey-patch tk (tkMacOSXKeyEvent.c).
- workaround crash on OSX 10.9 & 10.10 on seeing a composing character
- notice when modifier key state changes
- keep a copy of NSEvent.charactersIgnoringModifiers, which is what we need for the hotkey
(Would like to use a decorator but need to ensure the application is created before this is installed)
:param cls: ???
:param the_event: tk event
:return: ???
"""
if self.acquire_state:
if the_event.type() == NSFlagsChanged:
self.acquire_key = the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask
self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW
# suppress the event by not chaining the old function
return the_event
if the_event.type() in (NSKeyDown, NSKeyUp):
c = the_event.charactersIgnoringModifiers()
self.acquire_key = (c and ord(c[0]) or 0) | \
(the_event.modifierFlags() & NSDeviceIndependentModifierFlagsMask)
self.acquire_state = MacHotkeyMgr.ACQUIRE_NEW
# suppress the event by not chaining the old function
return the_event
# replace empty characters with charactersIgnoringModifiers to avoid crash
elif the_event.type() in (NSKeyDown, NSKeyUp) and not the_event.characters():
the_event = NSEvent.keyEventWithType_location_modifierFlags_timestamp_windowNumber_context_characters_charactersIgnoringModifiers_isARepeat_keyCode_( # noqa: E501
# noqa: E501
the_event.type(),
the_event.locationInWindow(),
the_event.modifierFlags(),
the_event.timestamp(),
the_event.windowNumber(),
the_event.context(),
the_event.charactersIgnoringModifiers(),
the_event.charactersIgnoringModifiers(),
the_event.isARepeat(),
the_event.keyCode()
)
return self.tkProcessKeyEvent_old(cls, the_event)
def _observe(self):
# Must be called after root.mainloop() so that the app's message loop has been created
self.observer = NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSKeyDownMask, self._handler)
def _poll(self):
if config.shutting_down:
return
# No way of signalling to Tkinter from within the callback handler block that doesn't
# cause Python to crash, so poll.
if self.activated:
self.activated = False
self.root.event_generate('<<Invoke>>', when="tail")
if self.keycode or self.modifiers:
self.root.after(MacHotkeyMgr.POLL, self._poll)
def unregister(self) -> None:
"""Remove hotkey registration."""
self.keycode = 0
self.modifiers = 0
@objc.callbackFor(NSEvent.addGlobalMonitorForEventsMatchingMask_handler_)
def _handler(self, event) -> None:
# use event.charactersIgnoringModifiers to handle composing characters like Alt-e
if (
(event.modifierFlags() & self.MODIFIERMASK) == self.modifiers
and ord(event.charactersIgnoringModifiers()[0]) == self.keycode
):
if config.get_int('hotkey_always'):
self.activated = True
else: # Only trigger if game client is front process
front = NSWorkspace.sharedWorkspace().frontmostApplication()
if front and front.bundleIdentifier() == 'uk.co.frontier.EliteDangerous':
self.activated = True
def acquire_start(self) -> None:
"""Start acquiring hotkey state via polling."""
self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE
self.root.after_idle(self._acquire_poll)
def acquire_stop(self) -> None:
"""Stop acquiring hotkey state."""
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
def _acquire_poll(self) -> None:
"""Perform a poll of current hotkey state."""
if config.shutting_down:
return
# No way of signalling to Tkinter from within the monkey-patched event handler that doesn't
# cause Python to crash, so poll.
if self.acquire_state:
if self.acquire_state == MacHotkeyMgr.ACQUIRE_NEW:
# Abuse tkEvent's keycode field to hold our acquired key & modifier
self.root.event_generate('<KeyPress>', keycode=self.acquire_key)
self.acquire_state = MacHotkeyMgr.ACQUIRE_ACTIVE
self.root.after(50, self._acquire_poll)
def fromevent(self, event) -> bool | tuple | None:
"""
Return configuration (keycode, modifiers) or None=clear or False=retain previous.
:param event: tk event ?
:return: False to retain previous, None to not use, else (keycode, modifiers)
"""
(keycode, modifiers) = (event.keycode & 0xffff, event.keycode & 0xffff0000) # Set by _acquire_poll()
if (
keycode
and not (modifiers & (NSShiftKeyMask | NSControlKeyMask | NSAlternateKeyMask | NSCommandKeyMask))
):
if keycode == 0x1b: # Esc = retain previous
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
return False
# BkSp, Del, Clear = clear hotkey
if keycode in (0x7f, ord(NSDeleteFunctionKey), ord(NSClearLineFunctionKey)):
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
return None
# don't allow keys needed for typing in System Map
if keycode in (0x13, 0x20, 0x2d) or 0x61 <= keycode <= 0x7a:
NSBeep()
self.acquire_state = MacHotkeyMgr.ACQUIRE_INACTIVE
return None
return keycode, modifiers
def display(self, keycode, modifiers) -> str:
"""
Return displayable form of given hotkey + modifiers.
:param keycode:
:param modifiers:
:return: string form
"""
text = ''
if modifiers & NSControlKeyMask:
text += u''
if modifiers & NSAlternateKeyMask:
text += u''
if modifiers & NSShiftKeyMask:
text += u''
if modifiers & NSCommandKeyMask:
text += u''
if (modifiers & NSNumericPadKeyMask) and keycode <= 0x7f:
text += u''
if not keycode:
pass
elif ord(NSF1FunctionKey) <= keycode <= ord(NSF35FunctionKey):
text += f'F{keycode + 1 - ord(NSF1FunctionKey)}'
elif keycode in MacHotkeyMgr.DISPLAY: # specials
text += MacHotkeyMgr.DISPLAY[keycode]
elif keycode < 0x20: # control keys
text += chr(keycode + 0x40)
elif keycode < 0xf700: # key char
text += chr(keycode).upper()
else:
text += u''
return text
def play_good(self):
"""Play the 'good' sound."""
self.snd_good.play()
def play_bad(self):
"""Play the 'bad' sound."""
self.snd_bad.play()

View File

@ -218,10 +218,6 @@ class JournalLock:
if sys.platform == 'win32':
self.attributes('-toolwindow', tk.TRUE)
elif sys.platform == 'darwin':
# http://wiki.tcl.tk/13428
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
self.resizable(tk.FALSE, tk.FALSE)
frame = ttk.Frame(self)

50
l10n.py
View File

@ -17,8 +17,8 @@ import re
import sys
import warnings
from contextlib import suppress
from os import pardir, listdir, sep, makedirs
from os.path import basename, dirname, isdir, isfile, join, abspath, exists
from os import listdir, sep, makedirs
from os.path import basename, dirname, isdir, join, abspath, exists
from typing import TYPE_CHECKING, Iterable, TextIO, cast
from config import config
from EDMCLogging import get_main_logger
@ -39,12 +39,7 @@ logger = get_main_logger()
LANGUAGE_ID = '!Language'
LOCALISATION_DIR = 'L10n'
if sys.platform == 'darwin':
from Foundation import ( # type: ignore # exists on Darwin
NSLocale, NSNumberFormatter, NSNumberFormatterDecimalStyle
)
elif sys.platform == 'win32':
if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import BOOL, DWORD, LPCVOID, LPCWSTR, LPWSTR
if TYPE_CHECKING:
@ -178,14 +173,8 @@ class _Translations:
def available(self) -> set[str]:
"""Return a list of available language codes."""
path = self.respath()
if getattr(sys, 'frozen', False) and sys.platform == 'darwin':
available = {
x[:-len('.lproj')] for x in listdir(path)
if x.endswith('.lproj') and isfile(join(x, 'Localizable.strings'))
}
else:
available = {x[:-len('.strings')] for x in listdir(path) if x.endswith('.strings')}
available = {x[:-len('.strings')] for x in listdir(path) if x.endswith('.strings')}
return available
@ -206,9 +195,6 @@ class _Translations:
def respath(self) -> str:
"""Path to localisation files."""
if getattr(sys, 'frozen', False):
if sys.platform == 'darwin':
return abspath(join(dirname(sys.executable), pardir, 'Resources'))
return abspath(join(dirname(sys.executable), LOCALISATION_DIR))
if __file__:
@ -234,10 +220,6 @@ class _Translations:
except OSError:
logger.exception(f'could not open {file_path}')
elif getattr(sys, 'frozen', False) and sys.platform == 'darwin':
res_path = join(self.respath(), f'{lang}.lproj', 'Localizable.strings')
return open(res_path, encoding='utf-16')
res_path = join(self.respath(), f'{lang}.strings')
return open(res_path, encoding='utf-8')
@ -245,15 +227,6 @@ class _Translations:
class _Locale:
"""Locale holds a few utility methods to convert data to and from localized versions."""
def __init__(self) -> None:
if sys.platform == 'darwin':
self.int_formatter = NSNumberFormatter.alloc().init()
self.int_formatter.setNumberStyle_(NSNumberFormatterDecimalStyle)
self.float_formatter = NSNumberFormatter.alloc().init()
self.float_formatter.setNumberStyle_(NSNumberFormatterDecimalStyle)
self.float_formatter.setMinimumFractionDigits_(5)
self.float_formatter.setMaximumFractionDigits_(5)
def stringFromNumber(self, number: float | int, decimals: int | None = None) -> str: # noqa: N802
warnings.warn(DeprecationWarning('use _Locale.string_from_number instead.'))
return self.string_from_number(number, decimals) # type: ignore
@ -279,14 +252,6 @@ class _Locale:
if decimals == 0 and not isinstance(number, numbers.Integral):
number = int(round(number))
if sys.platform == 'darwin':
if not decimals and isinstance(number, numbers.Integral):
return self.int_formatter.stringFromNumber_(number)
self.float_formatter.setMinimumFractionDigits_(decimals)
self.float_formatter.setMaximumFractionDigits_(decimals)
return self.float_formatter.stringFromNumber_(number)
if not decimals and isinstance(number, numbers.Integral):
return locale.format_string('%d', number, True)
return locale.format_string('%.*f', (decimals, number), True)
@ -299,9 +264,6 @@ class _Locale:
:param string: The string to convert
:return: None if the string cannot be parsed, otherwise an int or float dependant on input data.
"""
if sys.platform == 'darwin':
return self.float_formatter.numberFromString_(string)
with suppress(ValueError):
return locale.atoi(string)
@ -332,10 +294,8 @@ class _Locale:
:return: The preferred language list
"""
languages: Iterable[str]
if sys.platform == 'darwin':
languages = NSLocale.preferredLanguages()
elif sys.platform != 'win32':
if sys.platform != 'win32':
# POSIX
lang = locale.getlocale()[0]
languages = [lang.replace('_', '-')] if lang else []

BIN
modules.p

Binary file not shown.

View File

@ -38,16 +38,7 @@ if TYPE_CHECKING:
def _(x: str) -> str:
return x
if sys.platform == 'darwin':
from fcntl import fcntl
from AppKit import NSWorkspace
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from watchdog.observers.api import BaseObserver
F_GLOBAL_NOCACHE = 55
elif sys.platform == 'win32':
if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import BOOL, HWND, LPARAM, LPWSTR
@ -384,8 +375,6 @@ class EDLogs(FileSystemEventHandler):
logfile = self.logfile
if logfile:
loghandle: BinaryIO = open(logfile, 'rb', 0) # unbuffered
if sys.platform == 'darwin':
fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB
self.catching_up = True
for line in loghandle:
@ -452,7 +441,7 @@ class EDLogs(FileSystemEventHandler):
new_journal_file = None
if logfile:
loghandle.seek(0, SEEK_END) # required to make macOS notice log change over SMB
loghandle.seek(0, SEEK_END) # required for macOS to notice log change over SMB. TODO: Do we need this?
loghandle.seek(log_pos, SEEK_SET) # reset EOF flag # TODO: log_pos reported as possibly unbound
for line in loghandle:
# Paranoia check to see if we're shutting down
@ -485,9 +474,6 @@ class EDLogs(FileSystemEventHandler):
if logfile:
loghandle = open(logfile, 'rb', 0) # unbuffered
if sys.platform == 'darwin':
fcntl(loghandle, F_GLOBAL_NOCACHE, -1) # required to avoid corruption on macOS over SMB
log_pos = 0
sleep(self._POLL)
@ -2146,12 +2132,7 @@ class EDLogs(FileSystemEventHandler):
:return: bool - True if the game is running.
"""
if sys.platform == 'darwin':
for app in NSWorkspace.sharedWorkspace().runningApplications():
if app.bundleIdentifier() == 'uk.co.frontier.EliteDangerous':
return True
elif sys.platform == 'win32':
if sys.platform == 'win32':
def WindowTitle(h): # noqa: N802
if h:
length = GetWindowTextLength(h) + 1

View File

@ -1,12 +1,9 @@
"""
Custom `ttk.Notebook` to fix various display issues.
Hacks to fix various display issues with notebooks and their child widgets on
OSX and Windows.
Hacks to fix various display issues with notebooks and their child widgets on Windows.
- Windows: page background should be White, not SystemButtonFace
- OSX: page background should be a darker gray than systemWindowBody
selected tab foreground should be White when the window is active
Entire file may be imported by plugins.
"""
@ -21,13 +18,7 @@ from PIL import ImageGrab
if TYPE_CHECKING:
def _(x: str) -> str: return x
# Can't do this with styles on OSX - http://www.tkdocs.com/tutorial/styles.html#whydifficult
if sys.platform == 'darwin':
from platform import mac_ver
PAGEFG = 'systemButtonText'
PAGEBG = 'systemButtonActiveDarkShadow'
elif sys.platform == 'win32':
if sys.platform == 'win32':
PAGEFG = 'SystemWindowText'
PAGEBG = 'SystemWindow' # typically white
@ -37,39 +28,22 @@ class Notebook(ttk.Notebook):
def __init__(self, master: ttk.Frame | None = None, **kw):
ttk.Notebook.__init__(self, master, **kw)
super().__init__(master, **kw)
style = ttk.Style()
if sys.platform == 'darwin':
if list(map(int, mac_ver()[0].split('.'))) >= [10, 10]:
# Hack for tab appearance with 8.5 on Yosemite & El Capitan. For proper fix see
# https://github.com/tcltk/tk/commit/55c4dfca9353bbd69bbcec5d63bf1c8dfb461e25
style.configure('TNotebook.Tab', padding=(12, 10, 12, 2))
style.map('TNotebook.Tab', foreground=[('selected', '!background', 'systemWhite')])
self.grid(sticky=tk.NSEW) # Already padded apropriately
elif sys.platform == 'win32':
if sys.platform == 'win32':
style.configure('nb.TFrame', background=PAGEBG)
style.configure('nb.TButton', background=PAGEBG)
style.configure('nb.TCheckbutton', foreground=PAGEFG, background=PAGEBG)
style.configure('nb.TMenubutton', foreground=PAGEFG, background=PAGEBG)
style.configure('nb.TRadiobutton', foreground=PAGEFG, background=PAGEBG)
self.grid(padx=10, pady=10, sticky=tk.NSEW)
else:
self.grid(padx=10, pady=10, sticky=tk.NSEW)
self.grid(padx=10, pady=10, sticky=tk.NSEW)
# FIXME: The real fix for this 'dynamic type' would be to split this whole
# thing into being a module with per-platform files, as we've done with config
# That would also make the code cleaner.
class Frame(sys.platform == 'darwin' and tk.Frame or ttk.Frame): # type: ignore
class Frame(ttk.Frame):
"""Custom t(t)k.Frame class to fix some display issues."""
def __init__(self, master: ttk.Notebook | None = None, **kw):
if sys.platform == 'darwin':
kw['background'] = kw.pop('background', PAGEBG)
tk.Frame.__init__(self, master, **kw)
tk.Frame(self).grid(pady=5)
elif sys.platform == 'win32':
if sys.platform == 'win32':
ttk.Frame.__init__(self, master, style='nb.TFrame', **kw)
ttk.Frame(self).grid(pady=5) # top spacer
else:
@ -82,14 +56,11 @@ class Label(tk.Label):
"""Custom tk.Label class to fix some display issues."""
def __init__(self, master: ttk.Frame | None = None, **kw):
# This format chosen over `sys.platform in (...)` as mypy and friends don't understand that
if sys.platform in ('darwin', 'win32'):
kw['foreground'] = kw.pop('foreground', PAGEFG)
kw['background'] = kw.pop('background', PAGEBG)
else:
kw['foreground'] = kw.pop('foreground', ttk.Style().lookup('TLabel', 'foreground'))
kw['background'] = kw.pop('background', ttk.Style().lookup('TLabel', 'background'))
tk.Label.__init__(self, master, **kw) # Just use tk.Label on all platforms
kw['foreground'] = kw.pop('foreground', PAGEFG if sys.platform == 'win32'
else ttk.Style().lookup('TLabel', 'foreground'))
kw['background'] = kw.pop('background', PAGEBG if sys.platform == 'win32'
else ttk.Style().lookup('TLabel', 'background'))
super().__init__(master, **kw)
class EntryMenu(ttk.Entry):
@ -149,95 +120,61 @@ class EntryMenu(ttk.Entry):
pass
class Entry(sys.platform == 'darwin' and tk.Entry or EntryMenu or ttk.Entry): # type: ignore
class Entry(ttk.Entry or EntryMenu):
"""Custom t(t)k.Entry class to fix some display issues."""
# DEPRECATED: Migrate to ttk.Entry or EntryMenu. Will remove in 5.12 or later.
def __init__(self, master: ttk.Frame | None = None, **kw):
if sys.platform == 'darwin':
kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG)
tk.Entry.__init__(self, master, **kw)
else:
EntryMenu.__init__(self, master, **kw)
EntryMenu.__init__(self, master, **kw)
class Button(sys.platform == 'darwin' and tk.Button or ttk.Button): # type: ignore
class Button(ttk.Button): # type: ignore
"""Custom t(t)k.Button class to fix some display issues."""
# DEPRECATED: Migrate to ttk.Button. Will remove in 5.12 or later.
def __init__(self, master: ttk.Frame | None = None, **kw):
if sys.platform == 'darwin':
kw['highlightbackground'] = kw.pop('highlightbackground', PAGEBG)
tk.Button.__init__(self, master, **kw)
elif sys.platform == 'win32':
if sys.platform == 'win32':
ttk.Button.__init__(self, master, style='nb.TButton', **kw)
else:
ttk.Button.__init__(self, master, **kw)
class ColoredButton(sys.platform == 'darwin' and tk.Label or tk.Button): # type: ignore
class ColoredButton(tk.Button): # type: ignore
"""Custom t(t)k.ColoredButton class to fix some display issues."""
# DEPRECATED: Migrate to tk.Button. Will remove in 5.12 or later.
def __init__(self, master: ttk.Frame | None = None, **kw):
if sys.platform == 'darwin':
# Can't set Button background on OSX, so use a Label instead
kw['relief'] = kw.pop('relief', tk.RAISED)
self._command = kw.pop('command', None)
tk.Label.__init__(self, master, **kw)
self.bind('<Button-1>', self._press)
else:
tk.Button.__init__(self, master, **kw)
if sys.platform == 'darwin':
def _press(self, event):
self._command()
tk.Button.__init__(self, master, **kw)
class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton): # type: ignore
class Checkbutton(ttk.Checkbutton):
"""Custom t(t)k.Checkbutton class to fix some display issues."""
def __init__(self, master: ttk.Frame | None = None, **kw):
if sys.platform == 'darwin':
kw['foreground'] = kw.pop('foreground', PAGEFG)
kw['background'] = kw.pop('background', PAGEBG)
tk.Checkbutton.__init__(self, master, **kw)
elif sys.platform == 'win32':
ttk.Checkbutton.__init__(self, master, style='nb.TCheckbutton', **kw)
else:
ttk.Checkbutton.__init__(self, master, **kw)
def __init__(self, master=None, **kw):
style = 'nb.TCheckbutton' if sys.platform == 'win32' else None
super().__init__(master, style=style, **kw) # type: ignore
class Radiobutton(sys.platform == 'darwin' and tk.Radiobutton or ttk.Radiobutton): # type: ignore
class Radiobutton(ttk.Radiobutton):
"""Custom t(t)k.Radiobutton class to fix some display issues."""
def __init__(self, master: ttk.Frame | None = None, **kw):
if sys.platform == 'darwin':
kw['foreground'] = kw.pop('foreground', PAGEFG)
kw['background'] = kw.pop('background', PAGEBG)
tk.Radiobutton.__init__(self, master, **kw)
elif sys.platform == 'win32':
ttk.Radiobutton.__init__(self, master, style='nb.TRadiobutton', **kw)
else:
ttk.Radiobutton.__init__(self, master, **kw)
style = 'nb.TRadiobutton' if sys.platform == 'win32' else None
super().__init__(master, style=style, **kw) # type: ignore
class OptionMenu(sys.platform == 'darwin' and tk.OptionMenu or ttk.OptionMenu): # type: ignore
"""Custom t(t)k.OptionMenu class to fix some display issues."""
class OptionMenu(ttk.OptionMenu):
"""Custom ttk.OptionMenu class to fix some display issues."""
def __init__(self, master, variable, default=None, *values, **kw):
if sys.platform == 'darwin':
variable.set(default)
bg = kw.pop('background', PAGEBG)
tk.OptionMenu.__init__(self, master, variable, *values, **kw)
self['background'] = bg
elif sys.platform == 'win32':
if sys.platform == 'win32':
# OptionMenu derives from Menubutton at the Python level, so uses Menubutton's style
ttk.OptionMenu.__init__(self, master, variable, default, *values, style='nb.TMenubutton', **kw)
self['menu'].configure(background=PAGEBG)
# Workaround for https://bugs.python.org/issue25684
for i in range(0, self['menu'].index('end')+1):
self['menu'].entryconfig(i, variable=variable)
else:
ttk.OptionMenu.__init__(self, master, variable, default, *values, **kw)
self['menu'].configure(background=ttk.Style().lookup('TMenu', 'background'))
# Workaround for https://bugs.python.org/issue25684
for i in range(0, self['menu'].index('end')+1):
self['menu'].entryconfig(i, variable=variable)
# Workaround for https://bugs.python.org/issue25684
for i in range(0, self['menu'].index('end') + 1):
self['menu'].entryconfig(i, variable=variable)

View File

@ -106,25 +106,25 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Fr
# LANG: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL
nb.Label(conf_frame, text=_('Normal URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY)
nb.Entry(conf_frame,
textvariable=coriolis_config.normal_textvar).grid(
ttk.Entry(conf_frame,
textvariable=coriolis_config.normal_textvar).grid(
sticky=tk.EW, row=cur_row, column=1, padx=PADX, pady=BOXY
)
# LANG: Generic 'Reset' button label
nb.Button(conf_frame, text=_("Reset"),
command=lambda: coriolis_config.normal_textvar.set(value=DEFAULT_NORMAL_URL)).grid(
ttk.Button(conf_frame, text=_("Reset"),
command=lambda: coriolis_config.normal_textvar.set(value=DEFAULT_NORMAL_URL)).grid(
sticky=tk.W, row=cur_row, column=2, padx=PADX, pady=0
)
cur_row += 1
# LANG: Settings>Coriolis: Label for 'alpha/beta game version' URL
nb.Label(conf_frame, text=_('Beta URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY)
nb.Entry(conf_frame, textvariable=coriolis_config.beta_textvar).grid(
ttk.Entry(conf_frame, textvariable=coriolis_config.beta_textvar).grid(
sticky=tk.EW, row=cur_row, column=1, padx=PADX, pady=BOXY
)
# LANG: Generic 'Reset' button label
nb.Button(conf_frame, text=_('Reset'),
command=lambda: coriolis_config.beta_textvar.set(value=DEFAULT_BETA_URL)).grid(
ttk.Button(conf_frame, text=_('Reset'),
command=lambda: coriolis_config.beta_textvar.set(value=DEFAULT_BETA_URL)).grid(
sticky=tk.W, row=cur_row, column=2, padx=PADX, pady=0
)
cur_row += 1

View File

@ -27,7 +27,6 @@ import os
import pathlib
import re
import sqlite3
import sys
import tkinter as tk
from platform import system
from textwrap import dedent
@ -282,7 +281,7 @@ class EDDNSender:
msg['header'] = {
# We have to lie and say it's *this* version, but denote that
# it might not actually be this version.
'softwareName': f'{applongname} [{system() if sys.platform != "darwin" else "Mac OS"}]'
'softwareName': f'{applongname} [{system()}]'
' (legacy replay)',
'softwareVersion': str(appversion_nobuild()),
'uploaderID': cmdr,
@ -1074,7 +1073,7 @@ class EDDN:
gb = this.game_build
return {
'softwareName': f'{applongname} [{system() if sys.platform != "darwin" else "Mac OS"}]',
'softwareName': f'{applongname} [{system()}]',
'softwareVersion': str(appversion_nobuild()),
'uploaderID': this.cmdr_name,
'gameversion': gv,

View File

@ -113,10 +113,10 @@ class This:
self.cmdr_text: nb.Label | None = None
self.user_label: nb.Label | None = None
self.user: nb.Entry | None = None
self.user: ttk.Entry | None = None
self.apikey_label: nb.Label | None = None
self.apikey: nb.Entry | None = None
self.apikey: ttk.Entry | None = None
this = This()
@ -345,14 +345,14 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Fr
# LANG: EDSM Commander name label in EDSM settings
this.user_label = nb.Label(frame, text=_('Commander Name'))
this.user_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W)
this.user = nb.Entry(frame)
this.user = ttk.Entry(frame)
this.user.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW)
cur_row += 1
# LANG: EDSM API key label
this.apikey_label = nb.Label(frame, text=_('API Key'))
this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W)
this.apikey = nb.Entry(frame, show="*", width=50)
this.apikey = ttk.Entry(frame, show="*", width=50)
this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW)
cur_row += 1

View File

@ -125,7 +125,7 @@ class This:
self.log: 'tk.IntVar'
self.log_button: nb.Checkbutton
self.label: HyperlinkLabel
self.apikey: nb.Entry
self.apikey: ttk.Entry
self.apikey_label: tk.Label
self.events: dict[Credentials, Deque[Event]] = defaultdict(deque)
@ -292,7 +292,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> tk.Frame:
# LANG: Inara API key label
this.apikey_label = nb.Label(frame, text=_('API Key')) # Inara setting
this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W)
this.apikey = nb.Entry(frame, show="*", width=50)
this.apikey = ttk.Entry(frame, show="*", width=50)
this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW)
cur_row += 1

203
prefs.py
View File

@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Any, Callable, Optional, Type
import myNotebook as nb # noqa: N813
import plug
from config import applongname, appversion_nobuild, config
from config import appversion_nobuild, config
from EDMCLogging import edmclogger, get_main_logger
from constants import appname
from hotkey import hotkeymgr
@ -49,9 +49,6 @@ def help_open_log_folder() -> None:
if sys.platform.startswith('win'):
# On Windows, use the "start" command to open the folder
system(f'start "" "{logfile_loc}"')
elif sys.platform.startswith('darwin'):
# On macOS, use the "open" command to open the folder
system(f'open "{logfile_loc}"')
elif sys.platform.startswith('linux'):
# On Linux, use the "xdg-open" command to open the folder
system(f'xdg-open "{logfile_loc}"')
@ -172,32 +169,7 @@ class AutoInc(contextlib.AbstractContextManager):
return None
if sys.platform == 'darwin':
import objc # type: ignore
from Foundation import NSFileManager # type: ignore
try:
from ApplicationServices import ( # type: ignore
AXIsProcessTrusted, AXIsProcessTrustedWithOptions, kAXTrustedCheckOptionPrompt
)
except ImportError:
HIServices = objc.loadBundle(
'HIServices',
globals(),
'/System/Library/Frameworks/ApplicationServices.framework/Frameworks/HIServices.framework'
)
objc.loadBundleFunctions(
HIServices,
globals(),
[('AXIsProcessTrusted', 'B'), ('AXIsProcessTrustedWithOptions', 'B@')]
)
objc.loadBundleVariables(HIServices, globals(), [('kAXTrustedCheckOptionPrompt', '@^{__CFString=}')])
was_accessible_at_launch = AXIsProcessTrusted() # type: ignore
elif sys.platform == 'win32':
if sys.platform == 'win32':
import ctypes
import winreg
from ctypes.wintypes import HINSTANCE, HWND, LPCWSTR, LPWSTR, MAX_PATH, POINT, RECT, SIZE, UINT
@ -251,30 +223,21 @@ class PreferencesDialog(tk.Toplevel):
self.parent = parent
self.callback = callback
if sys.platform == 'darwin':
# LANG: File > Preferences menu entry for macOS
self.title(_('Preferences'))
else:
# LANG: File > Settings (macOS)
self.title(_('Settings'))
# LANG: File > Settings (macOS)
self.title(_('Settings'))
if parent.winfo_viewable():
self.transient(parent)
# position over parent
if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
# TODO this is fixed supposedly.
self.geometry(f'+{parent.winfo_rootx()}+{parent.winfo_rooty()}')
# http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
# TODO this is fixed supposedly.
self.geometry(f'+{parent.winfo_rootx()}+{parent.winfo_rooty()}')
# remove decoration
if sys.platform == 'win32':
self.attributes('-toolwindow', tk.TRUE)
elif sys.platform == 'darwin':
# http://wiki.tcl.tk/13428
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
self.resizable(tk.FALSE, tk.FALSE)
self.cmdr: str | bool | None = False # Note if Cmdr changes in the Journal
@ -302,19 +265,15 @@ class PreferencesDialog(tk.Toplevel):
self.__setup_appearance_tab(notebook)
self.__setup_plugin_tab(notebook)
if sys.platform == 'darwin':
self.protocol("WM_DELETE_WINDOW", self.apply) # close button applies changes
else:
buttonframe = ttk.Frame(frame)
buttonframe.grid(padx=self.PADX, pady=self.PADX, sticky=tk.NSEW)
buttonframe.columnconfigure(0, weight=1)
ttk.Label(buttonframe).grid(row=0, column=0) # spacer
# LANG: 'OK' button on Settings/Preferences window
button = ttk.Button(buttonframe, text=_('OK'), command=self.apply)
button.grid(row=0, column=1, sticky=tk.E)
button.bind("<Return>", lambda event: self.apply())
self.protocol("WM_DELETE_WINDOW", self._destroy)
buttonframe = ttk.Frame(frame)
buttonframe.grid(padx=self.PADX, pady=self.PADX, sticky=tk.NSEW)
buttonframe.columnconfigure(0, weight=1)
ttk.Label(buttonframe).grid(row=0, column=0) # spacer
# LANG: 'OK' button on Settings/Preferences window
button = ttk.Button(buttonframe, text=_('OK'), command=self.apply)
button.grid(row=0, column=1, sticky=tk.E)
button.bind("<Return>", lambda event: self.apply())
self.protocol("WM_DELETE_WINDOW", self._destroy)
# FIXME: Why are these being called when *creating* the Settings window?
# Selectively disable buttons depending on output settings
@ -326,7 +285,7 @@ class PreferencesDialog(tk.Toplevel):
# wait for window to appear on screen before calling grab_set
self.parent.update_idletasks()
self.parent.wm_attributes('-topmost', 0) # needed for dialog to appear ontop of parent on OSX & Linux
self.parent.wm_attributes('-topmost', 0) # needed for dialog to appear ontop of parent on Linux
self.wait_visibility()
self.grab_set()
@ -402,16 +361,12 @@ class PreferencesDialog(tk.Toplevel):
# Type ignored due to incorrect type annotation. a 2 tuple does padding for each side
self.outdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) # type: ignore
self.outdir_entry = nb.Entry(output_frame, takefocus=False)
self.outdir_entry = ttk.Entry(output_frame, takefocus=False)
self.outdir_entry.grid(columnspan=2, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
if sys.platform == 'darwin':
text = (_('Change...')) # LANG: macOS Preferences - files location selection button
text = _('Browse...') # LANG: NOT-macOS Settings - files location selection button
else:
text = (_('Browse...')) # LANG: NOT-macOS Settings - files location selection button
self.outbutton = nb.Button(
self.outbutton = ttk.Button(
output_frame,
text=text,
# Technically this is different from the label in Settings > Output, as *this* is used
@ -444,7 +399,7 @@ class PreferencesDialog(tk.Toplevel):
logdir = default
self.logdir.set(logdir)
self.logdir_entry = nb.Entry(config_frame, takefocus=False)
self.logdir_entry = ttk.Entry(config_frame, takefocus=False)
# Location of the Journal files
nb.Label(
@ -455,14 +410,10 @@ class PreferencesDialog(tk.Toplevel):
self.logdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
if sys.platform == 'darwin':
text = (_('Change...')) # LANG: macOS Preferences - files location selection button
else:
text = (_('Browse...')) # LANG: NOT-macOS Setting - files location selection button
text = _('Browse...') # LANG: NOT-macOS Setting - files location selection button
with row as cur_row:
self.logbutton = nb.Button(
self.logbutton = ttk.Button(
config_frame,
text=text,
# LANG: Settings > Configuration - Label for Journal files location
@ -472,7 +423,7 @@ class PreferencesDialog(tk.Toplevel):
if config.default_journal_dir_path:
# Appearance theme and language setting
nb.Button(
ttk.Button(
config_frame,
# LANG: Settings > Configuration - Label on 'reset journal files location to default' button
text=_('Default'),
@ -499,7 +450,7 @@ class PreferencesDialog(tk.Toplevel):
variable=self.capi_fleetcarrier
).grid(columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get())
if sys.platform in ('darwin', 'win32'):
if sys.platform == 'win32':
ttk.Separator(config_frame, orient=tk.HORIZONTAL).grid(
columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get()
)
@ -511,49 +462,21 @@ class PreferencesDialog(tk.Toplevel):
with row as cur_row:
nb.Label(
config_frame,
text=_('Keyboard shortcut') if # LANG: Hotkey/Shortcut settings prompt on OSX
sys.platform == 'darwin' else
_('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows
text=_('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
if sys.platform == 'darwin' and not was_accessible_at_launch:
if AXIsProcessTrusted():
# Shortcut settings prompt on OSX
nb.Label(
config_frame,
# LANG: macOS Preferences > Configuration - restart the app message
text=_('Re-start {APP} to use shortcuts').format(APP=applongname),
foreground='firebrick'
).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
self.hotkey_text = ttk.Entry(config_frame, width=30, justify=tk.CENTER)
self.hotkey_text.insert(
0,
# No hotkey/shortcut currently defined
# TODO: display Only shows up on windows
# LANG: No hotkey/shortcut set
hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else _('None')
)
else:
# Shortcut settings prompt on OSX
nb.Label(
config_frame,
# LANG: macOS - Configuration - need to grant the app permission for keyboard shortcuts
text=_('{APP} needs permission to use shortcuts').format(APP=applongname),
foreground='firebrick'
).grid(columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
# LANG: Shortcut settings button on OSX
nb.Button(config_frame, text=_('Open System Preferences'), command=self.enableshortcuts).grid(
padx=self.PADX, pady=self.BOXY, sticky=tk.E, row=cur_row
)
else:
self.hotkey_text = nb.Entry(config_frame, width=(
20 if sys.platform == 'darwin' else 30), justify=tk.CENTER)
self.hotkey_text.insert(
0,
# No hotkey/shortcut currently defined
# TODO: display Only shows up on darwin or windows
# LANG: No hotkey/shortcut set
hotkeymgr.display(self.hotkey_code, self.hotkey_mods) if self.hotkey_code else _('None')
)
self.hotkey_text.bind('<FocusIn>', self.hotkeystart)
self.hotkey_text.bind('<FocusOut>', self.hotkeyend)
self.hotkey_text.grid(column=1, columnspan=2, pady=self.BOXY, sticky=tk.W, row=cur_row)
self.hotkey_text.bind('<FocusIn>', self.hotkeystart)
self.hotkey_text.bind('<FocusOut>', self.hotkeyend)
self.hotkey_text.grid(column=1, columnspan=2, pady=self.BOXY, sticky=tk.W, row=cur_row)
# Hotkey/Shortcut setting
self.hotkey_only_btn = nb.Checkbutton(
@ -700,7 +623,7 @@ class PreferencesDialog(tk.Toplevel):
self.loglevel_dropdown.configure(width=15)
self.loglevel_dropdown.grid(column=1, pady=self.BOXY, sticky=tk.W, row=cur_row)
nb.Button(
ttk.Button(
config_frame,
# LANG: Label on button used to open a filesystem folder
text=_('Open Log Folder'), # Button that opens a folder in Explorer/Finder
@ -803,7 +726,7 @@ class PreferencesDialog(tk.Toplevel):
self.theme_label_0.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
# Main window
self.theme_button_0 = nb.ColoredButton(
self.theme_button_0 = tk.Button(
appearance_frame,
# LANG: Appearance - Example 'Normal' text
text=_('Station'),
@ -816,7 +739,7 @@ class PreferencesDialog(tk.Toplevel):
with row as cur_row:
self.theme_label_1 = nb.Label(appearance_frame, text=self.theme_prompts[1])
self.theme_label_1.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row)
self.theme_button_1 = nb.ColoredButton(
self.theme_button_1 = tk.Button(
appearance_frame,
text=' Hutton Orbital ', # Do not translate
background='grey4',
@ -947,7 +870,7 @@ class PreferencesDialog(tk.Toplevel):
padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()
)
plugdirentry = nb.Entry(plugins_frame, justify=tk.LEFT)
plugdirentry = ttk.Entry(plugins_frame, justify=tk.LEFT)
self.displaypath(plugdir, plugdirentry)
plugdirentry.grid(columnspan=2, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get())
@ -959,7 +882,7 @@ class PreferencesDialog(tk.Toplevel):
text=_("Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name").format(EXT='.disabled')
).grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=cur_row)
nb.Button(
ttk.Button(
plugins_frame,
# LANG: Label on button used to open a filesystem folder
text=_('Open'), # Button that opens a folder in Explorer/Finder
@ -1070,14 +993,6 @@ class PreferencesDialog(tk.Toplevel):
def tabchanged(self, event: tk.Event) -> None:
"""Handle preferences active tab changing."""
self.outvarchanged()
if sys.platform == 'darwin':
# Hack to recompute size so that buttons show up under Mojave
notebook = event.widget
frame = self.nametowidget(notebook.winfo_parent())
temp = nb.Label(frame)
temp.grid()
temp.update_idletasks()
temp.destroy()
def outvarchanged(self, event: Optional[tk.Event] = None) -> None:
"""Handle Output tab variable changes."""
@ -1139,16 +1054,6 @@ class PreferencesDialog(tk.Toplevel):
entryfield.insert(0, '\\'.join(display))
# None if path doesn't exist
elif sys.platform == 'darwin' and NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get()):
if pathvar.get().startswith(config.home):
display = ['~'] + NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get())[
len(NSFileManager.defaultManager().componentsToDisplayForPath_(config.home)):
]
else:
display = NSFileManager.defaultManager().componentsToDisplayForPath_(pathvar.get())
entryfield.insert(0, '/'.join(display))
else:
if pathvar.get().startswith(config.home):
entryfield.insert(0, '~' + pathvar.get()[len(config.home):])
@ -1288,7 +1193,7 @@ class PreferencesDialog(tk.Toplevel):
config.set('capi_fleetcarrier', self.capi_fleetcarrier.get())
if sys.platform in ('darwin', 'win32'):
if sys.platform == 'win32':
config.set('hotkey_code', self.hotkey_code)
config.set('hotkey_mods', self.hotkey_mods)
config.set('hotkey_always', int(not self.hotkey_only.get()))
@ -1333,25 +1238,3 @@ class PreferencesDialog(tk.Toplevel):
self.parent.wm_attributes('-topmost', 1 if config.get_int('always_ontop') else 0)
self.destroy()
if sys.platform == 'darwin':
def enableshortcuts(self) -> None:
"""Set up macOS preferences shortcut."""
self.apply()
# popup System Preferences dialog
try:
# http://stackoverflow.com/questions/6652598/cocoa-button-opens-a-system-preference-page/6658201
from ScriptingBridge import SBApplication # type: ignore
sysprefs = 'com.apple.systempreferences'
prefs = SBApplication.applicationWithBundleIdentifier_(sysprefs)
pane = [x for x in prefs.panes() if x.id() == 'com.apple.preference.security'][0]
prefs.setCurrentPane_(pane)
anchor = [x for x in pane.anchors() if x.name() == 'Privacy_Accessibility'][0]
anchor.reveal()
prefs.activate()
except Exception:
AXIsProcessTrustedWithOptions({kAXTrustedCheckOptionPrompt: True})
if not config.shutting_down:
self.parent.event_generate('<<Quit>>', when="tail")

View File

@ -26,6 +26,7 @@ is_wine = False
if sys.platform == 'win32':
from ctypes import windll # type: ignore
try:
if windll.ntdll.wine_get_version:
is_wine = True
@ -58,72 +59,13 @@ class GenericProtocolHandler:
self.master.event_generate('<<CompanionAuthEvent>>', when="tail")
if sys.platform == 'darwin' and getattr(sys, 'frozen', False): # noqa: C901 # its guarding ALL macos stuff.
import struct
import objc # type: ignore
from AppKit import NSAppleEventManager, NSObject # type: ignore
kInternetEventClass = kAEGetURL = struct.unpack('>l', b'GURL')[0] # noqa: N816 # API names
keyDirectObject = struct.unpack('>l', b'----')[0] # noqa: N816 # API names
class DarwinProtocolHandler(GenericProtocolHandler):
"""
MacOS protocol handler implementation.
Uses macOS event stuff.
"""
POLL = 100 # ms
def start(self, master: 'tkinter.Tk') -> None:
"""Start Protocol Handler."""
GenericProtocolHandler.start(self, master)
self.lasturl: str | None = None
self.eventhandler = EventHandler.alloc().init()
def poll(self) -> None:
"""Poll event until URL is updated."""
# No way of signalling to Tkinter from within the callback handler block that doesn't cause Python to crash,
# so poll. TODO: Resolved?
if self.lasturl and self.lasturl.startswith(self.redirect):
self.event(self.lasturl)
self.lasturl = None
class EventHandler(NSObject):
"""Handle NSAppleEventManager IPC stuff."""
def init(self) -> None:
"""
Init method for handler.
(I'd assume this is related to the subclassing of NSObject for why its not __init__)
"""
self = objc.super(EventHandler, self).init()
NSAppleEventManager.sharedAppleEventManager().setEventHandler_andSelector_forEventClass_andEventID_(
self,
'handleEvent:withReplyEvent:',
kInternetEventClass,
kAEGetURL
)
return self
def handleEvent_withReplyEvent_(self, event, replyEvent) -> None: # noqa: N802 N803 # Required to override
"""Actual event handling from NSAppleEventManager."""
protocolhandler.lasturl = parse.unquote(
event.paramDescriptorForKeyword_(keyDirectObject).stringValue()
).strip()
protocolhandler.master.after(DarwinProtocolHandler.POLL, protocolhandler.poll)
elif (config.auth_force_edmc_protocol
or (
sys.platform == 'win32'
and getattr(sys, 'frozen', False)
and not is_wine
and not config.auth_force_localserver
)):
if (config.auth_force_edmc_protocol # noqa: C901
or (
sys.platform == 'win32'
and getattr(sys, 'frozen', False)
and not is_wine
and not config.auth_force_localserver
)):
# This could be false if you use auth_force_edmc_protocol, but then you get to keep the pieces
assert sys.platform == 'win32'
# spell-checker: words HBRUSH HICON WPARAM wstring WNDCLASS HMENU HGLOBAL
@ -245,11 +187,11 @@ elif (config.auth_force_edmc_protocol
# which we can read out as shown below, and then compare.
target_is_valid = lparam_low == 0 or (
GlobalGetAtomNameW(lparam_low, service, 256) and service.value == appname
GlobalGetAtomNameW(lparam_low, service, 256) and service.value == appname
)
topic_is_valid = lparam_high == 0 or (
GlobalGetAtomNameW(lparam_high, topic, 256) and topic.value.lower() == 'system'
GlobalGetAtomNameW(lparam_high, topic, 256) and topic.value.lower() == 'system'
)
if target_is_valid and topic_is_valid:
@ -480,12 +422,9 @@ def get_handler_impl() -> Type[GenericProtocolHandler]:
:return: An instantiatable GenericProtocolHandler
"""
if sys.platform == 'darwin' and getattr(sys, 'frozen', False):
return DarwinProtocolHandler # pyright: reportUnboundVariable=false
if (
(sys.platform == 'win32' and config.auth_force_edmc_protocol)
or (getattr(sys, 'frozen', False) and not is_wine and not config.auth_force_localserver)
(sys.platform == 'win32' and config.auth_force_edmc_protocol)
or (getattr(sys, 'frozen', False) and not is_wine and not config.auth_force_localserver)
):
return WindowsProtocolHandler

View File

@ -23,6 +23,3 @@ sys-platform-not-darwin = "sys_platform == 'darwin'"
sys-platform-linux = "sys_platform != 'linux'"
sys-platform-not-linux = "sys_platform == 'linux'"
sys-platform-not-known = "sys_platform in ('darwin', 'linux', 'win32')"
[tool.pyright]
# pythonPlatform = 'Darwin'

View File

@ -10,6 +10,3 @@ infi.systray==0.1.12; sys_platform == 'win32'
# argh==0.26.2 watchdog dep
# pyyaml==5.3.1 watchdog dep
semantic-version==2.10.0
# Base requirement for MacOS
pyobjc; sys_platform == 'darwin'

BIN
ships.p

Binary file not shown.

View File

@ -374,7 +374,7 @@ class StatsResults(tk.Toplevel):
self.transient(parent)
# position over parent
if sys.platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
if parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
self.geometry(f"+{parent.winfo_rootx()}+{parent.winfo_rooty()}")
# remove decoration
@ -382,10 +382,6 @@ class StatsResults(tk.Toplevel):
if sys.platform == 'win32':
self.attributes('-toolwindow', tk.TRUE)
elif sys.platform == 'darwin':
# http://wiki.tcl.tk/13428
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
frame = ttk.Frame(self)
frame.grid(sticky=tk.NSEW)
@ -423,13 +419,6 @@ class StatsResults(tk.Toplevel):
ttk.Frame(page).grid(pady=5) # bottom spacer
notebook.add(page, text=_('Ships')) # LANG: Status dialog title
if sys.platform != 'darwin':
buttonframe = ttk.Frame(frame)
buttonframe.grid(padx=10, pady=(0, 10), sticky=tk.NSEW) # type: ignore # the tuple is supported
buttonframe.columnconfigure(0, weight=1)
ttk.Label(buttonframe).grid(row=0, column=0) # spacer
ttk.Button(buttonframe, text='OK', command=self.destroy).grid(row=0, column=1, sticky=tk.E)
# wait for window to appear on screen before calling grab_set
self.wait_visibility()
self.grab_set()

3
td.py
View File

@ -1,7 +1,6 @@
"""Export data for Trade Dangerous."""
import pathlib
import sys
import time
from collections import defaultdict
from operator import itemgetter
@ -32,7 +31,7 @@ def export(data: CAPIData) -> None:
with open(data_path / data_filename, 'wb') as h:
# Format described here: https://github.com/eyeonus/Trade-Dangerous/wiki/Price-Data
h.write('#! trade.py import -\n'.encode('utf-8'))
this_platform = "Mac OS" if sys.platform == 'darwin' else system()
this_platform = system()
cmdr_name = data['commander']['name'].strip()
h.write(
f'# Created by {applongname} {appversion()} on {this_platform} for Cmdr {cmdr_name}.\n'.encode('utf-8')

View File

@ -5,21 +5,15 @@ import numbers
import sys
import warnings
from configparser import NoOptionError
from os import getenv, makedirs, mkdir, pardir
from os.path import dirname, expanduser, isdir, join, normpath
from os import getenv, makedirs, mkdir
from os.path import dirname, expanduser, isdir, join
from typing import TYPE_CHECKING
from config import applongname, appname, update_interval
from EDMCLogging import get_main_logger
logger = get_main_logger()
if sys.platform == 'darwin':
from Foundation import ( # type: ignore
NSApplicationSupportDirectory, NSBundle, NSDocumentDirectory, NSSearchPathForDirectoriesInDomains,
NSUserDefaults, NSUserDomainMask
)
elif sys.platform == 'win32':
if sys.platform == 'win32':
import ctypes
import uuid
from ctypes.wintypes import DWORD, HANDLE, HKEY, LONG, LPCVOID, LPCWSTR
@ -115,91 +109,7 @@ class OldConfig:
OUT_EDDN_DELAY = 4096
OUT_STATION_ANY = OUT_EDDN_SEND_STATION_DATA | OUT_MKT_TD | OUT_MKT_CSV
if sys.platform == 'darwin': # noqa: C901 # It's gating *all* the functions
def __init__(self):
self.app_dir = join(
NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], appname
)
if not isdir(self.app_dir):
mkdir(self.app_dir)
self.plugin_dir = join(self.app_dir, 'plugins')
if not isdir(self.plugin_dir):
mkdir(self.plugin_dir)
if getattr(sys, 'frozen', False):
self.internal_plugin_dir = normpath(join(dirname(sys.executable), pardir, 'Library', 'plugins'))
self.respath = normpath(join(dirname(sys.executable), pardir, 'Resources'))
self.identifier = NSBundle.mainBundle().bundleIdentifier()
else:
self.internal_plugin_dir = join(dirname(__file__), 'plugins')
self.respath = dirname(__file__)
# Don't use Python's settings if interactive
self.identifier = f'uk.org.marginal.{appname.lower()}'
NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier
self.default_journal_dir: str | None = join(
NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0],
'Frontier Developments',
'Elite Dangerous'
)
self.home = expanduser('~')
self.defaults = NSUserDefaults.standardUserDefaults()
self.settings = dict(self.defaults.persistentDomainForName_(self.identifier) or {}) # make writeable
# Check out_dir exists
if not self.get('outdir') or not isdir(str(self.get('outdir'))):
self.set('outdir', NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, True)[0])
def get(self, key: str, default: None | list | str = None) -> None | list | str:
"""Look up a string configuration value."""
val = self.settings.get(key)
if val is None:
return default
if isinstance(val, str):
return str(val)
if isinstance(val, list):
return list(val) # make writeable
return default
def getint(self, key: str, default: int = 0) -> int:
"""Look up an integer configuration value."""
try:
return int(self.settings.get(key, default)) # should already be int, but check by casting
except ValueError as e:
logger.error(f"Failed to int({key=})", exc_info=e)
return default
except Exception as e:
logger.debug('The exception type is ...', exc_info=e)
return default
def set(self, key: str, val: int | str | list) -> None:
"""Set value on the specified configuration key."""
self.settings[key] = val
def delete(self, key: str) -> None:
"""Delete the specified configuration key."""
self.settings.pop(key, None)
def save(self) -> None:
"""Save current configuration to disk."""
self.defaults.setPersistentDomain_forName_(self.settings, self.identifier)
self.defaults.synchronize()
def close(self) -> None:
"""Close the configuration."""
self.save()
self.defaults = None
elif sys.platform == 'win32':
if sys.platform == 'win32': # noqa: C901
def __init__(self):
self.app_dir = join(known_folder_path(FOLDERID_LocalAppData), appname) # type: ignore

View File

@ -150,7 +150,7 @@ class _Theme:
# the widget has explicit fg or bg attributes.
assert isinstance(widget, (tk.BitmapImage, tk.Widget)), widget
if not self.defaults:
# Can't initialise this til window is created # Windows, MacOS
# Can't initialise this til window is created # Windows
self.defaults = {
'fg': tk.Label()['foreground'], # SystemButtonText, systemButtonText
'bg': tk.Label()['background'], # SystemButtonFace, White
@ -268,8 +268,7 @@ class _Theme:
# (Mostly) system colors
style = ttk.Style()
self.current = {
'background': (sys.platform == 'darwin' and 'systemMovableModalBackground' or
style.lookup('TLabel', 'background')),
'background': (style.lookup('TLabel', 'background')),
'foreground': style.lookup('TLabel', 'foreground'),
'activebackground': (sys.platform == 'win32' and 'SystemHighlight' or
style.lookup('TLabel', 'background', ['active'])),
@ -366,8 +365,6 @@ class _Theme:
if 'bg' not in attribs:
widget['background'] = self.current['background']
widget['activebackground'] = self.current['activebackground']
if sys.platform == 'darwin' and isinstance(widget, tk.Button):
widget['highlightbackground'] = self.current['background']
if 'font' not in attribs:
widget['font'] = self.current['font']
@ -426,21 +423,7 @@ class _Theme:
return # Don't need to mess with the window manager
self.active = theme
if sys.platform == 'darwin':
from AppKit import NSAppearance, NSApplication, NSMiniaturizableWindowMask, NSResizableWindowMask
root.update_idletasks() # need main window to be created
if theme == self.THEME_DEFAULT:
appearance = NSAppearance.appearanceNamed_('NSAppearanceNameAqua')
else: # Dark (Transparent only on win32)
appearance = NSAppearance.appearanceNamed_('NSAppearanceNameDarkAqua')
for window in NSApplication.sharedApplication().windows():
window.setStyleMask_(window.styleMask() & ~(
NSMiniaturizableWindowMask | NSResizableWindowMask)) # disable zoom
window.setAppearance_(appearance)
elif sys.platform == 'win32':
if sys.platform == 'win32':
GWL_STYLE = -16 # noqa: N806 # ctypes
WS_MAXIMIZEBOX = 0x00010000 # noqa: N806 # ctypes
# tk8.5.9/win/tkWinWm.c:342

View File

@ -22,7 +22,6 @@ from __future__ import annotations
import sys
import tkinter as tk
import warnings
import webbrowser
from tkinter import font as tk_font
from tkinter import ttk
@ -32,8 +31,7 @@ if TYPE_CHECKING:
def _(x: str) -> str: return x
# FIXME: Split this into multi-file module to separate the platforms
class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # type: ignore
class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore
"""Clickable label for HTTP links."""
def __init__(self, master: ttk.Frame | tk.Frame | None = None, **kw: Any) -> None:
@ -51,22 +49,14 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # typ
self.foreground = kw.get('foreground', 'blue')
self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup(
'TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option
if sys.platform == 'darwin':
# Use tk.Label 'cos can't set ttk.Label background - http://www.tkdocs.com/tutorial/styles.html#whydifficult
kw['background'] = kw.pop('background', 'systemDialogBackgroundActive')
kw['anchor'] = kw.pop('anchor', tk.W) # like ttk.Label
tk.Label.__init__(self, master, **kw)
else:
ttk.Label.__init__(self, master, **kw)
ttk.Label.__init__(self, master, **kw)
self.bind('<Button-1>', self._click)
self.menu = tk.Menu(tearoff=tk.FALSE)
# LANG: Label for 'Copy' as in 'Copy and Paste'
self.menu.add_command(label=_('Copy'), command=self.copy) # As in Copy and Paste
self.bind(sys.platform == 'darwin' and '<Button-2>' or '<Button-3>', self._contextmenu)
self.bind('<Button-3>', self._contextmenu)
self.bind('<Enter>', self._enter)
self.bind('<Leave>', self._leave)
@ -107,10 +97,9 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # typ
if state == tk.DISABLED:
kw['cursor'] = 'arrow' # System default
elif self.url and (kw['text'] if 'text' in kw else self['text']):
kw['cursor'] = 'pointinghand' if sys.platform == 'darwin' else 'hand2'
kw['cursor'] = 'hand2'
else:
kw['cursor'] = 'notallowed' if sys.platform == 'darwin' else (
'no' if sys.platform == 'win32' else 'circle')
kw['cursor'] = ('no' if sys.platform == 'win32' else 'circle')
return super().configure(cnf, **kw)
@ -140,50 +129,9 @@ class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label): # typ
def _contextmenu(self, event: tk.Event) -> None:
if self['text'] and (self.popup_copy(self['text']) if callable(self.popup_copy) else self.popup_copy):
self.menu.post(sys.platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root)
self.menu.post(event.x_root, event.y_root)
def copy(self) -> None:
"""Copy the current text to the clipboard."""
self.clipboard_clear()
self.clipboard_append(self['text'])
def openurl(url: str) -> None:
r"""
Open the given URL in appropriate browser.
2022-12-06:
Firefox itself will gladly attempt to use very long URLs in its URL
input. Up to 16384 was attempted, but the Apache instance this was
tested against only allowed up to 8207 total URL length to pass, that
being 8190 octets of REQUEST_URI (path + GET params).
Testing from Windows 10 Home 21H2 cmd.exe with:
"<path to>\firefox.exe" -osint -url "<test url>"
only allowed 8115 octest of REQUEST_URI to pass through.
Microsoft Edge yielded 8092 octets. Google Chrome yielded 8093 octets.
However, this is actually the limit of how long a CMD.EXE command-line
can be. The URL was being cut off *there*.
The 8207 octet URL makes it through `webbrowser.open(<url>)` to:
Firefox 107.0.1
Microsoft Edge 108.0.1462.42
Google Chrome 108.0.5359.95
This was also tested as working *with* the old winreg/subprocess code,
so it wasn't even suffering from the same limit as CMD.EXE.
Conclusion: No reason to not just use `webbrowser.open()`, as prior
to e280d6c2833c25867b8139490e68ddf056477917 there was a bug, introduced
in 5989acd0d3263e54429ff99769ff73a20476d863, which meant the code always
ended up using `webbrowser.open()` *anyway*.
:param url: URL to open.
"""
warnings.warn("This function is deprecated. "
"Please use `webbrowser.open() instead.", DeprecationWarning, stacklevel=2)
webbrowser.open(url)

View File

@ -7,10 +7,8 @@ See LICENSE file.
"""
from __future__ import annotations
import os
import sys
import threading
from os.path import dirname, join
from traceback import print_exc
from typing import TYPE_CHECKING
from xml.etree import ElementTree
@ -115,21 +113,6 @@ class Updater:
return
if sys.platform == 'darwin':
import objc
try:
objc.loadBundle(
'Sparkle', globals(), join(dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework')
)
# loadBundle presumably supplies `SUUpdater`
self.updater = SUUpdater.sharedUpdater() # noqa: F821
except Exception:
# can't load framework - not frozen or not included in app bundle?
print_exc()
self.updater = None
def set_automatic_updates_check(self, onoroff: bool) -> None:
"""
Set (Win)Sparkle to perform automatic update checks, or not.
@ -142,9 +125,6 @@ class Updater:
if sys.platform == 'win32' and self.updater:
self.updater.win_sparkle_set_automatic_check_for_updates(onoroff)
if sys.platform == 'darwin' and self.updater:
self.updater.SUEnableAutomaticChecks(onoroff)
def check_for_updates(self) -> None:
"""Trigger the requisite method to check for an update."""
if self.use_internal():
@ -155,9 +135,6 @@ class Updater:
elif sys.platform == 'win32' and self.updater:
self.updater.win_sparkle_check_update_with_ui()
elif sys.platform == 'darwin' and self.updater:
self.updater.checkForUpdates_(None)
def check_appcast(self) -> EDMCVersion | None:
"""
Manually (no Sparkle or WinSparkle) check the update_feed appcast file.
@ -184,13 +161,9 @@ class Updater:
return None
if sys.platform == 'darwin':
sparkle_platform = 'macos'
else:
# For *these* purposes anything else is the same as 'windows', as
# non-win32 would be running from source.
sparkle_platform = 'windows'
# For *these* purposes all systems are the same as 'windows', as
# non-win32 would be running from source.
sparkle_platform = 'windows'
for item in feed.findall('channel/item'):
# xml is a pain with types, hence these ignores