13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249 | class FluMenuBar(Frame, DObject, FluGradient):
def __init__(self, *args, mode="light", height=40, **kwargs):
self._init(mode)
#: 记下配置高度,菜单项据此算出自身高度(见 _item_height)
self._configured_height = height
super().__init__(*args, height=height, **kwargs)
self._draw(None)
self.bind("<Configure>", self._event_configure, add="+")
def _item_height(self) -> int:
"""菜单项按钮的高度。
历史实现沿用 ``FluButton`` 的默认高度 32,再配 ``pady=5``,
合计需要 42px;而菜单栏默认只有 40px,底部还有一条 1.2px 的分隔线。
结果按钮被垂直压扁,文字跟着变形。
这里按菜单栏的实际高度,留出上下各 4px 与分隔线的余量。
"""
total = getattr(self, "_configured_height", 40) or 40
try:
total = int(self.cget("height")) or total
except Exception:
pass
return max(18, int(total) - 10)
def _item_width(self, label: str, width=None) -> int:
"""菜单项的宽度。
``len(label) * 8`` 对中文会严重偏窄("文件" 只算出 16 像素),
改用 :func:`~tkflu.defs.measure_label_width` 做真实度量。
"""
if width is not None:
return width
from .defs import measure_label_width
return measure_label_width(self, label)
def _init(self, mode):
from easydict import EasyDict
self.attributes = EasyDict(
{"back_color": "#f3f3f3", "border_color": "#e5e5e5", "actions": {}}
)
self.theme(mode=mode)
def show(self):
self.pack(fill="x")
def add_command(self, custom_widget=None, width=None, label: str = "", **kwargs):
width = self._item_width(label, width)
if custom_widget:
widget = custom_widget(self)
else:
from .button import FluButton
widget = FluButton(self, width=width, height=self._item_height())
if "style" in kwargs:
style = kwargs.pop("style")
else:
style = "menu"
if "command" in kwargs:
command = kwargs.pop("command")
else:
def empty():
pass
command = empty
if "id" in kwargs:
id = kwargs.pop("id")
else:
id = widget._w
if hasattr(widget, "dconfigure"):
widget.dconfigure(text=label, command=command)
else:
if hasattr(widget, "configure"):
widget.configure(text=label, command=command)
if hasattr(widget, "theme"):
widget.theme(style=style)
widget.pack(side="left", padx=4, pady=4)
self.dcget("actions")[id] = widget
from .menu import FluMenu
def add_cascade(
self,
custom_widget=None,
width=None,
label: str = "",
menu: FluMenu = None,
**kwargs,
):
width = self._item_width(label, width)
if custom_widget:
widget = custom_widget(self)
else:
from .button import FluButton
widget = FluButton(self, width=width, height=self._item_height())
if "style" in kwargs:
style = kwargs.pop("style")
else:
style = "menu"
if "id" in kwargs:
id = kwargs.pop("id")
else:
id = widget._w
def command():
menu.focus_set()
menu.popup(
widget.winfo_rootx() - 5, widget.winfo_rooty() + widget.winfo_height()
)
# 弹窗尺寸按内容算,不再写死 100px 宽(中文菜单项会被截断)
menu_width, menu_height = menu.preferred_size()
menu.window.geometry(f"{menu_width}x{menu_height}")
menu.window.deiconify()
menu.window.attributes("-topmost")
if hasattr(widget, "dconfigure"):
widget.dconfigure(text=label, command=command)
else:
if hasattr(widget, "configure"):
widget.configure(text=label, command=command)
if hasattr(widget, "theme"):
widget.theme(style=style)
widget.pack(side="left", padx=4, pady=4)
self.dcget("actions")[id] = widget
def action(self, id):
return self.dcget("actions")[id]
def update_children(self):
"""把菜单项按钮重新换一次肤(历史 API)。
保留名字是为了兼容老代码;内部走统一过渡,因此不再逐个 ``update()``。
"""
for widget in self.dcget("actions").values():
if hasattr(widget, "theme"):
widget.theme(mode=self.mode)
def theme(self, mode="light"):
"""菜单栏(连同菜单项按钮)切到 ``mode``。
旧实现是"自己换 + 遍历按钮 + 每个按钮 ``update()``":那会让每个按钮
各排各的过渡帧,还会在遍历途中把别人的帧提前执行掉,于是菜单项
一个一个变色。现在整棵子树交给统一过渡:一条时间轴,同步插值。
"""
from .theme_transition import applying_themes, run_theme_transition
if applying_themes():
# 有人(FluThemeManager)正在统一驱动:只改自己的配色,
# 菜单项按钮由驱动方负责,别在这里另起一条时间轴。
self.theme_myself(mode=mode)
return
run_theme_transition(self, mode)
def theme_myself(
self, mode="light", animation_steps: int = None, animation_step_time: int = None
):
if animation_steps is None:
from .designs.animation import get_animation_steps
animation_steps = get_animation_steps()
if animation_step_time is None:
from .designs.animation import get_animation_step_time
animation_step_time = get_animation_step_time()
from .designs.menubar import menubar
m = menubar(mode)
self.mode = mode
if hasattr(self, "tk"):
if not animation_steps == 0 or not animation_step_time == 0:
if mode.lower() == "dark":
back_colors = self.generate_hex2hex(
self.attributes.back_color,
m["back_color"],
steps=animation_steps,
)
border_colors = self.generate_hex2hex(
self.attributes.border_color,
m["border_color"],
steps=animation_steps,
)
else:
back_colors = self.generate_hex2hex(
self.attributes.back_color,
m["back_color"],
steps=animation_steps,
)
border_colors = self.generate_hex2hex(
self.attributes.border_color,
m["border_color"],
steps=animation_steps,
)
for i in range(animation_steps):
def update(ii=i): # 使用默认参数立即捕获i的值
self.dconfigure(
back_color=back_colors[ii], border_color=border_colors[ii]
)
self._draw()
self.after(
i * animation_step_time, update
) # 直接传递函数,不需要lambda
self.after(
animation_steps * animation_step_time + 50,
lambda: self.update_children(),
)
else:
self.dconfigure(back_color=m["back_color"])
self.dconfigure(border_color=m["border_color"])
self._draw()
def _draw(self, event=None):
self.config(background=self.attributes.back_color)
if not hasattr(self, "border"):
self.border = Frame(
self, height=1.2, background=self.attributes.border_color
)
else:
self.border.configure(background=self.attributes.border_color)
self.border.pack(fill="x", expand="yes", side="bottom")
def _event_configure(self, event=None):
self._draw(event)
|