""" 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 os import path 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 = path.join(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)