diff --git a/EDMarketConnector.py b/EDMarketConnector.py index 80a958e1c..761c1e8b8 100755 --- a/EDMarketConnector.py +++ b/EDMarketConnector.py @@ -187,6 +187,12 @@ '--killswitches-file', help='Specify a custom killswitches file', ) + + parser.add_argument( + '--ttk-catalog', + help='Replace plugins with a catalog of Ttk widgets', + action='store_true', + ) ########################################################################### args: argparse.Namespace = parser.parse_args() @@ -223,6 +229,9 @@ if args.eddn_tracking_ui: config.set_eddn_tracking_ui() + if args.ttk_catalog: + config.set_ttk_catalog() + if args.force_edmc_protocol: if sys.platform == 'win32': config.set_auth_force_edmc_protocol() @@ -258,7 +267,7 @@ def handle_edmc_callback_or_foregrounding() -> None: # noqa: CCR001 # If *this* instance hasn't locked, then another already has and we # now need to do the edmc:// checks for auth callback if locked != JournalLockResult.LOCKED: - from ctypes import windll, create_unicode_buffer, WINFUNCTYPE + from ctypes import windll, WINFUNCTYPE from ctypes.wintypes import BOOL, HWND, LPARAM import win32gui import win32api @@ -278,7 +287,7 @@ def window_title(h: int) -> str | None: return None @WINFUNCTYPE(BOOL, HWND, LPARAM) - def enumwindowsproc(window_handle, l_param): # noqa: CCR001 + def enumwindowsproc(window_handle, l_param): """ Determine if any window for the Application exists. @@ -293,25 +302,22 @@ def enumwindowsproc(window_handle, l_param): # noqa: CCR001 :param l_param: The second parameter to the EnumWindows() call. :return: False if we found a match, else True to continue iteration """ - # class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576 - cls = create_unicode_buffer(257) # This conditional is exploded to make debugging slightly easier - if win32gui.GetClassName(window_handle): - if cls.value == 'TkTopLevel': - if window_title(window_handle) == applongname: - if GetProcessHandleFromHwnd(window_handle): - # If GetProcessHandleFromHwnd succeeds then the app is already running as this user - if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler_redirect): - CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) - # Wait for it to be responsive to avoid ShellExecute recursing - win32gui.ShowWindow(window_handle, win32con.SW_RESTORE) - win32api.ShellExecute(0, None, sys.argv[1], None, None, win32con.SW_RESTORE) - - else: - ShowWindowAsync(window_handle, win32con.SW_RESTORE) - win32gui.SetForegroundWindow(window_handle) - - return False # Indicate window found, so stop iterating + if win32gui.GetClassName(window_handle) == 'TkTopLevel': + if window_title(window_handle) == applongname: + if GetProcessHandleFromHwnd(window_handle): + # If GetProcessHandleFromHwnd succeeds then the app is already running as this user + if len(sys.argv) > 1 and sys.argv[1].startswith(protocolhandler_redirect): + CoInitializeEx(0, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) + # Wait for it to be responsive to avoid ShellExecute recursing + win32gui.ShowWindow(window_handle, win32con.SW_RESTORE) + win32api.ShellExecute(0, None, sys.argv[1], None, None, win32con.SW_RESTORE) + + else: + ShowWindowAsync(window_handle, win32con.SW_RESTORE) + win32gui.SetForegroundWindow(window_handle) + + return False # Indicate window found, so stop iterating # Indicate that EnumWindows() needs to continue iterating return True # Do not remove, else this function as a callback breaks @@ -393,12 +399,12 @@ def already_running_popup(): import tkinter.messagebox from tkinter import ttk import commodity +import companion import plug import prefs import protocol import stats import td -from commodity import COMMODITY_CSV from dashboard import dashboard from edmc_data import ship_name_map from hotkey import hotkeymgr @@ -432,6 +438,7 @@ def __init__(self, master: tk.Tk): # noqa: C901, CCR001 # TODO - can possibly f self.minimizing = False self.w.rowconfigure(0, weight=1) self.w.columnconfigure(0, weight=1) + theme.initialize(self.w) # companion needs to be able to send <> events companion.session.set_tk_master(self.w) @@ -458,27 +465,19 @@ def open_window(systray: 'SysTrayIcon', *args) -> None: image_path = config.respath_path / 'io.edcd.EDMarketConnector.png' self.w.tk.call('wm', 'iconphoto', self.w, '-default', tk.PhotoImage(file=image_path)) - # 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 = ttk.Frame(self.w, name=appname.lower()) frame.grid(sticky=tk.NSEW) frame.columnconfigure(1, weight=1) - self.cmdr_label = tk.Label(frame, name='cmdr_label') - self.cmdr = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='cmdr') - self.ship_label = tk.Label(frame, name='ship_label') + self.cmdr_label = ttk.Label(frame, name='cmdr_label') + self.cmdr = ttk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='cmdr') + self.ship_label = ttk.Label(frame, name='ship_label') self.ship = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.shipyard_url, name='ship', popup_copy=True) - self.suit_label = tk.Label(frame, name='suit_label') - self.suit = tk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='suit') - self.system_label = tk.Label(frame, name='system_label') + self.suit_label = ttk.Label(frame, name='suit_label') + self.suit = ttk.Label(frame, compound=tk.RIGHT, anchor=tk.W, name='suit') + self.system_label = ttk.Label(frame, name='system_label') self.system = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.system_url, popup_copy=True, name='system') - self.station_label = tk.Label(frame, name='station_label') + self.station_label = ttk.Label(frame, name='station_label') self.station = HyperlinkLabel(frame, compound=tk.RIGHT, url=self.station_url, name='station', popup_copy=True) # system and station text is set/updated by the 'provider' plugins # edsm and inara. Look for: @@ -511,11 +510,9 @@ def open_window(systray: 'SysTrayIcon', *args) -> None: plugin_no = 0 for plugin in plug.PLUGINS: # Per plugin separator - plugin_sep = tk.Frame( - frame, highlightthickness=1, name=f"plugin_hr_{plugin_no + 1}" - ) + plugin_sep = ttk.Separator(frame, name=f"plugin_hr_{plugin_no + 1}") # Per plugin frame, for it to use as its parent for own widgets - plugin_frame = tk.Frame( + plugin_frame = ttk.Frame( frame, name=f"plugin_{plugin_no + 1}" ) @@ -550,30 +547,18 @@ def open_window(systray: 'SysTrayIcon', *args) -> None: default=tk.ACTIVE, state=tk.DISABLED ) - self.theme_button = tk.Label( - frame, - name='themed_update_button', - width=28, - state=tk.DISABLED - ) ui_row = frame.grid_size()[1] self.button.grid(row=ui_row, columnspan=2, sticky=tk.NSEW) - self.theme_button.grid(row=ui_row, columnspan=2, sticky=tk.NSEW) - theme.register_alternate((self.button, self.theme_button, self.theme_button), - {'row': ui_row, 'columnspan': 2, 'sticky': tk.NSEW}) self.button.bind('', self.capi_request_data) - theme.button_bind(self.theme_button, self.capi_request_data) # Bottom 'status' line. - self.status = tk.Label(frame, name='status', anchor=tk.W) + self.status = ttk.Label(frame, name='status', anchor=tk.W) self.status.grid(columnspan=2, sticky=tk.EW) for child in frame.winfo_children(): child.grid_configure(padx=self.PADX, pady=( - sys.platform != 'win32' or isinstance(child, tk.Frame)) and 2 or 0) - - self.menubar = tk.Menu() + sys.platform != 'win32' or isinstance(child, ttk.Frame)) and 2 or 0) # This used to be *after* the menu setup for some reason, but is testing # as working (both internal and external) like this. -Ath @@ -587,17 +572,15 @@ def open_window(systray: 'SysTrayIcon', *args) -> None: self.updater = update.Updater(tkroot=self.w) self.updater.check_for_updates() # Sparkle / WinSparkle does this automatically for packaged apps - self.file_menu = self.view_menu = tk.Menu(self.menubar, tearoff=tk.FALSE) + self.file_menu = self.view_menu = tk.Menu(self.w, 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 = tk.Menu(self.w, 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 = tk.Menu(self.w, tearoff=tk.FALSE) 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 @@ -610,77 +593,36 @@ def open_window(systray: 'SysTrayIcon', *args) -> None: self.help_menu.add_command(command=lambda: prefs.open_folder(logfile_loc)) # Open Log Folder self.help_menu.add_command(command=lambda: prefs.help_open_system_profiler(self)) # Open Log Folde - 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 = tk.Menu(self.w, 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=tr.tl('Always on top'), variable=self.always_ontop, command=self.ontop_changed) # Appearance setting - self.menubar.add_cascade(menu=self.system_menu) self.w.bind('', self.copy) # Bind to the Default theme minimise button self.w.bind("", 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) - # Alternate title bar and menu for dark theme - self.theme_menubar = tk.Frame(frame, name="alternate_menubar") + self.theme_menubar = ttk.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('', self.drag_start) - theme_titlebar.bind('', self.drag_continue) - theme_titlebar.bind('', 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.title_gap = ttk.Frame(self.theme_menubar, name='title_gap') + self.title_gap.grid(row=0, columnspan=3) + self.theme_file_menu = ttk.Menubutton(self.theme_menubar, menu=self.file_menu, style='Menubar.TMenubutton') 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 = ttk.Menubutton(self.theme_menubar, menu=self.edit_menu, style='Menubar.TMenubutton') 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 = ttk.Menubutton(self.theme_menubar, menu=self.help_menu, style='Menubar.TMenubutton') 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) + ttk.Separator(self.theme_menubar).grid(columnspan=5, padx=self.PADX, sticky=tk.EW) + self.theme_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW) + self.w.resizable(tk.FALSE, tk.FALSE) + theme.apply() # update geometry if config.get_str('geometry'): @@ -702,14 +644,6 @@ def open_window(systray: 'SysTrayIcon', *args) -> None: self.w.attributes('-topmost', config.get_int('always_ontop') and 1 or 0) - theme.register(frame) - theme.apply(self.w) - - self.w.bind('', self.onmap) # Special handling for overrideredict - self.w.bind('', self.onenter) # Special handling for transparency - self.w.bind('', self.onenter) # Special handling for transparency - self.w.bind('', self.onleave) # Special handling for transparency - self.w.bind('', self.onleave) # Special handling for transparency self.w.bind('', self.capi_request_data) self.w.bind('', self.capi_request_data) self.w.bind_all('<>', self.capi_request_data) # Ask for CAPI queries to be performed @@ -721,7 +655,7 @@ def open_window(systray: 'SysTrayIcon', *args) -> None: self.w.bind_all('<>', self.onexit) # Updater # Check for Valid Providers - validate_providers() + validate_providers(self.w) if monitor.cmdr is None: self.status['text'] = tr.tl("Awaiting Full CMDR Login") # LANG: Await Full CMDR Login to Game @@ -739,10 +673,7 @@ def open_window(systray: 'SysTrayIcon', *args) -> None: self.toggle_suit_row(visible=False) if args.start_min: logger.warning("Trying to start minimized") - if root.overrideredirect(): - self.oniconify() - else: - self.w.wm_iconify() + self.w.wm_iconify() def update_suit_text(self) -> None: """Update the suit text for current type and loadout.""" @@ -846,7 +777,7 @@ def postprefs(self, dologin: bool = True, **postargs): ) restart_box.show() if restart_box: - app.onexit(restart=True) + self.onexit(restart=True) def set_labels(self): """Set main window labels, e.g. after language change.""" @@ -856,10 +787,7 @@ def set_labels(self): self.suit_label['text'] = tr.tl('Suit') + ':' # LANG: Label for 'Suit' line in main UI self.system_label['text'] = tr.tl('System') + ':' # LANG: Label for 'System' line in main UI self.station_label['text'] = tr.tl('Station') + ':' # LANG: Label for 'Station' line in main UI - self.button['text'] = self.theme_button['text'] = tr.tl('Update') # LANG: Update button in main window - self.menubar.entryconfigure(1, label=tr.tl('File')) # LANG: 'File' menu title - self.menubar.entryconfigure(2, label=tr.tl('Edit')) # LANG: 'Edit' menu title - self.menubar.entryconfigure(3, label=tr.tl('Help')) # LANG: 'Help' menu title + self.button['text'] = tr.tl('Update') # LANG: Update button in main window self.theme_file_menu['text'] = tr.tl('File') # LANG: 'File' menu title self.theme_edit_menu['text'] = tr.tl('Edit') # LANG: 'Edit' menu title self.theme_help_menu['text'] = tr.tl('Help') # LANG: 'Help' menu title @@ -900,7 +828,7 @@ def login(self): # LANG: Status - Attempting to get a Frontier Auth Access Token self.status['text'] = tr.tl('Logging in...') - self.button['state'] = self.theme_button['state'] = tk.DISABLED + self.button['state'] = tk.DISABLED self.file_menu.entryconfigure(0, state=tk.DISABLED) # Status self.file_menu.entryconfigure(1, state=tk.DISABLED) # Save Raw Data @@ -923,7 +851,7 @@ def login(self): self.cooldown() - def export_market_data(self, data: 'CAPIData') -> bool: # noqa: CCR001 + def export_market_data(self, data: 'companion.CAPIData') -> bool: # noqa: CCR001 """ Export CAPI market data. @@ -956,7 +884,7 @@ def export_market_data(self, data: 'CAPIData') -> bool: # noqa: CCR001 # Fixup anomalies in the comodity data fixed = companion.fixup(data) if output_flags & config.OUT_MKT_CSV: - commodity.export(fixed, COMMODITY_CSV) + commodity.export(fixed, commodity.COMMODITY_CSV) if output_flags & config.OUT_MKT_TD: td.export(fixed) @@ -1050,7 +978,7 @@ def capi_request_data(self, event=None) -> None: # noqa: CCR001 # LANG: Status - Attempting to retrieve data from Frontier CAPI self.status['text'] = tr.tl('Fetching data...') - self.button['state'] = self.theme_button['state'] = tk.DISABLED + self.button['state'] = tk.DISABLED self.w.update_idletasks() query_time = int(time()) @@ -1269,7 +1197,7 @@ def capi_handle_response(self, event=None): # noqa: C901, CCR001 if (suit := loadout.get('suit')) is not None: if (suitname := suit.get('edmcName')) is not None: # We've been paranoid about loadout->suit->suitname, now just assume loadouts is there - loadout_name = index_possibly_sparse_list( + loadout_name = companion.index_possibly_sparse_list( capi_response.capi_data['loadouts'], loadout['loadoutSlotId'] )['name'] @@ -1668,11 +1596,11 @@ def cooldown(self) -> None: # Update button in main window cooldown_time = int(self.capi_query_holdoff_time - time()) # LANG: Cooldown on 'Update' button - self.button['text'] = self.theme_button['text'] = tr.tl('cooldown {SS}s').format(SS=cooldown_time) + self.button['text'] = tr.tl('cooldown {SS}s').format(SS=cooldown_time) self.w.after(1000, self.cooldown) else: - self.button['text'] = self.theme_button['text'] = tr.tl('Update') # LANG: Update button in main window - self.button['state'] = self.theme_button['state'] = ( + self.button['text'] = tr.tl('Update') # LANG: Update button in main window + self.button['state'] = ( monitor.cmdr and monitor.mode and monitor.mode != 'CQC' and @@ -1916,57 +1844,14 @@ def onexit(self, event=None, restart: bool = False) -> None: if restart: os.execv(sys.executable, ['python'] + sys.argv) - def drag_start(self, event) -> None: - """Initiate dragging the window.""" - self.drag_offset = (event.x_root - self.w.winfo_rootx(), event.y_root - self.w.winfo_rooty()) - - def drag_continue(self, event) -> None: - """Continued handling of window drag.""" - if self.drag_offset[0]: - offset_x = event.x_root - self.drag_offset[0] - offset_y = event.y_root - self.drag_offset[1] - self.w.geometry(f'+{offset_x:d}+{offset_y:d}') - - def drag_end(self, event) -> None: - """Handle end of window dragging.""" - self.drag_offset = (None, None) - def default_iconify(self, event=None) -> None: - """Handle the Windows default theme 'minimise' button.""" + """Handle the Windows 'minimize' button.""" # If we're meant to "minimize to system tray" then hide the window so no taskbar icon is seen if sys.platform == 'win32' and config.get_bool('minimize_system_tray'): # This gets called for more than the root widget, so only react to that if str(event.widget) == '.': self.w.withdraw() - def oniconify(self, event=None) -> None: - """Handle the minimize button on non-Default theme main window.""" - self.w.overrideredirect(False) # Can't iconize while overrideredirect - self.w.iconify() - self.w.update_idletasks() # Size and windows styles get recalculated here - self.w.wait_visibility() # Need main window to be re-created before returning - theme.active = None # So theme will be re-applied on map - - # TODO: Confirm this is unused and remove. - def onmap(self, event=None) -> None: - """Perform a now unused function.""" - if event.widget == self.w: - theme.apply(self.w) - - def onenter(self, event=None) -> None: - """Handle when our window gains focus.""" - if config.get_int('theme') == theme.THEME_TRANSPARENT: - self.w.attributes("-transparentcolor", '') - self.blank_menubar.grid_remove() - self.theme_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW) - - def onleave(self, event=None) -> None: - """Handle when our window loses focus.""" - if config.get_int('theme') == theme.THEME_TRANSPARENT and event.widget == self.w: - self.w.attributes("-transparentcolor", 'grey4') - self.theme_menubar.grid_remove() - self.blank_menubar.grid(row=0, columnspan=2, sticky=tk.NSEW) - def test_logging() -> None: """Simple test of top level logging.""" @@ -1993,7 +1878,7 @@ def setup_killswitches(filename: str | None): killswitch.setup_main_list(filename) -def show_killswitch_poppup(root=None): +def show_killswitch_poppup(root: tk.Tk): """Show a warning popup if there are any killswitches that match the current version.""" if len(kills := killswitch.kills_for_version()) == 0: return @@ -2029,7 +1914,7 @@ def show_killswitch_poppup(root=None): ok_button.grid(columnspan=2, sticky=tk.EW) -def validate_providers(): +def validate_providers(root: tk.Tk): """Check if Config has an invalid provider set, and reset to default if we do.""" reset_providers = {} station_provider: str = config.get_str("station_provider") @@ -2076,7 +1961,8 @@ def validate_providers(): # Run the app -if __name__ == "__main__": # noqa: C901 +def main(): # noqa: C901, CCR001 + """Run the main code of the program.""" logger.info(f'Startup v{appversion()} : Running on Python v{sys.version}') logger.debug(f'''Platform: {sys.platform} {sys.platform == "win32" and sys.getwindowsversion()} argv[0]: {sys.argv[0]} @@ -2154,11 +2040,6 @@ def validate_providers(): else: log_locale('After switching to UTF-8 encoding (same language)') - # HACK: n/a | 2021-11-24: --force-localserver-auth does not work if companion is imported early -cont. - # HACK: n/a | 2021-11-24: as we modify config before this is used. - import companion - from companion import CAPIData, index_possibly_sparse_list - # Do this after locale silliness, just in case if args.forget_frontier_auth: logger.info("Dropping all fdev tokens as --forget-frontier-auth was passed") @@ -2331,3 +2212,13 @@ def messagebox_not_py3(): logger.info("Ctrl+C Detected, Attempting Clean Shutdown") app.onexit() logger.info('Exiting') + + +if __name__ == '__main__': + if sys.platform == 'win32': + from winrt.microsoft.windows.applicationmodel.dynamicdependency import bootstrap + + with bootstrap.initialize(options=bootstrap.InitializeOptions.ON_NO_MATCH_SHOW_UI): + main() + else: + main() diff --git a/config/__init__.py b/config/__init__.py index 956a5453b..d2969f05c 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -191,6 +191,7 @@ class AbstractConfig(abc.ABC): plugin_dir_path: pathlib.Path default_plugin_dir_path: pathlib.Path internal_plugin_dir_path: pathlib.Path + internal_theme_dir_path: pathlib.Path respath_path: pathlib.Path home_path: pathlib.Path default_journal_dir_path: pathlib.Path @@ -201,6 +202,7 @@ class AbstractConfig(abc.ABC): __auth_force_edmc_protocol = False # Should we force edmc:// protocol ? __eddn_url = None # Non-default EDDN URL __eddn_tracking_ui = False # Show EDDN tracking UI ? + __ttk_catalog = False # Load Ttk catalog plugin ? def __init__(self) -> None: self.home_path = pathlib.Path.home() @@ -244,6 +246,19 @@ def auth_force_edmc_protocol(self) -> bool: """ return self.__auth_force_edmc_protocol + def set_ttk_catalog(self): + """Set flag to load the Ttk widget catalog plugin.""" + self.__ttk_catalog = True + + @property + def ttk_catalog(self) -> bool: + """ + Determine if the Ttk widget catalog plugin is loaded. + + :return: bool - Should the Ttk catalog plugin be loaded? + """ + return self.__ttk_catalog + def set_eddn_url(self, eddn_url: str): """Set the specified eddn URL.""" self.__eddn_url = eddn_url diff --git a/config/linux.py b/config/linux.py index a7a472f48..8bf26fff2 100644 --- a/config/linux.py +++ b/config/linux.py @@ -35,6 +35,7 @@ def __init__(self, filename: str | None = None) -> None: self.respath_path = pathlib.Path(__file__).parent.parent self.internal_plugin_dir_path = self.respath_path / 'plugins' + self.internal_theme_dir_path = self.respath_path / 'themes' self.default_journal_dir_path = None # type: ignore self.identifier = f'uk.org.marginal.{appname.lower()}' # TODO: Unused? diff --git a/config/windows.py b/config/windows.py index 550b0e4a7..d887a6b62 100644 --- a/config/windows.py +++ b/config/windows.py @@ -59,10 +59,10 @@ def __init__(self) -> None: if getattr(sys, 'frozen', False): self.respath_path = pathlib.Path(sys.executable).parent - self.internal_plugin_dir_path = self.respath_path / 'plugins' else: self.respath_path = pathlib.Path(__file__).parent.parent - self.internal_plugin_dir_path = self.respath_path / 'plugins' + self.internal_plugin_dir_path = self.respath_path / 'plugins' + self.internal_theme_dir_path = self.respath_path / 'themes' self.home_path = pathlib.Path.home() diff --git a/docs/examples/click_counter/load.py b/docs/examples/click_counter/load.py index 3620e1594..1821a5c02 100644 --- a/docs/examples/click_counter/load.py +++ b/docs/examples/click_counter/load.py @@ -7,9 +7,10 @@ import logging import tkinter as tk +from tkinter import ttk -import myNotebook as nb # noqa: N813 from config import appname, config +from myNotebook import EntryMenu # This **MUST** match the name of the folder the plugin is in. PLUGIN_NAME = "click_counter" @@ -48,7 +49,7 @@ def on_unload(self) -> None: """ self.on_preferences_closed("", False) # Save our prefs - def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> nb.Frame | None: + def setup_preferences(self, parent: ttk.Notebook, cmdr: str, is_beta: bool) -> ttk.Frame | None: """ setup_preferences is called by plugin_prefs below. @@ -60,11 +61,11 @@ def setup_preferences(self, parent: nb.Notebook, cmdr: str, is_beta: bool) -> nb :return: The frame to add to the settings window """ current_row = 0 - frame = nb.Frame(parent) + frame = ttk.Frame(parent) # setup our config in a "Click Count: number" - nb.Label(frame, text='Click Count').grid(row=current_row) - nb.EntryMenu(frame, textvariable=self.click_count).grid(row=current_row, column=1) + ttk.Label(frame, text='Click Count').grid(row=current_row) + 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 @@ -81,7 +82,7 @@ def on_preferences_closed(self, cmdr: str, is_beta: bool) -> None: # `config.get_int()` will work for re-loading the value. config.set('click_counter_count', int(self.click_count.get())) - def setup_main_ui(self, parent: tk.Frame) -> tk.Frame: + def setup_main_ui(self, parent: ttk.Frame) -> ttk.Frame: """ Create our entry on the main EDMC UI. @@ -91,16 +92,16 @@ def setup_main_ui(self, parent: tk.Frame) -> tk.Frame: :return: Our frame """ current_row = 0 - frame = tk.Frame(parent) - button = tk.Button( + frame = ttk.Frame(parent) + button = ttk.Button( frame, text="Count me", command=lambda: self.click_count.set(str(int(self.click_count.get()) + 1)) ) button.grid(row=current_row) current_row += 1 - tk.Label(frame, text="Count:").grid(row=current_row, sticky=tk.W) - tk.Label(frame, textvariable=self.click_count).grid(row=current_row, column=1) + ttk.Label(frame, text="Count:").grid(row=current_row, sticky=tk.W) + ttk.Label(frame, textvariable=self.click_count).grid(row=current_row, column=1) return frame @@ -127,7 +128,7 @@ def plugin_stop() -> None: return cc.on_unload() -def plugin_prefs(parent: nb.Notebook, cmdr: str, is_beta: bool) -> nb.Frame | None: +def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> ttk.Frame | None: """ Handle preferences tab for the plugin. @@ -145,7 +146,7 @@ def prefs_changed(cmdr: str, is_beta: bool) -> None: return cc.on_preferences_closed(cmdr, is_beta) -def plugin_app(parent: tk.Frame) -> tk.Frame | None: +def plugin_app(parent: ttk.Frame) -> ttk.Frame | None: """ Set up the UI of the plugin. diff --git a/myNotebook.py b/myNotebook.py index c57a6deb4..603f8c98b 100644 --- a/myNotebook.py +++ b/myNotebook.py @@ -1,25 +1,16 @@ """ Custom `ttk.Notebook` to fix various display issues. -Hacks to fix various display issues with notebooks and their child widgets on Windows. - -- Windows: page background should be White, not SystemButtonFace - -Entire file may be imported by plugins. +This is mostly no longer necessary, with ttk themes applying consistent behaviour across the board. """ from __future__ import annotations -import sys import tkinter as tk import warnings from tkinter import ttk, messagebox from PIL import ImageGrab from l10n import translations as tr -if sys.platform == 'win32': - PAGEFG = 'SystemWindowText' - PAGEBG = 'SystemWindow' # typically white - class Notebook(ttk.Notebook): """Custom ttk.Notebook class to fix some display issues.""" @@ -27,13 +18,7 @@ class Notebook(ttk.Notebook): def __init__(self, master: ttk.Frame | None = None, **kw): super().__init__(master, **kw) - style = ttk.Style() - 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) + warnings.warn('Migrate to ttk.Notebook. Will be removed in 6.0 or later', DeprecationWarning, stacklevel=2) self.grid(padx=10, pady=10, sticky=tk.NSEW) @@ -41,24 +26,18 @@ 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 == 'win32': - ttk.Frame.__init__(self, master, style='nb.TFrame', **kw) - ttk.Frame(self).grid(pady=5) # top spacer - else: - ttk.Frame.__init__(self, master, **kw) - ttk.Frame(self).grid(pady=5) # top spacer + ttk.Frame.__init__(self, master, **kw) + ttk.Frame(self).grid(pady=5) # top spacer self.configure(takefocus=1) # let the frame take focus so that no particular child is focused + warnings.warn('Migrate to ttk.Frame. Will be removed in 6.0 or later', DeprecationWarning, stacklevel=2) class Label(tk.Label): """Custom tk.Label class to fix some display issues.""" def __init__(self, master: ttk.Frame | None = None, **kw): - 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) + warnings.warn('Migrate to ttk.Label. Will be removed in 6.0 or later', DeprecationWarning, stacklevel=2) class EntryMenu(ttk.Entry): @@ -135,18 +114,16 @@ 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 == 'win32': - ttk.Button.__init__(self, master, style='nb.TButton', **kw) - else: - ttk.Button.__init__(self, master, **kw) + warnings.warn('Migrate to ttk.Button. Will remove in 6.0 or later', DeprecationWarning, stacklevel=2) + ttk.Button.__init__(self, master, **kw) class ColoredButton(tk.Button): """Custom tk.Button class to fix some display issues.""" - # DEPRECATED: Migrate to tk.Button. Will remove in 6.0 or later. + # DEPRECATED: Migrate to ttk.Button. Will remove in 6.0 or later. def __init__(self, master: ttk.Frame | None = None, **kw): - warnings.warn('Migrate to tk.Button. Will remove in 6.0 or later.', DeprecationWarning, stacklevel=2) + warnings.warn('Migrate to ttk.Button. Will remove in 6.0 or later.', DeprecationWarning, stacklevel=2) tk.Button.__init__(self, master, **kw) @@ -154,30 +131,21 @@ class Checkbutton(ttk.Checkbutton): """Custom ttk.Checkbutton class to fix some display issues.""" def __init__(self, master: ttk.Frame | None = None, **kw): - style = 'nb.TCheckbutton' if sys.platform == 'win32' else None - super().__init__(master, style=style, **kw) # type: ignore + super().__init__(master, **kw) # type: ignore + warnings.warn('Migrate to ttk.Checkbutton. Will be removed in 6.0 or later', DeprecationWarning, stacklevel=2) class Radiobutton(ttk.Radiobutton): """Custom ttk.Radiobutton class to fix some display issues.""" def __init__(self, master: ttk.Frame | None = None, **kw): - style = 'nb.TRadiobutton' if sys.platform == 'win32' else None - super().__init__(master, style=style, **kw) # type: ignore + super().__init__(master, **kw) # type: ignore + warnings.warn('Migrate to ttk.Radiobutton. Will be removed in 6.0 or later', DeprecationWarning, stacklevel=2) 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 == '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) - 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) + ttk.OptionMenu.__init__(self, master, variable, default, *values, **kw) + warnings.warn('Migrate to ttk.OptionMenu. Will be removed in 6.0 or later', DeprecationWarning, stacklevel=2) diff --git a/plug.py b/plug.py index ee53e76b7..4194b9949 100644 --- a/plug.py +++ b/plug.py @@ -8,7 +8,7 @@ from __future__ import annotations import copy -import importlib +import importlib.util import logging import operator import os @@ -19,7 +19,6 @@ from typing import Any, Mapping, MutableMapping import companion -import myNotebook as nb # noqa: N813 from config import config from EDMCLogging import get_main_logger @@ -131,7 +130,7 @@ def get_app(self, parent: tk.Frame) -> tk.Frame | None: return None - def get_prefs(self, parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame | None: + def get_prefs(self, parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> ttk.Frame | None: """ If the plugin provides a prefs frame, create and return it. @@ -139,13 +138,13 @@ def get_prefs(self, parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb :param cmdr: current Cmdr name (or None). Relevant if you want to have different settings for different user accounts. :param is_beta: whether the player is in a Beta universe. - :returns: a myNotebook Frame + :returns: a ttk Frame """ plugin_prefs = self._get_func('plugin_prefs') if plugin_prefs: try: frame = plugin_prefs(parent, cmdr, is_beta) - if isinstance(frame, nb.Frame): + if isinstance(frame, ttk.Frame): return frame raise AssertionError except Exception: @@ -163,8 +162,11 @@ def load_plugins(master: tk.Tk) -> None: # Add plugin folder to load path so packages can be loaded from plugin folder sys.path.append(config.plugin_dir) - found = _load_found_plugins() - PLUGINS.extend(sorted(found, key=lambda p: operator.attrgetter('name')(p).lower())) + if config.ttk_catalog: + PLUGINS.append(_load_ttk_catalog_plugin()) + else: + found = _load_found_plugins() + PLUGINS.extend(sorted(found, key=lambda p: operator.attrgetter('name')(p).lower())) def _load_internal_plugins(): @@ -182,6 +184,15 @@ def _load_internal_plugins(): return internal +def _load_ttk_catalog_plugin(): + try: + plugin = Plugin('ttk_catalog', config.internal_plugin_dir_path / '_ttk_catalog.py', logger) + plugin.folder = None + return plugin + except Exception: + logger.exception('Failure loading internal Plugin "ttk_catalog"') + + def _load_found_plugins(): found = [] # Load any plugins that are also packages first, but note it's *still* diff --git a/plugins/_ttk_catalog.py b/plugins/_ttk_catalog.py new file mode 100644 index 000000000..a0b016067 --- /dev/null +++ b/plugins/_ttk_catalog.py @@ -0,0 +1,321 @@ +""" +_ttk_catalog.py - Catalog of ttk widgets. + +Copyright (c) EDCD, All Rights Reserved +Licensed under the GNU General Public License. +See LICENSE file. + +Based on https://github.com/rdbende/Azure-ttk-theme/blob/main/example.py +""" +import tkinter as tk +from tkinter import ttk + +from EDMCLogging import get_main_logger +from ttkHyperlinkLabel import HyperlinkLabel + +logger = get_main_logger() + +URL = 'https://github.com/EDCD/EDMarketConnector' + + +class Catalog(ttk.Frame): + """A display of ttk widgets under the EDMC themes.""" + + def __init__(self, parent: ttk.Frame): + super().__init__(parent) + + # Make the app responsive + for index in [0, 1, 2]: + self.columnconfigure(index=index, weight=1) + self.rowconfigure(index=index, weight=1) + + # Create value lists + self.option_menu_list = ["", "OptionMenu", "Option 1", "Option 2"] + self.tk_option_menu_list = ["", "tk.OptionMenu", "Option 1", "Option 2"] + self.combo_list = ["Combobox", "Editable item 1", "Editable item 2"] + self.readonly_combo_list = ["Readonly combobox", "Item 1", "Item 2"] + + # Create control variables + self.var_0 = tk.BooleanVar() + self.var_1 = tk.BooleanVar(value=True) + self.var_2 = tk.BooleanVar() + self.var_3 = tk.IntVar(value=2) + self.var_4 = tk.StringVar(value=self.option_menu_list[1]) + self.var_5 = tk.DoubleVar(value=75.0) + self.var_6 = tk.StringVar(value=self.tk_option_menu_list[1]) + + # Create widgets :) + self.setup_widgets() + + def setup_widgets(self) -> None: + """Widget creation.""" + check_frame = ttk.LabelFrame(self, text="Checkbutton", padding=(20, 10)) + check_frame.grid(row=0, column=0, padx=(20, 10), pady=(20, 10), sticky="nsew") + + check_1 = ttk.Checkbutton(check_frame, text="Unchecked", variable=self.var_0) + check_1.grid(row=0, column=0, sticky="nsew") + + check_2 = ttk.Checkbutton(check_frame, text="Checked", variable=self.var_1) + check_2.grid(row=1, column=0, sticky="nsew") + + check_3 = ttk.Checkbutton(check_frame, text="Third state", variable=self.var_2) + check_3.state(["alternate"]) + check_3.grid(row=2, column=0, sticky="nsew") + + check_4 = ttk.Checkbutton(check_frame, text="Disabled", state="disabled") + check_4.state(["disabled !alternate"]) + check_4.grid(row=3, column=0, sticky="nsew") + + # Separator + separator = ttk.Separator(self) + separator.grid(row=1, column=0, padx=(20, 10), pady=10, sticky="ew") + + # Create a Frame for the Radiobuttons + radio_frame = ttk.LabelFrame(self, text="Radiobutton", padding=(20, 10)) + radio_frame.grid(row=2, column=0, padx=(20, 10), pady=10, sticky="nsew") + + radio_1 = ttk.Radiobutton(radio_frame, text="Unselected", variable=self.var_3, value=1) + radio_1.grid(row=0, column=0, sticky="nsew") + + radio_2 = ttk.Radiobutton(radio_frame, text="Selected", variable=self.var_3, value=2) + radio_2.grid(row=1, column=0, sticky="nsew") + + radio_3 = ttk.Radiobutton(radio_frame, text="Disabled", state="disabled") + radio_3.grid(row=2, column=0, sticky="nsew") + + # Create a Frame for input widgets + widgets_frame = ttk.Frame(self, padding=(0, 0, 0, 10)) + widgets_frame.grid(row=0, column=1, padx=10, pady=(30, 10), sticky="nsew", rowspan=2) + widgets_frame.columnconfigure(index=0, weight=1) + + # Entry + entry = ttk.Entry(widgets_frame) + entry.insert(0, "Entry") + entry.grid(row=0, column=0, padx=5, pady=(0, 10), sticky="ew") + + # Spinbox + spinbox = ttk.Spinbox(widgets_frame, from_=0, to=100, increment=0.1) + spinbox.insert(0, "Spinbox") + spinbox.grid(row=1, column=0, padx=5, pady=10, sticky="ew") + + # Combobox + combobox = ttk.Combobox(widgets_frame, values=self.combo_list) + combobox.current(0) + combobox.grid(row=2, column=0, padx=5, pady=10, sticky="ew") + + # Read-only combobox + readonly_combo = ttk.Combobox(widgets_frame, state="readonly", values=self.readonly_combo_list) + readonly_combo.current(0) + readonly_combo.grid(row=3, column=0, padx=5, pady=10, sticky="ew") + + # Menu for the Menubutton + menu = tk.Menu(self) + menu.add_command(label="Menu item 1") + menu.add_command(label="Menu item 2") + menu.add_separator() + menu.add_command(label="Menu item 3") + menu.add_command(label="Menu item 4") + + # Menubutton + menubutton = ttk.Menubutton(widgets_frame, text="Menubutton", menu=menu, direction="below") + menubutton.grid(row=4, column=0, padx=5, pady=10, sticky="nsew") + + # OptionMenu + optionmenu = ttk.OptionMenu(widgets_frame, self.var_4, *self.option_menu_list) + optionmenu.grid(row=5, column=0, padx=5, pady=10, sticky="nsew") + + # Button + button = ttk.Button(widgets_frame, text="Button") + button.grid(row=6, column=0, sticky="nsew") + + hyperlink_frame = ttk.LabelFrame(self, text="HyperlinkLabel", padding=(20, 10)) + hyperlink_frame.grid(row=2, column=1, padx=10, pady=10, sticky="nsew") + + hyperlink_1 = HyperlinkLabel(hyperlink_frame, text="Default", url=URL) + hyperlink_1.grid(row=0, column=0, sticky="nsew") + + hyperlink_2 = HyperlinkLabel(hyperlink_frame, text="Underline", url=URL, underline=True) + hyperlink_2.grid(row=1, column=0, sticky="nsew") + + hyperlink_3 = HyperlinkLabel(hyperlink_frame, text="No underline", url=URL, underline=False) + hyperlink_3.grid(row=2, column=0, sticky="nsew") + + hyperlink_4 = HyperlinkLabel(hyperlink_frame, text="Disabled", url=URL, state=tk.DISABLED) + hyperlink_4.grid(row=3, column=0, sticky="nsew") + + # Panedwindow + paned = ttk.PanedWindow(self) + paned.grid(row=0, column=2, pady=(25, 5), sticky="nsew", rowspan=3) + + # Pane #1 + pane_1 = ttk.Frame(paned, padding=5) + paned.add(pane_1, weight=1) + + # Scrollbar + scrollbar = ttk.Scrollbar(pane_1) + scrollbar.pack(side="right", fill="y") + + # Treeview + treeview = ttk.Treeview( + pane_1, + selectmode="browse", + yscrollcommand=scrollbar.set, + columns=("1", "2"), + height=10, + ) + treeview.pack(expand=True, fill="both") + scrollbar.config(command=treeview.yview) + + # Treeview columns + treeview.column("#0", anchor="w", width=120) + treeview.column(1, anchor="w", width=120) + treeview.column(2, anchor="w", width=120) + + # Treeview headings + treeview.heading("#0", text="Column 1", anchor="center") + treeview.heading(1, text="Column 2", anchor="center") + treeview.heading(2, text="Column 3", anchor="center") + + # Define treeview data + treeview_data = [ + ("", 1, "Parent", ("Item 1", "Value 1")), + (1, 2, "Child", ("Subitem 1.1", "Value 1.1")), + (1, 3, "Child", ("Subitem 1.2", "Value 1.2")), + (1, 4, "Child", ("Subitem 1.3", "Value 1.3")), + (1, 5, "Child", ("Subitem 1.4", "Value 1.4")), + ("", 6, "Parent", ("Item 2", "Value 2")), + (6, 7, "Child", ("Subitem 2.1", "Value 2.1")), + (6, 8, "Sub-parent", ("Subitem 2.2", "Value 2.2")), + (8, 9, "Child", ("Subitem 2.2.1", "Value 2.2.1")), + (8, 10, "Child", ("Subitem 2.2.2", "Value 2.2.2")), + (8, 11, "Child", ("Subitem 2.2.3", "Value 2.2.3")), + (6, 12, "Child", ("Subitem 2.3", "Value 2.3")), + (6, 13, "Child", ("Subitem 2.4", "Value 2.4")), + ("", 14, "Parent", ("Item 3", "Value 3")), + (14, 15, "Child", ("Subitem 3.1", "Value 3.1")), + (14, 16, "Child", ("Subitem 3.2", "Value 3.2")), + (14, 17, "Child", ("Subitem 3.3", "Value 3.3")), + (14, 18, "Child", ("Subitem 3.4", "Value 3.4")), + ("", 19, "Parent", ("Item 4", "Value 4")), + (19, 20, "Child", ("Subitem 4.1", "Value 4.1")), + (19, 21, "Sub-parent", ("Subitem 4.2", "Value 4.2")), + (21, 22, "Child", ("Subitem 4.2.1", "Value 4.2.1")), + (21, 23, "Child", ("Subitem 4.2.2", "Value 4.2.2")), + (21, 24, "Child", ("Subitem 4.2.3", "Value 4.2.3")), + (19, 25, "Child", ("Subitem 4.3", "Value 4.3")), + ] + + # Insert treeview data + for parent, iid, text, values in treeview_data: + treeview.insert(parent=str(parent), index="end", iid=str(iid), text=text, values=values) + if parent == "" or iid in {8, 21}: + treeview.item(str(iid), open=True) + + # Select and scroll + treeview.selection_set("10") + treeview.see("7") + + # Notebook, pane #2 + pane_2 = ttk.Frame(paned, padding=5) + paned.add(pane_2, weight=3) + + # Notebook, pane #2 + notebook = ttk.Notebook(pane_2) + notebook.pack(fill="both", expand=True) + + # Tab #1 + tab_1 = ttk.Frame(notebook) + for index in [0, 1]: + tab_1.columnconfigure(index=index, weight=1) + tab_1.rowconfigure(index=index, weight=1) + notebook.add(tab_1, text="Tab 1") + + # Scale + scale = ttk.Scale( + tab_1, + from_=100, + to=0, + variable=self.var_5, + command=lambda event: self.var_5.set(scale.get()), + ) + scale.grid(row=0, column=0, padx=(20, 10), pady=(20, 0), sticky="ew") + + # Progressbar + progress = ttk.Progressbar(tab_1, value=0, variable=self.var_5, mode="determinate") + progress.grid(row=0, column=1, padx=(10, 20), pady=(20, 0), sticky="ew") + + # Label + label = ttk.Label( + tab_1, + text="ttk widgets for EDMC", + justify="center", + font=["-size", 15, "-weight", "bold"], + ) + label.grid(row=1, column=0, pady=10, columnspan=2) + + # Tab #2 + tab_2 = ttk.Frame(notebook) + notebook.add(tab_2, text="Tab 2") + + # Tab #3 + tab_3 = ttk.Frame(notebook) + notebook.add(tab_3, text="Tab 3") + + tk_check_frame = tk.LabelFrame(self, text="tk.Checkbutton", padx=20, pady=10) + tk_check_frame.grid(row=0, column=3, padx=(20, 10), pady=(20, 10), sticky="nsew") + + tk_check_1 = tk.Checkbutton(tk_check_frame, text="Unchecked", variable=self.var_0) + tk_check_1.grid(row=0, column=0, sticky="nsew") + + tk_check_2 = tk.Checkbutton(tk_check_frame, text="Checked", variable=self.var_1) + tk_check_2.grid(row=1, column=0, sticky="nsew") + + tk_check_4 = tk.Checkbutton(tk_check_frame, text="Disabled", state="disabled") + tk_check_4.grid(row=3, column=0, sticky="nsew") + + # Create a Frame for the Radiobuttons + tk_radio_frame = tk.LabelFrame(self, text="tk.Radiobutton", padx=20, pady=10) + tk_radio_frame.grid(row=2, column=3, padx=(20, 10), pady=10, sticky="nsew") + + tk_radio_1 = tk.Radiobutton(tk_radio_frame, text="Unselected", variable=self.var_3, value=1) + tk_radio_1.grid(row=0, column=0, sticky="nsew") + + tk_radio_2 = tk.Radiobutton(tk_radio_frame, text="Selected", variable=self.var_3, value=2) + tk_radio_2.grid(row=1, column=0, sticky="nsew") + + tk_radio_3 = tk.Radiobutton(tk_radio_frame, text="Disabled", state="disabled") + tk_radio_3.grid(row=2, column=0, sticky="nsew") + + # Create a Frame for input widgets + tk_widgets_frame = tk.Frame(self) + tk_widgets_frame.grid(row=0, column=4, padx=10, pady=(30, 10), sticky="nsew", rowspan=2) + tk_widgets_frame.columnconfigure(index=0, weight=1) + + # Entry + tk_entry = tk.Entry(tk_widgets_frame) + tk_entry.insert(0, "tk.Entry") + tk_entry.grid(row=0, column=0, padx=5, pady=(0, 10), sticky="ew") + + # Spinbox + tk_spinbox = tk.Spinbox(tk_widgets_frame, from_=0, to=100, increment=0.1) + tk_spinbox.grid(row=1, column=0, padx=5, pady=10, sticky="ew") + + # Menubutton + tk_menubutton = tk.Menubutton(tk_widgets_frame, text="tk.Menubutton", menu=menu, direction="below") + tk_menubutton.grid(row=4, column=0, padx=5, pady=10, sticky="nsew") + + # OptionMenu + tk_optionmenu = tk.OptionMenu(tk_widgets_frame, self.var_6, *self.tk_option_menu_list) + tk_optionmenu.grid(row=5, column=0, padx=5, pady=10, sticky="nsew") + + # Button + tk_button = tk.Button(tk_widgets_frame, text="tk.Button") + tk_button.grid(row=6, column=0, padx=5, pady=10, sticky="nsew") + + +def plugin_start3(path: str) -> str: + """Plugin initialization.""" + return 'TtkCatalog' + + +plugin_app = Catalog diff --git a/plugins/coriolis.py b/plugins/coriolis.py index a9eabcedb..0f2000de3 100644 --- a/plugins/coriolis.py +++ b/plugins/coriolis.py @@ -27,8 +27,8 @@ import json import tkinter as tk from tkinter import ttk -import myNotebook as nb # noqa: N813 # its not my fault. from EDMCLogging import get_main_logger +from myNotebook import EntryMenu from plug import show_error from config import config from l10n import translations as tr @@ -80,7 +80,7 @@ def plugin_start3(path: str) -> str: return 'Coriolis' -def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> ttk.Frame: """Set up plugin preferences.""" PADX = 10 # noqa: N806 PADY = 1 # noqa: N806 @@ -91,45 +91,45 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr coriolis_config.override_text_old_normal = tr.tl('Normal') # LANG: Coriolis normal/beta selection - normal coriolis_config.override_text_old_beta = tr.tl('Beta') # LANG: Coriolis normal/beta selection - beta - conf_frame = nb.Frame(parent) + conf_frame = ttk.Frame(parent) conf_frame.columnconfigure(index=1, weight=1) cur_row = 0 # LANG: Settings>Coriolis: Help/hint for changing coriolis URLs - nb.Label(conf_frame, text=tr.tl( + ttk.Label(conf_frame, text=tr.tl( "Set the URL to use with coriolis.io ship loadouts. Note that this MUST end with '/import?data='" )).grid(sticky=tk.EW, row=cur_row, column=0, padx=PADX, pady=PADY, columnspan=3) cur_row += 1 # LANG: Settings>Coriolis: Label for 'NOT alpha/beta game version' URL - nb.Label(conf_frame, text=tr.tl('Normal URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) - nb.EntryMenu(conf_frame, textvariable=coriolis_config.normal_textvar).grid( + ttk.Label(conf_frame, text=tr.tl('Normal URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) + 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 - nb.Button(conf_frame, text=tr.tl("Reset"), - command=lambda: coriolis_config.normal_textvar.set(value=DEFAULT_NORMAL_URL)).grid( + ttk.Button(conf_frame, text=tr.tl("Reset"), + command=lambda: coriolis_config.normal_textvar.set(value=DEFAULT_NORMAL_URL)).grid( sticky=tk.W, row=cur_row, column=2, padx=PADX, pady=0 ) cur_row += 1 # LANG: Settings>Coriolis: Label for 'alpha/beta game version' URL - nb.Label(conf_frame, text=tr.tl('Beta URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) - nb.EntryMenu(conf_frame, textvariable=coriolis_config.beta_textvar).grid( + ttk.Label(conf_frame, text=tr.tl('Beta URL')).grid(sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY) + 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=tr.tl('Reset'), - command=lambda: coriolis_config.beta_textvar.set(value=DEFAULT_BETA_URL)).grid( + ttk.Button(conf_frame, text=tr.tl('Reset'), + command=lambda: coriolis_config.beta_textvar.set(value=DEFAULT_BETA_URL)).grid( sticky=tk.W, row=cur_row, column=2, padx=PADX, pady=0 ) cur_row += 1 # TODO: This needs a help/hint text to be sure users know what it's for. # LANG: Settings>Coriolis: Label for selection of using Normal, Beta or 'auto' Coriolis URL - nb.Label(conf_frame, text=tr.tl('Override Beta/Normal Selection')).grid( + ttk.Label(conf_frame, text=tr.tl('Override Beta/Normal Selection')).grid( sticky=tk.W, row=cur_row, column=0, padx=PADX, pady=PADY ) - nb.OptionMenu( + ttk.OptionMenu( conf_frame, coriolis_config.override_textvar, coriolis_config.override_textvar.get(), diff --git a/plugins/eddn.py b/plugins/eddn.py index 0ece63273..ce861f925 100644 --- a/plugins/eddn.py +++ b/plugins/eddn.py @@ -31,18 +31,17 @@ from platform import system from textwrap import dedent from threading import Lock +from tkinter import ttk from typing import Any, Iterator, Mapping, MutableMapping import requests import companion import edmc_data import killswitch -import myNotebook as nb # noqa: N813 import plug from companion import CAPIData, category_map from config import applongname, appname, appversion_nobuild, config, debug_senders, user_agent from EDMCLogging import get_main_logger from monitor import monitor -from myNotebook import Frame from prefs import prefsVersion from ttkHyperlinkLabel import HyperlinkLabel from util import text @@ -102,25 +101,25 @@ def __init__(self): # tkinter UI bits. self.eddn_station: tk.IntVar - self.eddn_station_button: nb.Checkbutton + self.eddn_station_button: ttk.Checkbutton self.eddn_system: tk.IntVar - self.eddn_system_button: nb.Checkbutton + self.eddn_system_button: ttk.Checkbutton self.eddn_delay: tk.IntVar - self.eddn_delay_button: nb.Checkbutton + self.eddn_delay_button: ttk.Checkbutton # Tracking UI - self.ui: tk.Frame - self.ui_system_name: tk.Label - self.ui_system_address: tk.Label - self.ui_j_body_name: tk.Label - self.ui_j_body_id: tk.Label - self.ui_j_body_type: tk.Label - self.ui_s_body_name: tk.Label - self.ui_station_name: tk.Label - self.ui_station_type: tk.Label - self.ui_station_marketid: tk.Label + self.ui: ttk.Frame + self.ui_system_name: ttk.Label + self.ui_system_address: ttk.Label + self.ui_j_body_name: ttk.Label + self.ui_j_body_id: ttk.Label + self.ui_j_body_type: ttk.Label + self.ui_s_body_name: ttk.Label + self.ui_station_name: ttk.Label + self.ui_station_type: ttk.Label + self.ui_station_marketid: ttk.Label this = This() @@ -2009,7 +2008,7 @@ def plugin_start3(plugin_dir: str) -> str: return 'EDDN' -def plugin_app(parent: tk.Tk) -> tk.Frame | None: +def plugin_app(parent: tk.Tk) -> ttk.Frame | None: """ Set up any plugin-specific UI. @@ -2023,7 +2022,7 @@ def plugin_app(parent: tk.Tk) -> tk.Frame | None: this.eddn = EDDN(parent) if config.eddn_tracking_ui: - this.ui = tk.Frame(parent) + this.ui = ttk.Frame(parent) row = this.ui.grid_size()[1] @@ -2031,15 +2030,15 @@ def plugin_app(parent: tk.Tk) -> tk.Frame | None: # System ####################################################################### # SystemName - system_name_label = tk.Label(this.ui, text="J:SystemName:") + system_name_label = ttk.Label(this.ui, text="J:SystemName:") system_name_label.grid(row=row, column=0, sticky=tk.W) - this.ui_system_name = tk.Label(this.ui, name='eddn_track_system_name', anchor=tk.W) + this.ui_system_name = ttk.Label(this.ui, name='eddn_track_system_name', anchor=tk.W) this.ui_system_name.grid(row=row, column=1, sticky=tk.E) row += 1 # SystemAddress - system_address_label = tk.Label(this.ui, text="J:SystemAddress:") + system_address_label = ttk.Label(this.ui, text="J:SystemAddress:") system_address_label.grid(row=row, column=0, sticky=tk.W) - this.ui_system_address = tk.Label(this.ui, name='eddn_track_system_address', anchor=tk.W) + this.ui_system_address = ttk.Label(this.ui, name='eddn_track_system_address', anchor=tk.W) this.ui_system_address.grid(row=row, column=1, sticky=tk.E) row += 1 ####################################################################### @@ -2048,27 +2047,27 @@ def plugin_app(parent: tk.Tk) -> tk.Frame | None: # Body ####################################################################### # Body Name from Journal - journal_body_name_label = tk.Label(this.ui, text="J:BodyName:") + journal_body_name_label = ttk.Label(this.ui, text="J:BodyName:") journal_body_name_label.grid(row=row, column=0, sticky=tk.W) - this.ui_j_body_name = tk.Label(this.ui, name='eddn_track_j_body_name', anchor=tk.W) + this.ui_j_body_name = ttk.Label(this.ui, name='eddn_track_j_body_name', anchor=tk.W) this.ui_j_body_name.grid(row=row, column=1, sticky=tk.E) row += 1 # Body ID from Journal - journal_body_id_label = tk.Label(this.ui, text="J:BodyID:") + journal_body_id_label = ttk.Label(this.ui, text="J:BodyID:") journal_body_id_label.grid(row=row, column=0, sticky=tk.W) - this.ui_j_body_id = tk.Label(this.ui, name='eddn_track_j_body_id', anchor=tk.W) + this.ui_j_body_id = ttk.Label(this.ui, name='eddn_track_j_body_id', anchor=tk.W) this.ui_j_body_id.grid(row=row, column=1, sticky=tk.E) row += 1 # Body Type from Journal - journal_body_type_label = tk.Label(this.ui, text="J:BodyType:") + journal_body_type_label = ttk.Label(this.ui, text="J:BodyType:") journal_body_type_label.grid(row=row, column=0, sticky=tk.W) - this.ui_j_body_type = tk.Label(this.ui, name='eddn_track_j_body_type', anchor=tk.W) + this.ui_j_body_type = ttk.Label(this.ui, name='eddn_track_j_body_type', anchor=tk.W) this.ui_j_body_type.grid(row=row, column=1, sticky=tk.E) row += 1 # Body Name from Status.json - status_body_name_label = tk.Label(this.ui, text="S:BodyName:") + status_body_name_label = ttk.Label(this.ui, text="S:BodyName:") status_body_name_label.grid(row=row, column=0, sticky=tk.W) - this.ui_s_body_name = tk.Label(this.ui, name='eddn_track_s_body_name', anchor=tk.W) + this.ui_s_body_name = ttk.Label(this.ui, name='eddn_track_s_body_name', anchor=tk.W) this.ui_s_body_name.grid(row=row, column=1, sticky=tk.E) row += 1 ####################################################################### @@ -2077,21 +2076,21 @@ def plugin_app(parent: tk.Tk) -> tk.Frame | None: # Station ####################################################################### # Name - status_station_name_label = tk.Label(this.ui, text="J:StationName:") + status_station_name_label = ttk.Label(this.ui, text="J:StationName:") status_station_name_label.grid(row=row, column=0, sticky=tk.W) - this.ui_station_name = tk.Label(this.ui, name='eddn_track_station_name', anchor=tk.W) + this.ui_station_name = ttk.Label(this.ui, name='eddn_track_station_name', anchor=tk.W) this.ui_station_name.grid(row=row, column=1, sticky=tk.E) row += 1 # Type - status_station_type_label = tk.Label(this.ui, text="J:StationType:") + status_station_type_label = ttk.Label(this.ui, text="J:StationType:") status_station_type_label.grid(row=row, column=0, sticky=tk.W) - this.ui_station_type = tk.Label(this.ui, name='eddn_track_station_type', anchor=tk.W) + this.ui_station_type = ttk.Label(this.ui, name='eddn_track_station_type', anchor=tk.W) this.ui_station_type.grid(row=row, column=1, sticky=tk.E) row += 1 # MarketID - status_station_marketid_label = tk.Label(this.ui, text="J:StationID:") + status_station_marketid_label = ttk.Label(this.ui, text="J:StationID:") status_station_marketid_label.grid(row=row, column=0, sticky=tk.W) - this.ui_station_marketid = tk.Label(this.ui, name='eddn_track_station_id', anchor=tk.W) + this.ui_station_marketid = ttk.Label(this.ui, name='eddn_track_station_id', anchor=tk.W) this.ui_station_marketid.grid(row=row, column=1, sticky=tk.E) row += 1 ####################################################################### @@ -2107,11 +2106,11 @@ def tracking_ui_update() -> None: return this.ui_system_name['text'] = '≪None≫' - if this.ui_system_name is not None: + if this.system_name is not None: this.ui_system_name['text'] = this.system_name this.ui_system_address['text'] = '≪None≫' - if this.ui_system_address is not None: + if this.system_address is not None: this.ui_system_address['text'] = this.system_address this.ui_j_body_name['text'] = '≪None≫' @@ -2145,7 +2144,7 @@ def tracking_ui_update() -> None: this.ui.update_idletasks() -def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: +def plugin_prefs(parent, cmdr: str, is_beta: bool) -> ttk.Frame: """ Set up Preferences pane for this plugin. @@ -2164,20 +2163,19 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: else: output = config.get_int('output') - eddnframe = nb.Frame(parent) + eddnframe = ttk.Frame(parent) cur_row = 0 HyperlinkLabel( eddnframe, text='Elite Dangerous Data Network', - background=nb.Label().cget('background'), url='https://github.com/EDCD/EDDN#eddn---elite-dangerous-data-network', underline=True ).grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) # Don't translate cur_row += 1 this.eddn_station = tk.IntVar(value=(output & config.OUT_EDDN_SEND_STATION_DATA) and 1) - this.eddn_station_button = nb.Checkbutton( + this.eddn_station_button = ttk.Checkbutton( eddnframe, # LANG: Enable EDDN support for station data checkbox label text=tr.tl('Send station data to the Elite Dangerous Data Network'), @@ -2189,7 +2187,7 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: this.eddn_system = tk.IntVar(value=(output & config.OUT_EDDN_SEND_NON_STATION) and 1) # Output setting new in E:D 2.2 - this.eddn_system_button = nb.Checkbutton( + this.eddn_system_button = ttk.Checkbutton( eddnframe, # LANG: Enable EDDN support for system and other scan data checkbox label text=tr.tl('Send system and scan data to the Elite Dangerous Data Network'), @@ -2201,7 +2199,7 @@ def plugin_prefs(parent, cmdr: str, is_beta: bool) -> Frame: this.eddn_delay = tk.IntVar(value=(output & config.OUT_EDDN_DELAY) and 1) # Output setting under 'Send system and scan data to the Elite Dangerous Data Network' new in E:D 2.2 - this.eddn_delay_button = nb.Checkbutton( + this.eddn_delay_button = ttk.Checkbutton( eddnframe, # LANG: EDDN delay sending until docked option is on, this message notes that a send was skipped due to this text=tr.tl('Delay sending until docked'), diff --git a/plugins/edsm.py b/plugins/edsm.py index 1d9f8cb85..fbb056f8b 100644 --- a/plugins/edsm.py +++ b/plugins/edsm.py @@ -32,12 +32,12 @@ import requests import killswitch import monitor -import myNotebook as nb # noqa: N813 import plug from companion import CAPIData from config import applongname, appname, appversion, config, debug_senders, user_agent from edmc_data import DEBUG_WEBSERVER_HOST, DEBUG_WEBSERVER_PORT from EDMCLogging import get_main_logger +from myNotebook import EntryMenu from ttkHyperlinkLabel import HyperlinkLabel from l10n import translations as tr @@ -107,14 +107,14 @@ def __init__(self): self.label: tk.Widget | None = None - self.cmdr_label: nb.Label | None = None - self.cmdr_text: nb.Label | None = None + self.cmdr_label: ttk.Label | None = None + self.cmdr_text: ttk.Label | None = None - self.user_label: nb.Label | None = None - self.user: nb.EntryMenu | None = None + self.user_label: ttk.Label | None = None + self.user: EntryMenu | None = None - self.apikey_label: nb.Label | None = None - self.apikey: nb.EntryMenu | None = None + self.apikey_label: ttk.Label | None = None + self.apikey: EntryMenu | None = None this = This() @@ -277,7 +277,7 @@ def toggle_password_visibility(): this.apikey.config(show="*") # type: ignore -def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> ttk.Frame: """ Plugin preferences setup hook. @@ -295,21 +295,20 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr BOXY = 2 # noqa: N806 SEPY = 10 # noqa: N806 - frame = nb.Frame(parent) + frame = ttk.Frame(parent) frame.columnconfigure(1, weight=1) cur_row = 0 HyperlinkLabel( frame, text='Elite Dangerous Star Map', - background=nb.Label().cget('background'), url='https://www.edsm.net/', underline=True ).grid(row=cur_row, columnspan=2, padx=PADX, pady=PADY, sticky=tk.W) cur_row += 1 this.log = tk.IntVar(value=config.get_int('edsm_out') and 1) - this.log_button = nb.Checkbutton( + this.log_button = ttk.Checkbutton( frame, # LANG: Settings>EDSM - Label on checkbox for 'send data' text=tr.tl('Send flight log and CMDR status to EDSM'), @@ -328,30 +327,29 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr this.label = HyperlinkLabel( frame, text=tr.tl('Elite Dangerous Star Map credentials'), # LANG: Elite Dangerous Star Map credentials - background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True ) if this.label: this.label.grid(row=cur_row, columnspan=2, padx=PADX, pady=PADY, sticky=tk.W) cur_row += 1 - this.cmdr_label = nb.Label(frame, text=tr.tl('Cmdr')) # LANG: Game Commander name label in EDSM settings + this.cmdr_label = ttk.Label(frame, text=tr.tl('Cmdr')) # LANG: Game Commander name label in EDSM settings this.cmdr_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.cmdr_text = nb.Label(frame) + this.cmdr_text = ttk.Label(frame) this.cmdr_text.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.W) cur_row += 1 # LANG: EDSM Commander name label in EDSM settings - this.user_label = nb.Label(frame, text=tr.tl('Commander Name')) + this.user_label = ttk.Label(frame, text=tr.tl('Commander Name')) this.user_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.user = nb.EntryMenu(frame) + this.user = 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=tr.tl('API Key')) + this.apikey_label = ttk.Label(frame, text=tr.tl('API Key')) this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.apikey = nb.EntryMenu(frame, show="*", width=50) + this.apikey = EntryMenu(frame, show="*", width=50) this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) cur_row += 1 @@ -359,7 +357,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str | None, is_beta: bool) -> nb.Fr show_password_var.set(False) # Password is initially masked - show_password_checkbox = nb.Checkbutton( + show_password_checkbox = ttk.Checkbutton( frame, text=tr.tl('Show API Key'), # LANG: Text EDSM Show API Key variable=show_password_var, diff --git a/plugins/inara.py b/plugins/inara.py index f2c9830e7..603c05448 100644 --- a/plugins/inara.py +++ b/plugins/inara.py @@ -34,13 +34,13 @@ import requests import edmc_data import killswitch -import myNotebook as nb # noqa: N813 import plug import timeout_session from companion import CAPIData from config import applongname, appname, appversion, config, debug_senders from EDMCLogging import get_main_logger from monitor import monitor +from myNotebook import EntryMenu from ttkHyperlinkLabel import HyperlinkLabel from l10n import translations as tr @@ -120,10 +120,10 @@ def __init__(self): # Prefs UI self.log: 'tk.IntVar' - self.log_button: nb.Checkbutton + self.log_button: ttk.Checkbutton self.label: HyperlinkLabel - self.apikey: nb.EntryMenu - self.apikey_label: tk.Label + self.apikey: EntryMenu + self.apikey_label: ttk.Label self.events: dict[Credentials, Deque[Event]] = defaultdict(deque) self.event_lock: Lock = threading.Lock() # protects events, for use when rewriting events @@ -241,7 +241,7 @@ def toggle_password_visibility(): this.apikey.config(show="*") -def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: +def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> ttk.Frame: """Plugin Preferences UI hook.""" PADX = 10 # noqa: N806 BUTTONX = 12 # noqa: N806 # indent Checkbuttons and Radiobuttons @@ -250,16 +250,16 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: SEPY = 10 # noqa: N806 # seperator line spacing cur_row = 0 - frame = nb.Frame(parent) + frame = ttk.Frame(parent) frame.columnconfigure(1, weight=1) HyperlinkLabel( - frame, text='Inara', background=nb.Label().cget('background'), url='https://inara.cz/', underline=True + frame, text='Inara', url='https://inara.cz/', underline=True ).grid(row=cur_row, columnspan=2, padx=PADX, pady=PADY, sticky=tk.W) # Don't translate cur_row += 1 this.log = tk.IntVar(value=config.get_int('inara_out') and 1) - this.log_button = nb.Checkbutton( + this.log_button = ttk.Checkbutton( frame, text=tr.tl('Send flight log and Cmdr status to Inara'), # LANG: Checkbox to enable INARA API Usage variable=this.log, @@ -277,8 +277,7 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: # Section heading in settings this.label = HyperlinkLabel( frame, - text=tr.tl('Inara credentials'), # LANG: Text for INARA API keys link ( goes to https://inara.cz/settings-api ) - background=nb.Label().cget('background'), + text=tr.tl('Inara credentials'), # LANG: Text for INARA API keys link (goes to https://inara.cz/settings-api) url='https://inara.cz/settings-api', underline=True ) @@ -287,16 +286,16 @@ def plugin_prefs(parent: ttk.Notebook, cmdr: str, is_beta: bool) -> nb.Frame: cur_row += 1 # LANG: Inara API key label - this.apikey_label = nb.Label(frame, text=tr.tl('API Key')) # Inara setting + this.apikey_label = ttk.Label(frame, text=tr.tl('API Key')) # Inara setting this.apikey_label.grid(row=cur_row, padx=PADX, pady=PADY, sticky=tk.W) - this.apikey = nb.EntryMenu(frame, show="*", width=50) + this.apikey = EntryMenu(frame, show="*", width=50) this.apikey.grid(row=cur_row, column=1, padx=PADX, pady=BOXY, sticky=tk.EW) cur_row += 1 prefs_cmdr_changed(cmdr, is_beta) show_password_var.set(False) # Password is initially masked - show_password_checkbox = nb.Checkbutton( + show_password_checkbox = ttk.Checkbutton( frame, text=tr.tl('Show API Key'), # LANG: Text Inara Show API key variable=show_password_var, diff --git a/prefs.py b/prefs.py index 29e1e7b13..6d9e015cb 100644 --- a/prefs.py +++ b/prefs.py @@ -15,7 +15,6 @@ from tkinter import ttk from types import TracebackType from typing import Any, Callable, Optional, Type -import myNotebook as nb # noqa: N813 import plug from config import appversion_nobuild, config from EDMCLogging import edmclogger, get_main_logger @@ -263,7 +262,8 @@ def __init__(self, parent: tk.Tk, callback: Optional[Callable]): frame.rowconfigure(0, weight=1) frame.rowconfigure(1, weight=0) - notebook: nb.Notebook = nb.Notebook(frame) + notebook = ttk.Notebook(frame) + notebook.grid(padx=10, pady=10, sticky=tk.NSEW) notebook.bind('<>', self.tabchanged) # Recompute on tab change self.PADX = 10 @@ -327,7 +327,7 @@ def __init__(self, parent: tk.Tk, callback: Optional[Callable]): self.wm_minsize(min_width, min_height) def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: - output_frame = nb.Frame(root_notebook) + output_frame = ttk.Frame(root_notebook) output_frame.columnconfigure(0, weight=1) if prefsVersion.shouldSetDefaults('0.0.0.0', not bool(config.get_int('output'))): @@ -339,11 +339,11 @@ def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: row = AutoInc(start=0) # LANG: Settings > Output - choosing what data to save to files - self.out_label = nb.Label(output_frame, text=tr.tl('Please choose what data to save')) + self.out_label = ttk.Label(output_frame, text=tr.tl('Please choose what data to save')) self.out_label.grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) self.out_csv = tk.IntVar(value=1 if (output & config.OUT_MKT_CSV) else 0) - self.out_csv_button = nb.Checkbutton( + self.out_csv_button = ttk.Checkbutton( output_frame, text=tr.tl('Market data in CSV format file'), # LANG: Settings > Output option variable=self.out_csv, @@ -352,7 +352,7 @@ def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: self.out_csv_button.grid(columnspan=2, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) self.out_td = tk.IntVar(value=1 if (output & config.OUT_MKT_TD) else 0) - self.out_td_button = nb.Checkbutton( + self.out_td_button = ttk.Checkbutton( output_frame, text=tr.tl('Market data in Trade Dangerous format file'), # LANG: Settings > Output option variable=self.out_td, @@ -362,7 +362,7 @@ def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: self.out_ship = tk.IntVar(value=1 if (output & config.OUT_SHIP) else 0) # Output setting - self.out_ship_button = nb.Checkbutton( + self.out_ship_button = ttk.Checkbutton( output_frame, text=tr.tl('Ship loadout'), # LANG: Settings > Output option variable=self.out_ship, @@ -372,7 +372,7 @@ def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: self.out_auto = tk.IntVar(value=0 if output & config.OUT_MKT_MANUAL else 1) # inverted # Output setting - self.out_auto_button = nb.Checkbutton( + self.out_auto_button = ttk.Checkbutton( output_frame, text=tr.tl('Automatically update on docking'), # LANG: Settings > Output option variable=self.out_auto, @@ -383,7 +383,7 @@ def __setup_output_tab(self, root_notebook: ttk.Notebook) -> None: self.outdir = tk.StringVar() self.outdir.set(str(config.get_str('outdir'))) # LANG: Settings > Output - Label for "where files are located" - self.outdir_label = nb.Label(output_frame, text=tr.tl('File location')+':') # Section heading in settings + self.outdir_label = ttk.Label(output_frame, text=tr.tl('File location')+':') # Section heading in settings # 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 @@ -412,7 +412,7 @@ def __setup_plugin_tabs(self, notebook: ttk.Notebook) -> None: notebook.add(plugin_frame, text=plugin.name) def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 - config_frame = nb.Frame(notebook) + config_frame = ttk.Frame(notebook) config_frame.columnconfigure(1, weight=1) row = AutoInc(start=0) @@ -426,7 +426,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 self.logdir_entry = ttk.Entry(config_frame, takefocus=False) # Location of the Journal files - nb.Label( + ttk.Label( config_frame, # LANG: Settings > Configuration - Label for Journal files location text=tr.tl('E:D journal file location')+':' @@ -462,12 +462,12 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) - nb.Label( + ttk.Label( config_frame, text=tr.tl('CAPI Settings') # LANG: Settings > Configuration - Label for CAPI section ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) - nb.Checkbutton( + ttk.Checkbutton( config_frame, # LANG: Configuration - Enable or disable the Fleet Carrier CAPI calls text=tr.tl('Enable Fleet Carrier CAPI Queries'), @@ -484,7 +484,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 self.hotkey_only = tk.IntVar(value=not config.get_int('hotkey_always')) self.hotkey_play = tk.IntVar(value=not config.get_int('hotkey_mute')) with row as cur_row: - nb.Label( + ttk.Label( config_frame, text=tr.tl('Hotkey') # LANG: Hotkey/Shortcut settings prompt on Windows ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) @@ -503,7 +503,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 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( + self.hotkey_only_btn = ttk.Checkbutton( config_frame, # LANG: Configuration - Act on hotkey only when ED is in foreground text=tr.tl('Only when Elite: Dangerous is the active app'), @@ -514,7 +514,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 self.hotkey_only_btn.grid(columnspan=4, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) # Hotkey/Shortcut setting - self.hotkey_play_btn = nb.Checkbutton( + self.hotkey_play_btn = ttk.Checkbutton( config_frame, # LANG: Configuration - play sound when hotkey used text=tr.tl('Play sound'), @@ -530,7 +530,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 ) with row as curr_row: - nb.Label(config_frame, text=tr.tl('Update Track')).grid( # LANG: Select the Update Track (Beta, Stable) + ttk.Label(config_frame, text=tr.tl('Update Track')).grid( # LANG: Select the Update Track (Beta, Stable) padx=self.PADX, pady=self.PADY, sticky=tk.W, row=curr_row ) self.curr_update_track = "Beta" if config.get_bool('beta_optin') else "Stable" @@ -540,7 +540,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 tr.tl("Stable"), # LANG: Stable Version of EDMC tr.tl("Beta") # LANG: Beta Version of EDMC ] - self.update_track = nb.OptionMenu( + self.update_track = ttk.OptionMenu( config_frame, self.update_paths, self.update_paths.get(), *update_paths ) @@ -548,7 +548,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 self.update_track.grid(column=1, pady=self.BOXY, padx=self.PADX, sticky=tk.W, row=curr_row) self.disable_autoappupdatecheckingame = tk.IntVar(value=config.get_int('disable_autoappupdatecheckingame')) - self.disable_autoappupdatecheckingame_btn = nb.Checkbutton( + self.disable_autoappupdatecheckingame_btn = ttk.Checkbutton( config_frame, # LANG: Configuration - disable checks for app updates when in-game text=tr.tl('Disable Automatic Application Updates Check when in-game'), @@ -566,7 +566,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 # Settings prompt for preferred ship loadout, system and station info websites # LANG: Label for preferred shipyard, system and station 'providers' - nb.Label(config_frame, text=tr.tl('Preferred websites')).grid( + ttk.Label(config_frame, text=tr.tl('Preferred websites')).grid( columnspan=4, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) @@ -577,10 +577,10 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 ) # Setting to decide which ship outfitting website to link to - either E:D Shipyard or Coriolis # LANG: Label for Shipyard provider selection - nb.Label(config_frame, text=tr.tl('Shipyard')).grid( + ttk.Label(config_frame, text=tr.tl('Shipyard')).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) - self.shipyard_button = nb.OptionMenu( + self.shipyard_button = ttk.OptionMenu( config_frame, self.shipyard_provider, self.shipyard_provider.get(), *plug.provides('shipyard_url') ) @@ -588,7 +588,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 self.shipyard_button.grid(column=1, pady=self.BOXY, sticky=tk.W, row=cur_row) # Option for alternate URL opening self.alt_shipyard_open = tk.IntVar(value=config.get_int('use_alt_shipyard_open')) - self.alt_shipyard_open_btn = nb.Checkbutton( + self.alt_shipyard_open_btn = ttk.Checkbutton( config_frame, # LANG: Label for checkbox to utilise alternative Coriolis URL method text=tr.tl('Use alternate URL method'), @@ -605,8 +605,8 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 ) # LANG: Configuration - Label for selection of 'System' provider website - nb.Label(config_frame, text=tr.tl('System')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) - self.system_button = nb.OptionMenu( + ttk.Label(config_frame, text=tr.tl('System')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + self.system_button = ttk.OptionMenu( config_frame, self.system_provider, self.system_provider.get(), @@ -623,8 +623,9 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 ) # LANG: Configuration - Label for selection of 'Station' provider website - nb.Label(config_frame, text=tr.tl('Station')).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) - self.station_button = nb.OptionMenu( + ttk.Label(config_frame, text=tr.tl('Station')).grid( + padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) + self.station_button = ttk.OptionMenu( config_frame, self.station_provider, self.station_provider.get(), @@ -641,7 +642,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 with row as cur_row: # Set the current loglevel - nb.Label( + ttk.Label( config_frame, # LANG: Configuration - Label for selection of Log Level text=tr.tl('Log Level') @@ -658,7 +659,7 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 )) ) - self.loglevel_dropdown = nb.OptionMenu( + self.loglevel_dropdown = ttk.OptionMenu( config_frame, self.select_loglevel, self.select_loglevel.get(), @@ -676,28 +677,28 @@ def __setup_config_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 ).grid(column=2, padx=self.PADX, pady=0, sticky=tk.NSEW, row=cur_row) # Big spacer - nb.Label(config_frame).grid(sticky=tk.W, row=row.get()) + ttk.Label(config_frame).grid(sticky=tk.W, row=row.get()) # LANG: Label for 'Configuration' tab in Settings notebook.add(config_frame, text=tr.tl('Configuration')) def __setup_privacy_tab(self, notebook: ttk.Notebook) -> None: - privacy_frame = nb.Frame(notebook) + privacy_frame = ttk.Frame(notebook) self.hide_multicrew_captain = tk.BooleanVar(value=config.get_bool('hide_multicrew_captain', default=False)) self.hide_private_group = tk.BooleanVar(value=config.get_bool('hide_private_group', default=False)) row = AutoInc(start=0) # LANG: UI elements privacy section header in privacy tab of preferences - nb.Label(privacy_frame, text=tr.tl('Main UI privacy options')).grid( + ttk.Label(privacy_frame, text=tr.tl('Main UI privacy options')).grid( row=row.get(), column=0, sticky=tk.W, padx=self.PADX, pady=self.PADY ) - nb.Checkbutton( + ttk.Checkbutton( # LANG: Hide private group owner name from UI checkbox privacy_frame, text=tr.tl('Hide private group name in UI'), variable=self.hide_private_group ).grid(row=row.get(), column=0, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W) - nb.Checkbutton( + ttk.Checkbutton( # LANG: Hide multicrew captain name from main UI checkbox privacy_frame, text=tr.tl('Hide multi-crew captain name'), variable=self.hide_multicrew_captain @@ -723,14 +724,14 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: row = AutoInc(start=0) - appearance_frame = nb.Frame(notebook) + appearance_frame = ttk.Frame(notebook) appearance_frame.columnconfigure(2, weight=1) with row as cur_row: # LANG: Appearance - Label for selection of application display language - nb.Label(appearance_frame, text=tr.tl('Language')).grid( + ttk.Label(appearance_frame, text=tr.tl('Language')).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) - self.lang_button = nb.OptionMenu(appearance_frame, self.lang, self.lang.get(), *self.languages.values()) + self.lang_button = ttk.OptionMenu(appearance_frame, self.lang, self.lang.get(), *self.languages.values()) self.lang_button.grid(column=1, columnspan=2, padx=0, pady=self.BOXY, sticky=tk.W, row=cur_row) ttk.Separator(appearance_frame, orient=tk.HORIZONTAL).grid( @@ -739,26 +740,26 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: # Appearance setting # LANG: Label for Settings > Appearance > Theme selection - nb.Label(appearance_frame, text=tr.tl('Theme')).grid( + ttk.Label(appearance_frame, text=tr.tl('Theme')).grid( columnspan=3, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) # Appearance theme and language setting - nb.Radiobutton( + ttk.Radiobutton( # LANG: Label for 'Default' theme radio button appearance_frame, text=tr.tl('Default'), variable=self.theme, value=theme.THEME_DEFAULT, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) # Appearance theme setting - nb.Radiobutton( + ttk.Radiobutton( # LANG: Label for 'Dark' theme radio button appearance_frame, text=tr.tl('Dark'), variable=self.theme, value=theme.THEME_DARK, command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) if sys.platform == 'win32': - nb.Radiobutton( + ttk.Radiobutton( appearance_frame, # LANG: Label for 'Transparent' theme radio button text=tr.tl('Transparent'), # Appearance theme setting @@ -768,7 +769,7 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) with row as cur_row: - self.theme_label_0 = nb.Label(appearance_frame, text=self.theme_prompts[0]) + self.theme_label_0 = ttk.Label(appearance_frame, text=self.theme_prompts[0]) self.theme_label_0.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row) # Main window @@ -783,7 +784,7 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: self.theme_button_0.grid(column=1, padx=0, pady=self.BOXY, sticky=tk.NSEW, row=cur_row) with row as cur_row: - self.theme_label_1 = nb.Label(appearance_frame, text=self.theme_prompts[1]) + self.theme_label_1 = ttk.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 = tk.Button( appearance_frame, @@ -807,7 +808,7 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: ) with row as cur_row: # LANG: Appearance - Label for selection of UI scaling - nb.Label(appearance_frame, text=tr.tl('UI Scale Percentage')).grid( + ttk.Label(appearance_frame, text=tr.tl('UI Scale Percentage')).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) @@ -825,7 +826,7 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: ) self.uiscale_bar.grid(column=1, padx=0, pady=self.BOXY, sticky=tk.W, row=cur_row) - self.ui_scaling_defaultis = nb.Label( + self.ui_scaling_defaultis = ttk.Label( appearance_frame, # LANG: Appearance - Help/hint text for UI scaling selection text=tr.tl('100 means Default{CR}Restart Required for{CR}changes to take effect!') @@ -838,7 +839,7 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: with row as cur_row: # LANG: Appearance - Label for selection of main window transparency - nb.Label(appearance_frame, text=tr.tl("Main window transparency")).grid( + ttk.Label(appearance_frame, text=tr.tl("Main window transparency")).grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=cur_row ) self.transparency = tk.IntVar() @@ -855,7 +856,7 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: command=lambda _: self.parent.wm_attributes("-alpha", self.transparency.get() / 100) ) - nb.Label( + ttk.Label( appearance_frame, # LANG: Appearance - Help/hint text for Main window transparency selection text=tr.tl( @@ -877,7 +878,7 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) - self.ontop_button = nb.Checkbutton( + self.ontop_button = ttk.Checkbutton( appearance_frame, # LANG: Appearance - Label for checkbox to select if application always on top text=tr.tl('Always on top'), @@ -889,7 +890,7 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: ) # Appearance setting if sys.platform == 'win32': - nb.Checkbutton( + ttk.Checkbutton( appearance_frame, # LANG: Appearance option for Windows "minimize to system tray" text=tr.tl('Minimize to system tray'), @@ -897,26 +898,26 @@ def __setup_appearance_tab(self, notebook: ttk.Notebook) -> None: command=self.themevarchanged ).grid(columnspan=3, padx=self.BUTTONX, pady=self.PADY, sticky=tk.W, row=row.get()) # Appearance setting - nb.Label(appearance_frame).grid(sticky=tk.W) # big spacer + ttk.Label(appearance_frame).grid(sticky=tk.W) # big spacer # LANG: Label for Settings > Appearance tab notebook.add(appearance_frame, text=tr.tl('Appearance')) # Tab heading in settings def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 # Plugin settings and info - plugins_frame = nb.Frame(notebook) + plugins_frame = ttk.Frame(notebook) plugins_frame.columnconfigure(0, weight=1) row = AutoInc(start=0) self.plugdir = tk.StringVar() self.plugdir.set(str(config.get_str('plugin_dir'))) # LANG: Label for location of third-party plugins folder - self.plugdir_label = nb.Label(plugins_frame, text=tr.tl('Plugins folder') + ':') + self.plugdir_label = ttk.Label(plugins_frame, text=tr.tl('Plugins folder') + ':') self.plugdir_label.grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) self.plugdir_entry = ttk.Entry(plugins_frame, takefocus=False, textvariable=self.plugdir) # Link StringVar to Entry widget self.plugdir_entry.grid(columnspan=4, padx=self.PADX, pady=self.BOXY, sticky=tk.EW, row=row.get()) with row as cur_row: - nb.Label( + ttk.Label( plugins_frame, # Help text in settings # LANG: Tip/label about how to disable plugins @@ -958,7 +959,7 @@ def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid( columnspan=4, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) - nb.Label( + ttk.Label( plugins_frame, # LANG: Label on list of enabled plugins text=tr.tl('Enabled Plugins')+':' # List of plugins in settings @@ -966,10 +967,10 @@ def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 for plugin in enabled_plugins: if plugin.name == plugin.folder: - label = nb.Label(plugins_frame, text=plugin.name) + label = ttk.Label(plugins_frame, text=plugin.name) else: - label = nb.Label(plugins_frame, text=f'{plugin.folder} ({plugin.name})') + label = ttk.Label(plugins_frame, text=f'{plugin.folder} ({plugin.name})') label.grid(columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get()) @@ -981,21 +982,20 @@ def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) # LANG: Plugins - Label for list of 'enabled' plugins that don't work with Python 3.x - nb.Label(plugins_frame, text=tr.tl('Plugins Without Python 3.x Support')+':').grid( + ttk.Label(plugins_frame, text=tr.tl('Plugins Without Python 3.x Support')+':').grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) HyperlinkLabel( # LANG: Plugins - Label on URL to documentation about migrating plugins from Python 2.7 plugins_frame, text=tr.tl('Information on migrating plugins'), - background=nb.Label().cget('background'), url='https://github.com/EDCD/EDMarketConnector/blob/main/PLUGINS.md#migration-from-python-27', underline=True ).grid(columnspan=2, padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) for plugin in plug.PLUGINS_not_py3: if plugin.folder: # 'system' ones have this set to None to suppress listing in Plugins prefs tab - nb.Label(plugins_frame, text=plugin.name).grid( + ttk.Label(plugins_frame, text=plugin.name).grid( columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get() ) ############################################################ @@ -1006,14 +1006,14 @@ def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 ttk.Separator(plugins_frame, orient=tk.HORIZONTAL).grid( columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) - nb.Label( + ttk.Label( plugins_frame, # LANG: Label on list of user-disabled plugins text=tr.tl('Disabled Plugins')+':' # List of plugins in settings ).grid(padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get()) for plugin in disabled_plugins: - nb.Label(plugins_frame, text=plugin.name).grid( + ttk.Label(plugins_frame, text=plugin.name).grid( columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get() ) ############################################################ @@ -1024,13 +1024,13 @@ def __setup_plugin_tab(self, notebook: ttk.Notebook) -> None: # noqa: CCR001 columnspan=3, padx=self.PADX, pady=self.SEPY, sticky=tk.EW, row=row.get() ) # LANG: Plugins - Label for list of 'broken' plugins that failed to load - nb.Label(plugins_frame, text=tr.tl('Broken Plugins')+':').grid( + ttk.Label(plugins_frame, text=tr.tl('Broken Plugins')+':').grid( padx=self.PADX, pady=self.PADY, sticky=tk.W, row=row.get() ) for plugin in plug.PLUGINS_broken: if plugin.folder: # 'system' ones have this set to None to suppress listing in Plugins prefs tab - nb.Label(plugins_frame, text=plugin.name).grid( + ttk.Label(plugins_frame, text=plugin.name).grid( columnspan=2, padx=self.LISTX, pady=self.PADY, sticky=tk.W, row=row.get() ) @@ -1292,8 +1292,8 @@ def apply(self) -> None: # noqa: CCR001 config.set('theme', self.theme.get()) config.set('dark_text', self.theme_colors[0]) config.set('dark_highlight', self.theme_colors[1]) - theme.apply(self.parent) - if self.plugdir.get() != config.get('plugin_dir'): + theme.apply() + if self.plugdir.get() != config.get_str('plugin_dir'): config.set( 'plugin_dir', str(Path(config.home_path, self.plugdir.get()[2:])) if self.plugdir.get().startswith('~') else diff --git a/requirements-win.txt b/requirements-win.txt new file mode 100644 index 000000000..b87e41243 --- /dev/null +++ b/requirements-win.txt @@ -0,0 +1,17 @@ +simplesystray==0.1.0 +pywin32==306 + +winrt-Microsoft.UI==2.2.0 +winrt-Microsoft.UI.Interop==2.2.0 +winrt-Microsoft.UI.Windowing==2.2.0 +winrt-Microsoft.Windows.ApplicationModel.DynamicDependency==2.2.0 +winrt-Microsoft.Windows.ApplicationModel.DynamicDependency.Bootstrap==2.2.0 +winrt-Windows.Foundation.Numerics==2.2.0 +winrt-Windows.System==2.2.0 +winrt-Windows.System.Interop==2.2.0 +winrt-Windows.UI==2.2.0 +winrt-Windows.UI.Composition==2.2.0 +winrt-Windows.UI.Composition.Desktop==2.2.0 +winrt-Windows.UI.Composition.Interop==2.2.0 + +-r requirements.txt diff --git a/requirements.txt b/requirements.txt index 7aac47d10..93c9e6712 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,5 @@ requests==2.32.3 pillow==10.3.0 watchdog==4.0.1 -simplesystray==0.1.0; sys_platform == 'win32' semantic-version==2.10.0 -# For manipulating folder permissions and the like. -pywin32==306; sys_platform == 'win32' psutil==6.0.0 diff --git a/stats.py b/stats.py index b0ba34b32..36d713348 100644 --- a/stats.py +++ b/stats.py @@ -15,7 +15,6 @@ from typing import Any, AnyStr, Callable, NamedTuple, Sequence, cast import companion import EDMCLogging -import myNotebook as nb # noqa: N813 from edmc_data import ship_name_map from hotkey import hotkeymgr from l10n import Locale, translations as tr @@ -302,7 +301,7 @@ def export_ships(companion_data: dict[str, Any], filename: AnyStr) -> None: class StatsDialog: """Status dialog containing all of the current cmdr's stats.""" - def __init__(self, parent: tk.Tk, status: tk.Label) -> None: + def __init__(self, parent: tk.Tk, status: ttk.Label) -> None: self.parent: tk.Tk = parent self.status = status self.showstats() @@ -379,7 +378,8 @@ def __init__(self, parent: tk.Tk, data: dict[str, Any]) -> None: frame = ttk.Frame(self) frame.grid(sticky=tk.NSEW) - notebook = nb.Notebook(frame) + notebook = ttk.Notebook(frame) + notebook.grid(padx=10, pady=10, sticky=tk.NSEW) page = self.addpage(notebook) for thing in stats[CR_LINES_START:CR_LINES_END]: @@ -443,7 +443,7 @@ def addpage( if header is None: header = [] - page = nb.Frame(parent) + page = ttk.Frame(parent) page.grid(pady=10, sticky=tk.NSEW) page.columnconfigure(0, weight=1) if header: @@ -479,7 +479,7 @@ def addpagerow( row = -1 # To silence unbound warnings for i, col_content in enumerate(content): # label = HyperlinkLabel(parent, text=col_content, popup_copy=True) - label = nb.Label(parent, text=col_content) + label = ttk.Label(parent, text=col_content) if with_copy: label.bind('', self.copy_callback(label, col_content)) @@ -499,7 +499,7 @@ def credits(self, value: int) -> str: return Locale.string_from_number(value, 0) + ' Cr' # type: ignore @staticmethod - def copy_callback(label: tk.Label, text_to_copy: str) -> Callable[..., None]: + def copy_callback(label: ttk.Label, text_to_copy: str) -> Callable[..., None]: """Copy data in Label to clipboard.""" def do_copy(event: tk.Event) -> None: label.clipboard_clear() @@ -507,6 +507,6 @@ def do_copy(event: tk.Event) -> None: old_bg = label['bg'] label['bg'] = 'gray49' - label.after(100, (lambda: label.configure(bg=old_bg))) + label.after(100, (lambda: label.configure(background=old_bg))) return do_copy diff --git a/theme.py b/theme.py index 5eed9fe14..cf919325c 100644 --- a/theme.py +++ b/theme.py @@ -5,44 +5,38 @@ Licensed under the GNU General Public License. See LICENSE file. -Because of various ttk limitations this app is an unholy mix of Tk and ttk widgets. -So can't use ttk's theme support. So have to change colors manually. +Believe us, this used to be much worse before ttk's theme support was properly leveraged. """ from __future__ import annotations import os import sys import tkinter as tk -from tkinter import font as tk_font +import warnings from tkinter import ttk from typing import Callable -from l10n import translations as tr -from config import config +from config import appname, config from EDMCLogging import get_main_logger -from ttkHyperlinkLabel import HyperlinkLabel logger = get_main_logger() if __debug__: from traceback import print_exc -if sys.platform == "linux": - from ctypes import POINTER, Structure, byref, c_char_p, c_int, c_long, c_uint, c_ulong, c_void_p, cdll - - if sys.platform == 'win32': - import ctypes - from ctypes.wintypes import DWORD, LPCVOID, LPCWSTR + import win32con import win32gui - AddFontResourceEx = ctypes.windll.gdi32.AddFontResourceExW - AddFontResourceEx.restypes = [LPCWSTR, DWORD, LPCVOID] # type: ignore + from winrt.microsoft.ui.interop import get_window_id_from_window + from winrt.microsoft.ui.windowing import AppWindow + from winrt.windows.ui import Color, Colors + from ctypes import windll FR_PRIVATE = 0x10 - FR_NOT_ENUM = 0x20 - font_path = config.respath_path / 'EUROCAPS.TTF' - AddFontResourceEx(str(font_path), FR_PRIVATE, 0) + fonts_loaded = windll.gdi32.AddFontResourceExW(str(config.respath_path / 'EUROCAPS.TTF'), FR_PRIVATE, 0) + if fonts_loaded < 1: + logger.error('Unable to load Euro Caps font for Transparent theme') elif sys.platform == 'linux': - # pyright: reportUnboundVariable=false + from ctypes import POINTER, Structure, byref, c_char_p, c_int, c_long, c_uint, c_ulong, c_void_p, cdll XID = c_ulong # from X.h: typedef unsigned long XID Window = XID Atom = c_ulong @@ -125,136 +119,28 @@ class MotifWmHints(Structure): class _Theme: - - # Enum ? Remember these are, probably, based on 'value' of a tk - # RadioButton set. Looking in prefs.py, they *appear* to be hard-coded - # there as well. + # TODO ditch indexes, support additional themes in user folder THEME_DEFAULT = 0 THEME_DARK = 1 THEME_TRANSPARENT = 2 + packages = { + THEME_DEFAULT: 'light', # 'default' is the name of a builtin theme + THEME_DARK: 'dark', + THEME_TRANSPARENT: 'transparent', + } + style: ttk.Style + root: tk.Tk + binds: dict[str, str] = {} def __init__(self) -> None: self.active: int | None = None # Starts out with no theme self.minwidth: int | None = None - self.widgets: dict[tk.Widget | tk.BitmapImage, set] = {} - self.widgets_pair: list = [] - self.defaults: dict = {} - self.current: dict = {} self.default_ui_scale: float | None = None # None == not yet known self.startup_ui_scale: int | None = None - def register(self, widget: tk.Widget | tk.BitmapImage) -> None: # noqa: CCR001, C901 - # Note widget and children for later application of a theme. Note if - # 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 - self.defaults = { - 'fg': tk.Label()['foreground'], # SystemButtonText, systemButtonText - 'bg': tk.Label()['background'], # SystemButtonFace, White - 'font': tk.Label()['font'], # TkDefaultFont - 'bitmapfg': tk.BitmapImage()['foreground'], # '-foreground {} {} #000000 #000000' - 'bitmapbg': tk.BitmapImage()['background'], # '-background {} {} {} {}' - 'entryfg': tk.Entry()['foreground'], # SystemWindowText, Black - 'entrybg': tk.Entry()['background'], # SystemWindow, systemWindowBody - 'entryfont': tk.Entry()['font'], # TkTextFont - 'frame': tk.Frame()['background'], # SystemButtonFace, systemWindowBody - 'menufg': tk.Menu()['foreground'], # SystemMenuText, - 'menubg': tk.Menu()['background'], # SystemMenu, - 'menufont': tk.Menu()['font'], # TkTextFont - } - - if widget not in self.widgets: - # No general way to tell whether the user has overridden, so compare against widget-type specific defaults - attribs = set() - if isinstance(widget, tk.BitmapImage): - if widget['foreground'] not in ['', self.defaults['bitmapfg']]: - attribs.add('fg') - if widget['background'] not in ['', self.defaults['bitmapbg']]: - attribs.add('bg') - elif isinstance(widget, (tk.Entry, ttk.Entry)): - if widget['foreground'] not in ['', self.defaults['entryfg']]: - attribs.add('fg') - if widget['background'] not in ['', self.defaults['entrybg']]: - attribs.add('bg') - if 'font' in widget.keys() and str(widget['font']) not in ['', self.defaults['entryfont']]: - attribs.add('font') - elif isinstance(widget, (tk.Canvas, tk.Frame, ttk.Frame)): - if ( - ('background' in widget.keys() or isinstance(widget, tk.Canvas)) - and widget['background'] not in ['', self.defaults['frame']] - ): - attribs.add('bg') - elif isinstance(widget, HyperlinkLabel): - pass # Hack - HyperlinkLabel changes based on state, so skip - elif isinstance(widget, tk.Menu): - if widget['foreground'] not in ['', self.defaults['menufg']]: - attribs.add('fg') - if widget['background'] not in ['', self.defaults['menubg']]: - attribs.add('bg') - if widget['font'] not in ['', self.defaults['menufont']]: - attribs.add('font') - else: # tk.Button, tk.Label - if 'foreground' in widget.keys() and widget['foreground'] not in ['', self.defaults['fg']]: - attribs.add('fg') - if 'background' in widget.keys() and widget['background'] not in ['', self.defaults['bg']]: - attribs.add('bg') - if 'font' in widget.keys() and widget['font'] not in ['', self.defaults['font']]: - attribs.add('font') - self.widgets[widget] = attribs - - if isinstance(widget, (tk.Frame, ttk.Frame)): - for child in widget.winfo_children(): - self.register(child) - - def register_alternate(self, pair: tuple, gridopts: dict) -> None: - self.widgets_pair.append((pair, gridopts)) - - def button_bind( - self, widget: tk.Widget, command: Callable, image: tk.BitmapImage | None = None - ) -> None: - widget.bind('', command) - widget.bind('', lambda e: self._enter(e, image)) - widget.bind('', lambda e: self._leave(e, image)) - - def _enter(self, event: tk.Event, image: tk.BitmapImage | None) -> None: - widget = event.widget - if widget and widget['state'] != tk.DISABLED: - try: - widget.configure(state=tk.ACTIVE) - - except Exception: - logger.exception(f'Failure setting widget active: {widget=}') - - if image: - try: - image.configure(foreground=self.current['activeforeground'], - background=self.current['activebackground']) - - except Exception: - logger.exception(f'Failure configuring image: {image=}') - - def _leave(self, event: tk.Event, image: tk.BitmapImage | None) -> None: - widget = event.widget - if widget and widget['state'] != tk.DISABLED: - try: - widget.configure(state=tk.NORMAL) - - except Exception: - logger.exception(f'Failure setting widget normal: {widget=}') - - if image: - try: - image.configure(foreground=self.current['foreground'], background=self.current['background']) - - except Exception: - logger.exception(f'Failure configuring image: {image=}') - - # Set up colors - def _colors(self, root: tk.Tk, theme: int) -> None: - style = ttk.Style() - if sys.platform == 'linux': - style.theme_use('clam') + def initialize(self, root: tk.Tk): + self.style = ttk.Style() + self.root = root # Default dark theme colors if not config.get_str('dark_text'): @@ -262,37 +148,23 @@ def _colors(self, root: tk.Tk, theme: int) -> None: if not config.get_str('dark_highlight'): config.set('dark_highlight', 'white') - if theme == self.THEME_DEFAULT: - # (Mostly) system colors - style = ttk.Style() - self.current = { - 'background': (style.lookup('TLabel', 'background')), - 'foreground': style.lookup('TLabel', 'foreground'), - 'activebackground': (sys.platform == 'win32' and 'SystemHighlight' or - style.lookup('TLabel', 'background', ['active'])), - 'activeforeground': (sys.platform == 'win32' and 'SystemHighlightText' or - style.lookup('TLabel', 'foreground', ['active'])), - 'disabledforeground': style.lookup('TLabel', 'foreground', ['disabled']), - 'highlight': 'blue', - 'font': 'TkDefaultFont', - } - - else: # Dark *or* Transparent - (r, g, b) = root.winfo_rgb(config.get_str('dark_text')) - self.current = { - 'background': 'grey4', # OSX inactive dark titlebar color - 'foreground': config.get_str('dark_text'), - 'activebackground': config.get_str('dark_text'), - 'activeforeground': 'grey4', - 'disabledforeground': f'#{int(r/384):02x}{int(g/384):02x}{int(b/384):02x}', - 'highlight': config.get_str('dark_highlight'), - # Font only supports Latin 1 / Supplement / Extended, and a - # few General Punctuation and Mathematical Operators - # LANG: Label for commander name in main window - 'font': (theme > 1 and not 0x250 < ord(tr.tl('Cmdr')[0]) < 0x3000 and - tk_font.Font(family='Euro Caps', size=10, weight=tk_font.NORMAL) or - 'TkDefaultFont'), - } + for theme_file in config.internal_theme_dir_path.glob('*/pkgIndex.tcl'): + try: + self.root.tk.call('source', theme_file) + logger.info(f'loading theme package from "{theme_file}"') + except tk.TclError: + logger.exception(f'Failure loading theme package "{theme_file}"') + + def register(self, widget: tk.Widget | tk.BitmapImage) -> None: + assert isinstance(widget, (tk.BitmapImage, tk.Widget)), widget + warnings.warn('theme.register() is no longer necessary as theme attributes are set on tk level', + DeprecationWarning, stacklevel=2) + + def register_alternate(self, pair: tuple, gridopts: dict) -> None: + ... # does any plugin even use this? + + def button_bind(self, widget: tk.Widget, command: Callable) -> None: + ... # does any plugin even use this? def update(self, widget: tk.Widget) -> None: """ @@ -302,170 +174,74 @@ def update(self, widget: tk.Widget) -> None: :param widget: Target widget. """ assert isinstance(widget, (tk.BitmapImage, tk.Widget)), widget - if not self.current: - return # No need to call this for widgets created in plugin_app() - - self.register(widget) - self._update_widget(widget) - if isinstance(widget, (tk.Frame, ttk.Frame)): - for child in widget.winfo_children(): - self._update_widget(child) - - # Apply current theme to a single widget - def _update_widget(self, widget: tk.Widget | tk.BitmapImage) -> None: # noqa: CCR001, C901 - if widget not in self.widgets: - if isinstance(widget, tk.Widget): - w_class = widget.winfo_class() - w_keys: list[str] = widget.keys() - - else: - # There is no tk.BitmapImage.winfo_class() - w_class = '' - # There is no tk.BitmapImage.keys() - w_keys = [] - - assert_str = f'{w_class} {widget} "{"text" in w_keys and widget["text"]}"' - raise AssertionError(assert_str) - - attribs: set = self.widgets.get(widget, set()) + warnings.warn('theme.update() is no longer necessary as theme attributes are set on tk level', + DeprecationWarning, stacklevel=2) - try: - if isinstance(widget, tk.BitmapImage): - # not a widget - if 'fg' not in attribs: - widget['foreground'] = self.current['foreground'] - - if 'bg' not in attribs: - widget['background'] = self.current['background'] - - elif 'cursor' in widget.keys() and str(widget['cursor']) not in ['', 'arrow']: - # Hack - highlight widgets like HyperlinkLabel with a non-default cursor - if 'fg' not in attribs: - widget['foreground'] = self.current['highlight'] - if 'insertbackground' in widget.keys(): # tk.Entry - widget['insertbackground'] = self.current['foreground'] - - if 'bg' not in attribs: - widget['background'] = self.current['background'] - if 'highlightbackground' in widget.keys(): # tk.Entry - widget['highlightbackground'] = self.current['background'] - - if 'font' not in attribs: - widget['font'] = self.current['font'] - - elif 'activeforeground' in widget.keys(): - # e.g. tk.Button, tk.Label, tk.Menu - if 'fg' not in attribs: - widget['foreground'] = self.current['foreground'] - widget['activeforeground'] = self.current['activeforeground'] - widget['disabledforeground'] = self.current['disabledforeground'] - - if 'bg' not in attribs: - widget['background'] = self.current['background'] - widget['activebackground'] = self.current['activebackground'] - - if 'font' not in attribs: - widget['font'] = self.current['font'] - - elif 'foreground' in widget.keys(): - # e.g. ttk.Label - if 'fg' not in attribs: - widget['foreground'] = self.current['foreground'] - - if 'bg' not in attribs: - widget['background'] = self.current['background'] - - if 'font' not in attribs: - widget['font'] = self.current['font'] - - elif 'background' in widget.keys() or isinstance(widget, tk.Canvas): - # e.g. Frame, Canvas - if 'bg' not in attribs: - widget['background'] = self.current['background'] - widget['highlightbackground'] = self.current['disabledforeground'] + def transparent_onenter(self, event=None): + self.root.attributes("-transparentcolor", '') + if sys.platform == 'win32': + self.set_title_buttons_background(Color(255, 10, 10, 10)) - except Exception: - logger.exception(f'Plugin widget issue ? {widget=}') + def transparent_onleave(self, event=None): + if event.widget == self.root: + self.root.attributes("-transparentcolor", 'grey4') + if sys.platform == 'win32': + self.set_title_buttons_background(Colors.transparent) - # Apply configured theme + def set_title_buttons_background(self, color: Color): + hwnd = win32gui.GetParent(self.root.winfo_id()) + window = AppWindow.get_from_window_id(get_window_id_from_window(hwnd)) + window.title_bar.button_background_color = color + window.title_bar.button_inactive_background_color = color - def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 + def apply(self) -> None: theme = config.get_int('theme') - self._colors(root, theme) - - # Apply colors - for widget in set(self.widgets): - if isinstance(widget, tk.Widget) and not widget.winfo_exists(): - self.widgets.pop(widget) # has been destroyed - else: - self._update_widget(widget) - - # Switch menus - for pair, gridopts in self.widgets_pair: - for widget in pair: - if isinstance(widget, tk.Widget): - widget.grid_remove() - - if isinstance(pair[0], tk.Menu): - if theme == self.THEME_DEFAULT: - root['menu'] = pair[0] - - else: # Dark *or* Transparent - root['menu'] = '' - pair[theme].grid(**gridopts) - - else: - pair[theme].grid(**gridopts) + try: + self.root.tk.call('ttk::setTheme', self.packages[theme]) + except tk.TclError: + logger.exception(f'Failure setting theme: {self.packages[theme]}') if self.active == theme: return # Don't need to mess with the window manager self.active = theme + self.root.withdraw() + self.root.update_idletasks() # Size gets recalculated here if sys.platform == 'win32': - import win32con + hwnd = win32gui.GetParent(self.root.winfo_id()) + window = AppWindow.get_from_window_id(get_window_id_from_window(hwnd)) + title_gap: ttk.Frame = self.root.nametowidget(f'{appname.lower()}.alternate_menubar.title_gap') - # FIXME: Lose the "treat this like a boolean" bullshit if theme == self.THEME_DEFAULT: - root.overrideredirect(False) - + window.title_bar.reset_to_default() + title_gap['height'] = 0 else: - root.overrideredirect(True) - - if theme == self.THEME_TRANSPARENT: - root.attributes("-transparentcolor", 'grey4') - - else: - root.attributes("-transparentcolor", '') - - root.withdraw() - root.update_idletasks() # Size and windows styles get recalculated here - hwnd = win32gui.GetParent(root.winfo_id()) - win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, - win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE) - & ~win32con.WS_MAXIMIZEBOX) # disable maximize + window.title_bar.extends_content_into_title_bar = True + self.set_title_buttons_background(Color(255, 10, 10, 10)) + title_gap['height'] = window.title_bar.height if theme == self.THEME_TRANSPARENT: + # TODO prevent loss of focus when hovering the title bar area win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_APPWINDOW | win32con.WS_EX_LAYERED) # Add to taskbar - + self.binds[''] = self.root.bind('', self.transparent_onenter) + self.binds[''] = self.root.bind('', self.transparent_onenter) + self.binds[''] = self.root.bind('', self.transparent_onleave) + self.binds[''] = self.root.bind('', self.transparent_onleave) else: win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, win32con.WS_EX_APPWINDOW) # Add to taskbar - - root.deiconify() - root.wait_visibility() # need main window to be displayed before returning - + for event, bind in self.binds.items(): + self.root.unbind(event, bind) + self.binds.clear() else: - root.withdraw() - root.update_idletasks() # Size gets recalculated here if dpy: xroot = Window() parent = Window() children = Window() nchildren = c_uint() - XQueryTree(dpy, root.winfo_id(), byref(xroot), byref(parent), byref(children), byref(nchildren)) + XQueryTree(dpy, self.root.winfo_id(), byref(xroot), byref(parent), byref(children), byref(nchildren)) if theme == self.THEME_DEFAULT: wm_hints = motif_wm_hints_normal - else: # Dark *or* Transparent wm_hints = motif_wm_hints_dark @@ -475,19 +251,12 @@ def apply(self, root: tk.Tk) -> None: # noqa: CCR001, C901 XFlush(dpy) - else: - if theme == self.THEME_DEFAULT: - root.overrideredirect(False) - - else: # Dark *or* Transparent - root.overrideredirect(True) - - root.deiconify() - root.wait_visibility() # need main window to be displayed before returning + self.root.deiconify() + self.root.wait_visibility() # need main window to be displayed before returning if not self.minwidth: - self.minwidth = root.winfo_width() # Minimum width = width on first creation - root.minsize(self.minwidth, -1) + self.minwidth = self.root.winfo_width() # Minimum width = width on first creation + self.root.minsize(self.minwidth, -1) # singleton diff --git a/themes/dark/dark.tcl b/themes/dark/dark.tcl new file mode 100644 index 000000000..7825a88f9 --- /dev/null +++ b/themes/dark/dark.tcl @@ -0,0 +1,114 @@ +package require Tk 8.6 + +namespace eval ttk::theme::dark { + + variable version 1.0 + package provide ttk::theme::dark $version + variable colors + array set colors { + -fg "#ff8000" + -bg "grey4" + -disabledfg "#aa5500" + -selectfg "grey4" + -selectbg "#ff8000" + -highlight "white" + } + variable font TkDefaultFont + variable font_u [font create {*}[font configure TkDefaultFont] -underline 1] + variable flatborder [list -relief groove -bordercolor $colors(-fg) -darkcolor $colors(-bg) -lightcolor $colors(-bg)] + + ttk::style theme create dark -parent clam -settings { + ttk::style configure . \ + -background $colors(-bg) \ + -foreground $colors(-fg) \ + -troughcolor $colors(-bg) \ + -focuscolor $colors(-selectbg) \ + -selectbackground $colors(-selectbg) \ + -selectforeground $colors(-selectfg) \ + -insertwidth 1 \ + -insertcolor $colors(-fg) \ + -fieldbackground $colors(-bg) \ + -font $font \ + -borderwidth 1 \ + -relief flat + + tk_setPalette background $colors(-bg) \ + foreground $colors(-fg) \ + highlightColor $colors(-selectbg) \ + selectBackground $colors(-selectbg) \ + selectForeground $colors(-fg) \ + activeBackground $colors(-selectbg) \ + activeForeground $colors(-fg) + + ttk::style map . -foreground [list disabled $colors(-disabledfg)] + + option add *Font $font + option add *Menu.selectcolor $colors(-fg) + + ttk::style configure TLabel -padding 1 + + ttk::style configure Link.TLabel -foreground $colors(-highlight) -relief flat + ttk::style map Link.TLabel \ + -font [list active $font_u] \ + -foreground [list disabled $colors(-disabledfg)] \ + -cursor [list disabled arrow] + + ttk::style configure TLabelframe {*}$flatborder + + ttk::style configure TSeparator -background $colors(-fg) + + ttk::style configure TEntry -padding 2 {*}$flatborder + + ttk::style configure TButton -padding {8 4 8 4} -width -10 -anchor center {*}$flatborder + ttk::style map TButton \ + -background [list pressed $colors(-selectbg)] \ + -foreground [list pressed $colors(-selectfg)] + + ttk::style configure Toolbutton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style configure TMenubutton -padding {8 4 4 4} {*}$flatborder -arrowcolor $colors(-fg) + ttk::style map TMenubutton \ + -background [list active $colors(-selectbg)] \ + -foreground [list active $colors(-selectfg)] \ + -arrowcolor [list active $colors(-selectfg)] + ttk::style configure Menubar.TMenubutton -padding 2 -relief flat -arrowsize 0 + + ttk::style configure TCheckbutton -padding 4 -indicatormargin 4 + + ttk::style configure ToggleButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style configure TRadiobutton -padding 4 -indicatormargin 4 + + ttk::style configure TSpinbox -padding 2 {*}$flatborder -arrowcolor $colors(-fg) -arrowsize 10 + + ttk::style configure TCombobox -padding 2 {*}$flatborder -arrowcolor $colors(-fg) + ttk::style map TCombobox \ + -selectbackground [list \ + {!focus} $colors(-selectbg) \ + {readonly hover} $colors(-selectbg) \ + {readonly focus} $colors(-selectbg)] \ + -selectforeground [list \ + {!focus} $colors(-selectfg) \ + {readonly hover} $colors(-selectfg) \ + {readonly focus} $colors(-selectfg)] + + ttk::style configure TNotebook -padding 2 {*}$flatborder + ttk::style configure TNotebook.Tab -padding 2 {*}$flatborder + ttk::style map TNotebook.Tab \ + -background [list selected $colors(-selectbg)] \ + -foreground [list selected $colors(-selectfg)] \ + -lightcolor [list selected $colors(-selectbg)] + + ttk::style configure Treeview {*}$flatborder + ttk::style map Treeview \ + -background [list selected $colors(-selectbg)] \ + -foreground [list selected $colors(-selectfg)] + ttk::style configure Treeview.Item -padding {2 0 0 0} + + ttk::style configure TScrollbar {*}$flatborder -background $colors(-selectbg) + + ttk::style configure TScale {*}$flatborder -background $colors(-selectbg) + + ttk::style configure TProgressbar {*}$flatborder -background $colors(-selectbg) + } +} diff --git a/themes/dark/pkgIndex.tcl b/themes/dark/pkgIndex.tcl new file mode 100644 index 000000000..50341687c --- /dev/null +++ b/themes/dark/pkgIndex.tcl @@ -0,0 +1,2 @@ +package ifneeded ttk::theme::dark 1.0 \ + [list source [file join [file dirname [info script]] dark.tcl]] diff --git a/themes/light/light.tcl b/themes/light/light.tcl new file mode 100644 index 000000000..e201ae37c --- /dev/null +++ b/themes/light/light.tcl @@ -0,0 +1,97 @@ +package require Tk 8.6 + +namespace eval ttk::theme::light { + + variable version 1.0 + package provide ttk::theme::light $version + variable colors + array set colors { + -fg "black" + -bg "#dcdad5" + -disabledfg "#999999" + -selectfg "white" + -selectbg "#9e9a91" + -highlight "blue" + } + variable font TkDefaultFont + variable font_u [font create {*}[font configure TkDefaultFont] -underline 1] + + ttk::style theme create light -parent clam -settings { + ttk::style configure . \ + -background $colors(-bg) \ + -foreground $colors(-fg) \ + -troughcolor $colors(-bg) \ + -focuscolor $colors(-selectbg) \ + -selectbackground $colors(-selectbg) \ + -selectforeground $colors(-selectfg) \ + -insertwidth 1 \ + -insertcolor $colors(-fg) \ + -fieldbackground $colors(-bg) \ + -font $font \ + -borderwidth 1 \ + -relief flat + + tk_setPalette background $colors(-bg) \ + foreground $colors(-fg) \ + highlightColor $colors(-selectbg) \ + selectBackground $colors(-selectbg) \ + selectForeground $colors(-fg) \ + activeBackground $colors(-selectbg) \ + activeForeground $colors(-fg) + + ttk::style map . -foreground [list disabled $colors(-disabledfg)] + + option add *Font $font + option add *Menu.selectcolor $colors(-fg) + + ttk::style configure TLabel -padding 1 + + ttk::style configure Link.TLabel -foreground $colors(-highlight) + ttk::style map Link.TLabel \ + -font [list active $font_u] \ + -foreground [list disabled $colors(-disabledfg)] \ + -cursor [list disabled arrow] + + ttk::style configure TLabelframe -relief groove + + ttk::style configure TEntry -padding 2 + + ttk::style configure TButton -padding {8 4 8 4} -width -10 -anchor center -relief groove + ttk::style map TButton \ + -background [list pressed $colors(-selectbg)] \ + -foreground [list pressed $colors(-selectfg)] + + ttk::style configure Toolbutton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style configure TMenubutton -padding {8 4 4 4} -relief groove -arrowcolor $colors(-fg) + ttk::style map TMenubutton \ + -background [list active $colors(-selectbg)] \ + -foreground [list active $colors(-selectfg)] \ + -arrowcolor [list active $colors(-selectfg)] + ttk::style configure Menubar.TMenubutton -padding 2 -relief flat -arrowsize 0 + + ttk::style configure TCheckbutton -padding 4 -indicatormargin 4 + + ttk::style configure ToggleButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style configure TRadiobutton -padding 4 -indicatormargin 4 + + ttk::style configure TSpinbox -padding 2 -arrowsize 10 + + ttk::style configure TCombobox -padding 2 + + ttk::style configure TNotebook -padding 2 + ttk::style configure TNotebook.Tab -padding 2 + + ttk::style configure Treeview.Item -padding {2 0 0 0} + ttk::style map Treeview \ + -background [list selected $colors(-selectbg)] \ + -foreground [list selected $colors(-selectfg)] + + ttk::style configure TScrollbar -troughcolor $colors(-selectbg) + + ttk::style configure TScale -troughcolor $colors(-selectbg) + + ttk::style configure TProgressbar -troughcolor $colors(-selectbg) + } +} diff --git a/themes/light/pkgIndex.tcl b/themes/light/pkgIndex.tcl new file mode 100644 index 000000000..b22a47876 --- /dev/null +++ b/themes/light/pkgIndex.tcl @@ -0,0 +1,2 @@ +package ifneeded ttk::theme::light 1.0 \ + [list source [file join [file dirname [info script]] light.tcl]] diff --git a/themes/transparent/pkgIndex.tcl b/themes/transparent/pkgIndex.tcl new file mode 100644 index 000000000..3387f8b24 --- /dev/null +++ b/themes/transparent/pkgIndex.tcl @@ -0,0 +1,2 @@ +package ifneeded ttk::theme::transparent 1.0 \ + [list source [file join [file dirname [info script]] transparent.tcl]] diff --git a/themes/transparent/transparent.tcl b/themes/transparent/transparent.tcl new file mode 100644 index 000000000..b4e3f1115 --- /dev/null +++ b/themes/transparent/transparent.tcl @@ -0,0 +1,114 @@ +package require Tk 8.6 + +namespace eval ttk::theme::transparent { + + variable version 1.0 + package provide ttk::theme::transparent $version + variable colors + array set colors { + -fg "#ff8000" + -bg "grey4" + -disabledfg "#aa5500" + -selectfg "grey4" + -selectbg "#ff8000" + -highlight "white" + } + variable font [font create -family "Euro Caps" -size 10] + variable font_u [font create -family "Euro Caps" -size 10 -underline 1] + variable flatborder [list -relief groove -bordercolor $colors(-fg) -darkcolor $colors(-bg) -lightcolor $colors(-bg)] + + ttk::style theme create transparent -parent clam -settings { + ttk::style configure . \ + -background $colors(-bg) \ + -foreground $colors(-fg) \ + -troughcolor $colors(-bg) \ + -focuscolor $colors(-selectbg) \ + -selectbackground $colors(-selectbg) \ + -selectforeground $colors(-selectfg) \ + -insertwidth 1 \ + -insertcolor $colors(-fg) \ + -fieldbackground $colors(-bg) \ + -font $font \ + -borderwidth 1 \ + -relief flat + + tk_setPalette background $colors(-bg) \ + foreground $colors(-fg) \ + highlightColor $colors(-selectbg) \ + selectBackground $colors(-selectbg) \ + selectForeground $colors(-fg) \ + activeBackground $colors(-selectbg) \ + activeForeground $colors(-fg) + + ttk::style map . -foreground [list disabled $colors(-disabledfg)] + + option add *Font $font + option add *Menu.selectcolor $colors(-fg) + + ttk::style configure TLabel -padding 1 + + ttk::style configure Link.TLabel -foreground $colors(-highlight) + ttk::style map Link.TLabel \ + -font [list active $font_u] \ + -foreground [list disabled $colors(-disabledfg)] \ + -cursor [list disabled arrow] + + ttk::style configure TLabelframe {*}$flatborder + + ttk::style configure TSeparator -background $colors(-fg) + + ttk::style configure TEntry -padding 2 {*}$flatborder + + ttk::style configure TButton -padding {8 4 8 4} -width -10 -anchor center {*}$flatborder + ttk::style map TButton \ + -background [list pressed $colors(-selectbg)] \ + -foreground [list pressed $colors(-selectfg)] + + ttk::style configure Toolbutton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style configure TMenubutton -padding {8 4 4 4} {*}$flatborder -arrowcolor $colors(-fg) + ttk::style map TMenubutton \ + -background [list active $colors(-selectbg)] \ + -foreground [list active $colors(-selectfg)] \ + -arrowcolor [list active $colors(-selectfg)] + ttk::style configure Menubar.TMenubutton -padding 2 -relief flat -arrowsize 0 + + ttk::style configure TCheckbutton -padding 4 -indicatormargin 4 + + ttk::style configure ToggleButton -padding {8 4 8 4} -width -10 -anchor center + + ttk::style configure TRadiobutton -padding 4 -indicatormargin 4 + + ttk::style configure TSpinbox -padding 2 {*}$flatborder -arrowcolor $colors(-fg) -arrowsize 10 + + ttk::style configure TCombobox -padding 2 {*}$flatborder -arrowcolor $colors(-fg) + ttk::style map TCombobox \ + -selectbackground [list \ + {!focus} $colors(-selectbg) \ + {readonly hover} $colors(-selectbg) \ + {readonly focus} $colors(-selectbg)] \ + -selectforeground [list \ + {!focus} $colors(-selectfg) \ + {readonly hover} $colors(-selectfg) \ + {readonly focus} $colors(-selectfg)] + + ttk::style configure TNotebook -padding 2 {*}$flatborder + ttk::style configure TNotebook.Tab -padding 2 {*}$flatborder + ttk::style map TNotebook.Tab \ + -background [list selected $colors(-selectbg)] \ + -foreground [list selected $colors(-selectfg)] \ + -lightcolor [list selected $colors(-selectbg)] + + ttk::style configure Treeview {*}$flatborder + ttk::style map Treeview \ + -background [list selected $colors(-selectbg)] \ + -foreground [list selected $colors(-selectfg)] + ttk::style configure Treeview.Item -padding {2 0 0 0} + + ttk::style configure TScrollbar {*}$flatborder -background $colors(-selectbg) + + ttk::style configure TScale {*}$flatborder -background $colors(-selectbg) + + ttk::style configure TProgressbar {*}$flatborder -background $colors(-selectbg) + } +} diff --git a/ttkHyperlinkLabel.py b/ttkHyperlinkLabel.py index 3d62185d9..c2c376991 100644 --- a/ttkHyperlinkLabel.py +++ b/ttkHyperlinkLabel.py @@ -21,12 +21,12 @@ from __future__ import annotations import html from functools import partial -import sys +import random +import string import tkinter as tk import webbrowser -from tkinter import font as tk_font from tkinter import ttk -from typing import Any +from typing import Any, no_type_check import plug from config import config, logger from l10n import translations as tr @@ -46,38 +46,64 @@ """ +LABEL_TO_STYLE = ['anchor', 'background', 'font', 'foreground', 'relief'] -class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore +class HyperlinkLabel(ttk.Button): """Clickable label for HTTP links.""" - def __init__(self, master: ttk.Frame | tk.Frame | None = None, **kw: Any) -> None: + _legacy_style: str | None = None + + def _handle_legacy_options(self, options: dict): # noqa: CCR001 + label_options = {opt: options.pop(opt) for opt in LABEL_TO_STYLE if opt in options} + disabledforeground = options.pop('disabledforeground', None) + justify = options.pop('justify', None) # noqa: F841 + wraplength = options.pop('wraplength', None) # noqa: F841 + if len(label_options) > 0 or disabledforeground or self.font or self.underline is not None: + if not self._legacy_style: + self._legacy_style = f'{"".join(random.choices(string.ascii_letters+string.digits, k=8))}.Link.TLabel' + if len(label_options) > 0: + ttk.Style().configure(self._legacy_style, **label_options) + if disabledforeground: + ttk.Style().map(self._legacy_style, foreground=[('disabled', disabledforeground)]) + if self.font: + font_u = tk.font.Font(font=self.font) + if self.underline is None: + ttk.Style().configure(self._legacy_style, font=self.font) + font_u.configure(underline=True) + ttk.Style().map(self._legacy_style, font=[('active', font_u.name)]) + else: + font_u.configure(underline=self.underline) + ttk.Style().configure(self._legacy_style, font=font_u.name) + else: + font_n = ttk.Style().lookup('Link.TLabel', 'font') + font_u = ttk.Style().lookup('Link.TLabel', 'font', ['active']) + font_default = font_u if self.underline else font_n + font_active = font_n if self.underline is False else font_u + ttk.Style().configure(self._legacy_style, font=font_default) + ttk.Style().map(self._legacy_style, font=[('active', font_active)]) + # TODO emulate justify and wraplength + options['style'] = self._legacy_style + return options + + def __init__(self, master: tk.Widget | None = None, **kw: Any) -> None: """ Initialize the HyperlinkLabel. :param master: The master widget. :param kw: Additional keyword arguments. """ - self.font_u: tk_font.Font - self.font_n = None self.url = kw.pop('url', None) self.popup_copy = kw.pop('popup_copy', False) - self.underline = kw.pop('underline', None) # override ttk.Label's underline - self.foreground = kw.get('foreground', 'blue') - self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup( - 'TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option - ttk.Label.__init__(self, master, **kw) + self.underline = kw.pop('underline', None) + self.font = kw.pop('font', None) + kw.setdefault('command', self._click) + kw.setdefault('style', 'Link.TLabel') + kw = self._handle_legacy_options(kw) + super().__init__(master, **kw) - self.bind('', self._click) self.bind('', self._contextmenu) - - self.bind('', self._enter) - self.bind('', self._leave) - - # set up initial appearance - self.configure(state=kw.get('state', tk.NORMAL), - text=kw.get('text'), - font=kw.get('font', ttk.Style().lookup('TLabel', 'font'))) + self.bind('<>', self._theme) # Add Menu Options self.plug_options = kw.pop('plug_options', None) @@ -121,41 +147,15 @@ def open_station(self, url: str): if opener: return webbrowser.open(opener) - def configure( # noqa: CCR001 + @no_type_check + def configure( self, cnf: dict[str, Any] | None = None, **kw: Any ) -> dict[str, tuple[str, str, str, Any, Any]] | None: """Change cursor and appearance depending on state and text.""" - # This class' state - for thing in ('url', 'popup_copy', 'underline'): + for thing in ('url', 'popup_copy', 'underline', 'font'): if thing in kw: setattr(self, thing, kw.pop(thing)) - for thing in ('foreground', 'disabledforeground'): - if thing in kw: - setattr(self, thing, kw[thing]) - - # Emulate disabledforeground option for ttk.Label - if 'state' in kw: - state = kw['state'] - if state == tk.DISABLED and 'foreground' not in kw: - kw['foreground'] = self.disabledforeground - elif state != tk.DISABLED and 'foreground' not in kw: - kw['foreground'] = self.foreground - - if 'font' in kw: - self.font_n = kw['font'] - self.font_u = tk_font.Font(font=self.font_n) - self.font_u.configure(underline=True) - kw['font'] = self.font_u if self.underline is True else self.font_n - - if 'cursor' not in kw: - state = kw.get('state', str(self['state'])) - if state == tk.DISABLED: - kw['cursor'] = 'arrow' # System default - elif self.url and (kw['text'] if 'text' in kw else self['text']): - kw['cursor'] = 'hand2' - else: - kw['cursor'] = ('no' if sys.platform == 'win32' else 'circle') - + kw = self._handle_legacy_options(kw) return super().configure(cnf, **kw) def __setitem__(self, key: str, value: Any) -> None: @@ -167,19 +167,13 @@ def __setitem__(self, key: str, value: Any) -> None: """ self.configure(**{key: value}) - def _enter(self, event: tk.Event) -> None: - if self.url and self.underline is not False and str(self['state']) != tk.DISABLED: - super().configure(font=self.font_u) - - def _leave(self, event: tk.Event) -> None: - if not self.underline: - super().configure(font=self.font_n) + def _theme(self, event: tk.Event): + self._handle_legacy_options({}) - def _click(self, event: tk.Event) -> None: + def _click(self) -> None: if self.url and self['text'] and str(self['state']) != tk.DISABLED: url = self.url(self['text']) if callable(self.url) else self.url if url: - self._leave(event) # Remove underline before we change window to browser webbrowser.open(url) def _contextmenu(self, event: tk.Event) -> None: @@ -231,4 +225,4 @@ def copy(self) -> None: def copy_slef(self) -> None: """Copy the current text to the clipboard.""" self.clipboard_clear() - self.clipboard_append(monitor.slef) + self.clipboard_append(monitor.slef or '')