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.
Files changed (193) hide show
  1. ff9mapkit/__init__.py +18 -0
  2. ff9mapkit/__main__.py +36 -0
  3. ff9mapkit/_animdb.py +2994 -0
  4. ff9mapkit/_animdb_all.py +14125 -0
  5. ff9mapkit/_fieldtable.py +1516 -0
  6. ff9mapkit/_fieldtext.py +845 -0
  7. ff9mapkit/_held_poses.py +44 -0
  8. ff9mapkit/_itemdb.py +65 -0
  9. ff9mapkit/_modeldb.py +725 -0
  10. ff9mapkit/_narrowmap_data.py +10 -0
  11. ff9mapkit/_npcparams.py +634 -0
  12. ff9mapkit/_regen_animdb.py +72 -0
  13. ff9mapkit/_regen_animdb_all.py +66 -0
  14. ff9mapkit/_regen_fieldtable.py +95 -0
  15. ff9mapkit/_regen_fieldtext.py +66 -0
  16. ff9mapkit/_regen_modeldb.py +67 -0
  17. ff9mapkit/_regen_npcparams.py +123 -0
  18. ff9mapkit/_regen_scenedb.py +57 -0
  19. ff9mapkit/_scenedb.py +869 -0
  20. ff9mapkit/abilities.py +225 -0
  21. ff9mapkit/animations.py +120 -0
  22. ff9mapkit/archetypes.py +218 -0
  23. ff9mapkit/areatitle.py +76 -0
  24. ff9mapkit/battle/__init__.py +21 -0
  25. ff9mapkit/battle/abilityfeatures.py +294 -0
  26. ff9mapkit/battle/actiondelta.py +441 -0
  27. ff9mapkit/battle/aiauthor.py +305 -0
  28. ff9mapkit/battle/ailint.py +140 -0
  29. ff9mapkit/battle/aipatch.py +175 -0
  30. ff9mapkit/battle/battleai.py +148 -0
  31. ff9mapkit/battle/battlecsv.py +390 -0
  32. ff9mapkit/battle/battlepatch.py +395 -0
  33. ff9mapkit/battle/build.py +558 -0
  34. ff9mapkit/battle/camera_codec.py +332 -0
  35. ff9mapkit/battle/camera_data.py +128 -0
  36. ff9mapkit/battle/characterdelta.py +789 -0
  37. ff9mapkit/battle/event_data.py +72 -0
  38. ff9mapkit/battle/extract.py +540 -0
  39. ff9mapkit/battle/fbx.py +223 -0
  40. ff9mapkit/battle/reskin.py +149 -0
  41. ff9mapkit/battle/scene_codec.py +314 -0
  42. ff9mapkit/battle/scene_data.py +369 -0
  43. ff9mapkit/battle/scenelint.py +125 -0
  44. ff9mapkit/battle/seqasm.py +131 -0
  45. ff9mapkit/battle/seqauthor.py +220 -0
  46. ff9mapkit/battle/seqcodec.py +300 -0
  47. ff9mapkit/battle/seqdis.py +106 -0
  48. ff9mapkit/battle/seqpatch.py +137 -0
  49. ff9mapkit/battle_bgm.py +133 -0
  50. ff9mapkit/binutils.py +60 -0
  51. ff9mapkit/build.py +5445 -0
  52. ff9mapkit/campaign.py +1276 -0
  53. ff9mapkit/catalog.py +316 -0
  54. ff9mapkit/chain.py +358 -0
  55. ff9mapkit/cli.py +3114 -0
  56. ff9mapkit/config.py +360 -0
  57. ff9mapkit/content/__init__.py +13 -0
  58. ff9mapkit/content/areatitle.py +36 -0
  59. ff9mapkit/content/ate.py +118 -0
  60. ff9mapkit/content/camera.py +123 -0
  61. ff9mapkit/content/chest.py +186 -0
  62. ff9mapkit/content/choice.py +163 -0
  63. ff9mapkit/content/conductor.py +217 -0
  64. ff9mapkit/content/cutscene.py +290 -0
  65. ff9mapkit/content/encounter.py +41 -0
  66. ff9mapkit/content/entry_settle.py +50 -0
  67. ff9mapkit/content/equipment.py +93 -0
  68. ff9mapkit/content/event.py +191 -0
  69. ff9mapkit/content/gateway.py +101 -0
  70. ff9mapkit/content/inventory.py +59 -0
  71. ff9mapkit/content/itemdata.py +644 -0
  72. ff9mapkit/content/itemtext.py +168 -0
  73. ff9mapkit/content/jump.py +114 -0
  74. ff9mapkit/content/ladder.py +633 -0
  75. ff9mapkit/content/movement.py +53 -0
  76. ff9mapkit/content/music.py +97 -0
  77. ff9mapkit/content/npc.py +348 -0
  78. ff9mapkit/content/object.py +340 -0
  79. ff9mapkit/content/onentry.py +135 -0
  80. ff9mapkit/content/party.py +111 -0
  81. ff9mapkit/content/pathfind.py +138 -0
  82. ff9mapkit/content/platform.py +314 -0
  83. ff9mapkit/content/player.py +168 -0
  84. ff9mapkit/content/prop.py +75 -0
  85. ff9mapkit/content/region.py +340 -0
  86. ff9mapkit/content/reinit.py +59 -0
  87. ff9mapkit/content/savepoint.py +90 -0
  88. ff9mapkit/content/shop.py +178 -0
  89. ff9mapkit/content/sps_trigger.py +66 -0
  90. ff9mapkit/content/startup.py +71 -0
  91. ff9mapkit/content/synthesis.py +106 -0
  92. ff9mapkit/content/text.py +183 -0
  93. ff9mapkit/content/textcarry.py +290 -0
  94. ff9mapkit/content/verbatim.py +86 -0
  95. ff9mapkit/content/walkmesh_hotfix.py +38 -0
  96. ff9mapkit/data/__init__.py +48 -0
  97. ff9mapkit/data/_regen_provenance.py +142 -0
  98. ff9mapkit/data/provenance/blank.es.patch +1 -0
  99. ff9mapkit/data/provenance/blank.fr.patch +1 -0
  100. ff9mapkit/data/provenance/blank.gr.patch +1 -0
  101. ff9mapkit/data/provenance/blank.it.patch +1 -0
  102. ff9mapkit/data/provenance/blank.jp.patch +1 -0
  103. ff9mapkit/data/provenance/blank.uk.patch +1 -0
  104. ff9mapkit/data/provenance/blank.us.patch +1 -0
  105. ff9mapkit/data/provenance/manifest.json +65 -0
  106. ff9mapkit/data/provenance/region_template.patch +1 -0
  107. ff9mapkit/data/reference_arcs.toml +89 -0
  108. ff9mapkit/data/region_catalog.toml +593 -0
  109. ff9mapkit/deploystack.py +358 -0
  110. ff9mapkit/dialogue.py +803 -0
  111. ff9mapkit/eb/__init__.py +12 -0
  112. ff9mapkit/eb/_exprtable.py +59 -0
  113. ff9mapkit/eb/_membertable.py +38 -0
  114. ff9mapkit/eb/_optables.py +537 -0
  115. ff9mapkit/eb/_regen_optables.py +76 -0
  116. ff9mapkit/eb/cmdasm.py +323 -0
  117. ff9mapkit/eb/disasm.py +332 -0
  118. ff9mapkit/eb/edit.py +439 -0
  119. ff9mapkit/eb/exprasm.py +158 -0
  120. ff9mapkit/eb/model.py +178 -0
  121. ff9mapkit/eb/opcodes.py +463 -0
  122. ff9mapkit/eblint.py +177 -0
  123. ff9mapkit/editor/__init__.py +20 -0
  124. ff9mapkit/editor/app.py +950 -0
  125. ff9mapkit/editor/battle_forms.py +240 -0
  126. ff9mapkit/editor/breadcrumb.py +89 -0
  127. ff9mapkit/editor/dialogs.py +116 -0
  128. ff9mapkit/editor/feedback.py +208 -0
  129. ff9mapkit/editor/forms.py +632 -0
  130. ff9mapkit/editor/graphview.py +350 -0
  131. ff9mapkit/editor/jobs.py +342 -0
  132. ff9mapkit/editor/model.py +243 -0
  133. ff9mapkit/editor/picker.py +120 -0
  134. ff9mapkit/editor/theme.py +212 -0
  135. ff9mapkit/eventscan.py +1441 -0
  136. ff9mapkit/extract.py +2279 -0
  137. ff9mapkit/flags.py +693 -0
  138. ff9mapkit/forkreport.py +1383 -0
  139. ff9mapkit/hub.py +477 -0
  140. ff9mapkit/idgated.py +101 -0
  141. ff9mapkit/infohub.py +580 -0
  142. ff9mapkit/items.py +63 -0
  143. ff9mapkit/itemstats.py +346 -0
  144. ff9mapkit/journey.py +1902 -0
  145. ff9mapkit/keyitems.py +93 -0
  146. ff9mapkit/logic_add.py +632 -0
  147. ff9mapkit/logic_edit.py +728 -0
  148. ff9mapkit/logic_map.py +526 -0
  149. ff9mapkit/pack.py +175 -0
  150. ff9mapkit/playerswap.py +231 -0
  151. ff9mapkit/prop_archetypes.py +228 -0
  152. ff9mapkit/provision.py +282 -0
  153. ff9mapkit/refarc.py +825 -0
  154. ff9mapkit/save.py +337 -0
  155. ff9mapkit/save_items.py +1673 -0
  156. ff9mapkit/scene/__init__.py +11 -0
  157. ff9mapkit/scene/arena.py +63 -0
  158. ff9mapkit/scene/bgart.py +140 -0
  159. ff9mapkit/scene/bgi.py +732 -0
  160. ff9mapkit/scene/bgs.py +174 -0
  161. ff9mapkit/scene/bgx.py +185 -0
  162. ff9mapkit/scene/cam.py +345 -0
  163. ff9mapkit/scene/guide.py +311 -0
  164. ff9mapkit/scene/paint.py +506 -0
  165. ff9mapkit/scene/placeholder.py +107 -0
  166. ff9mapkit/sjbinary.py +285 -0
  167. ff9mapkit/sps/__init__.py +17 -0
  168. ff9mapkit/sps/author.py +294 -0
  169. ff9mapkit/sps/catalog.py +88 -0
  170. ff9mapkit/sps/codec.py +264 -0
  171. ff9mapkit/sps/edit.py +184 -0
  172. ff9mapkit/sps/lint.py +58 -0
  173. ff9mapkit/sps/render.py +116 -0
  174. ff9mapkit/sps/templates.py +47 -0
  175. ff9mapkit/sps/texture.py +131 -0
  176. ff9mapkit/walkmesh_hotfixes.py +163 -0
  177. ff9mapkit/workspace/__init__.py +18 -0
  178. ff9mapkit/workspace/battledoc.py +985 -0
  179. ff9mapkit/workspace/builddoc.py +607 -0
  180. ff9mapkit/workspace/forms_qt.py +586 -0
  181. ff9mapkit/workspace/importdoc.py +665 -0
  182. ff9mapkit/workspace/mapview.py +131 -0
  183. ff9mapkit/workspace/palette.py +85 -0
  184. ff9mapkit/workspace/savedoc.py +664 -0
  185. ff9mapkit/workspace/shell.py +6907 -0
  186. ff9mapkit/workspace/style.py +105 -0
  187. ff9mapkit/workspace/tuningdialog.py +223 -0
  188. ff9mapkit-1.0.0b3.dist-info/METADATA +155 -0
  189. ff9mapkit-1.0.0b3.dist-info/RECORD +193 -0
  190. ff9mapkit-1.0.0b3.dist-info/WHEEL +5 -0
  191. ff9mapkit-1.0.0b3.dist-info/entry_points.txt +5 -0
  192. ff9mapkit-1.0.0b3.dist-info/licenses/LICENSE +31 -0
  193. 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