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

Merge branch 'develop' into enhancement/2188/translation-override

This commit is contained in:
aussig 2024-04-28 07:10:27 +01:00
commit 4e802b46df
55 changed files with 1347 additions and 1448 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

@ -155,7 +155,7 @@ jobs:
run: sha256sum EDMarketConnector_Installer_*.exe EDMarketConnector-release-*.{zip,tar.gz} > ./hashes.sum
- name: Create Draft Release
uses: "softprops/action-gh-release@v1"
uses: "softprops/action-gh-release@v2"
with:
token: "${{secrets.GITHUB_TOKEN}}"
draft: 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

@ -6,6 +6,30 @@ This is the master changelog for Elite Dangerous Market Connector. Entries are
in the source (not distributed with the Windows installer) for the
currently used version.
---
Release 5.10.4
===
This release contains updated dependencies, modules files, translations, and adds two new EDDN schemas. It also
adds Turkish translations to EDMC!
We now sign our code! This does mean that built EXEs are now slightly modified on our developer's machines.
For information on what this means, and opt-out options, please visit https://github.com/EDCD/EDMarketConnector/wiki/Code-Signing-and-EDMC
**Changes and Enhancements**
* Adds Turkish Translations to EDMC
* Adds DockingDenied and DockingGranted EDDN Schemas
* Updated FDevIDs Dependency
* Updated Translations
* Updated modules files to process several missing module types used for bug squishing or going fast
* Updated Python Dependencies
**Bug Fixes**
* Fixed a bug on older Python versions which couldn't import updated type annotations
**Plugin Developers**
* modules.p and ships.p are deprecated, and slated for removal in 5.11+!
* The `openurl()` function in ttkHyperlinkLabel has been deprecated,
and slated for removal in 5.11+! Please migrate to `webbrowser.open()`.
Release 5.10.3
===
This release contains a bugfix for the shipyard outfitting parsing system and an update to the French translations.

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

@ -1 +1 @@
Subproject commit 7205c79331f42c1a28b757b27467f79ff106716b
Subproject commit 9b3f40612017b43a8b826017e1e2befebd9074f2

View File

@ -178,13 +178,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Výchozí";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Auto";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normal";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

View File

@ -231,6 +231,18 @@
/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */
"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Eins oder mehr deiner aktivierten Plugins konnte nicht geladen werden. Du kannst dir die Liste im '{PLUGINS}'-Tab unter '{FILE}' > '{SETTINGS}' ansehen. Dies könnte an einer falschen Ordnerstruktur liegen. Die Datei load.py sollte sich unter plugins/PLUGIN_NAME/load.py befinden.\n\nDu kannst ein Plugin deaktivieren, indem du dessen Ordner ein '{DISABLED}' am Ende des Namens anhängst.";
/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */
"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Einer oder mehrere deiner URL-Anbieter waren ungültig und wurden zurückgesetzt:\n\n";
/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */
"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} war zuvor auf {OLDPROV} festgelegt und wurde zu {NEWPROV} zurückgesetzt.\n";
/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */
"EDMC: Default Providers Reset" = "EDMC: Standardanbieter wurden zurückgesetzt";
/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */
"Awaiting Full CMDR Login" = "Warte auf vollständige CMDR Anmeldung";
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */
"Journal directory already locked" = "Journal-Ordner bereits gesperrt";
@ -246,13 +258,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Standard";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Auto";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normal";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */
@ -768,3 +780,5 @@
/* stats.py: Status dialog title; In files: stats.py:418; */
"Ships" = "Schiffe";
/* update.py: Update Available Text; In files: update.py:229; */
"{NEWVER} is available" = "{NEWVER} ist verfügbar";

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";
@ -802,4 +778,7 @@
"Ships" = "Ships";
/* update.py: Update Available Text; In files: update.py:229; */
"{NEWVER} is available" = "{NEWVER} is available";
"{NEWVER} is available" = "{NEWVER} is available";
/* myNotebook.py: Can't Paste Images or Files in Text; */
"Cannot paste non-text content." = "Cannot paste non-text content.";

View File

@ -184,13 +184,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Por defecto";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Auto";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normal";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

View File

@ -231,6 +231,18 @@
/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */
"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Impossibile caricare uno o più plugin abilitati. Consulta l'elenco nella scheda '{PLUGINS}' di '{FILE}' > '{SETTINGS}'. Ciò potrebbe essere causato da un'errata struttura delle cartelle. Il file load.py dovrebbe trovarsi in plugins/PLUGIN_NAME/load.py.\n\nPuoi disabilitare un plugin rinominando la sua cartella in modo che abbia '{DISABLED}' alla fine del nome.";
/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */
"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Uno o più Provider URL non erano validi e sono stati ripristinati:\n";
/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */
"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} era impostato su {OLDPROV} ed è stato reimpostato su {NEWPROV}\n";
/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */
"EDMC: Default Providers Reset" = "EDMC: Reset dei provider predefiniti";
/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */
"Awaiting Full CMDR Login" = "In attesa del login completo al CMDR";
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */
"Journal directory already locked" = "cartella Journal già bloccata";
@ -246,13 +258,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Predefinito";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Auto";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normale";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */
@ -789,3 +801,5 @@
/* stats.py: Status dialog title; In files: stats.py:418; */
"Ships" = "Navi";
/* update.py: Update Available Text; In files: update.py:229; */
"{NEWVER} is available" = "{NEWVER} è disponibile";

View File

@ -246,13 +246,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "デフォルト";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "自動";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "通常版";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "ベータ版";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

View File

@ -202,13 +202,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "기본값";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "자동";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "일반";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "베타";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

View File

@ -234,13 +234,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Domyślne";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Auto";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normalny";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

View File

@ -234,13 +234,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Padrão";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Automático";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normal";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

View File

@ -246,13 +246,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Predefinido";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Auto";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normal";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

View File

@ -213,12 +213,36 @@
/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */
"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Один или несколько ваших подключённых плагинов ещё не имеют поддержки Python 3.x. Пожалуйста, ознакомьтесь со списком во вкладке '{PLUGINS}' '{FILE}' > '{SETTINGS}'. Вы должны проверить наличие обновлённой версии, в противном случае предупредите разработчика о необходимости обновления кода на Python 3.x.\n\nВы можете отключить плагин, переименовав его папку в '{DISABLED}'.";
/* EDMarketConnector.py: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */
"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "FDevID файлы не найдены! Некоторые функции, связанные с товарами, могут быть отключены.\n\nВы хотите открыть страницу Wiki о том, как настроить вспомогательные модули?";
/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */
"FDevIDs: Missing Commodity Files" = "FDevIDs: отсутствуют файлы товаров";
/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */
"Plugins" = "Плагины";
/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */
"EDMC: Plugins Without Python 3.x Support" = "EDMC: Без поддержки Python 3.x";
/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */
"EDMC: Broken Plugins" = "EDMC: Неработающие плагины";
/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */
"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Не удалось загрузить один или несколько включенных плагинов. Пожалуйста, посмотрите список на вкладке '{PLUGINS}' в разделе '{FILE}' > '{SETTINGS}'. Причиной может быть неправильная структура папок. Файл load.py должен находиться в папке plugins/PLUGIN_NAME/load.py.\n\nВы можете отключить плагин, переименовав его папку так, чтобы в конце названия стояло '{DISABLED}'.";
/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */
"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Один или несколько ваших URL-провайдеров оказались некорректными и были сброшены:";
/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */
"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} был установлен на {OLDPROV} и был сброшен на {NEWPROV}";
/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */
"EDMC: Default Providers Reset" = "EDMC: сброс провайдеров по умолчанию";
/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */
"Awaiting Full CMDR Login" = "Ожидание полного входа в систему";
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */
"Journal directory already locked" = "Каталог журнала уже заблокирован";
@ -234,13 +258,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "По умолчанию";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Автоматический";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Стандартный";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Бета";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */
@ -471,6 +495,9 @@
/* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */
"Information on migrating plugins" = "Информация о мигрирующих плагинах";
/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */
"Broken Plugins" = "Неработающие плагины";
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Отключенные плагины";
@ -774,3 +801,5 @@
/* stats.py: Status dialog title; In files: stats.py:418; */
"Ships" = "Корабли";
/* update.py: Update Available Text; In files: update.py:229; */
"{NEWVER} is available" = "{NEWVER} доступен";

View File

@ -246,13 +246,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Standardna";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Automatski";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normalni";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

View File

@ -231,6 +231,18 @@
/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */
"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Jedan ili više vaših aktivnih pluginova se nije učitao. Molimo pogledajte listu na '{PLUGINS}' tabu u okviru '{FILE}' > '{SETTINGS}'. Obo vi moglo da se desi zbog pogrešne strukture foldera. load.py fajl bi trebao da se nalazi unutar plugins/PLUGIN_NAME/load.py.\n\nPojedinačni plugin možete deaktivirati promenom imena njegovog foldera dodavanjem '{DISABLED}' na kraju.";
/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */
"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "Jedan ili više vaših URL Provajdera su pogrešni, pa su resetovani:\n";
/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */
"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} je bio postavljen na {OLDPROV} i sada je resetovan na {NEWPROV}\n";
/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */
"EDMC: Default Providers Reset" = "EDMC: Podrazumevani Provajderi resetovani";
/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */
"Awaiting Full CMDR Login" = "Čekam da se CMDR potpuno uloguje";
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */
"Journal directory already locked" = "Žurnal direktorijum je već zaključan";
@ -246,13 +258,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Podrazumevano";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Auto";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normal";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */
@ -789,3 +801,5 @@
/* stats.py: Status dialog title; In files: stats.py:418; */
"Ships" = "Brodovi";
/* update.py: Update Available Text; In files: update.py:229; */
"{NEWVER} is available" = "{NEWVER} je dostupna";

805
L10n/tr.strings Normal file
View File

@ -0,0 +1,805 @@
/* edsm.py:Settings>EDSM - Label on checkbox for 'send data'; In files: edsm.py:316; */
"Send flight log and CMDR status to EDSM" = "Uçuş günlüğünü ve CMDR durumunu EDSM'e gönder";
/* prefs.py:Label on button used to open a filesystem folder; In files: prefs.py:706; */
"Open Log Folder" = "Günlük Klasörünü Aç";
/* inara.py:Text Inara Show API key; In files: inara.py:305; */
"Show API Key" = "API Anahtarını Göster";
/* Language name */
"!Language" = "Türkçe";
/* companion.py: Frontier CAPI didn't respond; In files: companion.py:226; */
"Error: Frontier CAPI didn't respond" = "Hata: Frontier CAPI yanıt vermedi";
/* companion.py: Frontier CAPI data doesn't agree with latest Journal game location; In files: companion.py:245; */
"Error: Frontier server is lagging" = "Hata: Frontier sunucusu gecikmesi";
/* companion.py: Commander is docked at an EDO settlement, got out and back in, we forgot the station; In files: companion.py:261; */
"Docked but unknown station: EDO Settlement?" = "Kenetlendi ancak bilinmeyen istasyon: EDO yerleşimi?";
/* companion.py: Generic "something went wrong with Frontier Auth" error; In files: companion.py:271; */
"Error: Invalid Credentials" = "Hata: Geçersiz Kimlik";
/* companion.py: Frontier CAPI authorisation not for currently game-active commander; In files: companion.py:296; */
"Error: Wrong Cmdr" = "Hata: Yanlış CMDR";
/* companion.py: Generic error prefix - following text is from Frontier auth service; In files: companion.py:432; companion.py:517; */
"Error" = "Hata";
/* companion.py: Frontier auth, no 'usr' section in returned data; companion.py: Frontier auth, no 'customer_id' in 'usr' section in returned data; In files: companion.py:475; companion.py:480; */
"Error: Couldn't check token customer_id" = "Hata: token kontrol edilemedi customer_id";
/* companion.py: Frontier auth customer_id doesn't match game session FID; In files: companion.py:486; */
"Error: customer_id doesn't match!" = "Hata: customer_id uyuşmuyor!";
/* companion.py: Failed to get Access Token from Frontier Auth service; In files: companion.py:508; */
"Error: unable to get token" = "Hata: token erişilemedi";
/* companion.py: Frontier CAPI returned 418, meaning down for maintenance; In files: companion.py:844; */
"Frontier CAPI down for maintenance" = "Frontier CAPI bakım için kapalı durumda";
/* companion.py: Frontier CAPI data retrieval failed; In files: companion.py:856; */
"Frontier CAPI query failure" = "Frontier CAPI sorgulama hatası";
/* EDMarketConnector.py: Main UI Update button; EDMarketConnector.py: Update button in main window; In files: EDMarketConnector.py:601; EDMarketConnector.py:919; EDMarketConnector.py:1748; */
"Update" = "Güncelle";
/* EDMarketConnector.py: Appearance - Label for checkbox to select if application always on top; prefs.py: Appearance - Label for checkbox to select if application always on top; In files: EDMarketConnector.py:710; prefs.py:875; */
"Always on top" = "Her zaman üstte";
/* EDMarketConnector.py: Unknown suit; In files: EDMarketConnector.py:837; */
"Unknown" = "Bilinmeyen";
/* EDMarketConnector.py: ED Journal file location appears to be in error; In files: EDMarketConnector.py:906; */
"Error: Check E:D journal file location" = "Hata: E:D günlük dosyası konumunu kontrol edin";
/* EDMarketConnector.py: Label for commander name in main window; edsm.py: Game Commander name label in EDSM settings; stats.py: Cmdr stats; theme.py: Label for commander name in main window; In files: EDMarketConnector.py:913; edsm.py:332; stats.py:57; theme.py:290; */
"Cmdr" = "Cmdr";
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: Multicrew role label in main window; In files: EDMarketConnector.py:915; EDMarketConnector.py:1487; */
"Role" = "Rol";
/* EDMarketConnector.py: 'Ship' or multi-crew role label in main window, as applicable; EDMarketConnector.py: 'Ship' label in main UI; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:915; EDMarketConnector.py:1497; EDMarketConnector.py:1520; stats.py:405; */
"Ship" = "Gemi";
/* EDMarketConnector.py: Label for 'Suit' line in main UI; In files: EDMarketConnector.py:916; */
"Suit" = "Süit";
/* EDMarketConnector.py: Label for 'System' line in main UI; prefs.py: Configuration - Label for selection of 'System' provider website; stats.py: Main window; In files: EDMarketConnector.py:917; prefs.py:614; stats.py:407; */
"System" = "Sistem";
/* EDMarketConnector.py: Label for 'Station' line in main UI; prefs.py: Configuration - Label for selection of 'Station' provider website; prefs.py: Appearance - Example 'Normal' text; stats.py: Status dialog subtitle; In files: EDMarketConnector.py:918; prefs.py:632; prefs.py:770; stats.py:408; */
"Station" = "İstasyon";
/* EDMarketConnector.py: 'File' menu title on OSX; EDMarketConnector.py: 'File' menu title; EDMarketConnector.py: 'File' menu; In files: EDMarketConnector.py:921; EDMarketConnector.py:939; EDMarketConnector.py:942; EDMarketConnector.py:2264; */
"File" = "Dosya";
/* EDMarketConnector.py: 'Edit' menu title on OSX; EDMarketConnector.py: 'Edit' menu title; In files: EDMarketConnector.py:922; EDMarketConnector.py:940; EDMarketConnector.py:943; */
"Edit" = "Düzenle";
/* EDMarketConnector.py: 'View' menu title on OSX; In files: EDMarketConnector.py:923; */
"View" = "Görüntüle";
/* EDMarketConnector.py: 'Window' menu title on OSX; In files: EDMarketConnector.py:924; */
"Window" = "Pencere";
/* EDMarketConnector.py: Help' menu title on OSX; EDMarketConnector.py: 'Help' menu title; In files: EDMarketConnector.py:925; EDMarketConnector.py:941; EDMarketConnector.py:944; */
"Help" = "Yardım";
/* EDMarketConnector.py: App menu entry on OSX; EDMarketConnector.py: Help > About App; In files: EDMarketConnector.py:928; EDMarketConnector.py:959; EDMarketConnector.py:1804; */
"About {APP}" = "Hakkında {APP}";
/* EDMarketConnector.py: Help > Check for Updates...; In files: EDMarketConnector.py:930; EDMarketConnector.py:958; */
"Check for Updates..." = "Güncellemeleri Denetle...";
/* EDMarketConnector.py: File > Save Raw Data...; In files: EDMarketConnector.py:931; EDMarketConnector.py:948; */
"Save Raw Data..." = "Ham Verileri Kaydet...";
/* EDMarketConnector.py: File > Status; stats.py: Status dialog title; In files: EDMarketConnector.py:932; EDMarketConnector.py:947; stats.py:402; */
"Status" = "Durum";
/* EDMarketConnector.py: Help > Documentation; In files: EDMarketConnector.py:933; EDMarketConnector.py:953; */
"Documentation" = "Dokümantasyon";
/* EDMarketConnector.py: Help > Troubleshooting; In files: EDMarketConnector.py:934; EDMarketConnector.py:954; */
"Troubleshooting" = "Sorun giderme";
/* EDMarketConnector.py: Help > Report A Bug; In files: EDMarketConnector.py:935; EDMarketConnector.py:955; */
"Report A Bug" = "Hata Bildir";
/* EDMarketConnector.py: Help > Privacy Policy; In files: EDMarketConnector.py:936; EDMarketConnector.py:956; */
"Privacy Policy" = "Gizlilik Politikası";
/* EDMarketConnector.py: Help > Release Notes; In files: EDMarketConnector.py:937; EDMarketConnector.py:957; EDMarketConnector.py:1838; */
"Release Notes" = "Sürüm notları";
/* EDMarketConnector.py: File > Settings; prefs.py: File > Settings (macOS); In files: EDMarketConnector.py:949; EDMarketConnector.py:2265; prefs.py:241; */
"Settings" = "Ayarlar";
/* EDMarketConnector.py: File > Exit; In files: EDMarketConnector.py:950; */
"Exit" = "Çıkış";
/* EDMarketConnector.py: Label for 'Copy' as in 'Copy and Paste'; ttkHyperlinkLabel.py: Label for 'Copy' as in 'Copy and Paste'; In files: EDMarketConnector.py:962; ttkHyperlinkLabel.py:53; */
"Copy" = "Kopyala";
/* EDMarketConnector.py: CAPI auth aborted because of killswitch; EDMarketConnector.py: CAPI auth query aborted because of killswitch; In files: EDMarketConnector.py:973; EDMarketConnector.py:1067; */
"CAPI auth disabled by killswitch" = "CAPI kimlik doğrulaması killswitch tarafından devre dışı bırakıldı";
/* EDMarketConnector.py: Status - Attempting to get a Frontier Auth Access Token; In files: EDMarketConnector.py:978; */
"Logging in..." = "Giriş yapılıyor...";
/* EDMarketConnector.py: Successfully authenticated with the Frontier website; In files: EDMarketConnector.py:994; EDMarketConnector.py:1657; */
"Authentication successful" = "Kimlik doğrulama başarılı";
/* EDMarketConnector.py: Player is not docked at a station, when we expect them to be; In files: EDMarketConnector.py:1025; */
"You're not docked at a station!" = "Bir istasyona kenetli değilsiniz!";
/* EDMarketConnector.py: Status - Either no market or no modules data for station from Frontier CAPI; In files: EDMarketConnector.py:1033; */
"Station doesn't have anything!" = "İstasyon içeriği mevcut değil.";
/* EDMarketConnector.py: Status - No station market data from Frontier CAPI; In files: EDMarketConnector.py:1038; */
"Station doesn't have a market!" = "İstasyon'da market bulunmuyor!";
/* EDMarketConnector.py: CAPI queries aborted because Cmdr name is unknown; EDMarketConnector.py: CAPI fleetcarrier query aborted because Cmdr name is unknown; In files: EDMarketConnector.py:1077; EDMarketConnector.py:1164; */
"CAPI query aborted: Cmdr name unknown" = "CAPI sorgusu iptal edildi: Cmdr adı bilinmiyor";
/* EDMarketConnector.py: CAPI queries aborted because game mode unknown; In files: EDMarketConnector.py:1083; */
"CAPI query aborted: Game mode unknown" = "CAPI sorgusu iptal edildi: Oyun modu bilinmiyor";
/* EDMarketConnector.py: CAPI queries aborted because GameVersion unknown; EDMarketConnector.py: CAPI fleetcarrier query aborted because GameVersion unknown; In files: EDMarketConnector.py:1089; EDMarketConnector.py:1170; */
"CAPI query aborted: GameVersion unknown" = "CAPI sorgusu iptal edildi: GameVersion bilinmiyor";
/* EDMarketConnector.py: CAPI queries aborted because current star system name unknown; In files: EDMarketConnector.py:1095; */
"CAPI query aborted: Current system unknown" = "CAPI sorgusu iptal edildi: Mevcut sistem bilinmiyor";
/* EDMarketConnector.py: CAPI queries aborted because player is in multi-crew on other Cmdr's ship; In files: EDMarketConnector.py:1101; */
"CAPI query aborted: In other-ship multi-crew" = "CAPI sorgusu iptal edildi: Başka gemi çoklu-mürettebatı";
/* EDMarketConnector.py: CAPI queries aborted because player is in CQC (Arena); In files: EDMarketConnector.py:1107; */
"CAPI query aborted: CQC (Arena) detected" = "CAPI sorgusu iptal edildi: CQC (Arena) tespit edildi";
/* EDMarketConnector.py: Status - Attempting to retrieve data from Frontier CAPI; In files: EDMarketConnector.py:1128; EDMarketConnector.py:1179; */
"Fetching data..." = "Veri işleniyor...";
/* EDMarketConnector.py: CAPI fleetcarrier query aborted because of killswitch; In files: EDMarketConnector.py:1157; */
"CAPI fleetcarrier disabled by killswitch" = "CAPI filo taşıyıcısı killswitch tarafından devre dışı bırakıldı";
/* EDMarketConnector.py: No data was returned for the fleetcarrier from the Frontier CAPI; In files: EDMarketConnector.py:1219; */
"CAPI: No fleetcarrier data returned" = "CAPI: Filo taşıyıcı verisi gelmedi.";
/* EDMarketConnector.py: We didn't have the fleetcarrier callsign when we should have; In files: EDMarketConnector.py:1223; */
"CAPI: Fleetcarrier data incomplete" = "CAPI: Filo taşıyıcı verileri eksik";
/* EDMarketConnector.py: No data was returned for the commander from the Frontier CAPI; In files: EDMarketConnector.py:1242; */
"CAPI: No commander data returned" = "CAPI: Cmdr verisi döndürülmedi";
/* EDMarketConnector.py: We didn't have the commander name when we should have; stats.py: Unknown commander; In files: EDMarketConnector.py:1246; stats.py:333; */
"Who are you?!" = "Sen kimsin?!";
/* EDMarketConnector.py: We don't know where the commander is, when we should; stats.py: Unknown location; In files: EDMarketConnector.py:1252; stats.py:341; */
"Where are you?!" = "Neredesin?!";
/* EDMarketConnector.py: We don't know what ship the commander is in, when we should; stats.py: Unknown ship; In files: EDMarketConnector.py:1259; stats.py:349; */
"What are you flying?!" = "Ne uçuruyorsun?!";
/* EDMarketConnector.py: Frontier CAPI server error when fetching data; In files: EDMarketConnector.py:1384; */
"Frontier CAPI server error" = "Frontier CAPI sunucu hatası";
/* EDMarketConnector.py: Frontier CAPI Access Token expired, trying to get a new one; In files: EDMarketConnector.py:1390; */
"CAPI: Refreshing access token..." = "CAPI: Erişim token yenileniyor...";
/* EDMarketConnector.py: Time when we last obtained Frontier CAPI data; In files: EDMarketConnector.py:1434; */
"Last updated at %H:%M:%S" = "Son güncelleme %H:%M:%S";
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1462; */
"Fighter" = "Avcı";
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1463; */
"Gunner" = "Nişancı";
/* EDMarketConnector.py: Multicrew role; In files: EDMarketConnector.py:1464; */
"Helm" = "Dümen";
/* EDMarketConnector.py: Cooldown on 'Update' button; In files: EDMarketConnector.py:1742; */
"cooldown {SS}s" = "bekleme süresi {SS}s";
/* EDMarketConnector.py: Generic 'OK' button label; prefs.py: 'OK' button on Settings/Preferences window; In files: EDMarketConnector.py:1864; prefs.py:292; */
"OK" = "Tamam";
/* EDMarketConnector.py: The application is shutting down; In files: EDMarketConnector.py:1936; */
"Shutting down..." = "Kapanıyor...";
/* EDMarketConnector.py: Popup-text about 'active' plugins without Python 3.x support; In files: EDMarketConnector.py:2253:2259; */
"One or more of your enabled plugins do not yet have support for Python 3.x. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. You should check if there is an updated version available, else alert the developer that they need to update the code for Python 3.x.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Etkin eklentilerinizden bir veya daha fazlası henüz Python 3.x desteğine sahip değil. '{PLUGINS}' sekmesindeki '{FILE}' > '{SETTINGS}' bölümünde bulunan listeyi bulun. Güncellenmiş bir sürümün mevcut olup olmadığını kontrol edin, aksi takdirde geliştiriciyi Python 3.x kodunu güncellemesi gerektiği konusunda uyarmalısınız.\n\nBir eklentiyi, klasörünü adının sonunda '{DISABLED}' olacak şekilde yeniden adlandırarak devre dışı bırakabilirsiniz.";
/* EDMarketConnector.py: Popup-text about missing FDEVID Files; In files: EDMarketConnector.py:2329; */
"FDevID Files not found! Some functionality regarding commodities may be disabled.\r\n\r\n Do you want to open the Wiki page on how to set up submodules?" = "FDevID Dosyaları bulunamadı! Ürünlerle ilgili bazı işlevler devre dışı bırakılabilir.\n\nAlt modüllerin nasıl kurulacağına ilişkin Wiki sayfasını açmak ister misiniz?";
/* EDMarketConnector.py: Popup window title for missing FDEVID files; In files: EDMarketConnector.py:2340; */
"FDevIDs: Missing Commodity Files" = "FDevIDs: Eksik Ürün Dosyaları";
/* EDMarketConnector.py: Settings > Plugins tab; prefs.py: Label on Settings > Plugins tab; In files: EDMarketConnector.py:2263; prefs.py:986; */
"Plugins" = "Eklentiler";
/* EDMarketConnector.py: Popup window title for list of 'enabled' plugins that don't work with Python 3.x; In files: EDMarketConnector.py:2274; */
"EDMC: Plugins Without Python 3.x Support" = "EDMC: Python 3.x Desteği Olmayan Eklentiler";
/* EDMarketConnector.py: Popup window title for list of 'broken' plugins that failed to load; In files: EDMarketConnector.py:2285; */
"EDMC: Broken Plugins" = "EDMC: Bozuk Eklentiler";
/* EDMarketConnector.py: Popup-text about 'broken' plugins that failed to load; In files: EDMarketConnector.py:2266; */
"One or more of your enabled plugins failed to load. Please see the list on the '{PLUGINS}' tab of '{FILE}' > '{SETTINGS}'. This could be caused by a wrong folder structure. The load.py file should be located under plugins/PLUGIN_NAME/load.py.\r\n\r\nYou can disable a plugin by renaming its folder to have '{DISABLED}' on the end of the name." = "Etkin eklentilerinizden bir veya daha fazlası yüklenemedi. '{PLUGINS}' sekmesindeki '{FILE}' > '{SETTINGS}' bölümünde bulunan listeyi bulun. Sorunun sebebi yanlış klasör yapısı olabilir. Load.py dosyası, plugins/PLUGIN_NAME/load.py altında bulunmalıdır.\n\nBir eklentiyi, klasörünü adının sonunda '{DISABLED}' olacak şekilde yeniden adlandırarak devre dışı bırakabilirsiniz.";
/* EDMarketConnector.py: Popup-text about Reset Providers; In files: EDMarketConnector.py:2146; */
"One or more of your URL Providers were invalid, and have been reset:\r\n\r\n" = "URL Sağlayıcılarınızdan bir veya daha fazlası geçersizdi ve sıfırlandı:";
/* EDMarketConnector.py: Text About What Provider Was Reset; In files: EDMarketConnector.py:2148; */
"{PROVIDER} was set to {OLDPROV}, and has been reset to {NEWPROV}\r\n" = "{PROVIDER} , {OLDPROV}'a ayarlanmıştı ancak {NEWPROV}'a sıfırlandı";
/* EDMarketConnector.py: Popup window title for Reset Providers; In files: EDMarketConnector.py:2161; */
"EDMC: Default Providers Reset" = "EDMC: Varsayılan Sağlayıcıların Sıfırlanması";
/* EDMarketConnector.py: Await Full CMDR Login to Game; In files: EDMarketConnector.py:813; */
"Awaiting Full CMDR Login" = "Tam CMDR Girişi Bekleniyor";
/* journal_lock.py: Title text on popup when Journal directory already locked; In files: journal_lock.py:208; */
"Journal directory already locked" = "Günlük dizini zaten kilitli";
/* journal_lock.py: Text for when newly selected Journal directory is already locked; In files: journal_lock.py:225:226; */
"The new Journal Directory location is already locked.{CR}You can either attempt to resolve this and then Retry, or choose to Ignore this." = "Yeni Günlük Dizini konumu zaten kilitli.{CR}Çözüm için Tekrar deneyebilir veya Yoksay'ı seçebilirsiniz.";
/* journal_lock.py: Generic 'Retry' button label; In files: journal_lock.py:230; */
"Retry" = "Tekrar Dene";
/* journal_lock.py: Generic 'Ignore' button label; In files: journal_lock.py:234; */
"Ignore" = "Yoksay";
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "Varsayılan";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "Otomatik";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "Normal";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "Beta";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */
"Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" = "Coriolis.io gemi ekipman tasarımları ile kullanılacak URL'yi ayarlayın. Link'in '/import?data=' ile bitmesi GEREKTİĞİNİ unutmayın.";
/* coriolis.py: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL; In files: coriolis.py:97; */
"Normal URL" = "Normal URL";
/* coriolis.py: Generic 'Reset' button label; In files: coriolis.py:100; coriolis.py:109; */
"Reset" = "Sıfırla";
/* coriolis.py: Settings>Coriolis: Label for 'alpha/beta game version' URL; In files: coriolis.py:106; */
"Beta URL" = "Beta URL";
/* coriolis.py: Settings>Coriolis: Label for selection of using Normal, Beta or 'auto' Coriolis URL; In files: coriolis.py:116; */
"Override Beta/Normal Selection" = "Beta/Normal Seçimi Geçersiz Kıl";
/* coriolis.py: Settings>Coriolis - invalid override mode found; In files: coriolis.py:156; */
"Invalid Coriolis override mode!" = "Geçersiz Coriolis geçersiz kılma modu!";
/* eddn.py: Error while trying to send data to EDDN; In files: eddn.py:458; eddn.py:2413; eddn.py:2451; eddn.py:2519; */
"Error: Can't connect to EDDN" = "Hata: EDDN'e bağlanılamıyor";
/* eddn.py: EDDN has banned this version of our client; In files: eddn.py:576; */
"EDDN Error: EDMC is too old for EDDN. Please update." = "EDDN Hatası: EDMC sürümü EDDN için çok eski. Lütfen güncelle.";
/* eddn.py: EDDN returned an error that indicates something about what we sent it was wrong; In files: eddn.py:582; */
"EDDN Error: Validation Failed (EDMC Too Old?). See Log" = "EDDN Hatası: Doğrulama Başarısız Oldu (EDMC sürümü eski olabilir?). Günlüğe bakın";
/* eddn.py: EDDN returned some sort of HTTP error, one we didn't expect. {STATUS} contains a number; In files: eddn.py:587; */
"EDDN Error: Returned {STATUS} status code" = "EDDN Hatası: {STATUS} durum kodu ile döndü.";
/* eddn.py: Enable EDDN support for station data checkbox label; In files: eddn.py:2041; */
"Send station data to the Elite Dangerous Data Network" = "İstasyon verilerini Elite Dangerous Data Network'e gönderin";
/* eddn.py: Enable EDDN support for system and other scan data checkbox label; In files: eddn.py:2052; */
"Send system and scan data to the Elite Dangerous Data Network" = "Sistem ve tarama verilerini Elite Dangerous Data Network'e gönderin";
/* eddn.py: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this; In files: eddn.py:2063; */
"Delay sending until docked" = "Kenetlenmeye kadar gönderimi ertele";
/* eddn.py: Killswitch disabled EDDN; In files: eddn.py:2178; */
"EDDN journal handler disabled. See Log." = "EDDN günlük düzenleyici devre dışı. Kayıt geçmişini kontrol edin.";
/* eddn.py: Status text shown while attempting to send data; In files: eddn.py:2507; */
"Sending data to EDDN..." = "Veriler EDDN'e gönderiliyor...";
/* edsm.py: Settings>EDSM - Label on header/URL to EDSM API key page; In files: edsm.py:319; */
"Elite Dangerous Star Map credentials" = "Elite Dangerous Star Map kimlik bilgileri";
/* edsm.py: EDSM Commander name label in EDSM settings; In files: edsm.py:341; */
"Commander Name" = "Cmdr ismi";
/* edsm.py: EDSM API key label; inara.py: Inara API key label; In files: edsm.py:350; inara.py:278; */
"API Key" = "API Anahtarı";
/* edsm.py: We have no data on the current commander; prefs.py: No hotkey/shortcut set; stats.py: No rank; In files: edsm.py:394; prefs.py:527; prefs.py:1157; prefs.py:1190; stats.py:154; stats.py:173; stats.py:192; stats.py:209; */
"None" = "Hiçbiri";
/* edsm.py: EDSM plugin - Journal handling disabled by killswitch; In files: edsm.py:516; */
"EDSM Handler disabled. See Log." = "EDSM düzenleyici devre dışı. Kayıt geçmişini kontrol edin.";
/* edsm.py: EDSM - Only Live data; In files: edsm.py:632; */
"EDSM only accepts Live galaxy data" = "EDSM sadece canlı galaxy verilerini kabul eder";
/* edsm.py: EDSM Plugin - Error message from EDSM API; In files: edsm.py:916; edsm.py:1048; */
"Error: EDSM {MSG}" = "Hata: EDSM {MSG}";
/* edsm.py: EDSM Plugin - Error connecting to EDSM API; In files: edsm.py:953; edsm.py:1043; */
"Error: Can't connect to EDSM" = "Hata: EDSM'e bağlanılamıyor";
/* inara.py: Checkbox to enable INARA API Usage; In files: inara.py:257; */
"Send flight log and Cmdr status to Inara" = "Uçuş günlüğünü ve Cmdr durumunu Inara'ya gönder";
/* inara.py: Text for INARA API keys link ( goes to https://inara.cz/settings-api ); In files: inara.py:269; */
"Inara credentials" = "Inara kimlik bilgileri";
/* inara.py: The Inara API only accepts Live galaxy data, not Legacy galaxy data; inara.py: Inara - Only Live data; In files: inara.py:384; inara.py:386; */
"Inara only accepts Live galaxy data" = "Inara sadece canlı galaxy verilerini kabul eder";
/* inara.py: INARA support disabled via killswitch; In files: inara.py:395; */
"Inara disabled. See Log." = "Inara devre dışı. Kayıt geçmişini kontrol edin.";
/* 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}" = "Hata: Inara {MSG}";
/* prefs.py: File > Preferences menu entry for macOS; In files: prefs.py:237; */
"Preferences" = "Tercihler";
/* prefs.py: Settings > Output - choosing what data to save to files; In files: prefs.py:335; */
"Please choose what data to save" = "Lütfen hangi verilerin kaydedileceğini seçin";
/* prefs.py: Settings > Output option; In files: prefs.py:341; */
"Market data in CSV format file" = "CSV formatında Market verileri";
/* prefs.py: Settings > Output option; In files: prefs.py:350; */
"Market data in Trade Dangerous format file" = "Trade Dangerous formatında Market verileri";
/* prefs.py: Settings > Output option; In files: prefs.py:360; */
"Ship loadout" = "Gemi Ekipmanları";
/* prefs.py: Settings > Output option; In files: prefs.py:370; */
"Automatically update on docking" = "Kenetlenme sırasında otomatik güncelle";
/* prefs.py: Settings > Output - Label for "where files are located"; In files: prefs.py:379; prefs.py:398; */
"File location" = "Dosya konumu";
/* prefs.py: macOS Preferences - files location selection button; In files: prefs.py:387; prefs.py:437; */
"Change..." = "Değiştir...";
/* 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..." = "Gezin...";
/* prefs.py: Label for 'Output' Settings/Preferences tab; In files: prefs.py:405; */
"Output" = "Kayıt";
/* prefs.py: Settings > Configuration - Label for Journal files location; In files: prefs.py:431; prefs.py:446; */
"E:D journal file location" = "E:D günlük dosya konumu";
/* prefs.py: Settings > Configuration - Label for CAPI section; In files: prefs.py:469; */
"CAPI Settings" = "CAPI ayarları";
/* prefs.py: Configuration - Enable or disable the Fleet Carrier CAPI calls; In files: prefs.py:475; */
"Enable Fleetcarrier CAPI Queries" = "Filo Taşıyıcı CAPI sorgulamalarını etkinleştir";
/* prefs.py: Hotkey/Shortcut settings prompt on OSX; In files: prefs.py:490; */
"Keyboard shortcut" = "Klavye Kısayolu";
/* prefs.py: Hotkey/Shortcut settings prompt on Windows; In files: prefs.py:492; */
"Hotkey" = "Kısayoltuşu";
/* prefs.py: macOS Preferences > Configuration - restart the app message; In files: prefs.py:501; */
"Re-start {APP} to use shortcuts" = "Kısayolları kullanmak için {APP}'i yeniden başlat";
/* 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}'nin kısayolları kullanabilmesi için izne ihtiyacı var";
/* prefs.py: Shortcut settings button on OSX; In files: prefs.py:515; */
"Open System Preferences" = "Sistem Tercihlerini Aç";
/* 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" = "Sadece Elite: Dangerous aktif uygulama olduğunda";
/* prefs.py: Configuration - play sound when hotkey used; In files: prefs.py:549; */
"Play sound" = "Ses çal";
/* prefs.py: Configuration - disable checks for app updates when in-game; In files: prefs.py:564; */
"Disable Automatic Application Updates Check when in-game" = "Otomatik Uygulama Güncellemelerini Oyun sırasında Devre Dışı Bırak";
/* prefs.py: Label for preferred shipyard, system and station 'providers'; In files: prefs.py:577; */
"Preferred websites" = "Tercih edilen web siteleri";
/* prefs.py: Label for Shipyard provider selection; In files: prefs.py:588; */
"Shipyard" = "Tersane";
/* prefs.py: Label for checkbox to utilise alternative Coriolis URL method; In files: prefs.py:600; */
"Use alternate URL method" = "Alternatif URL yöntemi kullan";
/* prefs.py: Configuration - Label for selection of Log Level; In files: prefs.py:653; */
"Log Level" = "Günlük Seviyesi";
/* prefs.py: Label for 'Configuration' tab in Settings; In files: prefs.py:681; */
"Configuration" = "Yapılandırma";
/* prefs.py: UI elements privacy section header in privacy tab of preferences; In files: prefs.py:690; */
"Main UI privacy options" = "Ana Arayüz gizlilik seçenekleri";
/* prefs.py: Hide private group owner name from UI checkbox; In files: prefs.py:695; */
"Hide private group name in UI" = "Özel grup ismini arayüzde gizle";
/* prefs.py: Hide multicrew captain name from main UI checkbox; In files: prefs.py:699; */
"Hide multi-crew captain name" = "Çoklu-mürettebat ismini gizle";
/* prefs.py: Preferences privacy tab title; In files: prefs.py:703; */
"Privacy" = "Gizlilik";
/* prefs.py: Label for Settings > Appeareance > selection of 'normal' text colour; In files: prefs.py:716; */
"Normal text" = "Normal metin";
/* prefs.py: Label for Settings > Appeareance > selection of 'highlightes' text colour; In files: prefs.py:718; */
"Highlighted text" = "Vurgulanan metin";
/* prefs.py: Appearance - Label for selection of application display language; In files: prefs.py:727; */
"Language" = "Dil";
/* prefs.py: Label for Settings > Appearance > Theme selection; In files: prefs.py:737; */
"Theme" = "Tema";
/* prefs.py: Label for 'Dark' theme radio button; In files: prefs.py:749; */
"Dark" = "Karanlık";
/* prefs.py: Label for 'Transparent' theme radio button; In files: prefs.py:756; */
"Transparent" = "Şeffaflık";
/* prefs.py: Appearance - Label for selection of UI scaling; In files: prefs.py:802; */
"UI Scale Percentage" = "Arayüz ölçek yüzdesi";
/* prefs.py: Appearance - Help/hint text for UI scaling selection; In files: prefs.py:823; */
"100 means Default{CR}Restart Required for{CR}changes to take effect!" = "100 Varsayılan değerdir. Değişikliklerin geçerli olması için {CR}Yeniden Başlatma Gereklidir{CR}!";
/* prefs.py: Appearance - Label for selection of main window transparency; In files: prefs.py:833; */
"Main window transparency" = "Ana pencere şeffaflığı";
/* prefs.py: Appearance - Help/hint text for Main window transparency selection; In files: prefs.py:853:856; */
"100 means fully opaque.{CR}Window is updated in real time" = "100 tamamen opak anlamına gelir.{CR}Pencere gerçek zamanlı olarak güncellenir";
/* prefs.py: Appearance option for Windows "minimize to system tray"; In files: prefs.py:885; */
"Minimize to system tray" = "Simge durumuna küçült";
/* prefs.py: Label for Settings > Appearance tab; In files: prefs.py:893; */
"Appearance" = "Görünüm";
/* prefs.py: Label for location of third-party plugins folder; In files: prefs.py:908; */
"Plugins folder" = "Eklentiler klasörü";
/* prefs.py: Label on button used to open a filesystem folder; In files: prefs.py:915; */
"Open" = "Aç";
/* prefs.py: Tip/label about how to disable plugins; In files: prefs.py:923; */
"Tip: You can disable a plugin by{CR}adding '{EXT}' to its folder name" = "İpucu: Bir eklentiyi, klasör adına{CR}'{EXT}' ekleyerek devre dışı bırakabilirsiniz";
/* prefs.py: Label on list of enabled plugins; In files: prefs.py:934; */
"Enabled Plugins" = "Etkin Eklentiler";
/* prefs.py: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x; In files: prefs.py:954; */
"Plugins Without Python 3.x Support" = "Python 3.x Desteği Olmayan Eklentiler";
/* prefs.py: Plugins - Label on URL to documentation about migrating plugins from Python 2.7; In files: prefs.py:962; */
"Information on migrating plugins" = "Eklentilerin taşınmasıyla ilgili bilgiler";
/* prefs.py: Plugins - Label for list of 'broken' plugins that failed to load; In files: prefs.py:1039; */
"Broken Plugins" = "Bozuk Eklentiler";
/* prefs.py: Lable on list of user-disabled plugins; In files: prefs.py:977; */
"Disabled Plugins" = "Devre Dışı Eklentiler";
/* stats.py: Cmdr stats; In files: stats.py:58; */
"Balance" = "Bakiye";
/* stats.py: Cmdr stats; In files: stats.py:59; */
"Loan" = "Borç";
/* stats.py: Top rank; In files: stats.py:63; */
"Elite" = "Elite";
/* stats.py: Top rank +1; In files: stats.py:64; */
"Elite I" = "Elite I";
/* stats.py: Top rank +2; In files: stats.py:65; */
"Elite II" = "Elite II";
/* stats.py: Top rank +3; In files: stats.py:66; */
"Elite III" = "Elite III";
/* stats.py: Top rank +4; In files: stats.py:67; */
"Elite IV" = "Elite IV";
/* stats.py: Top rank +5; In files: stats.py:68; */
"Elite V" = "Elite V";
/* stats.py: Ranking; In files: stats.py:74; */
"Combat" = "Combat";
/* stats.py: Ranking; In files: stats.py:75; */
"Trade" = "Trade";
/* stats.py: Ranking; In files: stats.py:76; */
"Explorer" = "Explorer";
/* stats.py: Ranking; In files: stats.py:77; */
"Mercenary" = "Mercenary";
/* stats.py: Ranking; In files: stats.py:78; */
"Exobiologist" = "Exobiologist";
/* stats.py: Ranking; In files: stats.py:79; */
"CQC" = "CQC";
/* stats.py: Ranking; In files: stats.py:80; */
"Federation" = "Federation";
/* stats.py: Ranking; In files: stats.py:81; */
"Empire" = "Empire";
/* stats.py: Ranking; In files: stats.py:82; */
"Powerplay" = "Powerplay";
/* stats.py: Combat rank; In files: stats.py:91; */
"Harmless" = "Harmless";
/* stats.py: Combat rank; In files: stats.py:92; */
"Mostly Harmless" = "Mostly Harmless";
/* stats.py: Combat rank; In files: stats.py:93; */
"Novice" = "Novice";
/* stats.py: Combat rank; In files: stats.py:94; */
"Competent" = "Competent";
/* stats.py: Combat rank; In files: stats.py:95; */
"Expert" = "Expert";
/* stats.py: Combat rank; stats.py: Empire rank; In files: stats.py:96; stats.py:176; */
"Master" = "Master";
/* stats.py: Combat rank; In files: stats.py:97; */
"Dangerous" = "Dangerous";
/* stats.py: Combat rank; In files: stats.py:98; */
"Deadly" = "Deadly";
/* stats.py: Trade rank; In files: stats.py:101; */
"Penniless" = "Penniless";
/* stats.py: Trade rank; In files: stats.py:102; */
"Mostly Penniless" = "Mostly Penniless";
/* stats.py: Trade rank; In files: stats.py:103; */
"Peddler" = "Peddler";
/* stats.py: Trade rank; In files: stats.py:104; */
"Dealer" = "Dealer";
/* stats.py: Trade rank; In files: stats.py:105; */
"Merchant" = "Merchant";
/* stats.py: Trade rank; In files: stats.py:106; */
"Broker" = "Broker";
/* stats.py: Trade rank; In files: stats.py:107; */
"Entrepreneur" = "Entrepreneur";
/* stats.py: Trade rank; In files: stats.py:108; */
"Tycoon" = "Tycoon";
/* stats.py: Explorer rank; In files: stats.py:111; */
"Aimless" = "Aimless";
/* stats.py: Explorer rank; In files: stats.py:112; */
"Mostly Aimless" = "Mostly Aimless";
/* stats.py: Explorer rank; In files: stats.py:113; */
"Scout" = "Scout";
/* stats.py: Explorer rank; In files: stats.py:114; */
"Surveyor" = "Surveyor";
/* stats.py: Explorer rank; In files: stats.py:115; */
"Trailblazer" = "Trailblazer";
/* stats.py: Explorer rank; In files: stats.py:116; */
"Pathfinder" = "Pathfinder";
/* stats.py: Explorer rank; In files: stats.py:117; */
"Ranger" = "Ranger";
/* stats.py: Explorer rank; In files: stats.py:118; */
"Pioneer" = "Pioneer";
/* stats.py: Mercenary rank; In files: stats.py:122; */
"Defenceless" = "Defenceless";
/* stats.py: Mercenary rank; In files: stats.py:123; */
"Mostly Defenceless" = "Mostly Defenceless";
/* stats.py: Mercenary rank; In files: stats.py:124; */
"Rookie" = "Rookie";
/* stats.py: Mercenary rank; In files: stats.py:125; */
"Soldier" = "Soldier";
/* stats.py: Mercenary rank; In files: stats.py:126; stats.py:128; */
"Gunslinger" = "Gunslinger";
/* stats.py: Mercenary rank; In files: stats.py:127; */
"Warrior" = "Warrior";
/* stats.py: Mercenary rank; In files: stats.py:129; */
"Deadeye" = "Deadeye";
/* stats.py: Exobiologist rank; In files: stats.py:132; */
"Directionless" = "Directionless";
/* stats.py: Exobiologist rank; In files: stats.py:133; */
"Mostly Directionless" = "Mostly Directionless";
/* stats.py: Exobiologist rank; In files: stats.py:134; */
"Compiler" = "Compiler";
/* stats.py: Exobiologist rank; In files: stats.py:135; */
"Collector" = "Collector";
/* stats.py: Exobiologist rank; In files: stats.py:136; */
"Cataloguer" = "Cataloguer";
/* stats.py: Exobiologist rank; In files: stats.py:137; */
"Taxonomist" = "Taxonomist";
/* stats.py: Exobiologist rank; In files: stats.py:138; */
"Ecologist" = "Ecologist";
/* stats.py: Exobiologist rank; In files: stats.py:139; */
"Geneticist" = "Geneticist";
/* stats.py: CQC rank; In files: stats.py:142; */
"Helpless" = "Helpless";
/* stats.py: CQC rank; In files: stats.py:143; */
"Mostly Helpless" = "Mostly Helpless";
/* stats.py: CQC rank; In files: stats.py:144; */
"Amateur" = "Amateur";
/* stats.py: CQC rank; In files: stats.py:145; */
"Semi Professional" = "Semi Professional";
/* stats.py: CQC rank; In files: stats.py:146; */
"Professional" = "Professional";
/* stats.py: CQC rank; In files: stats.py:147; */
"Champion" = "Champion";
/* stats.py: CQC rank; In files: stats.py:148; */
"Hero" = "Hero";
/* stats.py: CQC rank; In files: stats.py:149; */
"Gladiator" = "Gladiator";
/* stats.py: Federation rank; In files: stats.py:155; */
"Recruit" = "Recruit";
/* stats.py: Federation rank; In files: stats.py:156; */
"Cadet" = "Cadet";
/* stats.py: Federation rank; In files: stats.py:157; */
"Midshipman" = "Midshipman";
/* stats.py: Federation rank; In files: stats.py:158; */
"Petty Officer" = "Petty Officer";
/* stats.py: Federation rank; In files: stats.py:159; */
"Chief Petty Officer" = "Chief Petty Officer";
/* stats.py: Federation rank; In files: stats.py:160; */
"Warrant Officer" = "Warrant Officer";
/* stats.py: Federation rank; In files: stats.py:161; */
"Ensign" = "Ensign";
/* stats.py: Federation rank; In files: stats.py:162; */
"Lieutenant" = "Lieutenant";
/* stats.py: Federation rank; In files: stats.py:163; */
"Lieutenant Commander" = "Lieutenant Commander";
/* stats.py: Federation rank; In files: stats.py:164; */
"Post Commander" = "Post Commander";
/* stats.py: Federation rank; In files: stats.py:165; */
"Post Captain" = "Post Captain";
/* stats.py: Federation rank; In files: stats.py:166; */
"Rear Admiral" = "Rear Admiral";
/* stats.py: Federation rank; In files: stats.py:167; */
"Vice Admiral" = "Vice Admiral";
/* stats.py: Federation rank; In files: stats.py:168; */
"Admiral" = "Admiral";
/* stats.py: Empire rank; In files: stats.py:174; */
"Outsider" = "Outsider";
/* stats.py: Empire rank; In files: stats.py:175; */
"Serf" = "Serf";
/* stats.py: Empire rank; In files: stats.py:177; */
"Squire" = "Squire";
/* stats.py: Empire rank; In files: stats.py:178; */
"Knight" = "Knight";
/* stats.py: Empire rank; In files: stats.py:179; */
"Lord" = "Lord";
/* stats.py: Empire rank; In files: stats.py:180; */
"Baron" = "Baron";
/* stats.py: Empire rank; In files: stats.py:181; */
"Viscount" = "Viscount";
/* stats.py: Empire rank; In files: stats.py:182; */
"Count" = "Count";
/* stats.py: Empire rank; In files: stats.py:183; */
"Earl" = "Earl";
/* stats.py: Empire rank; In files: stats.py:184; */
"Marquis" = "Marquis";
/* stats.py: Empire rank; In files: stats.py:185; */
"Duke" = "Duke";
/* stats.py: Empire rank; In files: stats.py:186; */
"Prince" = "Prince";
/* stats.py: Empire rank; In files: stats.py:187; */
"King" = "King";
/* stats.py: Power rank; In files: stats.py:193; */
"Rating 1" = "Reyting 1";
/* stats.py: Power rank; In files: stats.py:194; */
"Rating 2" = "Reyting 2";
/* stats.py: Power rank; In files: stats.py:195; */
"Rating 3" = "Reyting 3";
/* stats.py: Power rank; In files: stats.py:196; */
"Rating 4" = "Reyting 4";
/* stats.py: Power rank; In files: stats.py:197; */
"Rating 5" = "Reyting 5";
/* stats.py: Current commander unknown when trying to use 'File' > 'Status'; In files: stats.py:315; */
"Status: Don't yet know your Commander name" = "Durum: Cmdr adınızı henüz bilmiyorum";
/* stats.py: No Frontier CAPI data yet when trying to use 'File' > 'Status'; In files: stats.py:323; */
"Status: No CAPI data yet" = "Durum: Henüz CAPI verisi yok";
/* stats.py: Status dialog subtitle - CR value of ship; In files: stats.py:409; */
"Value" = "Değer";
/* stats.py: Status dialog title; In files: stats.py:418; */
"Ships" = "Gemiler";
/* update.py: Update Available Text; In files: update.py:229; */
"{NEWVER} is available" = "{NEWVER} sürüm mevcut";

View File

@ -226,13 +226,13 @@
/* l10n.py: The system default language choice in Settings > Appearance; prefs.py: Settings > Configuration - Label on 'reset journal files location to default' button; prefs.py: The system default language choice in Settings > Appearance; prefs.py: Label for 'Default' theme radio button; In files: l10n.py:193; prefs.py:455; prefs.py:709; prefs.py:742; */
"Default" = "默认";
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:74; coriolis.py:77; coriolis.py:123; coriolis.py:139; coriolis.py:145; */
/* coriolis.py: 'Auto' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - auto; In files: coriolis.py:48; coriolis.py:74; coriolis.py:77; coriolis.py:94; coriolis.py:123; coriolis.py:139; coriolis.py:145; coriolis.py:179; coriolis.py:182; */
"Auto" = "自动";
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:75; coriolis.py:121; coriolis.py:137; */
/* coriolis.py: 'Normal' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - normal; In files: coriolis.py:49; coriolis.py:75; coriolis.py:95; coriolis.py:121; coriolis.py:137; coriolis.py:180; */
"Normal" = "普通版";
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:76; coriolis.py:122; coriolis.py:138; */
/* coriolis.py: 'Beta' label for Coriolis site override selection; coriolis.py: Coriolis normal/beta selection - beta; In files: coriolis.py:50; coriolis.py:76; coriolis.py:96; coriolis.py:122; coriolis.py:138; coriolis.py:181; */
"Beta" = "测试版";
/* coriolis.py: Settings>Coriolis: Help/hint for changing coriolis URLs; In files: coriolis.py:91:93; */

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",
@ -133,7 +131,6 @@ def build() -> None:
"distutils",
"_markerlib",
"optparse",
"PIL",
"simplejson",
"unittest",
"doctest",

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.10.4'
_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

@ -5,6 +5,8 @@ Copyright (c) EDCD, All Rights Reserved
Licensed under the GNU General Public License.
See LICENSE file.
"""
from __future__ import annotations
import os
import pathlib
import sys

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,7 @@ from __future__ import annotations
import logging
import tkinter as tk
import myNotebook as nb # noqa: N813
from config import appname, config
@ -47,7 +48,7 @@ class ClickCounter:
"""
self.on_preferences_closed("", False) # Save our prefs
def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> tk.Frame | None:
def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> nb.Frame | None:
"""
setup_preferences is called by plugin_prefs below.
@ -63,7 +64,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)
nb.EntryMenu(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
@ -126,7 +127,7 @@ def plugin_stop() -> None:
return cc.on_unload()
def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> tk.Frame | None:
def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> nb.Frame | None:
"""
Handle preferences tab for the plugin.

View File

@ -69,6 +69,8 @@ outfitting_weapon_map = {
'advancedtorppylon': 'Torpedo Pylon',
'atdumbfiremissile': 'AX Missile Rack',
'atmulticannon': 'AX Multi-Cannon',
('atmulticannon', 'v2'): 'Enhanced AX Multi-Cannon',
('atdumbfiremissile', 'v2'): 'Enhanced AX Missile Rack',
'basicmissilerack': 'Seeker Missile Rack',
'beamlaser': 'Beam Laser',
('beamlaser', 'heat'): 'Retributor Beam Laser',
@ -88,6 +90,8 @@ outfitting_weapon_map = {
'mining_abrblstr': 'Abrasion Blaster',
'mining_seismchrgwarhd': 'Seismic Charge Launcher',
'mining_subsurfdispmisle': 'Sub-Surface Displacement Missile',
'human_extraction': 'Sub-Surface Extraction Missile',
'atventdisruptorpylon': 'Guardian Nanite Torpedo Pylon',
'mininglaser': 'Mining Laser',
('mininglaser', 'advanced'): 'Mining Lance Beam Laser',
'multicannon': 'Multi-Cannon',
@ -266,6 +270,11 @@ outfitting_weaponrating_map = {
'hpt_slugshot_turret_medium': 'D',
'hpt_slugshot_turret_large': 'C',
'hpt_xenoscannermk2_basic_tiny': '?',
'hpt_atmulticannon_gimbal_large': 'C',
'hpt_atmulticannon_gimbal_medium': 'E',
'hpt_human_extraction_fixed_medium': 'B',
'hpt_atventdisruptorpylon_fixed_medium': 'I',
'hpt_atventdisruptorpylon_fixed_large': 'I',
}
# Old standard weapon variants
@ -278,13 +287,14 @@ outfitting_weaponoldvariant_map = {
}
outfitting_countermeasure_map = {
'antiunknownshutdown': ('Shutdown Field Neutraliser', 'F'),
'chafflauncher': ('Chaff Launcher', 'I'),
'electroniccountermeasure': ('Electronic Countermeasure', 'F'),
'heatsinklauncher': ('Heat Sink Launcher', 'I'),
'plasmapointdefence': ('Point Defence', 'I'),
'xenoscanner': ('Xeno Scanner', 'E'),
'xenoscannermk2': ('Unknown Xeno Scanner Mk II', '?'),
'antiunknownshutdown': ('Shutdown Field Neutraliser', 'F'),
('antiunknownshutdown', 'v2'): ('Thargoid Pulse Neutraliser', 'E'),
'chafflauncher': ('Chaff Launcher', 'I'),
'electroniccountermeasure': ('Electronic Countermeasure', 'F'),
'heatsinklauncher': ('Heat Sink Launcher', 'I'),
'plasmapointdefence': ('Point Defence', 'I'),
'xenoscanner': ('Xeno Scanner', 'E'),
'xenoscannermk2': ('Unknown Xeno Scanner Mk II', '?'),
}
outfitting_utility_map = {
@ -347,6 +357,7 @@ outfitting_standard_map = {
'guardianpowerdistributor': 'Guardian Hybrid Power Distributor',
'guardianpowerplant': 'Guardian Hybrid Power Plant',
'hyperdrive': 'Frame Shift Drive',
('hyperdrive', 'overcharge'): 'Frame Shift Drive',
'lifesupport': 'Life Support',
# 'planetapproachsuite': handled separately
'powerdistributor': 'Power Distributor',
@ -376,6 +387,11 @@ outfitting_internal_map = {
'refinery': 'Refinery',
'recon': 'Recon Limpet Controller',
'repair': 'Repair Limpet Controller',
'rescue': 'Rescue Limpet Controller',
'mining': 'Mining Multi Limpet Controller',
'xeno': 'Xeno Multi Limpet Controller',
'operations': 'Operations Multi Limpet Controller',
'universal': 'Universal Multi Limpet Controller',
'repairer': 'Auto Field-Maintenance Unit',
'resourcesiphon': 'Hatch Breaker Limpet Controller',
'shieldcellbank': 'Shield Cell Bank',
@ -383,6 +399,7 @@ outfitting_internal_map = {
('shieldgenerator', 'fast'): 'Bi-Weave Shield Generator',
('shieldgenerator', 'strong'): 'Prismatic Shield Generator',
'unkvesselresearch': 'Research Limpet Controller',
'expmodulestabiliser': 'Experimental Weapon Stabiliser',
}
# Dashboard Flags constants

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)

View File

@ -361,8 +361,8 @@ def fetch_kill_switches(target=DEFAULT_KILLSWITCH_URL) -> KillSwitchJSONFile | N
logger.warning(f"Failed to get kill switches, data was invalid: {e}")
return None
except (requests.exceptions.BaseHTTPError, requests.exceptions.ConnectionError) as e: # type: ignore
logger.warning(f"unable to connect to {target!r}: {e}")
except requests.exceptions.RequestException as requ_err:
logger.warning(f"unable to connect to {target!r}: {requ_err}")
return None
return data

50
l10n.py
View File

@ -17,8 +17,8 @@ import re
import sys
import warnings
from contextlib import suppress
from os import listdir, makedirs, pardir, sep
from os.path import abspath, basename, dirname, exists, isdir, isfile, join
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
@ -40,12 +40,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:
@ -199,14 +194,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
@ -227,9 +216,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__:
@ -255,10 +241,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')
@ -266,15 +248,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
@ -300,14 +273,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)
@ -320,9 +285,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)
@ -353,10 +315,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
@ -63,6 +54,8 @@ elif sys.platform == 'win32':
GetWindowText = ctypes.windll.user32.GetWindowTextW
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int]
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
GetWindowTextLength.argtypes = [ctypes.wintypes.HWND]
GetWindowTextLength.restype = ctypes.c_int
GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd
@ -382,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:
@ -450,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
@ -483,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)
@ -2144,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.
"""
@ -14,15 +11,14 @@ from __future__ import annotations
import sys
import tkinter as tk
from tkinter import ttk
from tkinter import ttk, messagebox
from typing import TYPE_CHECKING
from PIL import ImageGrab
# 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'
if TYPE_CHECKING:
def _(x: str) -> str: return x
elif sys.platform == 'win32':
if sys.platform == 'win32':
PAGEFG = 'SystemWindowText'
PAGEBG = 'SystemWindow' # typically white
@ -32,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
"""Custom t(t)k.Frame class to fix some display issues."""
class Frame(ttk.Frame):
"""Custom ttk.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:
@ -77,105 +56,124 @@ 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 dont 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 Entry(sys.platform == 'darwin' and tk.Entry or ttk.Entry): # type: ignore
"""Custom t(t)k.Entry class to fix some display issues."""
class EntryMenu(ttk.Entry):
"""Extended entry widget that includes a context menu with Copy, Cut-and-Paste commands."""
def __init__(self, *args, **kwargs) -> None:
ttk.Entry.__init__(self, *args, **kwargs)
self.menu = tk.Menu(self, tearoff=False)
self.menu.add_command(label="Copy", command=self.copy)
self.menu.add_command(label="Cut", command=self.cut)
self.menu.add_separator()
self.menu.add_command(label="Paste", command=self.paste)
self.menu.add_separator()
self.menu.add_command(label="Select All", command=self.select_all)
self.bind("<Button-3>", self.display_popup)
def display_popup(self, event: tk.Event) -> None:
"""Display the menu popup."""
self.menu.post(event.x_root, event.y_root)
def select_all(self) -> None:
"""Select all the text within the Entry."""
self.selection_range(0, tk.END)
self.focus_set()
def copy(self) -> None:
"""Copy the selected Entry text."""
if self.selection_present():
self.clipboard_clear()
self.clipboard_append(self.selection_get())
def cut(self) -> None:
"""Cut the selected Entry text."""
if self.selection_present():
self.copy()
self.delete(tk.SEL_FIRST, tk.SEL_LAST)
def paste(self) -> None:
"""Paste the selected Entry text."""
try:
# Attempt to grab an image from the clipboard (apprently also works for files)
img = ImageGrab.grabclipboard()
if img:
# Hijack existing translation, yes it doesn't exactly match here.
# LANG: Generic error prefix - following text is from Frontier auth service;
messagebox.showwarning(_('Error'),
_('Cannot paste non-text content.')) # LANG: Can't Paste Images or Files in Text
return
text = self.clipboard_get()
if self.selection_present() and text:
self.delete(tk.SEL_FIRST, tk.SEL_LAST)
self.insert(tk.INSERT, text)
except tk.TclError:
# No text in clipboard or clipboard is not text
pass
class Entry(EntryMenu):
"""Custom ttk.Entry class to fix some display issues."""
# DEPRECATED: Migrate to EntryMenu. Will remove in 5.12 or later.
def __init__(self, master: ttk.Frame | None = None, **kw):
EntryMenu.__init__(self, master, **kw)
class Button(ttk.Button):
"""Custom ttk.Button class to fix some display issues."""
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:
ttk.Entry.__init__(self, master, **kw)
class Button(sys.platform == 'darwin' and tk.Button or ttk.Button): # type: ignore
"""Custom t(t)k.Button class to fix some display issues."""
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
"""Custom t(t)k.ColoredButton class to fix some display issues."""
class ColoredButton(tk.Button):
"""Custom tk.Button 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):
tk.Button.__init__(self, master, **kw)
class Checkbutton(ttk.Checkbutton):
"""Custom ttk.Checkbutton class to fix some display issues."""
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()
style = 'nb.TCheckbutton' if sys.platform == 'win32' else None
super().__init__(master, style=style, **kw) # type: ignore
class Checkbutton(sys.platform == 'darwin' and tk.Checkbutton or ttk.Checkbutton): # type: ignore
"""Custom t(t)k.Checkbutton class to fix some display issues."""
class Radiobutton(ttk.Radiobutton):
"""Custom ttk.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.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)
style = 'nb.TRadiobutton' 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
"""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)
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

@ -98,6 +98,12 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0
elif not entitled and name[1] == 'planetapproachsuite':
return None
# V2 Shutdown Field Neutralizer - Hpt_AntiUnknownShutdown_Tiny_V2
elif name[0] == 'hpt' and name[1] in countermeasure_map and len(name) == 4 and name[3] == 'v2':
new['category'] = 'utility'
new['name'], new['rating'] = countermeasure_map[name[1]]
new['class'] = weaponclass_map[name[-2]]
# Countermeasures - e.g. Hpt_PlasmaPointDefence_Turret_Tiny
elif name[0] == 'hpt' and name[1] in countermeasure_map:
new['category'] = 'utility'
@ -179,12 +185,18 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0
if name[1] == 'dronecontrol': # e.g. Int_DroneControl_Collection_Size1_Class1
name.pop(0)
elif name[1] == 'multidronecontrol': # e.g. Int_MultiDroneControl_Rescue_Size3_Class3
name.pop(0)
elif name[-1] == 'free': # Starter Sidewinder or Freagle modules - just treat them like vanilla modules
name.pop()
if name[1] in standard_map: # e.g. Int_Engine_Size2_Class1, Int_ShieldGenerator_Size8_Class5_Strong
new['category'] = 'standard'
new['name'] = standard_map[len(name) > 4 and (name[1], name[4]) or name[1]]
if name[2] == 'overcharge':
new['name'] = standard_map[(name[1], name[2])]
else:
new['name'] = standard_map[len(name) > 4 and (name[1], name[4]) or name[1]]
elif name[1] in internal_map: # e.g. Int_CargoRack_Size8_Class1
new['category'] = 'internal'
@ -209,6 +221,9 @@ def lookup(module, ship_map, entitled=False) -> dict | None: # noqa: C901, CCR0
elif len(name) < 4 and name[1] == 'guardianfsdbooster': # Hack! No class.
(new['class'], new['rating']) = (str(name[2][4:]), 'H')
elif len(name) > 4 and name[1] == 'hyperdrive': # e.g. Int_Hyperdrive_Overcharge_Size6_Class3
(new['class'], new['rating']) = (str(name[4][-1:]), 'C')
else:
if len(name) < 3:
raise AssertionError(f'{name}: length < 3]')

View File

@ -126,7 +126,7 @@ class Plugin:
return None
def get_prefs(self, parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Frame | None:
def get_prefs(self, parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame | None:
"""
If the plugin provides a prefs frame, create and return it.

View File

@ -84,7 +84,7 @@ def plugin_start3(path: str) -> str:
return 'Coriolis'
def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Frame:
def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame:
"""Set up plugin preferences."""
PADX = 10 # noqa: N806
PADY = 1 # noqa: N806
@ -106,8 +106,7 @@ 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(
nb.EntryMenu(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
@ -119,8 +118,8 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Fr
# 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(
sticky=tk.EW, row=cur_row, column=1, padx=PADX, pady=BOXY
nb.EntryMenu(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'),

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,
@ -1899,7 +1898,7 @@ class EDDN:
:param cmdr: the commander under which this upload is made
:param is_beta: whether or not we are in beta mode
:param entry: the journal entry to send
___
Example:
{
"timestamp":"2022-06-10T10:09:41Z",
@ -1933,7 +1932,7 @@ class EDDN:
:param cmdr: the commander under which this upload is made
:param is_beta: whether or not we are in beta mode
:param entry: the journal entry to send
___
Example:
{
"timestamp":"2023-10-01T14:56:34Z",

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: nb.EntryMenu | None = None
self.apikey_label: nb.Label | None = None
self.apikey: nb.Entry | None = None
self.apikey: nb.EntryMenu | None = None
this = This()
@ -279,7 +279,7 @@ def toggle_password_visibility():
this.apikey.config(show="*") # type: ignore
def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> tk.Frame:
def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame:
"""
Plugin preferences setup hook.
@ -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 = nb.EntryMenu(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 = nb.EntryMenu(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: nb.EntryMenu
self.apikey_label: tk.Label
self.events: dict[Credentials, Deque[Event]] = defaultdict(deque)
@ -244,7 +244,7 @@ def toggle_password_visibility():
this.apikey.config(show="*")
def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> tk.Frame:
def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame:
"""Plugin Preferences UI hook."""
PADX = 10 # noqa: N806
BUTTONX = 12 # noqa: N806 # indent Checkbuttons and Radiobuttons
@ -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 = nb.EntryMenu(frame, show="*", width=50)
this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW)
cur_row += 1

205
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
@ -421,8 +376,6 @@ class PreferencesDialog(tk.Toplevel):
)
self.outbutton.grid(column=1, padx=self.PADX, pady=self.PADY, sticky=tk.EW, row=row.get())
nb.Frame(output_frame).grid(row=row.get()) # bottom spacer # TODO: does nothing?
# LANG: Label for 'Output' Settings/Preferences tab
root_notebook.add(output_frame, text=_('Output')) # Tab heading in settings
@ -444,7 +397,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 +408,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 +421,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 +448,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 +460,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 +621,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 +724,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 +737,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 +868,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 +880,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 +991,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 +1052,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 +1191,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 +1236,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

@ -5,7 +5,7 @@ wheel
# We can't rely on just picking this up from either the base (not venv),
# or venv-init-time version. Specify here so that dependabot will prod us
# about new versions.
setuptools==69.1.1
setuptools==69.2.0
# Static analysis tools
flake8==7.0.0
@ -18,14 +18,14 @@ flake8-noqa==1.4.0
flake8-polyfill==1.0.2
flake8-use-fstring==1.4
mypy==1.8.0
mypy==1.9.0
pep8-naming==0.13.3
safety==2.3.5
types-requests==2.31.0.20240125
safety==3.0.1
types-requests==2.31.0.20240311
types-pkg-resources==0.1.3
# Code formatting tools
autopep8==2.0.4
autopep8==2.1.0
# Git pre-commit checking
pre-commit==3.6.2
@ -38,9 +38,9 @@ grip==4.6.2
py2exe==0.13.0.1; sys_platform == 'win32'
# Testing
pytest==8.0.2
pytest==8.1.1
pytest-cov==4.1.0 # Pytest code coverage support
coverage[toml]==7.4.1 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
coverage[toml]==7.4.4 # pytest-cov dep. This is here to ensure that it includes TOML support for pyproject.toml configs
coverage-conditional-plugin==0.9.0
# For manipulating folder permissions and the like.
pywin32==306; sys_platform == 'win32'

View File

@ -1,7 +1,8 @@
certifi==2023.11.17
certifi==2024.2.2
requests==2.31.0
pillow==10.3.0
# requests depends on this now ?
charset-normalizer==2.1.1
charset-normalizer==3.3.2
watchdog==3.0.0
# Commented out because this doesn't package well with py2exe
@ -9,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