ff9mapkit 1.0.0b3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ff9mapkit/__init__.py +18 -0
- ff9mapkit/__main__.py +36 -0
- ff9mapkit/_animdb.py +2994 -0
- ff9mapkit/_animdb_all.py +14125 -0
- ff9mapkit/_fieldtable.py +1516 -0
- ff9mapkit/_fieldtext.py +845 -0
- ff9mapkit/_held_poses.py +44 -0
- ff9mapkit/_itemdb.py +65 -0
- ff9mapkit/_modeldb.py +725 -0
- ff9mapkit/_narrowmap_data.py +10 -0
- ff9mapkit/_npcparams.py +634 -0
- ff9mapkit/_regen_animdb.py +72 -0
- ff9mapkit/_regen_animdb_all.py +66 -0
- ff9mapkit/_regen_fieldtable.py +95 -0
- ff9mapkit/_regen_fieldtext.py +66 -0
- ff9mapkit/_regen_modeldb.py +67 -0
- ff9mapkit/_regen_npcparams.py +123 -0
- ff9mapkit/_regen_scenedb.py +57 -0
- ff9mapkit/_scenedb.py +869 -0
- ff9mapkit/abilities.py +225 -0
- ff9mapkit/animations.py +120 -0
- ff9mapkit/archetypes.py +218 -0
- ff9mapkit/areatitle.py +76 -0
- ff9mapkit/battle/__init__.py +21 -0
- ff9mapkit/battle/abilityfeatures.py +294 -0
- ff9mapkit/battle/actiondelta.py +441 -0
- ff9mapkit/battle/aiauthor.py +305 -0
- ff9mapkit/battle/ailint.py +140 -0
- ff9mapkit/battle/aipatch.py +175 -0
- ff9mapkit/battle/battleai.py +148 -0
- ff9mapkit/battle/battlecsv.py +390 -0
- ff9mapkit/battle/battlepatch.py +395 -0
- ff9mapkit/battle/build.py +558 -0
- ff9mapkit/battle/camera_codec.py +332 -0
- ff9mapkit/battle/camera_data.py +128 -0
- ff9mapkit/battle/characterdelta.py +789 -0
- ff9mapkit/battle/event_data.py +72 -0
- ff9mapkit/battle/extract.py +540 -0
- ff9mapkit/battle/fbx.py +223 -0
- ff9mapkit/battle/reskin.py +149 -0
- ff9mapkit/battle/scene_codec.py +314 -0
- ff9mapkit/battle/scene_data.py +369 -0
- ff9mapkit/battle/scenelint.py +125 -0
- ff9mapkit/battle/seqasm.py +131 -0
- ff9mapkit/battle/seqauthor.py +220 -0
- ff9mapkit/battle/seqcodec.py +300 -0
- ff9mapkit/battle/seqdis.py +106 -0
- ff9mapkit/battle/seqpatch.py +137 -0
- ff9mapkit/battle_bgm.py +133 -0
- ff9mapkit/binutils.py +60 -0
- ff9mapkit/build.py +5445 -0
- ff9mapkit/campaign.py +1276 -0
- ff9mapkit/catalog.py +316 -0
- ff9mapkit/chain.py +358 -0
- ff9mapkit/cli.py +3114 -0
- ff9mapkit/config.py +360 -0
- ff9mapkit/content/__init__.py +13 -0
- ff9mapkit/content/areatitle.py +36 -0
- ff9mapkit/content/ate.py +118 -0
- ff9mapkit/content/camera.py +123 -0
- ff9mapkit/content/chest.py +186 -0
- ff9mapkit/content/choice.py +163 -0
- ff9mapkit/content/conductor.py +217 -0
- ff9mapkit/content/cutscene.py +290 -0
- ff9mapkit/content/encounter.py +41 -0
- ff9mapkit/content/entry_settle.py +50 -0
- ff9mapkit/content/equipment.py +93 -0
- ff9mapkit/content/event.py +191 -0
- ff9mapkit/content/gateway.py +101 -0
- ff9mapkit/content/inventory.py +59 -0
- ff9mapkit/content/itemdata.py +644 -0
- ff9mapkit/content/itemtext.py +168 -0
- ff9mapkit/content/jump.py +114 -0
- ff9mapkit/content/ladder.py +633 -0
- ff9mapkit/content/movement.py +53 -0
- ff9mapkit/content/music.py +97 -0
- ff9mapkit/content/npc.py +348 -0
- ff9mapkit/content/object.py +340 -0
- ff9mapkit/content/onentry.py +135 -0
- ff9mapkit/content/party.py +111 -0
- ff9mapkit/content/pathfind.py +138 -0
- ff9mapkit/content/platform.py +314 -0
- ff9mapkit/content/player.py +168 -0
- ff9mapkit/content/prop.py +75 -0
- ff9mapkit/content/region.py +340 -0
- ff9mapkit/content/reinit.py +59 -0
- ff9mapkit/content/savepoint.py +90 -0
- ff9mapkit/content/shop.py +178 -0
- ff9mapkit/content/sps_trigger.py +66 -0
- ff9mapkit/content/startup.py +71 -0
- ff9mapkit/content/synthesis.py +106 -0
- ff9mapkit/content/text.py +183 -0
- ff9mapkit/content/textcarry.py +290 -0
- ff9mapkit/content/verbatim.py +86 -0
- ff9mapkit/content/walkmesh_hotfix.py +38 -0
- ff9mapkit/data/__init__.py +48 -0
- ff9mapkit/data/_regen_provenance.py +142 -0
- ff9mapkit/data/provenance/blank.es.patch +1 -0
- ff9mapkit/data/provenance/blank.fr.patch +1 -0
- ff9mapkit/data/provenance/blank.gr.patch +1 -0
- ff9mapkit/data/provenance/blank.it.patch +1 -0
- ff9mapkit/data/provenance/blank.jp.patch +1 -0
- ff9mapkit/data/provenance/blank.uk.patch +1 -0
- ff9mapkit/data/provenance/blank.us.patch +1 -0
- ff9mapkit/data/provenance/manifest.json +65 -0
- ff9mapkit/data/provenance/region_template.patch +1 -0
- ff9mapkit/data/reference_arcs.toml +89 -0
- ff9mapkit/data/region_catalog.toml +593 -0
- ff9mapkit/deploystack.py +358 -0
- ff9mapkit/dialogue.py +803 -0
- ff9mapkit/eb/__init__.py +12 -0
- ff9mapkit/eb/_exprtable.py +59 -0
- ff9mapkit/eb/_membertable.py +38 -0
- ff9mapkit/eb/_optables.py +537 -0
- ff9mapkit/eb/_regen_optables.py +76 -0
- ff9mapkit/eb/cmdasm.py +323 -0
- ff9mapkit/eb/disasm.py +332 -0
- ff9mapkit/eb/edit.py +439 -0
- ff9mapkit/eb/exprasm.py +158 -0
- ff9mapkit/eb/model.py +178 -0
- ff9mapkit/eb/opcodes.py +463 -0
- ff9mapkit/eblint.py +177 -0
- ff9mapkit/editor/__init__.py +20 -0
- ff9mapkit/editor/app.py +950 -0
- ff9mapkit/editor/battle_forms.py +240 -0
- ff9mapkit/editor/breadcrumb.py +89 -0
- ff9mapkit/editor/dialogs.py +116 -0
- ff9mapkit/editor/feedback.py +208 -0
- ff9mapkit/editor/forms.py +632 -0
- ff9mapkit/editor/graphview.py +350 -0
- ff9mapkit/editor/jobs.py +342 -0
- ff9mapkit/editor/model.py +243 -0
- ff9mapkit/editor/picker.py +120 -0
- ff9mapkit/editor/theme.py +212 -0
- ff9mapkit/eventscan.py +1441 -0
- ff9mapkit/extract.py +2279 -0
- ff9mapkit/flags.py +693 -0
- ff9mapkit/forkreport.py +1383 -0
- ff9mapkit/hub.py +477 -0
- ff9mapkit/idgated.py +101 -0
- ff9mapkit/infohub.py +580 -0
- ff9mapkit/items.py +63 -0
- ff9mapkit/itemstats.py +346 -0
- ff9mapkit/journey.py +1902 -0
- ff9mapkit/keyitems.py +93 -0
- ff9mapkit/logic_add.py +632 -0
- ff9mapkit/logic_edit.py +728 -0
- ff9mapkit/logic_map.py +526 -0
- ff9mapkit/pack.py +175 -0
- ff9mapkit/playerswap.py +231 -0
- ff9mapkit/prop_archetypes.py +228 -0
- ff9mapkit/provision.py +282 -0
- ff9mapkit/refarc.py +825 -0
- ff9mapkit/save.py +337 -0
- ff9mapkit/save_items.py +1673 -0
- ff9mapkit/scene/__init__.py +11 -0
- ff9mapkit/scene/arena.py +63 -0
- ff9mapkit/scene/bgart.py +140 -0
- ff9mapkit/scene/bgi.py +732 -0
- ff9mapkit/scene/bgs.py +174 -0
- ff9mapkit/scene/bgx.py +185 -0
- ff9mapkit/scene/cam.py +345 -0
- ff9mapkit/scene/guide.py +311 -0
- ff9mapkit/scene/paint.py +506 -0
- ff9mapkit/scene/placeholder.py +107 -0
- ff9mapkit/sjbinary.py +285 -0
- ff9mapkit/sps/__init__.py +17 -0
- ff9mapkit/sps/author.py +294 -0
- ff9mapkit/sps/catalog.py +88 -0
- ff9mapkit/sps/codec.py +264 -0
- ff9mapkit/sps/edit.py +184 -0
- ff9mapkit/sps/lint.py +58 -0
- ff9mapkit/sps/render.py +116 -0
- ff9mapkit/sps/templates.py +47 -0
- ff9mapkit/sps/texture.py +131 -0
- ff9mapkit/walkmesh_hotfixes.py +163 -0
- ff9mapkit/workspace/__init__.py +18 -0
- ff9mapkit/workspace/battledoc.py +985 -0
- ff9mapkit/workspace/builddoc.py +607 -0
- ff9mapkit/workspace/forms_qt.py +586 -0
- ff9mapkit/workspace/importdoc.py +665 -0
- ff9mapkit/workspace/mapview.py +131 -0
- ff9mapkit/workspace/palette.py +85 -0
- ff9mapkit/workspace/savedoc.py +664 -0
- ff9mapkit/workspace/shell.py +6907 -0
- ff9mapkit/workspace/style.py +105 -0
- ff9mapkit/workspace/tuningdialog.py +223 -0
- ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
- ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
- ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
- ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
- ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
- ff9mapkit-1.0.0b3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""A modal Info Hub CATALOG PICKER for the editor (Tk).
|
|
2
|
+
|
|
3
|
+
Browse the same catalogs as the standalone viewer (via the UI-agnostic :mod:`..infohub` spine) and return
|
|
4
|
+
the chosen entry's NAME, so a form's name field (an NPC's preset, a give_item, ...) can be filled by
|
|
5
|
+
browse-and-pick instead of typing a model/archetype/item name blind. :func:`pick` is modal and returns the
|
|
6
|
+
chosen name (or ``None`` if cancelled); the editor wires a "Browse..." button next to each catalog-backed
|
|
7
|
+
field. It reuses the spine, so the editor and the standalone viewer stay in lockstep with one search core.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import tkinter as tk
|
|
12
|
+
from tkinter import ttk
|
|
13
|
+
|
|
14
|
+
from .. import infohub
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def pick(parent, *, kinds=None, title="Pick from the catalog", initial="", campaign_context=None):
|
|
18
|
+
"""Open a modal catalog picker over the spine; return the chosen entry name (str) or None if cancelled.
|
|
19
|
+
|
|
20
|
+
``kinds`` restricts the search to those catalog kinds (e.g. ``["archetype", "creature"]``); None = all.
|
|
21
|
+
``initial`` pre-fills the search box (e.g. the field's current value). ``campaign_context`` (a
|
|
22
|
+
CampaignPlan) lets the picker also surface the open campaign's members/shared flags (kind 'field'/'flag')."""
|
|
23
|
+
dlg = _PickerDialog(parent, kinds=kinds, title=title, initial=initial, campaign_context=campaign_context)
|
|
24
|
+
parent.wait_window(dlg.win)
|
|
25
|
+
return dlg.result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _PickerDialog:
|
|
29
|
+
def __init__(self, parent, *, kinds=None, title="Pick", initial="", campaign_context=None):
|
|
30
|
+
self.kinds = list(kinds) if kinds else None
|
|
31
|
+
self.campaign_context = campaign_context
|
|
32
|
+
self.result = None
|
|
33
|
+
self._entries = []
|
|
34
|
+
|
|
35
|
+
win = self.win = tk.Toplevel(parent)
|
|
36
|
+
win.title(title)
|
|
37
|
+
win.transient(parent)
|
|
38
|
+
win.geometry("560x440")
|
|
39
|
+
win.configure(background=parent.winfo_toplevel()["background"]) # inherit the themed window bg
|
|
40
|
+
|
|
41
|
+
top = ttk.Frame(win, padding=6)
|
|
42
|
+
top.pack(fill="x")
|
|
43
|
+
ttk.Label(top, text="Search:").pack(side="left")
|
|
44
|
+
self.q = tk.StringVar(value=initial or "")
|
|
45
|
+
ent = ttk.Entry(top, textvariable=self.q)
|
|
46
|
+
ent.pack(side="left", fill="x", expand=True, padx=4)
|
|
47
|
+
ent.bind("<KeyRelease>", lambda e: self._refresh())
|
|
48
|
+
ent.bind("<Return>", lambda e: self._ok())
|
|
49
|
+
ent.bind("<Escape>", lambda e: self._cancel())
|
|
50
|
+
|
|
51
|
+
mid = ttk.Frame(win, padding=(6, 0))
|
|
52
|
+
mid.pack(fill="both", expand=True)
|
|
53
|
+
self.lst = tk.Listbox(mid, activestyle="none", exportselection=False)
|
|
54
|
+
self.lst.pack(side="left", fill="both", expand=True)
|
|
55
|
+
sb = ttk.Scrollbar(mid, command=self.lst.yview)
|
|
56
|
+
sb.pack(side="right", fill="y")
|
|
57
|
+
self.lst.config(yscrollcommand=sb.set)
|
|
58
|
+
self.lst.bind("<Double-Button-1>", lambda e: self._ok())
|
|
59
|
+
self.lst.bind("<<ListboxSelect>>", lambda e: self._describe())
|
|
60
|
+
|
|
61
|
+
self.info = ttk.Label(win, text="", anchor="w", padding=(8, 2), wraplength=540, justify="left")
|
|
62
|
+
self.info.pack(fill="x")
|
|
63
|
+
|
|
64
|
+
bar = ttk.Frame(win, padding=6)
|
|
65
|
+
bar.pack(fill="x")
|
|
66
|
+
ttk.Button(bar, text="Use this", command=self._ok).pack(side="left")
|
|
67
|
+
ttk.Button(bar, text="Cancel", command=self._cancel).pack(side="left", padx=6)
|
|
68
|
+
|
|
69
|
+
self._refresh()
|
|
70
|
+
ent.focus_set()
|
|
71
|
+
win.grab_set()
|
|
72
|
+
|
|
73
|
+
def _refresh(self):
|
|
74
|
+
self._entries = infohub.browse(self.q.get(), kinds=self.kinds, limit=300,
|
|
75
|
+
campaign_context=self.campaign_context)
|
|
76
|
+
self.lst.delete(0, "end")
|
|
77
|
+
for e in self._entries:
|
|
78
|
+
self.lst.insert("end", f"{e.name} [{e.kind}]")
|
|
79
|
+
where = f" in {', '.join(self.kinds)}" if self.kinds else ""
|
|
80
|
+
self.info.config(text=f"{len(self._entries)} match(es){where}")
|
|
81
|
+
|
|
82
|
+
def _describe(self):
|
|
83
|
+
sel = self.lst.curselection()
|
|
84
|
+
if sel:
|
|
85
|
+
e = self._entries[sel[0]]
|
|
86
|
+
self.info.config(text=f"{e.name} [{e.kind}] -- {e.summary}")
|
|
87
|
+
|
|
88
|
+
def _ok(self):
|
|
89
|
+
sel = self.lst.curselection()
|
|
90
|
+
if not sel and len(self._entries) == 1: # a single match + Enter -> take it
|
|
91
|
+
sel = (0,)
|
|
92
|
+
if sel:
|
|
93
|
+
self.result = self._entries[sel[0]].name
|
|
94
|
+
self.win.destroy()
|
|
95
|
+
|
|
96
|
+
def _cancel(self):
|
|
97
|
+
self.result = None
|
|
98
|
+
self.win.destroy()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _smoke():
|
|
102
|
+
"""Headless self-test: build the picker, filter, simulate a pick."""
|
|
103
|
+
root = tk.Tk()
|
|
104
|
+
root.withdraw()
|
|
105
|
+
dlg = _PickerDialog(root, kinds=["archetype", "creature"], title="smoke")
|
|
106
|
+
dlg.q.set("vivi")
|
|
107
|
+
dlg._refresh()
|
|
108
|
+
names = [e.name for e in dlg._entries]
|
|
109
|
+
assert "vivi" in names, names # the search finds the vivi archetype
|
|
110
|
+
assert all(e.kind in ("archetype", "creature") for e in dlg._entries), names # kinds filter holds
|
|
111
|
+
dlg.lst.selection_set(names.index("vivi"))
|
|
112
|
+
dlg._ok()
|
|
113
|
+
print(f"picker smoke ok: {len(names)} archetype/creature match(es) for 'vivi' {names}; picked = {dlg.result!r}")
|
|
114
|
+
root.destroy()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
import sys
|
|
119
|
+
if "--smoke" in sys.argv:
|
|
120
|
+
_smoke()
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""A modern visual theme for the editor (palette + ttk styling).
|
|
2
|
+
|
|
3
|
+
The palette dicts, the OS dark-mode probe, and the palette picker are **tk-FREE** so they're
|
|
4
|
+
unit-testable on a headless machine (exactly like :mod:`.forms` / :mod:`.model`). The actual Tk/ttk
|
|
5
|
+
styling lives in :func:`apply_theme`, which imports tkinter lazily and only does anything useful with
|
|
6
|
+
a real display.
|
|
7
|
+
|
|
8
|
+
Why ``clam``: on Windows the default ttk theme (``vista``) draws widgets natively and ignores most
|
|
9
|
+
colour options, so a cohesive restyle is impossible. ``clam`` honours every colour we set, so we
|
|
10
|
+
build the modern look on top of it (flat widgets, an accent on the primary actions, a styled tree and
|
|
11
|
+
console log). ``apply_theme`` returns the chosen palette so the app can colour its own labels (muted
|
|
12
|
+
hints, the "placed in Blender" note, log lines) from the same source.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
# --- palettes ----------------------------------------------------------------------------
|
|
18
|
+
# Two cohesive schemes. Keep the KEY SET identical (a test asserts it) so the app can read any colour
|
|
19
|
+
# from whichever palette is active. Colours are plain "#rrggbb" strings.
|
|
20
|
+
LIGHT = {
|
|
21
|
+
"dark": False,
|
|
22
|
+
"bg": "#f4f5f7", # window background
|
|
23
|
+
"surface": "#ffffff", # tree / form surface
|
|
24
|
+
"surface_btn": "#ffffff", # neutral button face
|
|
25
|
+
"field": "#ffffff", # entry / listbox background
|
|
26
|
+
"text": "#1b1f24", # primary text
|
|
27
|
+
"muted": "#6b7280", # secondary text (hints)
|
|
28
|
+
"accent": "#2f6feb", # primary buttons, tree selection
|
|
29
|
+
"accent_fg": "#ffffff", # text on accent
|
|
30
|
+
"accent_hover": "#256ae0",
|
|
31
|
+
"accent_pressed": "#1f5fcc",
|
|
32
|
+
"help": "#7c3aed", # help / info affordance (violet -- distinct from accent/success/warn)
|
|
33
|
+
"help_hover": "#6d28d9",
|
|
34
|
+
"border": "#d6dae0",
|
|
35
|
+
"success": "#1a8f5a", # "placed in Blender" / OK lines
|
|
36
|
+
"hover": "#eef1f4", # neutral button hover
|
|
37
|
+
"pressed": "#e3e7ec",
|
|
38
|
+
"scroll": "#c3c8cf", # scrollbar thumb
|
|
39
|
+
"log_bg": "#eef0f3",
|
|
40
|
+
"log_fg": "#374151",
|
|
41
|
+
"error": "#c0392b",
|
|
42
|
+
"warn": "#9a6b00",
|
|
43
|
+
}
|
|
44
|
+
DARK = {
|
|
45
|
+
"dark": True,
|
|
46
|
+
"bg": "#1e2127",
|
|
47
|
+
"surface": "#262a31",
|
|
48
|
+
"surface_btn": "#2b3038",
|
|
49
|
+
"field": "#2b3038",
|
|
50
|
+
"text": "#e6e8eb",
|
|
51
|
+
"muted": "#9aa3ad",
|
|
52
|
+
"accent": "#4c8dff",
|
|
53
|
+
"accent_fg": "#ffffff",
|
|
54
|
+
"accent_hover": "#3d7df0",
|
|
55
|
+
"accent_pressed": "#356fda",
|
|
56
|
+
"help": "#9d7bff", # help / info affordance (violet -- distinct from accent/success/warn)
|
|
57
|
+
"help_hover": "#8b66f5",
|
|
58
|
+
"border": "#3a404a",
|
|
59
|
+
"success": "#46c98a",
|
|
60
|
+
"hover": "#30353d",
|
|
61
|
+
"pressed": "#373d46",
|
|
62
|
+
"scroll": "#3f4651",
|
|
63
|
+
"log_bg": "#181b20",
|
|
64
|
+
"log_fg": "#c7ccd3",
|
|
65
|
+
"error": "#ff6b6b",
|
|
66
|
+
"warn": "#e0a93b",
|
|
67
|
+
}
|
|
68
|
+
THEMES = {"light": LIGHT, "dark": DARK}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def detect_os_dark() -> bool:
|
|
72
|
+
"""True if Windows is set to dark mode (HKCU ``Personalize\\AppsUseLightTheme`` == 0).
|
|
73
|
+
|
|
74
|
+
Pure + defensive: any failure (non-Windows, missing key, no winreg) -> ``False`` (light)."""
|
|
75
|
+
try:
|
|
76
|
+
import winreg
|
|
77
|
+
|
|
78
|
+
key = winreg.OpenKey(
|
|
79
|
+
winreg.HKEY_CURRENT_USER,
|
|
80
|
+
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
|
81
|
+
)
|
|
82
|
+
try:
|
|
83
|
+
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
|
|
84
|
+
finally:
|
|
85
|
+
winreg.CloseKey(key)
|
|
86
|
+
return value == 0
|
|
87
|
+
except Exception: # noqa: BLE001 (winreg missing / key absent / anything) -> light
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def pick_palette(mode: str = "auto") -> dict:
|
|
92
|
+
"""Choose a palette. ``mode``: ``"light"`` / ``"dark"`` / ``"auto"`` (match the OS, default)."""
|
|
93
|
+
if mode == "light":
|
|
94
|
+
return LIGHT
|
|
95
|
+
if mode == "dark":
|
|
96
|
+
return DARK
|
|
97
|
+
return DARK if detect_os_dark() else LIGHT
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def apply_theme(root, mode: str = "auto") -> dict:
|
|
101
|
+
"""Style ``root`` (a Tk window) with the modern look and return the active palette.
|
|
102
|
+
|
|
103
|
+
Builds on the ``clam`` ttk theme; reconfigures the named fonts to Segoe UI so classic and ttk
|
|
104
|
+
widgets share typography. Safe to call once at startup before building widgets."""
|
|
105
|
+
import tkinter as tk # noqa: F401 (lazy: keep this module headless-importable)
|
|
106
|
+
import tkinter.font as tkfont
|
|
107
|
+
from tkinter import ttk
|
|
108
|
+
|
|
109
|
+
pal = pick_palette(mode)
|
|
110
|
+
|
|
111
|
+
# Typography: reconfigure the shared named fonts so EVERY widget (ttk + classic Text/Listbox) and
|
|
112
|
+
# every ``font=("", 11, "bold")`` (family "" == TkDefaultFont) picks up Segoe UI.
|
|
113
|
+
for name, size in (("TkDefaultFont", 10), ("TkTextFont", 10), ("TkMenuFont", 10),
|
|
114
|
+
("TkHeadingFont", 10)):
|
|
115
|
+
try:
|
|
116
|
+
tkfont.nametofont(name).configure(family="Segoe UI", size=size)
|
|
117
|
+
except Exception: # noqa: BLE001 (a font name not present on this Tk)
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
root.configure(background=pal["bg"])
|
|
121
|
+
# Combobox dropdowns are classic Listboxes -> colour them via the option DB.
|
|
122
|
+
root.option_add("*TCombobox*Listbox.background", pal["field"])
|
|
123
|
+
root.option_add("*TCombobox*Listbox.foreground", pal["text"])
|
|
124
|
+
root.option_add("*TCombobox*Listbox.selectBackground", pal["accent"])
|
|
125
|
+
root.option_add("*TCombobox*Listbox.selectForeground", pal["accent_fg"])
|
|
126
|
+
# Plain classic Listboxes (the catalog picker) aren't ttk -> theme them via the option DB too, so a
|
|
127
|
+
# widget that doesn't pass explicit colours (unlike the editor's own lists) still matches the app.
|
|
128
|
+
root.option_add("*Listbox.background", pal["field"])
|
|
129
|
+
root.option_add("*Listbox.foreground", pal["text"])
|
|
130
|
+
root.option_add("*Listbox.selectBackground", pal["accent"])
|
|
131
|
+
root.option_add("*Listbox.selectForeground", pal["accent_fg"])
|
|
132
|
+
root.option_add("*Listbox.highlightColor", pal["accent"])
|
|
133
|
+
root.option_add("*Listbox.highlightBackground", pal["border"])
|
|
134
|
+
|
|
135
|
+
st = ttk.Style(root)
|
|
136
|
+
try:
|
|
137
|
+
st.theme_use("clam") # the only built-in theme that honours our colours
|
|
138
|
+
except tk.TclError:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
st.configure(".", background=pal["bg"], foreground=pal["text"],
|
|
142
|
+
fieldbackground=pal["field"], bordercolor=pal["border"],
|
|
143
|
+
lightcolor=pal["border"], darkcolor=pal["border"], focuscolor=pal["accent"])
|
|
144
|
+
st.configure("TFrame", background=pal["bg"])
|
|
145
|
+
st.configure("TLabel", background=pal["bg"], foreground=pal["text"])
|
|
146
|
+
st.configure("TSeparator", background=pal["border"])
|
|
147
|
+
st.configure("TPanedwindow", background=pal["bg"])
|
|
148
|
+
|
|
149
|
+
# Buttons: flat + padded; neutral face with a hover, plus an Accent.TButton for primary actions.
|
|
150
|
+
st.configure("TButton", background=pal["surface_btn"], foreground=pal["text"],
|
|
151
|
+
bordercolor=pal["border"], lightcolor=pal["surface_btn"],
|
|
152
|
+
darkcolor=pal["surface_btn"], relief="flat", padding=(12, 6))
|
|
153
|
+
st.map("TButton",
|
|
154
|
+
background=[("pressed", pal["pressed"]), ("active", pal["hover"]),
|
|
155
|
+
("disabled", pal["bg"])],
|
|
156
|
+
foreground=[("disabled", pal["muted"])],
|
|
157
|
+
bordercolor=[("focus", pal["accent"]), ("active", pal["border"])])
|
|
158
|
+
st.configure("Accent.TButton", background=pal["accent"], foreground=pal["accent_fg"],
|
|
159
|
+
bordercolor=pal["accent"], lightcolor=pal["accent"], darkcolor=pal["accent"],
|
|
160
|
+
relief="flat", padding=(12, 6))
|
|
161
|
+
st.map("Accent.TButton",
|
|
162
|
+
background=[("pressed", pal["accent_pressed"]), ("active", pal["accent_hover"]),
|
|
163
|
+
("disabled", pal["border"])],
|
|
164
|
+
foreground=[("disabled", pal["muted"])])
|
|
165
|
+
|
|
166
|
+
# Entries + comboboxes: flat field, accent focus ring.
|
|
167
|
+
for s in ("TEntry", "TCombobox"):
|
|
168
|
+
st.configure(s, fieldbackground=pal["field"], foreground=pal["text"],
|
|
169
|
+
bordercolor=pal["border"], lightcolor=pal["border"],
|
|
170
|
+
darkcolor=pal["border"], insertcolor=pal["text"],
|
|
171
|
+
arrowcolor=pal["muted"], padding=4, relief="flat")
|
|
172
|
+
st.map(s, bordercolor=[("focus", pal["accent"])],
|
|
173
|
+
lightcolor=[("focus", pal["accent"])], darkcolor=[("focus", pal["accent"])])
|
|
174
|
+
st.configure("TCombobox", background=pal["surface_btn"]) # the arrow-button area
|
|
175
|
+
st.map("TCombobox",
|
|
176
|
+
fieldbackground=[("readonly", pal["field"])],
|
|
177
|
+
foreground=[("readonly", pal["text"])],
|
|
178
|
+
selectbackground=[("readonly", pal["field"])],
|
|
179
|
+
selectforeground=[("readonly", pal["text"])],
|
|
180
|
+
arrowcolor=[("active", pal["text"])])
|
|
181
|
+
|
|
182
|
+
# Checkbutton: a filled accent box with a light check when on.
|
|
183
|
+
st.configure("TCheckbutton", background=pal["bg"], foreground=pal["text"],
|
|
184
|
+
indicatorbackground=pal["field"], indicatorforeground=pal["accent_fg"],
|
|
185
|
+
focuscolor=pal["bg"])
|
|
186
|
+
st.map("TCheckbutton",
|
|
187
|
+
indicatorbackground=[("selected", pal["accent"]), ("active", pal["hover"])],
|
|
188
|
+
indicatorforeground=[("selected", pal["accent_fg"])])
|
|
189
|
+
|
|
190
|
+
# Treeview: roomy flat rows, accent selection.
|
|
191
|
+
st.configure("Treeview", background=pal["surface"], fieldbackground=pal["surface"],
|
|
192
|
+
foreground=pal["text"], rowheight=26, borderwidth=0, relief="flat")
|
|
193
|
+
st.map("Treeview", background=[("selected", pal["accent"])],
|
|
194
|
+
foreground=[("selected", pal["accent_fg"])])
|
|
195
|
+
st.configure("Treeview.Heading", background=pal["surface_btn"], foreground=pal["text"],
|
|
196
|
+
relief="flat")
|
|
197
|
+
|
|
198
|
+
# Scrollbars: subtle.
|
|
199
|
+
for s in ("Vertical.TScrollbar", "Horizontal.TScrollbar"):
|
|
200
|
+
st.configure(s, background=pal["scroll"], troughcolor=pal["bg"],
|
|
201
|
+
bordercolor=pal["bg"], arrowcolor=pal["muted"], relief="flat")
|
|
202
|
+
st.map(s, background=[("active", pal["muted"])])
|
|
203
|
+
|
|
204
|
+
# Notebook (the Campaign Editor's tab strip): flat themed tabs, the active one on the page bg.
|
|
205
|
+
st.configure("TNotebook", background=pal["bg"], bordercolor=pal["border"])
|
|
206
|
+
st.configure("TNotebook.Tab", background=pal["surface_btn"], foreground=pal["muted"],
|
|
207
|
+
bordercolor=pal["border"], lightcolor=pal["surface_btn"], padding=(14, 7))
|
|
208
|
+
st.map("TNotebook.Tab",
|
|
209
|
+
background=[("selected", pal["bg"]), ("active", pal["hover"])],
|
|
210
|
+
foreground=[("selected", pal["text"]), ("active", pal["text"])])
|
|
211
|
+
|
|
212
|
+
return pal
|