1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-04 19:40:02 +03:00
EDMarketConnector/ttkHyperlinkLabel.py

235 lines
8.9 KiB
Python

"""
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 = """
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="refresh" content="0; url={link}">
<title>Redirecting you to your {ship_name} at {provider_name}...</title>
</head>
<body>
<a href="{link}">
You should be redirected to your {ship_name} at {provider_name} shortly...
</a>
</body>
</html>
"""
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('<Button-1>', self._click)
self.bind('<Button-3>', self._contextmenu)
self.bind('<Enter>', self._enter)
self.bind('<Leave>', 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)