""" ttkHyperlinkLabel.py - Clickable ttk labels. Copyright (c) EDCD, All Rights Reserved Licensed under the GNU General Public License. See LICENSE file. In addition to standard ttk.Label arguments, takes the following arguments: url: The URL as a string that the user will be sent to on clicking on non-empty label text. If url is a function it will be called on click with the current label text and should return the URL as a string. underline: If True/False the text is always/never underlined. If None (the default) the text is underlined only on hover. popup_copy: Whether right-click on non-empty label text pops up a context menu with a 'Copy' option. Defaults to no context menu. If popup_copy is a function it will be called with the current label text and should return a boolean. May be imported by plugins """ from __future__ import annotations import html from functools import partial import sys import tkinter as tk import webbrowser from tkinter import font as tk_font from tkinter import ttk from typing import Any import plug from config import config, logger from l10n import translations as tr from monitor import monitor SHIPYARD_HTML_TEMPLATE = """ Redirecting you to your {ship_name} at {provider_name}... You should be redirected to your {ship_name} at {provider_name} shortly... """ class HyperlinkLabel(tk.Label or ttk.Label): # type: ignore """Clickable label for HTTP links.""" def __init__(self, master: ttk.Frame | tk.Frame | None = None, **kw: Any) -> None: """ 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.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'))) # Add Menu Options self.plug_options = kw.pop('plug_options', None) self.name = kw.get('name', None) def open_shipyard(self, url: str): """Open the Current Ship Loadout in the Selected Provider.""" if not (loadout := monitor.ship()): logger.warning('No ship loadout, aborting.') return '' if not bool(config.get_int("use_alt_shipyard_open")): opener = plug.invoke(url, 'EDSY', 'shipyard_url', loadout, monitor.is_beta) if opener: return webbrowser.open(opener) else: # Avoid file length limits if possible target = plug.invoke(url, 'EDSY', 'shipyard_url', loadout, monitor.is_beta) file_name = config.app_dir_path / "last_shipyard.html" with open(file_name, 'w') as f: f.write(SHIPYARD_HTML_TEMPLATE.format( link=html.escape(str(target)), provider_name=html.escape(str(url)), ship_name=html.escape("Ship") )) webbrowser.open(f'file://localhost/{file_name}') def open_system(self, url: str): """Open the Current System in the Selected Provider.""" opener = plug.invoke(url, 'EDSM', 'system_url', monitor.state['SystemName']) if opener: return webbrowser.open(opener) def open_station(self, url: str): """Open the Current Station in the Selected Provider.""" opener = plug.invoke( url, 'EDSM', 'station_url', monitor.state['SystemName'], monitor.state['StationName'] ) if opener: return webbrowser.open(opener) def configure( # noqa: CCR001 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'): 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') return super().configure(cnf, **kw) def __setitem__(self, key: str, value: Any) -> None: """ Allow for dict member style setting of options. :param key: option name :param value: option value """ 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 _click(self, event: tk.Event) -> 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: """ Display the context menu when right-clicked. :param event: The event object. """ menu = tk.Menu(tearoff=tk.FALSE) # LANG: Label for 'Copy' as in 'Copy and Paste' menu.add_command(label=tr.tl('Copy'), command=self.copy) # As in Copy and Paste if self.name == 'ship': # LANG: Copy the Inara SLEF Format of the active ship to the clipboard menu.add_command(label=tr.tl('Copy Inara SLEF'), command=self.copy_slef, state=tk.DISABLED) menu.entryconfigure(1, state=monitor.slef and tk.NORMAL or tk.DISABLED) menu.add_separator() for url in plug.provides('shipyard_url'): menu.add_command( label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider command=partial(self.open_shipyard, url) ) if self.name == 'station': menu.add_separator() for url in plug.provides('station_url'): menu.add_command( label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider command=partial(self.open_station, url) ) if self.name == 'system': menu.add_separator() for url in plug.provides('system_url'): menu.add_command( label=tr.tl("Open in {URL}").format(URL=url), # LANG: Open Element In Selected Provider command=partial(self.open_system, url) ) if self['text'] and (self.popup_copy(self['text']) if callable(self.popup_copy) else self.popup_copy): menu.post(event.x_root, event.y_root) def copy(self) -> None: """Copy the current text to the clipboard.""" self.clipboard_clear() self.clipboard_append(self['text']) def copy_slef(self) -> None: """Copy the current text to the clipboard.""" self.clipboard_clear() self.clipboard_append(monitor.slef)