tintkit 0.1.0__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.
- tintkit/__init__.py +99 -0
- tintkit/composites.py +459 -0
- tintkit/containers.py +296 -0
- tintkit/controls.py +794 -0
- tintkit/icons/check.png +0 -0
- tintkit/icons/chevron-down.png +0 -0
- tintkit/icons/chevron-left.png +0 -0
- tintkit/icons/chevron-right.png +0 -0
- tintkit/icons/chevron-up.png +0 -0
- tintkit/icons/crop.png +0 -0
- tintkit/icons/droplets.png +0 -0
- tintkit/icons/folder-check.png +0 -0
- tintkit/icons/folder-open.png +0 -0
- tintkit/icons/info.png +0 -0
- tintkit/icons/layout-grid.png +0 -0
- tintkit/icons/menu.png +0 -0
- tintkit/icons/redo.png +0 -0
- tintkit/icons/rotate-ccw.png +0 -0
- tintkit/icons/save.png +0 -0
- tintkit/icons/search.png +0 -0
- tintkit/icons/settings.png +0 -0
- tintkit/icons/sliders-horizontal.png +0 -0
- tintkit/icons/star.png +0 -0
- tintkit/icons/trash-2.png +0 -0
- tintkit/icons/undo.png +0 -0
- tintkit/icons/upload.png +0 -0
- tintkit/icons/x.png +0 -0
- tintkit/icons.py +72 -0
- tintkit/primitives.py +387 -0
- tintkit/scaling.py +24 -0
- tintkit/theme.py +193 -0
- tintkit-0.1.0.dist-info/METADATA +124 -0
- tintkit-0.1.0.dist-info/RECORD +36 -0
- tintkit-0.1.0.dist-info/WHEEL +5 -0
- tintkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- tintkit-0.1.0.dist-info/top_level.txt +1 -0
tintkit/__init__.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""TintKit — a small, themeable Tkinter widget kit.
|
|
2
|
+
|
|
3
|
+
A dark/light photo-tool look as reusable controls. One theme drives every
|
|
4
|
+
colour; controls reuse each other; everything is interactive.
|
|
5
|
+
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
from tintkit import Theme, Button, setup_dpi
|
|
8
|
+
|
|
9
|
+
root = tk.Tk()
|
|
10
|
+
setup_dpi(root) # crisp icons on high-DPI screens
|
|
11
|
+
theme = Theme(scheme="dark", accent="#8fae9b")
|
|
12
|
+
Button(root, theme, "Save", icon="save", command=lambda: None).pack()
|
|
13
|
+
root.mainloop()
|
|
14
|
+
|
|
15
|
+
Switch the look at any time — the whole window repaints::
|
|
16
|
+
|
|
17
|
+
theme.set(scheme="light")
|
|
18
|
+
theme.set(accent="#c08457")
|
|
19
|
+
|
|
20
|
+
See ``gallery.py`` for every component rendered together with a live switcher.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from . import icons
|
|
24
|
+
from .icons import set_icon_dir
|
|
25
|
+
from .scaling import s, set_scale
|
|
26
|
+
from .theme import (Theme, mix, lighten, darken, on_color,
|
|
27
|
+
SCHEMES, DEFAULT_ACCENT)
|
|
28
|
+
from .primitives import (CanvasControl, Surface, Label, IconLabel,
|
|
29
|
+
rounded_rect, put_icon, font, measure, FONT_FAMILY)
|
|
30
|
+
from .controls import (Button, IconButton, Slider, Toggle, Radio, RadioGroup,
|
|
31
|
+
Checkbox, SegmentedTabs, Badge, Tag, ProgressBar,
|
|
32
|
+
Tooltip, TextField, Dropdown, MultiDropdown)
|
|
33
|
+
from .containers import (Card, SectionHeader, hero_line, callout, dialog,
|
|
34
|
+
v_sash, h_sash, themed_scrollbar)
|
|
35
|
+
from .composites import (toolbar, tool_rail, FolderNav, folder_tree,
|
|
36
|
+
SelectTile, SelectRow, MultiSelectRow,
|
|
37
|
+
multiselect_list, SettingsWindow)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Theme", "mix", "lighten", "darken", "on_color", "SCHEMES",
|
|
41
|
+
"DEFAULT_ACCENT", "icons", "set_icon_dir", "setup_dpi",
|
|
42
|
+
"enable_dpi_awareness", "s", "set_scale",
|
|
43
|
+
"CanvasControl", "Surface", "Label", "IconLabel",
|
|
44
|
+
"rounded_rect", "put_icon", "font", "measure", "FONT_FAMILY",
|
|
45
|
+
"Button", "IconButton", "Slider", "Toggle", "Radio", "RadioGroup",
|
|
46
|
+
"Checkbox", "SegmentedTabs", "Badge", "Tag", "ProgressBar", "Tooltip",
|
|
47
|
+
"TextField", "Dropdown", "MultiDropdown",
|
|
48
|
+
"Card", "SectionHeader", "hero_line", "callout", "dialog",
|
|
49
|
+
"v_sash", "h_sash", "themed_scrollbar",
|
|
50
|
+
"toolbar", "tool_rail", "FolderNav", "folder_tree", "SelectTile",
|
|
51
|
+
"SelectRow", "MultiSelectRow", "multiselect_list", "SettingsWindow",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def enable_dpi_awareness():
|
|
56
|
+
"""Tell Windows this process draws at the real screen resolution.
|
|
57
|
+
|
|
58
|
+
MUST run before the first ``tk.Tk()`` — otherwise Tk caches the virtualised
|
|
59
|
+
96 DPI and everything renders tiny. The kit calls this automatically on
|
|
60
|
+
import, so importing ``tintkit`` before creating the root is enough.
|
|
61
|
+
"""
|
|
62
|
+
import sys
|
|
63
|
+
if sys.platform != "win32":
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
import ctypes
|
|
67
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2) # per-monitor aware
|
|
68
|
+
except Exception:
|
|
69
|
+
try:
|
|
70
|
+
ctypes.windll.user32.SetProcessDPIAware() # older fallback
|
|
71
|
+
except Exception:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def setup_dpi(root, zoom=1.0):
|
|
76
|
+
"""Scale fonts + icons + geometry to the screen. Returns the scale factor.
|
|
77
|
+
|
|
78
|
+
Call once, right after creating the root window and BEFORE building the
|
|
79
|
+
Theme. ``zoom`` is an extra comfort multiplier on top of the screen DPI
|
|
80
|
+
(1.0 = true size; raise it to make the whole UI bigger).
|
|
81
|
+
|
|
82
|
+
* Tk text scaling → ``(screen_dpi / 72) * zoom`` (fonts + text measurement).
|
|
83
|
+
* The kit's geometry scale ``S`` → ``(screen_dpi / 96) * zoom`` (canvas px).
|
|
84
|
+
"""
|
|
85
|
+
from . import scaling
|
|
86
|
+
enable_dpi_awareness() # idempotent; real fix is at import
|
|
87
|
+
try:
|
|
88
|
+
fpix = root.winfo_fpixels("1i")
|
|
89
|
+
root.tk.call("tk", "scaling", (fpix / 72.0) * zoom)
|
|
90
|
+
factor = max(1.0, fpix / 96.0) * zoom
|
|
91
|
+
except Exception:
|
|
92
|
+
factor = zoom
|
|
93
|
+
scaling.set_scale(factor)
|
|
94
|
+
icons.DPI = factor
|
|
95
|
+
return factor
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Set DPI awareness at import — before any tk.Tk() the caller creates.
|
|
99
|
+
enable_dpi_awareness()
|
tintkit/composites.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""TintKit — composites.
|
|
2
|
+
|
|
3
|
+
Bigger pieces assembled *only* from the kit: a toolbar is a row of
|
|
4
|
+
:class:`IconButton`, the folder nav reuses :class:`Badge` and :class:`IconLabel`,
|
|
5
|
+
the settings window reuses :class:`Toggle`, :class:`Dropdown`, :class:`Button`.
|
|
6
|
+
No composite re-implements a primitive, and every part restyles on theme change.
|
|
7
|
+
|
|
8
|
+
Geometry literals go through ``s()`` to scale to the screen DPI.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import tkinter as tk
|
|
12
|
+
|
|
13
|
+
from .scaling import s
|
|
14
|
+
from .primitives import Surface, Label, IconLabel, rounded_rect, put_icon, font
|
|
15
|
+
from . import icons
|
|
16
|
+
from .controls import (IconButton, Toggle, Dropdown, SegmentedTabs, Slider,
|
|
17
|
+
Button, Badge)
|
|
18
|
+
from .containers import Card, SectionHeader
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ----------------------------------------------------------------------------
|
|
22
|
+
# Toolbar — a row of square icon buttons (exclusive active item)
|
|
23
|
+
# ----------------------------------------------------------------------------
|
|
24
|
+
def toolbar(parent, theme, items, active=0, bg="bar", command=None):
|
|
25
|
+
"items: list of icon names. Returns a Surface; one item stays active."
|
|
26
|
+
bar = Surface(parent, theme, bg=bg)
|
|
27
|
+
btns = []
|
|
28
|
+
|
|
29
|
+
def choose(i):
|
|
30
|
+
for j, b in enumerate(btns):
|
|
31
|
+
b.set_active(j == i)
|
|
32
|
+
if command:
|
|
33
|
+
command(i, items[i])
|
|
34
|
+
for i, name in enumerate(items):
|
|
35
|
+
b = IconButton(bar.widget, theme, name, active=(i == active), bg=bg,
|
|
36
|
+
command=lambda i=i: choose(i))
|
|
37
|
+
b.pack(side="left", padx=s(3), pady=s(5))
|
|
38
|
+
btns.append(b)
|
|
39
|
+
return bar
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ----------------------------------------------------------------------------
|
|
43
|
+
# Tool rail — captioned tiles (icon above a label)
|
|
44
|
+
# ----------------------------------------------------------------------------
|
|
45
|
+
def tool_rail(parent, theme, items, active=0, bg="bar", command=None):
|
|
46
|
+
"items: list of (icon, label). Returns a Surface; one tile stays active."
|
|
47
|
+
box = Surface(parent, theme, bg=bg)
|
|
48
|
+
btns = []
|
|
49
|
+
|
|
50
|
+
def choose(i):
|
|
51
|
+
for j, b in enumerate(btns):
|
|
52
|
+
b.set_active(j == i)
|
|
53
|
+
if command:
|
|
54
|
+
command(i, items[i][1])
|
|
55
|
+
for i, (name, label) in enumerate(items):
|
|
56
|
+
b = IconButton(box.widget, theme, name, w=70, h=56, label=label,
|
|
57
|
+
active=(i == active), icon_px=20, bg=bg,
|
|
58
|
+
command=lambda i=i: choose(i))
|
|
59
|
+
b.pack(side="left", padx=s(2))
|
|
60
|
+
btns.append(b)
|
|
61
|
+
return box
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------------------
|
|
65
|
+
# Folder navigation — path bar + collapsible folder tree
|
|
66
|
+
# ----------------------------------------------------------------------------
|
|
67
|
+
class FolderNav:
|
|
68
|
+
def __init__(self, parent, theme, crumbs, tree_rows, count_text,
|
|
69
|
+
filter_text="Filter folders…"):
|
|
70
|
+
self.theme = theme
|
|
71
|
+
self.tree_rows = tree_rows
|
|
72
|
+
self.filter_text = filter_text
|
|
73
|
+
self.open = True
|
|
74
|
+
self.box = Surface(parent, theme, bg="bg")
|
|
75
|
+
|
|
76
|
+
outer = Surface(self.box.widget, theme, bg="border")
|
|
77
|
+
outer.widget.pack(fill="x")
|
|
78
|
+
bar = Surface(outer.widget, theme, bg="chip")
|
|
79
|
+
bar.widget.pack(fill="x", padx=s(1), pady=s(1))
|
|
80
|
+
self.bar = bar.widget
|
|
81
|
+
|
|
82
|
+
IconButton(self.bar, theme, "chevron-up", w=32, h=38, bg="chip").pack(
|
|
83
|
+
side="left", padx=(s(4), s(2)))
|
|
84
|
+
Surface(self.bar, theme, bg="border", width=s(1), height=s(22)).pack(
|
|
85
|
+
side="left", padx=s(4))
|
|
86
|
+
for i, (nm, cur) in enumerate(crumbs):
|
|
87
|
+
if i:
|
|
88
|
+
IconLabel(self.bar, theme, "chevron-right", 12, fg="fg_dim",
|
|
89
|
+
bg="chip").pack(side="left", padx=s(2))
|
|
90
|
+
self._crumb(nm, cur)
|
|
91
|
+
|
|
92
|
+
self.toggle_btn = IconButton(self.bar, theme, "chevron-up", w=26, h=38,
|
|
93
|
+
active=True, bg="chip",
|
|
94
|
+
command=self._toggle)
|
|
95
|
+
self.toggle_btn.pack(side="left", padx=(s(2), 0))
|
|
96
|
+
Badge(self.bar, theme, count_text, bg="chip").pack(side="right",
|
|
97
|
+
padx=s(8))
|
|
98
|
+
|
|
99
|
+
self.drop = Surface(self.box.widget, theme, bg="bg")
|
|
100
|
+
self._render()
|
|
101
|
+
|
|
102
|
+
def _crumb(self, name, current):
|
|
103
|
+
lb = Label(self.bar, self.theme, name, fg=("fg" if current else "fg_dim"),
|
|
104
|
+
bg="chip", size=11, bold=current, cursor="hand2",
|
|
105
|
+
padx=s(5), pady=s(9))
|
|
106
|
+
lb.widget.pack(side="left")
|
|
107
|
+
if not current:
|
|
108
|
+
w = lb.widget
|
|
109
|
+
w.bind("<Enter>", lambda e: w.configure(fg=self.theme["accent"]))
|
|
110
|
+
w.bind("<Leave>", lambda e: w.configure(fg=self.theme["fg_dim"]))
|
|
111
|
+
|
|
112
|
+
def _toggle(self):
|
|
113
|
+
self.open = not self.open
|
|
114
|
+
self.toggle_btn.icon_name = "chevron-up" if self.open else "chevron-down"
|
|
115
|
+
self.toggle_btn.set_active(self.open)
|
|
116
|
+
self._render()
|
|
117
|
+
|
|
118
|
+
def _render(self):
|
|
119
|
+
for w in self.drop.widget.winfo_children():
|
|
120
|
+
w.destroy()
|
|
121
|
+
if self.open:
|
|
122
|
+
folder_tree(self.drop.widget, self.theme, self.tree_rows,
|
|
123
|
+
self.filter_text)
|
|
124
|
+
self.drop.widget.pack(fill="x", pady=(s(6), 0))
|
|
125
|
+
else:
|
|
126
|
+
self.drop.widget.pack_forget()
|
|
127
|
+
|
|
128
|
+
def pack(self, **k):
|
|
129
|
+
self.box.pack(**k)
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def grid(self, **k):
|
|
133
|
+
self.box.grid(**k)
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def place(self, **k):
|
|
137
|
+
self.box.place(**k)
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def folder_tree(parent, theme, rows, filter_text=None):
|
|
142
|
+
outer = Surface(parent, theme, bg="border")
|
|
143
|
+
outer.widget.pack(fill="x")
|
|
144
|
+
panel = Surface(outer.widget, theme, bg="sidebar")
|
|
145
|
+
panel.widget.pack(fill="x", padx=s(1), pady=s(1))
|
|
146
|
+
p = panel.widget
|
|
147
|
+
if filter_text is not None:
|
|
148
|
+
_tree_filter(p, theme, filter_text)
|
|
149
|
+
else:
|
|
150
|
+
Label(p, theme, "Folders", fg="fg_dim", bg="sidebar", size=8,
|
|
151
|
+
bold=True, anchor="w").pack(fill="x", padx=s(12), pady=(s(8), s(4)))
|
|
152
|
+
for depth, name, kind, current in rows:
|
|
153
|
+
_tree_row(p, theme, depth, name, kind, current)
|
|
154
|
+
Surface(p, theme, bg="sidebar", height=s(8)).pack()
|
|
155
|
+
return panel
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _tree_filter(parent, theme, text):
|
|
159
|
+
box = Surface(parent, theme, bg="sidebar")
|
|
160
|
+
box.widget.pack(fill="x", padx=s(10), pady=(s(8), s(6)))
|
|
161
|
+
outer = Surface(box.widget, theme, bg="border")
|
|
162
|
+
outer.widget.pack(fill="x")
|
|
163
|
+
inner = Surface(outer.widget, theme, bg="bg")
|
|
164
|
+
inner.widget.pack(fill="x", padx=s(1), pady=s(1))
|
|
165
|
+
row = Surface(inner.widget, theme, bg="bg")
|
|
166
|
+
row.widget.pack(fill="x", padx=s(8), pady=s(5))
|
|
167
|
+
IconLabel(row.widget, theme, "search", 14, fg="fg_dim", bg="bg").pack(
|
|
168
|
+
side="left", padx=(0, s(6)))
|
|
169
|
+
Label(row.widget, theme, text, fg="fg_dim", bg="bg", size=9).pack(side="left")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _tree_row(parent, theme, depth, name, kind, current):
|
|
173
|
+
base = "lift" if current else "sidebar"
|
|
174
|
+
row = Surface(parent, theme, bg=base)
|
|
175
|
+
row.widget.pack(fill="x")
|
|
176
|
+
Surface(row.widget, theme, bg=("accent" if current else base),
|
|
177
|
+
width=s(3)).pack(side="left", fill="y")
|
|
178
|
+
Surface(row.widget, theme, bg=base, width=s(4 + depth * 16)).pack(
|
|
179
|
+
side="left")
|
|
180
|
+
if kind in ("open", "closed"):
|
|
181
|
+
IconLabel(row.widget, theme,
|
|
182
|
+
"chevron-down" if kind == "open" else "chevron-right",
|
|
183
|
+
12, fg="fg_dim", bg=base).pack(side="left")
|
|
184
|
+
else:
|
|
185
|
+
Surface(row.widget, theme, bg=base, width=s(12)).pack(side="left")
|
|
186
|
+
IconLabel(row.widget, theme, "folder-open", 16,
|
|
187
|
+
fg=("accent" if current else "fg"), bg=base).pack(side="left",
|
|
188
|
+
padx=(s(2), 0))
|
|
189
|
+
Label(row.widget, theme, name, fg=("accent" if current else "fg"), bg=base,
|
|
190
|
+
size=10, bold=current, cursor="hand2", padx=s(6)).pack(side="left",
|
|
191
|
+
pady=s(4))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ----------------------------------------------------------------------------
|
|
195
|
+
# Selection — the same frame in every view (list · thumbnails)
|
|
196
|
+
# ----------------------------------------------------------------------------
|
|
197
|
+
class _Selectable:
|
|
198
|
+
"Common theme handling for the selection demo views."
|
|
199
|
+
|
|
200
|
+
def _bind(self, theme, root):
|
|
201
|
+
self.theme = theme
|
|
202
|
+
self.root = root
|
|
203
|
+
theme.subscribe(self._restyle)
|
|
204
|
+
root.bind("<Destroy>", self._destroyed)
|
|
205
|
+
self._restyle()
|
|
206
|
+
|
|
207
|
+
def _destroyed(self, e):
|
|
208
|
+
if e.widget is self.root:
|
|
209
|
+
self.theme.unsubscribe(self._restyle)
|
|
210
|
+
|
|
211
|
+
def pack(self, **k):
|
|
212
|
+
self.root.pack(**k)
|
|
213
|
+
return self
|
|
214
|
+
|
|
215
|
+
def grid(self, **k):
|
|
216
|
+
self.root.grid(**k)
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def place(self, **k):
|
|
220
|
+
self.root.place(**k)
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class SelectTile(_Selectable):
|
|
225
|
+
"A thumbnail with a selection frame; the caption never recolours."
|
|
226
|
+
|
|
227
|
+
def __init__(self, parent, theme, image, name, selected=False, size=120):
|
|
228
|
+
self.selected = selected
|
|
229
|
+
cell = tk.Frame(parent)
|
|
230
|
+
self.cell = cell
|
|
231
|
+
self.holder = tk.Frame(cell, highlightthickness=s(2))
|
|
232
|
+
self.holder.pack(padx=s(4), pady=(s(4), 0))
|
|
233
|
+
self.pic = tk.Label(self.holder, image=image, cursor="hand2")
|
|
234
|
+
self.pic.image = image
|
|
235
|
+
self.pic.pack()
|
|
236
|
+
self.cap = tk.Label(cell, text=name, font=font(7),
|
|
237
|
+
wraplength=s(size + 8))
|
|
238
|
+
self.cap.pack(pady=(s(2), s(4)))
|
|
239
|
+
self._bind(theme, cell)
|
|
240
|
+
|
|
241
|
+
def _restyle(self):
|
|
242
|
+
try:
|
|
243
|
+
t = self.theme
|
|
244
|
+
# selection is the frame alone — an accent border, nothing tinted;
|
|
245
|
+
# the caption never recolours either.
|
|
246
|
+
edge = t["accent"] if self.selected else t["sidebar"]
|
|
247
|
+
self.cell.configure(bg=t["sidebar"])
|
|
248
|
+
self.holder.configure(bg=t["sidebar"], highlightbackground=edge,
|
|
249
|
+
highlightcolor=edge)
|
|
250
|
+
self.pic.configure(bg=t["sidebar"])
|
|
251
|
+
self.cap.configure(bg=t["sidebar"], fg=t["fg"])
|
|
252
|
+
except tk.TclError:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class SelectRow(_Selectable):
|
|
257
|
+
"A list row with the selection frame; the name never recolours."
|
|
258
|
+
|
|
259
|
+
def __init__(self, parent, theme, image, name, selected=False):
|
|
260
|
+
self.selected = selected
|
|
261
|
+
cell = tk.Frame(parent)
|
|
262
|
+
self.cell = cell
|
|
263
|
+
self.holder = tk.Frame(cell, highlightthickness=s(2))
|
|
264
|
+
self.holder.pack(fill="x")
|
|
265
|
+
self.pic = tk.Label(self.holder, image=image, cursor="hand2")
|
|
266
|
+
self.pic.image = image
|
|
267
|
+
self.pic.pack(side="left", padx=(s(4), s(8)), pady=s(2))
|
|
268
|
+
self.name = tk.Label(self.holder, text=name, anchor="w", cursor="hand2",
|
|
269
|
+
font=font(9))
|
|
270
|
+
self.name.pack(side="left", fill="x", expand=True)
|
|
271
|
+
self._bind(theme, cell)
|
|
272
|
+
|
|
273
|
+
def _restyle(self):
|
|
274
|
+
try:
|
|
275
|
+
t = self.theme
|
|
276
|
+
# selection is the frame alone — an accent border, name never recolours.
|
|
277
|
+
edge = t["accent"] if self.selected else t["sidebar"]
|
|
278
|
+
for w in (self.cell, self.holder, self.pic, self.name):
|
|
279
|
+
w.configure(bg=t["sidebar"])
|
|
280
|
+
self.holder.configure(highlightbackground=edge, highlightcolor=edge)
|
|
281
|
+
self.name.configure(fg=t["fg"])
|
|
282
|
+
except tk.TclError:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ----------------------------------------------------------------------------
|
|
287
|
+
# Multi-select list — checkable rows; selected rows tint + name turns accent
|
|
288
|
+
# ----------------------------------------------------------------------------
|
|
289
|
+
class MultiSelectRow(_Selectable):
|
|
290
|
+
def __init__(self, parent, theme, name, selected=False, command=None):
|
|
291
|
+
self.selected = selected
|
|
292
|
+
self.name_text = name
|
|
293
|
+
self.command = command
|
|
294
|
+
row = tk.Frame(parent)
|
|
295
|
+
self.row = row
|
|
296
|
+
self.stripe = tk.Frame(row, width=s(3))
|
|
297
|
+
self.stripe.pack(side="left", fill="y")
|
|
298
|
+
self.box = tk.Canvas(row, width=s(20), height=s(30),
|
|
299
|
+
highlightthickness=0, cursor="hand2")
|
|
300
|
+
self.box.pack(side="left", padx=(s(7), 0))
|
|
301
|
+
self.lbl = tk.Label(row, text=name, anchor="w", font=font(9))
|
|
302
|
+
self.lbl.pack(side="left", padx=(s(8), 0), fill="x", expand=True)
|
|
303
|
+
for w in (row, self.box, self.lbl):
|
|
304
|
+
w.bind("<Button-1>", self._click)
|
|
305
|
+
self._bind(theme, row)
|
|
306
|
+
|
|
307
|
+
def _click(self, _e):
|
|
308
|
+
self.selected = not self.selected
|
|
309
|
+
self._restyle()
|
|
310
|
+
if self.command:
|
|
311
|
+
self.command(self.selected)
|
|
312
|
+
|
|
313
|
+
def _restyle(self):
|
|
314
|
+
try:
|
|
315
|
+
t = self.theme
|
|
316
|
+
base = t["lift"] if self.selected else t["sidebar"]
|
|
317
|
+
self.row.configure(bg=base)
|
|
318
|
+
self.lbl.configure(bg=base,
|
|
319
|
+
fg=t["accent"] if self.selected else t["fg"],
|
|
320
|
+
font=font(9, self.selected))
|
|
321
|
+
self.stripe.configure(bg=t["accent"] if self.selected else base)
|
|
322
|
+
self.box.configure(bg=base)
|
|
323
|
+
self.box.delete("all")
|
|
324
|
+
if self.selected:
|
|
325
|
+
rounded_rect(self.box, s(3), s(8), s(17), s(22), s(3),
|
|
326
|
+
fill=t["accent"])
|
|
327
|
+
put_icon(self.box, s(10), s(15),
|
|
328
|
+
icons.load("check", 12, t["on_accent"]))
|
|
329
|
+
else:
|
|
330
|
+
rounded_rect(self.box, s(3), s(8), s(17), s(22), s(3), fill=base,
|
|
331
|
+
outline=t["ring"], width=s(2))
|
|
332
|
+
except tk.TclError:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def multiselect_list(parent, theme, rows, width=240):
|
|
337
|
+
"rows: list of (name, selected). Returns the bordered panel Surface."
|
|
338
|
+
holder = Surface(parent, theme, bg="border")
|
|
339
|
+
panel = Surface(holder.widget, theme, bg="sidebar")
|
|
340
|
+
panel.widget.pack(padx=s(1), pady=s(1))
|
|
341
|
+
Surface(panel.widget, theme, bg="sidebar", width=s(width), height=s(1)).pack()
|
|
342
|
+
for name, sel in rows:
|
|
343
|
+
MultiSelectRow(panel.widget, theme, name, selected=sel).pack(fill="x")
|
|
344
|
+
Surface(panel.widget, theme, bg="sidebar", height=s(6)).pack()
|
|
345
|
+
return holder
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ----------------------------------------------------------------------------
|
|
349
|
+
# Settings window — a left tab rail + a swappable pane (kit-built)
|
|
350
|
+
# ----------------------------------------------------------------------------
|
|
351
|
+
class SettingsWindow:
|
|
352
|
+
TABS = ["General", "Export", "Culling", "About"]
|
|
353
|
+
|
|
354
|
+
def __init__(self, parent, theme, width=560, height=400):
|
|
355
|
+
self.theme = theme
|
|
356
|
+
self.active = 0
|
|
357
|
+
self._tabs = []
|
|
358
|
+
card = Card(theme=theme, parent=parent, pad=0, bg="panel", width=s(width))
|
|
359
|
+
self.card = card
|
|
360
|
+
body = card.body
|
|
361
|
+
body.configure(height=s(height))
|
|
362
|
+
body.pack_propagate(False)
|
|
363
|
+
rail = Surface(body, theme, bg="sidebar")
|
|
364
|
+
rail.widget.pack(side="left", fill="y")
|
|
365
|
+
rail.widget.configure(width=s(140))
|
|
366
|
+
rail.widget.pack_propagate(False)
|
|
367
|
+
Label(rail.widget, theme, " SETTINGS", fg="fg_dim", bg="sidebar",
|
|
368
|
+
size=8, bold=True, anchor="w").pack(fill="x", pady=(s(16), s(8)))
|
|
369
|
+
for i, name in enumerate(self.TABS):
|
|
370
|
+
self._tab(rail.widget, i, name)
|
|
371
|
+
self.pane = Surface(body, theme, bg="panel")
|
|
372
|
+
self.pane.widget.pack(side="left", fill="both", expand=True,
|
|
373
|
+
padx=s(20), pady=s(16))
|
|
374
|
+
self._show(0)
|
|
375
|
+
|
|
376
|
+
def _tab(self, parent, i, name):
|
|
377
|
+
row = Surface(parent, self.theme, bg="sidebar")
|
|
378
|
+
row.widget.pack(fill="x")
|
|
379
|
+
stripe = Surface(row.widget, self.theme, bg="sidebar", width=s(3))
|
|
380
|
+
stripe.widget.pack(side="left", fill="y")
|
|
381
|
+
lbl = Label(row.widget, self.theme, name, fg="fg_dim", bg="sidebar",
|
|
382
|
+
size=10, cursor="hand2", anchor="w", padx=s(12), pady=s(8))
|
|
383
|
+
lbl.widget.pack(side="left", fill="x", expand=True)
|
|
384
|
+
self._tabs.append((stripe, lbl))
|
|
385
|
+
for w in (row.widget, lbl.widget):
|
|
386
|
+
w.bind("<Button-1>", lambda e, idx=i: self._show(idx))
|
|
387
|
+
|
|
388
|
+
def _show(self, i):
|
|
389
|
+
self.active = i
|
|
390
|
+
for j, (stripe, lbl) in enumerate(self._tabs):
|
|
391
|
+
on = (j == i)
|
|
392
|
+
stripe._bg = "accent" if on else "sidebar"
|
|
393
|
+
stripe._restyle()
|
|
394
|
+
lbl._fg = "accent" if on else "fg_dim"
|
|
395
|
+
lbl.widget.configure(font=font(10, on))
|
|
396
|
+
lbl._restyle()
|
|
397
|
+
for w in self.pane.widget.winfo_children():
|
|
398
|
+
w.destroy()
|
|
399
|
+
[self._general, self._export, self._culling, self._about][i]()
|
|
400
|
+
|
|
401
|
+
# -- panes -------------------------------------------------------------
|
|
402
|
+
def _row(self, label, control_factory):
|
|
403
|
+
r = Surface(self.pane.widget, self.theme, bg="panel")
|
|
404
|
+
r.widget.pack(fill="x", pady=s(6))
|
|
405
|
+
Label(r.widget, self.theme, label, fg="fg", bg="panel", size=10).pack(
|
|
406
|
+
side="left")
|
|
407
|
+
control_factory(r.widget).pack(side="right")
|
|
408
|
+
|
|
409
|
+
def _general(self):
|
|
410
|
+
SectionHeader(self.pane.widget, self.theme, "General", bg="panel").pack(
|
|
411
|
+
fill="x")
|
|
412
|
+
self._row("Dark theme", lambda p: Toggle(p, self.theme, value=True,
|
|
413
|
+
bg="panel"))
|
|
414
|
+
self._row("Confirm before delete",
|
|
415
|
+
lambda p: Toggle(p, self.theme, value=True, bg="panel"))
|
|
416
|
+
self._row("Thumbnail size",
|
|
417
|
+
lambda p: Dropdown(p, self.theme, ["Small", "Medium", "Large"],
|
|
418
|
+
selected=2, bg="panel"))
|
|
419
|
+
|
|
420
|
+
def _export(self):
|
|
421
|
+
SectionHeader(self.pane.widget, self.theme, "Export", bg="panel").pack(
|
|
422
|
+
fill="x")
|
|
423
|
+
self._row("Format", lambda p: SegmentedTabs(p, self.theme,
|
|
424
|
+
["JPG", "PNG", "TIFF"],
|
|
425
|
+
bg="panel"))
|
|
426
|
+
Slider(self.pane.widget, self.theme, "Quality", value=85, lo=0, hi=100,
|
|
427
|
+
neutral=0, bg="panel").pack(fill="x", pady=(s(8), 0))
|
|
428
|
+
self._row("Convert to sRGB",
|
|
429
|
+
lambda p: Toggle(p, self.theme, value=True, bg="panel"))
|
|
430
|
+
|
|
431
|
+
def _culling(self):
|
|
432
|
+
SectionHeader(self.pane.widget, self.theme, "Culling", bg="panel").pack(
|
|
433
|
+
fill="x")
|
|
434
|
+
self._row("Keep on right arrow",
|
|
435
|
+
lambda p: Toggle(p, self.theme, value=False, bg="panel"))
|
|
436
|
+
self._row("Reject folder name",
|
|
437
|
+
lambda p: Dropdown(p, self.theme, ["Rejected", "Trash",
|
|
438
|
+
"_cull"], bg="panel"))
|
|
439
|
+
|
|
440
|
+
def _about(self):
|
|
441
|
+
SectionHeader(self.pane.widget, self.theme, "About", bg="panel").pack(
|
|
442
|
+
fill="x")
|
|
443
|
+
Label(self.pane.widget, self.theme, "TintKit — a themeable Tkinter "
|
|
444
|
+
"UI kit.", fg="fg_dim", bg="panel", size=10,
|
|
445
|
+
justify="left").pack(anchor="w", pady=(s(4), s(12)))
|
|
446
|
+
Button(self.pane.widget, self.theme, "Check for updates",
|
|
447
|
+
role="neutral", variant="outline", bg="panel").pack(anchor="w")
|
|
448
|
+
|
|
449
|
+
def pack(self, **k):
|
|
450
|
+
self.card.pack(**k)
|
|
451
|
+
return self
|
|
452
|
+
|
|
453
|
+
def grid(self, **k):
|
|
454
|
+
self.card.grid(**k)
|
|
455
|
+
return self
|
|
456
|
+
|
|
457
|
+
def place(self, **k):
|
|
458
|
+
self.card.place(**k)
|
|
459
|
+
return self
|