Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ttk-based theming #2241

Draft
wants to merge 67 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
d0ca7aa
Excluded ttk widgets from theme.theme
ElSaico May 26, 2024
343ee8a
Changed all main window widgets to ttk
ElSaico May 26, 2024
34dab8e
Fixed type signatures caught up by mypy
ElSaico May 26, 2024
fa3ab46
As I was saying, fixed type signatures...
ElSaico May 26, 2024
a017139
meh
ElSaico May 26, 2024
d18a8d4
We don't need to actually change the killswitch
ElSaico May 26, 2024
a40a10e
Added JSON theme
ElSaico May 27, 2024
19994ac
Simplified style, fixed HyperlinkLabel underline font
ElSaico May 27, 2024
9ba7ee5
Set baseline for themes
ElSaico May 27, 2024
a034bb7
Integrated ttk themes in theme.apply()
ElSaico May 27, 2024
7fd086b
oops
ElSaico May 27, 2024
899c773
Leftover import
ElSaico May 27, 2024
47de607
Applying theme palette to Tk widgets more reliably
ElSaico May 27, 2024
7c4ea70
Caught a plugin using theme.register() directly, so reinstated as stub
ElSaico May 27, 2024
881840c
Added Ttk catalog plugin (enabled by flag)
ElSaico May 27, 2024
b971c26
Fixed TTK Catalog positioning
ElSaico May 28, 2024
2b8158d
Ttk catalog: replaced alternative styles with HyperlinkLabel frame
ElSaico Jun 1, 2024
08a031c
Nicer labels
ElSaico Jun 1, 2024
6bb0475
Using clam as the base ttk theme on Windows
ElSaico Jun 2, 2024
4cfe990
Added Tk widgets to Ttk catalog for comparison
ElSaico Jun 2, 2024
ca4a5ef
Theme in pure Tcl
ElSaico Jun 2, 2024
65a33bb
Fixed theme font
ElSaico Jun 2, 2024
960fef9
Ttk catalog: properly labeled Tk widgets
ElSaico Jun 2, 2024
f1ad8d7
Added default theme; final touches in dark?
ElSaico Jun 2, 2024
8f94a02
Merge branch 'develop' into ttk
ElSaico Jun 3, 2024
0955304
flake8
ElSaico Jun 3, 2024
c332892
mypy
ElSaico Jun 3, 2024
18c1c3a
Themes as tcl packages, deprecated most of myNotebook
ElSaico Jun 3, 2024
4f54b49
HyperlinkLabel is a Button. Obviously.
ElSaico Jun 4, 2024
de89528
Fixed underline font
ElSaico Jun 4, 2024
2fd9f7f
A possible solution for HyperlinkLabel on the way?
ElSaico Jun 7, 2024
168bc3e
Dropped most of myNotebook from core plugins (except for EntryMenu)
ElSaico Jun 8, 2024
853accd
HyperlinkLabel seems to work now
ElSaico Jun 8, 2024
a33c07e
Dropped unnecessary tk call
ElSaico Jun 8, 2024
1f0bedd
Dropping justify config in HyperlinkLabel
ElSaico Jun 8, 2024
4ef4880
Slight cleanup on font loading code
ElSaico Jun 9, 2024
87fb27a
Added check to whether Euro Caps actually loads on Windows
ElSaico Jun 9, 2024
254ec5d
Some words of encouragement
ElSaico Jun 9, 2024
94399cd
Better deprecation notices on theme.register() and theme.update()
ElSaico Jun 9, 2024
f13636d
Something to the logs
ElSaico Jun 9, 2024
9829575
Proper theme/myNotebook deprecation warnings
ElSaico Jun 9, 2024
2634e6e
We don't need most of nb now, so...
ElSaico Jun 9, 2024
ebb3a70
Dropped ttk::theme lookup on theme files
ElSaico Jun 9, 2024
0f7b48a
Merge remote-tracking branch 'refs/remotes/edcd/develop' into ttk
ElSaico Jun 9, 2024
9be31f6
Merge branch 'develop' into ttk
Rixxan Jun 10, 2024
3a0e49e
Merge remote-tracking branch 'origin/ttk' into ttk
ElSaico Jun 10, 2024
9156918
Dropped post-merge repetition
ElSaico Jun 11, 2024
e45e8c0
Dropped alternative title bar and update button
ElSaico Jun 11, 2024
e6f8580
Using native dark mode title bar on Windows
ElSaico Jun 11, 2024
199110e
Removed widget switcheroo, all themes use the same menu
ElSaico Jun 13, 2024
2a7db41
Style tweaks
ElSaico Jun 14, 2024
6ceda6a
Merge remote-tracking branch 'refs/remotes/edcd/develop' into ttk
ElSaico Jun 19, 2024
44d1e68
Customizing title bar via PyWinRT
ElSaico Jun 24, 2024
a7361fe
Import shenanigans now that pywinrt works
ElSaico Jun 24, 2024
f680682
We no longer need those drag events
ElSaico Jun 24, 2024
0217c35
Merge branch 'refs/heads/develop' into ttk-rt
ElSaico Jul 22, 2024
810fa2b
Bump WinRT dependency (as it missed a DLL)
ElSaico Jul 22, 2024
ff35b8d
Merge branch 'refs/heads/develop' into ttk
ElSaico Jul 22, 2024
64be73c
Post-merge clarity
ElSaico Jul 22, 2024
50d9e41
Fixed pywin32 call
ElSaico Jul 22, 2024
d3fef97
whoops
ElSaico Jul 22, 2024
ee589e7
Added gap for custom title bar under Transparent
ElSaico Jul 23, 2024
0ad93ad
Using PyWinRT visual layer
ElSaico Aug 20, 2024
bf04ce9
Merge remote-tracking branch 'edcd/develop' into ttk
ElSaico Aug 20, 2024
5f444e7
Post-merge ttk catalog fix
ElSaico Aug 20, 2024
c4fab48
Some simplification and making mypy happier
ElSaico Aug 21, 2024
9f87cd6
Ditched visual layer + setting Windows caption button colors
ElSaico Aug 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 81 additions & 190 deletions EDMarketConnector.py

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
4 changes: 2 additions & 2 deletions config/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
25 changes: 13 additions & 12 deletions docs/examples/click_counter/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand All @@ -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.

Expand All @@ -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


Expand All @@ -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.

Expand All @@ -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.

Expand Down
64 changes: 16 additions & 48 deletions myNotebook.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,43 @@
"""
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."""

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)


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):
Expand Down Expand Up @@ -135,49 +114,38 @@ 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)


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)
25 changes: 18 additions & 7 deletions plug.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from __future__ import annotations

import copy
import importlib
import importlib.util
import logging
import operator
import os
Expand All @@ -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

Expand Down Expand Up @@ -131,21 +130,21 @@ 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.

:param parent: the parent frame for this preference tab.
: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:
Expand All @@ -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():
Expand All @@ -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*
Expand Down
Loading