1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-14 16:27:13 +03:00
EDMarketConnector/ttkHyperlinkLabel.py
Athanasius e280d6c283
ttkHyperlinklabel.py: Initial flake8 pass (and some mypy)
This fixes an apparently harmless bug in the `openurl()` function defined
in this module (it's not part of the class).

1. On `win32` lookup the user setting for opening HTTPS URLs.
2. If that doesn't look like IE or Edge...
3. Set `cls` to the value for that.
4. Now look up the 'use this command' for *that* ...
5. And if it doesn't have `iexplore` in it...
6. Use `subprocess.Popen()` to invoke that browser with the given URL.

The problem is that step 6 still tries to use `buf.value`.  But `buf` is
no longer present as it was from before 5989acd0d3263e54429ff99769ff73a20476d863
changed over to `winreg`.  It should be just `value` from the winreg
calls.

That exception is then caught and ignored, and it ends up just running
`webbrowser.open(url)` anyway.

To be honest, this feels like we should just make this an unconditional
call to `webbrowser.open(url)` now, given apparently no-one's complained
about it always actually using that not working for them.  Given Edge is
Chrome-based now, and any supported OS should have Edge, Chrome or Firefox
(OK, maybe Safari and some others) as the HTTPS browser, I don't see this
being an issue.
2022-12-03 18:21:16 +00:00

179 lines
7.1 KiB
Python

"""
A clickable ttk label for HTTP links.
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
"""
import sys
import tkinter as tk
import webbrowser
from tkinter import font as tk_font
from tkinter import ttk
if sys.platform == 'win32':
import subprocess
from winreg import HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, CloseKey, OpenKeyEx, QueryValueEx
# A clickable ttk Label
#
class HyperlinkLabel(sys.platform == 'darwin' and tk.Label or ttk.Label, object):
"""Clickable label for HTTP links."""
def __init__(self, master=None, **kw):
self.url = 'url' in kw and kw.pop('url') or 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') or 'blue'
self.disabledforeground = kw.pop('disabledforeground', ttk.Style().lookup(
'TLabel', 'foreground', ('disabled',))) # ttk.Label doesn't support disabledforeground option
if sys.platform == 'darwin':
# Use tk.Label 'cos can't set ttk.Label background - http://www.tkdocs.com/tutorial/styles.html#whydifficult
kw['background'] = kw.pop('background', 'systemDialogBackgroundActive')
kw['anchor'] = kw.pop('anchor', tk.W) # like ttk.Label
tk.Label.__init__(self, master, **kw)
else:
ttk.Label.__init__(self, master, **kw)
self.bind('<Button-1>', self._click)
self.menu = tk.Menu(None, tearoff=tk.FALSE)
# LANG: Label for 'Copy' as in 'Copy and Paste'
self.menu.add_command(label=_('Copy'), command=self.copy) # As in Copy and Paste
self.bind(sys.platform == 'darwin' and '<Button-2>' or '<Button-3>', self._contextmenu)
self.bind('<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')))
def configure(self, cnf=None, **kw): # noqa: CCR001
"""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 kw.get('state') == tk.DISABLED:
if 'foreground' not in kw:
kw['foreground'] = self.disabledforeground
elif 'state' in kw:
if '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.underline is True and self.font_u or self.font_n
if 'cursor' not in kw:
if (kw['state'] if 'state' in kw else str(self['state'])) == tk.DISABLED:
kw['cursor'] = 'arrow' # System default
elif self.url and (kw['text'] if 'text' in kw else self['text']):
kw['cursor'] = sys.platform == 'darwin' and 'pointinghand' or 'hand2'
else:
kw['cursor'] = (sys.platform == 'darwin' and 'notallowed') or (
sys.platform == 'win32' and 'no') or 'circle'
super(HyperlinkLabel, self).configure(cnf, **kw)
def __setitem__(self, key, value) -> None:
"""
Allow for dict member style setting of options.
:param key: option name
:param value: option value
"""
self.configure(None, **{key: value})
def _enter(self, event):
if self.url and self.underline is not False and str(self['state']) != tk.DISABLED:
super(HyperlinkLabel, self).configure(font=self.font_u)
def _leave(self, event):
if not self.underline:
super(HyperlinkLabel, self).configure(font=self.font_n)
def _click(self, event):
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
openurl(url)
def _contextmenu(self, event):
if self['text'] and (self.popup_copy(self['text']) if callable(self.popup_copy) else self.popup_copy):
self.menu.post(sys.platform == 'darwin' and event.x_root + 1 or event.x_root, event.y_root)
def copy(self) -> None:
"""Copy the current text to the clipboard."""
self.clipboard_clear()
self.clipboard_append(self['text'])
def openurl(url) -> None: # noqa: CCR001
"""
Open the given URL in appropriate browser.
:param url: URL to open.
"""
if sys.platform == 'win32':
# FIXME: Is still still true with supported Windows 10 and 11 ?
# On Windows webbrowser.open calls os.startfile which calls ShellExecute which can't handle long arguments,
# so discover and launch the browser directly.
# https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553
try:
hkey = OpenKeyEx(HKEY_CURRENT_USER,
r'Software\Microsoft\Windows\Shell\Associations\UrlAssociations\https\UserChoice')
(value, typ) = QueryValueEx(hkey, 'ProgId')
CloseKey(hkey)
if value in ['IE.HTTP', 'AppXq0fevzme2pys62n3e0fbqa7peapykr8v']:
# IE and Edge can't handle long arguments so just use webbrowser.open and hope
# https://blogs.msdn.microsoft.com/ieinternals/2014/08/13/url-length-limits/
cls = None
else:
cls = value
except Exception:
cls = 'https'
if cls:
try:
hkey = OpenKeyEx(HKEY_CLASSES_ROOT, rf'{cls}\shell\open\command')
(value, typ) = QueryValueEx(hkey, None)
CloseKey(hkey)
if 'iexplore' not in value.lower():
if '%1' in value:
subprocess.Popen(value.replace('%1', url))
else:
subprocess.Popen(f'{value} "{url}"')
return
except Exception:
pass
webbrowser.open(url)