tkinter-web-backend 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.
- tkinter_web/__init__.py +89 -0
- tkinter_web/__main__.py +69 -0
- tkinter_web/_tkinter.py +321 -0
- tkinter_web/_vendor/LICENSE-Python.txt +277 -0
- tkinter_web/_vendor/README.md +9 -0
- tkinter_web/_vendor/tkinter/__init__.py +4988 -0
- tkinter_web/_vendor/tkinter/__main__.py +7 -0
- tkinter_web/_vendor/tkinter/colorchooser.py +86 -0
- tkinter_web/_vendor/tkinter/commondialog.py +53 -0
- tkinter_web/_vendor/tkinter/constants.py +110 -0
- tkinter_web/_vendor/tkinter/dialog.py +49 -0
- tkinter_web/_vendor/tkinter/dnd.py +324 -0
- tkinter_web/_vendor/tkinter/filedialog.py +492 -0
- tkinter_web/_vendor/tkinter/font.py +239 -0
- tkinter_web/_vendor/tkinter/messagebox.py +146 -0
- tkinter_web/_vendor/tkinter/scrolledtext.py +56 -0
- tkinter_web/_vendor/tkinter/simpledialog.py +441 -0
- tkinter_web/_vendor/tkinter/ttk.py +1647 -0
- tkinter_web/app.py +358 -0
- tkinter_web/client/fonts/LICENSE-DejaVu.txt +47 -0
- tkinter_web/client/fonts/mono-bold-italic.woff2 +0 -0
- tkinter_web/client/fonts/mono-bold.woff2 +0 -0
- tkinter_web/client/fonts/mono-italic.woff2 +0 -0
- tkinter_web/client/fonts/mono.woff2 +0 -0
- tkinter_web/client/fonts/sans-bold-italic.woff2 +0 -0
- tkinter_web/client/fonts/sans-bold.woff2 +0 -0
- tkinter_web/client/fonts/sans-italic.woff2 +0 -0
- tkinter_web/client/fonts/sans.woff2 +0 -0
- tkinter_web/client/fonts/serif-bold-italic.woff2 +0 -0
- tkinter_web/client/fonts/serif-bold.woff2 +0 -0
- tkinter_web/client/fonts/serif-italic.woff2 +0 -0
- tkinter_web/client/fonts/serif.woff2 +0 -0
- tkinter_web/client/index.html +17 -0
- tkinter_web/client/js/bitmaps.js +82 -0
- tkinter_web/client/js/main.js +1427 -0
- tkinter_web/client/js/paint.js +694 -0
- tkinter_web/client/tkweb.css +342 -0
- tkinter_web/embed.py +204 -0
- tkinter_web/page.py +49 -0
- tkinter_web/tk/LICENSE-Tk.txt +40 -0
- tkinter_web/tk/__init__.py +11 -0
- tkinter_web/tk/_keysyms.py +547 -0
- tkinter_web/tk/_specs.py +1137 -0
- tkinter_web/tk/colors.json +1 -0
- tkinter_web/tk/dialogs.py +554 -0
- tkinter_web/tk/events.py +458 -0
- tkinter_web/tk/fontmetrics.json +1 -0
- tkinter_web/tk/fonts.py +467 -0
- tkinter_web/tk/geometry.py +1170 -0
- tkinter_web/tk/images.py +875 -0
- tkinter_web/tk/input.py +361 -0
- tkinter_web/tk/interp.py +1287 -0
- tkinter_web/tk/layout.py +192 -0
- tkinter_web/tk/options.py +368 -0
- tkinter_web/tk/render.py +258 -0
- tkinter_web/tk/tcl.py +262 -0
- tkinter_web/tk/ttk/__init__.py +16 -0
- tkinter_web/tk/ttk/style.py +357 -0
- tkinter_web/tk/ttk/treeview.py +817 -0
- tkinter_web/tk/ttk/widgets.py +1818 -0
- tkinter_web/tk/variables.py +152 -0
- tkinter_web/tk/widgets/__init__.py +89 -0
- tkinter_web/tk/widgets/base.py +255 -0
- tkinter_web/tk/widgets/buttons.py +452 -0
- tkinter_web/tk/widgets/canvas.py +1643 -0
- tkinter_web/tk/widgets/entry.py +752 -0
- tkinter_web/tk/widgets/frame.py +189 -0
- tkinter_web/tk/widgets/listbox.py +450 -0
- tkinter_web/tk/widgets/menu.py +723 -0
- tkinter_web/tk/widgets/message.py +85 -0
- tkinter_web/tk/widgets/panedwindow.py +249 -0
- tkinter_web/tk/widgets/scale.py +423 -0
- tkinter_web/tk/widgets/scrollbar.py +224 -0
- tkinter_web/tk/widgets/text.py +1546 -0
- tkinter_web/tk/winfo.py +147 -0
- tkinter_web/tk/wm.py +236 -0
- tkinter_web/transport/__init__.py +1 -0
- tkinter_web/transport/base.py +34 -0
- tkinter_web/transport/port.py +88 -0
- tkinter_web/transport/websocket.py +142 -0
- tkinter_web_backend-0.1.0.dist-info/METADATA +247 -0
- tkinter_web_backend-0.1.0.dist-info/RECORD +84 -0
- tkinter_web_backend-0.1.0.dist-info/WHEEL +4 -0
- tkinter_web_backend-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Menus (tkMenu.c, tkUnixMenu.c, library/menu.tcl) and menubuttons.
|
|
3
|
+
|
|
4
|
+
A menu is a toplevel window: posted menus are displayed by the client as
|
|
5
|
+
undecorated windows, menubars (``toplevel -menu``) as an HTML bar above the
|
|
6
|
+
window content.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from ..events import Event
|
|
12
|
+
from ..options import OptionSet, menu_entry_specs, parse_color
|
|
13
|
+
from ..tcl import TclError, get_int, split_list, to_str
|
|
14
|
+
from .base import Widget
|
|
15
|
+
from .buttons import Label, TYPE_BUTTON
|
|
16
|
+
|
|
17
|
+
TEAROFF_HEIGHT = 8
|
|
18
|
+
SEPARATOR_HEIGHT = 8
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MenuEntry:
|
|
22
|
+
def __init__(self, menu, kind):
|
|
23
|
+
self.menu = menu
|
|
24
|
+
self.kind = kind
|
|
25
|
+
specs = menu_entry_specs("separator" if kind == "tearoff" else kind)
|
|
26
|
+
self.options = OptionSet(specs, "menuentry", menu.interp.scaling)
|
|
27
|
+
if kind == "checkbutton" or kind == "radiobutton":
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def label(self):
|
|
31
|
+
return self.options.str("-label") if "-label" in self.options.specs else ""
|
|
32
|
+
|
|
33
|
+
def state(self):
|
|
34
|
+
return self.options.str("-state") if "-state" in self.options.specs else "normal"
|
|
35
|
+
|
|
36
|
+
def selected(self):
|
|
37
|
+
o = self.options
|
|
38
|
+
var = o.str("-variable") if "-variable" in o.specs else ""
|
|
39
|
+
if not var:
|
|
40
|
+
return False
|
|
41
|
+
v = self.menu.interp.variables.peek(var)
|
|
42
|
+
if v is None:
|
|
43
|
+
return False
|
|
44
|
+
if self.kind == "checkbutton":
|
|
45
|
+
return to_str(v) == to_str(o["-onvalue"])
|
|
46
|
+
if self.kind == "radiobutton":
|
|
47
|
+
value = o.str("-value") or self.label()
|
|
48
|
+
return to_str(v) == value
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Menu(Widget):
|
|
53
|
+
kind = "menu"
|
|
54
|
+
default_class = "Menu"
|
|
55
|
+
is_menu = True
|
|
56
|
+
|
|
57
|
+
def __init__(self, interp, path, parent, args):
|
|
58
|
+
self.entries = []
|
|
59
|
+
self.active = None
|
|
60
|
+
self.posted_cascade = None # submenu posted from this menu
|
|
61
|
+
self.parent_menu = None # menu (or menubutton/toplevel) that posted this one
|
|
62
|
+
self.wm = {
|
|
63
|
+
"title": None, "geometry": None, "position": None, "state": "withdrawn",
|
|
64
|
+
"resizable": (False, False), "minsize": (1, 1), "maxsize": None,
|
|
65
|
+
"protocols": {}, "attributes": {"-alpha": 1.0, "-topmost": 1, "-type": "menu"},
|
|
66
|
+
"overrideredirect": True, "transient": "", "iconphoto": None, "group": "",
|
|
67
|
+
"user_size": None, "grid": None, "aspect": None, "iconname": "",
|
|
68
|
+
}
|
|
69
|
+
self.grabbed = False
|
|
70
|
+
super().__init__(interp, path, parent, args)
|
|
71
|
+
if self.options.boolean("-tearoff"):
|
|
72
|
+
self.entries.append(MenuEntry(self, "tearoff"))
|
|
73
|
+
|
|
74
|
+
def is_toplevel_like(self):
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
def viewable(self):
|
|
78
|
+
return self.wm["state"] == "normal"
|
|
79
|
+
|
|
80
|
+
def title(self):
|
|
81
|
+
return self.options.str("-title") or self.name
|
|
82
|
+
|
|
83
|
+
def options_changed(self, names, creating):
|
|
84
|
+
if "-tearoff" in names and not creating:
|
|
85
|
+
has = bool(self.entries) and self.entries[0].kind == "tearoff"
|
|
86
|
+
want = self.options.boolean("-tearoff")
|
|
87
|
+
if want and not has:
|
|
88
|
+
self.entries.insert(0, MenuEntry(self, "tearoff"))
|
|
89
|
+
elif has and not want:
|
|
90
|
+
self.entries.pop(0)
|
|
91
|
+
super().options_changed(names, creating)
|
|
92
|
+
self._menubar_changed()
|
|
93
|
+
|
|
94
|
+
def _menubar_changed(self):
|
|
95
|
+
for top in self.interp.toplevels():
|
|
96
|
+
if "-menu" in top.options.specs and top.options.str("-menu") == self.path:
|
|
97
|
+
top.mark_dirty()
|
|
98
|
+
|
|
99
|
+
def mark_dirty(self, layout=False):
|
|
100
|
+
super().mark_dirty(layout=True)
|
|
101
|
+
|
|
102
|
+
# -- geometry
|
|
103
|
+
|
|
104
|
+
def font(self, entry=None):
|
|
105
|
+
if entry is not None and "-font" in entry.options.specs and entry.options.str("-font"):
|
|
106
|
+
return self.interp.font(entry.options["-font"])
|
|
107
|
+
return self.interp.font(self.options["-font"])
|
|
108
|
+
|
|
109
|
+
def entry_metrics(self):
|
|
110
|
+
"""[(y, height)] of the entries and the column widths."""
|
|
111
|
+
o = self.options
|
|
112
|
+
bw = o.pixels("-borderwidth")
|
|
113
|
+
abw = o.pixels("-activeborderwidth")
|
|
114
|
+
font = self.font()
|
|
115
|
+
rows = []
|
|
116
|
+
y = bw
|
|
117
|
+
label_w = accel_w = 0
|
|
118
|
+
indicator = 0
|
|
119
|
+
for e in self.entries:
|
|
120
|
+
f = self.font(e)
|
|
121
|
+
if e.kind == "tearoff":
|
|
122
|
+
h = TEAROFF_HEIGHT
|
|
123
|
+
elif e.kind == "separator":
|
|
124
|
+
h = SEPARATOR_HEIGHT
|
|
125
|
+
else:
|
|
126
|
+
h = f.linespace + 2 * abw + 2
|
|
127
|
+
img = e.options.str("-image") if "-image" in e.options.specs else ""
|
|
128
|
+
if img and img in self.interp.images:
|
|
129
|
+
h = max(h, self.interp.images[img].height + 2 * abw + 2)
|
|
130
|
+
label_w = max(label_w, f.measure(e.label()) + self._image_w(e))
|
|
131
|
+
acc = e.options.str("-accelerator") if "-accelerator" in e.options.specs else ""
|
|
132
|
+
if acc:
|
|
133
|
+
accel_w = max(accel_w, f.measure(acc))
|
|
134
|
+
if e.kind in ("checkbutton", "radiobutton", "cascade"):
|
|
135
|
+
indicator = max(indicator, f.linespace)
|
|
136
|
+
rows.append((y, h))
|
|
137
|
+
y += h
|
|
138
|
+
return rows, y + bw, label_w, accel_w, indicator
|
|
139
|
+
|
|
140
|
+
def _image_w(self, e):
|
|
141
|
+
img = e.options.str("-image") if "-image" in e.options.specs else ""
|
|
142
|
+
if img and img in self.interp.images:
|
|
143
|
+
w = self.interp.images[img].width
|
|
144
|
+
compound = e.options.str("-compound")
|
|
145
|
+
return w + (4 if compound in ("left", "right") and e.label() else 0) if compound != "none" else w
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
def natural_size(self):
|
|
149
|
+
o = self.options
|
|
150
|
+
bw = o.pixels("-borderwidth")
|
|
151
|
+
abw = o.pixels("-activeborderwidth")
|
|
152
|
+
rows, height, label_w, accel_w, indicator = self.entry_metrics()
|
|
153
|
+
font = self.font()
|
|
154
|
+
ind = max(font.linespace, indicator)
|
|
155
|
+
width = bw + abw + ind + label_w + (font.measure("0") * 3 if accel_w else 0) \
|
|
156
|
+
+ accel_w + ind + abw + bw
|
|
157
|
+
return max(width, 40, getattr(self, "_min_width", 0)), max(height, 2 * bw + 4)
|
|
158
|
+
|
|
159
|
+
def menubar_height(self):
|
|
160
|
+
return 22
|
|
161
|
+
|
|
162
|
+
def entry_at(self, y):
|
|
163
|
+
rows, *_ = self.entry_metrics()
|
|
164
|
+
for i, (ey, h) in enumerate(rows):
|
|
165
|
+
if ey <= y < ey + h:
|
|
166
|
+
return i
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
# -- display
|
|
170
|
+
|
|
171
|
+
def display(self):
|
|
172
|
+
o = self.options
|
|
173
|
+
bg = o.color("-background")
|
|
174
|
+
fg = o.color("-foreground")
|
|
175
|
+
bw = o.pixels("-borderwidth")
|
|
176
|
+
abw = o.pixels("-activeborderwidth")
|
|
177
|
+
W = self.width
|
|
178
|
+
res = [["fill", 0, 0, W, self.height, bg]]
|
|
179
|
+
rows, _, label_w, accel_w, indicator = self.entry_metrics()
|
|
180
|
+
font = self.font()
|
|
181
|
+
ind = max(font.linespace, indicator)
|
|
182
|
+
for i, (e, (y, h)) in enumerate(zip(self.entries, rows)):
|
|
183
|
+
if e.kind == "tearoff":
|
|
184
|
+
res.append(["dash", bw + 2, y + h // 2, W - 2 * bw - 4, fg])
|
|
185
|
+
continue
|
|
186
|
+
if e.kind == "separator":
|
|
187
|
+
res.append(["border", bw + 2, y + h // 2 - 1, W - 2 * bw - 4, 2, 1, "sunken", bg])
|
|
188
|
+
continue
|
|
189
|
+
eo = e.options
|
|
190
|
+
state = e.state()
|
|
191
|
+
active = i == self.active and state != "disabled"
|
|
192
|
+
ebg = eo.color("-background") if "-background" in eo.specs and eo.str("-background") else bg
|
|
193
|
+
efg = eo.color("-foreground") if "-foreground" in eo.specs and eo.str("-foreground") else fg
|
|
194
|
+
if active:
|
|
195
|
+
ebg = (eo.color("-activebackground") if eo.str("-activebackground") else None) or \
|
|
196
|
+
o.color("-activebackground")
|
|
197
|
+
efg = (eo.color("-activeforeground") if eo.str("-activeforeground") else None) or \
|
|
198
|
+
o.color("-activeforeground")
|
|
199
|
+
res.append(["fill", bw, y, W - 2 * bw, h, ebg])
|
|
200
|
+
if abw > 0:
|
|
201
|
+
res.append(["border", bw, y, W - 2 * bw, h, abw, "raised", ebg])
|
|
202
|
+
elif ebg != bg:
|
|
203
|
+
res.append(["fill", bw, y, W - 2 * bw, h, ebg])
|
|
204
|
+
if state == "disabled":
|
|
205
|
+
efg = o.color("-disabledforeground") or "#a3a3a3"
|
|
206
|
+
f = self.font(e)
|
|
207
|
+
css = f.css()
|
|
208
|
+
ty = y + (h - f.linespace) // 2 + f.ascent
|
|
209
|
+
x = bw + abw + ind
|
|
210
|
+
img = eo.str("-image") if "-image" in eo.specs else ""
|
|
211
|
+
if img and img in self.interp.images:
|
|
212
|
+
im = self.interp.images[img]
|
|
213
|
+
res.append(["image", x, y + (h - im.height) // 2, img])
|
|
214
|
+
x += im.width + 4
|
|
215
|
+
if e.kind != "tearoff":
|
|
216
|
+
res.append(["text", x, ty, e.label(), css, efg,
|
|
217
|
+
eo.int("-underline", -1) if "-underline" in eo.specs else -1])
|
|
218
|
+
acc = eo.str("-accelerator") if "-accelerator" in eo.specs else ""
|
|
219
|
+
if acc:
|
|
220
|
+
res.append(["text", W - bw - abw - ind - f.measure(acc), ty, acc, css, efg, -1])
|
|
221
|
+
cy = y + h // 2
|
|
222
|
+
if e.kind == "cascade":
|
|
223
|
+
res.append(["arrow", W - bw - abw - ind // 2, cy, max(ind // 2, 5), "right", efg])
|
|
224
|
+
elif e.kind in ("checkbutton", "radiobutton") and (
|
|
225
|
+
"-indicatoron" not in eo.specs or eo.boolean("-indicatoron")):
|
|
226
|
+
if e.selected():
|
|
227
|
+
sel = eo.color("-selectcolor") if eo.str("-selectcolor") else efg
|
|
228
|
+
if e.kind == "checkbutton":
|
|
229
|
+
res.append(["checkmark", bw + abw + ind // 2, cy, max(ind - 6, 7), sel])
|
|
230
|
+
else:
|
|
231
|
+
res.append(["dot", bw + abw + ind // 2, cy, max(ind // 5, 2), sel])
|
|
232
|
+
res.append(["border", 0, 0, W, self.height, bw, o.str("-relief"), bg])
|
|
233
|
+
return res
|
|
234
|
+
|
|
235
|
+
# -- menubar description for the client
|
|
236
|
+
|
|
237
|
+
def menubar_description(self):
|
|
238
|
+
res = []
|
|
239
|
+
for i, e in enumerate(self.entries):
|
|
240
|
+
if e.kind in ("tearoff", "separator"):
|
|
241
|
+
continue
|
|
242
|
+
sub = e.options.str("-menu") if "-menu" in e.options.specs else ""
|
|
243
|
+
res.append({"label": e.label(), "menu": sub or None, "index": i,
|
|
244
|
+
"state": e.state(), "kind": e.kind,
|
|
245
|
+
"open": bool(sub) and sub in self.interp.widgets and
|
|
246
|
+
self.interp.widgets[sub].viewable()})
|
|
247
|
+
return res
|
|
248
|
+
|
|
249
|
+
# -- indices
|
|
250
|
+
|
|
251
|
+
def index(self, value, allow_none=True):
|
|
252
|
+
v = value if isinstance(value, int) else to_str(value)
|
|
253
|
+
n = len(self.entries)
|
|
254
|
+
if isinstance(v, int):
|
|
255
|
+
return v
|
|
256
|
+
if v == "active":
|
|
257
|
+
return self.active if self.active is not None else "none"
|
|
258
|
+
if v in ("end", "last"):
|
|
259
|
+
return n - 1 if n else "none"
|
|
260
|
+
if v == "none" or v == "":
|
|
261
|
+
return "none"
|
|
262
|
+
if v.startswith("@"):
|
|
263
|
+
try:
|
|
264
|
+
y = int(v[1:].split(",")[-1])
|
|
265
|
+
except ValueError:
|
|
266
|
+
raise TclError(f'bad menu entry index "{v}"') from None
|
|
267
|
+
i = self.entry_at(y)
|
|
268
|
+
return i if i is not None else "none"
|
|
269
|
+
try:
|
|
270
|
+
return get_int(v)
|
|
271
|
+
except TclError:
|
|
272
|
+
pass
|
|
273
|
+
import fnmatch
|
|
274
|
+
for i, e in enumerate(self.entries):
|
|
275
|
+
if e.kind not in ("tearoff", "separator") and fnmatch.fnmatchcase(e.label(), v):
|
|
276
|
+
return i
|
|
277
|
+
raise TclError(f'bad menu entry index "{v}"')
|
|
278
|
+
|
|
279
|
+
def _entry(self, value):
|
|
280
|
+
i = self.index(value)
|
|
281
|
+
if i == "none" or not 0 <= i < len(self.entries):
|
|
282
|
+
return None, None
|
|
283
|
+
return i, self.entries[i]
|
|
284
|
+
|
|
285
|
+
# -- widget command
|
|
286
|
+
|
|
287
|
+
def subcommand(self, sub, args):
|
|
288
|
+
if sub in ("add", "insert"):
|
|
289
|
+
if sub == "insert":
|
|
290
|
+
pos = self.index(args[0])
|
|
291
|
+
pos = len(self.entries) if pos == "none" else max(0, min(pos, len(self.entries)))
|
|
292
|
+
if self.entries and self.entries[0].kind == "tearoff" and pos == 0:
|
|
293
|
+
pos = 1
|
|
294
|
+
args = args[1:]
|
|
295
|
+
else:
|
|
296
|
+
pos = len(self.entries)
|
|
297
|
+
kind = to_str(args[0])
|
|
298
|
+
kinds = ("cascade", "checkbutton", "command", "radiobutton", "separator")
|
|
299
|
+
m = [k for k in kinds if k.startswith(kind)] if kind else []
|
|
300
|
+
if len(m) != 1:
|
|
301
|
+
raise TclError(f'bad menu entry type "{kind}": must be cascade, checkbutton, '
|
|
302
|
+
'command, radiobutton, or separator')
|
|
303
|
+
e = MenuEntry(self, m[0])
|
|
304
|
+
self._configure_entry(e, args[1:])
|
|
305
|
+
self.entries.insert(pos, e)
|
|
306
|
+
self.mark_dirty()
|
|
307
|
+
self._menubar_changed()
|
|
308
|
+
return ""
|
|
309
|
+
if sub == "delete":
|
|
310
|
+
i1 = self.index(args[0])
|
|
311
|
+
i2 = self.index(args[1]) if len(args) > 1 else i1
|
|
312
|
+
if i1 == "none":
|
|
313
|
+
return ""
|
|
314
|
+
if i2 == "none":
|
|
315
|
+
i2 = i1
|
|
316
|
+
for i in range(min(i2, len(self.entries) - 1), i1 - 1, -1):
|
|
317
|
+
if self.entries[i].kind != "tearoff":
|
|
318
|
+
del self.entries[i]
|
|
319
|
+
self.active = None
|
|
320
|
+
self.mark_dirty()
|
|
321
|
+
self._menubar_changed()
|
|
322
|
+
return ""
|
|
323
|
+
if sub in ("entryconfigure", "entrycget"):
|
|
324
|
+
i, e = self._entry(args[0])
|
|
325
|
+
if e is None:
|
|
326
|
+
raise TclError(f'bad menu entry index "{to_str(args[0])}"')
|
|
327
|
+
if sub == "entrycget":
|
|
328
|
+
return e.options.values[e.options.resolve(args[1])]
|
|
329
|
+
if len(args) == 1:
|
|
330
|
+
return e.options.configure_info()
|
|
331
|
+
if len(args) == 2:
|
|
332
|
+
return e.options.configure_info(args[1])
|
|
333
|
+
self._configure_entry(e, args[1:])
|
|
334
|
+
self.mark_dirty()
|
|
335
|
+
self._menubar_changed()
|
|
336
|
+
return ""
|
|
337
|
+
if sub == "index":
|
|
338
|
+
i = self.index(args[0])
|
|
339
|
+
return i
|
|
340
|
+
if sub == "type":
|
|
341
|
+
i, e = self._entry(args[0])
|
|
342
|
+
return e.kind if e is not None else ""
|
|
343
|
+
if sub == "invoke":
|
|
344
|
+
i, e = self._entry(args[0])
|
|
345
|
+
if e is None:
|
|
346
|
+
return ""
|
|
347
|
+
return self.invoke(i)
|
|
348
|
+
if sub == "activate":
|
|
349
|
+
i = self.index(args[0])
|
|
350
|
+
self.active = None if i == "none" else i
|
|
351
|
+
self.mark_dirty()
|
|
352
|
+
return ""
|
|
353
|
+
if sub == "post":
|
|
354
|
+
x, y = get_int(args[0]), get_int(args[1])
|
|
355
|
+
self.post(x, y)
|
|
356
|
+
return ""
|
|
357
|
+
if sub == "unpost":
|
|
358
|
+
self.unpost()
|
|
359
|
+
return ""
|
|
360
|
+
if sub == "postcascade":
|
|
361
|
+
i, e = self._entry(args[0])
|
|
362
|
+
if e is not None and e.kind == "cascade":
|
|
363
|
+
self._post_cascade(i)
|
|
364
|
+
return ""
|
|
365
|
+
if sub == "yposition":
|
|
366
|
+
i = self.index(args[0])
|
|
367
|
+
rows, *_ = self.entry_metrics()
|
|
368
|
+
return rows[i][0] if i != "none" and 0 <= i < len(rows) else 0
|
|
369
|
+
if sub == "xposition":
|
|
370
|
+
return 0
|
|
371
|
+
if sub == "clone":
|
|
372
|
+
return ""
|
|
373
|
+
raise TclError(f'bad option "{sub}": must be activate, add, cget, clone, configure, '
|
|
374
|
+
'delete, entrycget, entryconfigure, index, insert, invoke, post, '
|
|
375
|
+
'postcascade, type, unpost, xposition, or yposition')
|
|
376
|
+
|
|
377
|
+
def _configure_entry(self, e, args):
|
|
378
|
+
if len(args) % 2:
|
|
379
|
+
raise TclError(f'value for "{to_str(args[-1])}" missing')
|
|
380
|
+
for name, value in zip(args[::2], args[1::2]):
|
|
381
|
+
e.options.set(name, value)
|
|
382
|
+
if e.kind == "radiobutton" and not e.options.str("-variable"):
|
|
383
|
+
e.options.values["-variable"] = "selectedButton"
|
|
384
|
+
if e.kind == "checkbutton" and not e.options.str("-variable"):
|
|
385
|
+
e.options.values["-variable"] = e.label()
|
|
386
|
+
var = e.options.str("-variable") if "-variable" in e.options.specs else ""
|
|
387
|
+
if var:
|
|
388
|
+
self.interp.variables.link(var, self, "-variable")
|
|
389
|
+
|
|
390
|
+
def variable_changed(self, option):
|
|
391
|
+
self.mark_dirty()
|
|
392
|
+
|
|
393
|
+
# -- posting
|
|
394
|
+
|
|
395
|
+
def post(self, x, y, parent=None):
|
|
396
|
+
self.interp.run_idle()
|
|
397
|
+
from ..layout import compute_requests
|
|
398
|
+
compute_requests(self)
|
|
399
|
+
w, h = self.req_w, self.req_h
|
|
400
|
+
sw, sh = self.interp.screen_width, self.interp.screen_height
|
|
401
|
+
x = max(0, min(x, sw - w))
|
|
402
|
+
y = max(0, min(y, sh - h)) if y + h > sh else y
|
|
403
|
+
self.x, self.y = x, y
|
|
404
|
+
self.wm["position"] = (x, y)
|
|
405
|
+
self.wm["state"] = "normal"
|
|
406
|
+
self.parent_menu = parent
|
|
407
|
+
self.active = None
|
|
408
|
+
self.post_time = time.monotonic()
|
|
409
|
+
self.pressed_here = False
|
|
410
|
+
cmd = self.options.str("-postcommand")
|
|
411
|
+
if cmd:
|
|
412
|
+
self.interp.run_protected(self.interp.eval_script, cmd)
|
|
413
|
+
if self in self.interp.toplevel_order:
|
|
414
|
+
self.interp.toplevel_order.remove(self)
|
|
415
|
+
self.interp.toplevel_order.append(self)
|
|
416
|
+
if self.interp.focus_widget is not None and not getattr(self.interp.focus_widget, "is_menu", False):
|
|
417
|
+
self._saved_focus = self.interp.focus_widget
|
|
418
|
+
self.mark_dirty()
|
|
419
|
+
self._menubar_changed()
|
|
420
|
+
if parent is not None and hasattr(parent, "_menubar_changed"):
|
|
421
|
+
parent._menubar_changed()
|
|
422
|
+
self._owner_changed()
|
|
423
|
+
|
|
424
|
+
def _owner_changed(self):
|
|
425
|
+
for top in self.interp.toplevels():
|
|
426
|
+
m = top.options.str("-menu") if "-menu" in top.options.specs else ""
|
|
427
|
+
if m and m in self.interp.widgets:
|
|
428
|
+
top.mark_dirty()
|
|
429
|
+
|
|
430
|
+
def unpost(self):
|
|
431
|
+
if self.posted_cascade is not None:
|
|
432
|
+
sub = self.posted_cascade
|
|
433
|
+
self.posted_cascade = None
|
|
434
|
+
sub.unpost()
|
|
435
|
+
if self.wm["state"] != "withdrawn":
|
|
436
|
+
self.wm["state"] = "withdrawn"
|
|
437
|
+
self.active = None
|
|
438
|
+
self.mark_dirty()
|
|
439
|
+
self._owner_changed()
|
|
440
|
+
owner = self.parent_menu
|
|
441
|
+
if isinstance(owner, Menubutton):
|
|
442
|
+
owner.posted = False
|
|
443
|
+
owner.mark_dirty()
|
|
444
|
+
|
|
445
|
+
@staticmethod
|
|
446
|
+
def unpost_all(interp, keep=None):
|
|
447
|
+
for w in list(interp.toplevel_order):
|
|
448
|
+
if isinstance(w, Menu) and w.viewable():
|
|
449
|
+
if keep is not None and (keep is w or _menu_chain_contains(w, keep)
|
|
450
|
+
or w.root_menu().parent_menu is keep):
|
|
451
|
+
# the owner (menubutton, combobox) toggles its own menu
|
|
452
|
+
continue
|
|
453
|
+
w.unpost()
|
|
454
|
+
|
|
455
|
+
def root_menu(self):
|
|
456
|
+
m = self
|
|
457
|
+
while isinstance(m.parent_menu, Menu) and m.parent_menu.viewable():
|
|
458
|
+
m = m.parent_menu
|
|
459
|
+
return m
|
|
460
|
+
|
|
461
|
+
def _post_cascade(self, i):
|
|
462
|
+
e = self.entries[i]
|
|
463
|
+
sub_path = e.options.str("-menu")
|
|
464
|
+
sub = self.interp.widgets.get(sub_path)
|
|
465
|
+
if not isinstance(sub, Menu):
|
|
466
|
+
return
|
|
467
|
+
if self.posted_cascade is sub and sub.viewable():
|
|
468
|
+
return
|
|
469
|
+
if self.posted_cascade is not None:
|
|
470
|
+
self.posted_cascade.unpost()
|
|
471
|
+
rows, *_ = self.entry_metrics()
|
|
472
|
+
y = self.y + rows[i][0] - self.options.pixels("-borderwidth")
|
|
473
|
+
x = self.x + self.width - 2
|
|
474
|
+
self.posted_cascade = sub
|
|
475
|
+
sub.post(x, y, parent=self)
|
|
476
|
+
|
|
477
|
+
def invoke(self, i):
|
|
478
|
+
e = self.entries[i]
|
|
479
|
+
if e.state() == "disabled" or e.kind in ("separator", "tearoff", "cascade"):
|
|
480
|
+
return ""
|
|
481
|
+
o = e.options
|
|
482
|
+
if e.kind == "checkbutton":
|
|
483
|
+
var = o.str("-variable")
|
|
484
|
+
if var:
|
|
485
|
+
self.interp.variables.set(var, o["-offvalue"] if e.selected() else o["-onvalue"])
|
|
486
|
+
elif e.kind == "radiobutton":
|
|
487
|
+
var = o.str("-variable")
|
|
488
|
+
if var:
|
|
489
|
+
self.interp.variables.set(var, o.str("-value") or e.label())
|
|
490
|
+
cmd = o["-command"] if "-command" in o.specs else ""
|
|
491
|
+
if to_str(cmd):
|
|
492
|
+
return self.interp.eval_script(cmd)
|
|
493
|
+
return ""
|
|
494
|
+
|
|
495
|
+
# -- events
|
|
496
|
+
|
|
497
|
+
def class_event(self, event):
|
|
498
|
+
t = event.type
|
|
499
|
+
if t in ("Motion", "Enter"):
|
|
500
|
+
i = self.entry_at(event.y) if 0 <= event.x < self.width else None
|
|
501
|
+
if i is not None and self.entries[i].kind in ("separator", "tearoff"):
|
|
502
|
+
i = None
|
|
503
|
+
if i != self.active:
|
|
504
|
+
self.active = i
|
|
505
|
+
self.mark_dirty()
|
|
506
|
+
if i is not None and self.entries[i].kind == "cascade" and \
|
|
507
|
+
self.entries[i].state() != "disabled":
|
|
508
|
+
self._post_cascade(i)
|
|
509
|
+
elif self.posted_cascade is not None and i is not None:
|
|
510
|
+
self.posted_cascade.unpost()
|
|
511
|
+
self.posted_cascade = None
|
|
512
|
+
elif t == "Leave":
|
|
513
|
+
if self.active is not None and (self.posted_cascade is None or
|
|
514
|
+
not self.posted_cascade.viewable()):
|
|
515
|
+
self.active = None
|
|
516
|
+
self.mark_dirty()
|
|
517
|
+
elif t == "ButtonPress":
|
|
518
|
+
self.pressed_here = True
|
|
519
|
+
elif t == "ButtonRelease":
|
|
520
|
+
if not getattr(self, "pressed_here", False) and \
|
|
521
|
+
time.monotonic() - getattr(self, "post_time", 0) < 0.4:
|
|
522
|
+
# the release of the click that posted the menu
|
|
523
|
+
return None
|
|
524
|
+
i = self.entry_at(event.y) if 0 <= event.x < self.width else None
|
|
525
|
+
if i is not None:
|
|
526
|
+
e = self.entries[i]
|
|
527
|
+
if e.kind == "cascade":
|
|
528
|
+
self._post_cascade(i)
|
|
529
|
+
return None
|
|
530
|
+
if e.kind in ("separator", "tearoff") or e.state() == "disabled":
|
|
531
|
+
return None
|
|
532
|
+
self.root_menu().unpost()
|
|
533
|
+
Menu.unpost_all(self.interp)
|
|
534
|
+
self.interp.run_protected(self.invoke, i)
|
|
535
|
+
elif t == "KeyPress":
|
|
536
|
+
self.key(event.keysym)
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
def key(self, keysym):
|
|
540
|
+
selectable = [i for i, e in enumerate(self.entries)
|
|
541
|
+
if e.kind not in ("separator", "tearoff") and e.state() != "disabled"]
|
|
542
|
+
if keysym in ("Down", "Up") and selectable:
|
|
543
|
+
if self.active not in selectable:
|
|
544
|
+
self.active = selectable[0] if keysym == "Down" else selectable[-1]
|
|
545
|
+
else:
|
|
546
|
+
k = selectable.index(self.active) + (1 if keysym == "Down" else -1)
|
|
547
|
+
self.active = selectable[k % len(selectable)]
|
|
548
|
+
self.mark_dirty()
|
|
549
|
+
elif keysym in ("Return", "KP_Enter", "space") and self.active is not None:
|
|
550
|
+
e = self.entries[self.active]
|
|
551
|
+
if e.kind == "cascade":
|
|
552
|
+
self._post_cascade(self.active)
|
|
553
|
+
else:
|
|
554
|
+
i = self.active
|
|
555
|
+
Menu.unpost_all(self.interp)
|
|
556
|
+
self.interp.run_protected(self.invoke, i)
|
|
557
|
+
elif keysym == "Right" and self.active is not None and \
|
|
558
|
+
self.entries[self.active].kind == "cascade":
|
|
559
|
+
self._post_cascade(self.active)
|
|
560
|
+
sub = self.posted_cascade
|
|
561
|
+
if sub is not None:
|
|
562
|
+
sub.key("Down")
|
|
563
|
+
elif keysym in ("Escape", "Left"):
|
|
564
|
+
if isinstance(self.parent_menu, Menu):
|
|
565
|
+
self.unpost()
|
|
566
|
+
else:
|
|
567
|
+
Menu.unpost_all(self.interp)
|
|
568
|
+
|
|
569
|
+
def client_event(self, msg):
|
|
570
|
+
"""Requests from the client menubar: post a cascade or invoke an entry."""
|
|
571
|
+
op = msg.get("op")
|
|
572
|
+
if op == "post":
|
|
573
|
+
top = self.interp.widgets.get(msg.get("win", "."))
|
|
574
|
+
if self.viewable():
|
|
575
|
+
Menu.unpost_all(self.interp)
|
|
576
|
+
return
|
|
577
|
+
Menu.unpost_all(self.interp)
|
|
578
|
+
self.post(int(msg.get("x", 0)), int(msg.get("y", 0)), parent=top)
|
|
579
|
+
elif op == "invoke":
|
|
580
|
+
Menu.unpost_all(self.interp)
|
|
581
|
+
self.interp.run_protected(self.invoke, int(msg.get("index", 0)))
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _menu_chain_contains(menu, w):
|
|
585
|
+
while w is not None:
|
|
586
|
+
if w is menu:
|
|
587
|
+
return True
|
|
588
|
+
w = w.parent_menu if isinstance(w, Menu) else None
|
|
589
|
+
return False
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
class Menubutton(Label):
|
|
593
|
+
kind = "menubutton"
|
|
594
|
+
default_class = "Menubutton"
|
|
595
|
+
btype = TYPE_BUTTON
|
|
596
|
+
traversal = True
|
|
597
|
+
|
|
598
|
+
def __init__(self, interp, path, parent, args):
|
|
599
|
+
self.posted = False
|
|
600
|
+
super().__init__(interp, path, parent, args)
|
|
601
|
+
|
|
602
|
+
def indicator_size(self):
|
|
603
|
+
"""(width, height) of the option menu indicator (tkUnixMenubu.c)."""
|
|
604
|
+
if not self.options.boolean("-indicatoron"):
|
|
605
|
+
return 0, 0
|
|
606
|
+
px_per_mm = self.interp.scaling * 72 / 25.4
|
|
607
|
+
h = int(17 * px_per_mm / 10)
|
|
608
|
+
return int(40 * px_per_mm / 10) + 2 * h, h
|
|
609
|
+
|
|
610
|
+
def geometry_info(self):
|
|
611
|
+
g = super().geometry_info()
|
|
612
|
+
g["width"] -= 2
|
|
613
|
+
g["height"] -= 2
|
|
614
|
+
g["width"] += self.indicator_size()[0]
|
|
615
|
+
return g
|
|
616
|
+
|
|
617
|
+
def display(self):
|
|
618
|
+
o = self.options
|
|
619
|
+
ind_w, ind_h = self.indicator_size()
|
|
620
|
+
if not ind_w:
|
|
621
|
+
return super().display()
|
|
622
|
+
# the label is drawn without the indicator, which goes on the right
|
|
623
|
+
o.values["-indicatoron"] = 0
|
|
624
|
+
try:
|
|
625
|
+
self.width -= ind_w
|
|
626
|
+
res = super().display()
|
|
627
|
+
finally:
|
|
628
|
+
self.width += ind_w
|
|
629
|
+
o.values["-indicatoron"] = 1
|
|
630
|
+
bg = o.color("-activebackground") if o.str("-state") == "active" else o.color("-background")
|
|
631
|
+
inset = o.pixels("-highlightthickness") + o.pixels("-borderwidth")
|
|
632
|
+
W, H = self.width, self.height
|
|
633
|
+
# widen the background and border drawn for the narrower label
|
|
634
|
+
res = [r for r in res if r[0] not in ("border", "ring")]
|
|
635
|
+
res.insert(1, ["fill", W - ind_w - inset, 0, ind_w + inset, H, bg])
|
|
636
|
+
bw = max((ind_h + 1) // 3, 1)
|
|
637
|
+
res.append(["fill", W - inset - ind_w + ind_h, (H - ind_h) // 2, ind_w - 2 * ind_h,
|
|
638
|
+
ind_h, bg])
|
|
639
|
+
res.append(["border", W - inset - ind_w + ind_h, (H - ind_h) // 2, ind_w - 2 * ind_h,
|
|
640
|
+
ind_h, bw, "raised", bg])
|
|
641
|
+
if o.str("-relief") != "flat" and o.pixels("-borderwidth") > 0:
|
|
642
|
+
ht = o.pixels("-highlightthickness")
|
|
643
|
+
res.append(["border", ht, ht, W - 2 * ht, H - 2 * ht, o.pixels("-borderwidth"),
|
|
644
|
+
o.str("-relief"), bg])
|
|
645
|
+
res.extend(self.highlight_display())
|
|
646
|
+
return res
|
|
647
|
+
|
|
648
|
+
def _shift(self, relief, x, y, width, height):
|
|
649
|
+
return x, y
|
|
650
|
+
|
|
651
|
+
def subcommand(self, sub, args):
|
|
652
|
+
raise TclError(f'bad option "{sub}": must be cget or configure')
|
|
653
|
+
|
|
654
|
+
def class_event(self, event):
|
|
655
|
+
o = self.options
|
|
656
|
+
t = event.type
|
|
657
|
+
if o.str("-state") == "disabled":
|
|
658
|
+
return None
|
|
659
|
+
if t == "Enter":
|
|
660
|
+
self.inside = True
|
|
661
|
+
o.values["-state"] = "active" if o.str("-state") == "normal" else o.str("-state")
|
|
662
|
+
self.mark_dirty()
|
|
663
|
+
elif t == "Leave":
|
|
664
|
+
self.inside = False
|
|
665
|
+
if o.str("-state") == "active":
|
|
666
|
+
o.values["-state"] = "normal"
|
|
667
|
+
self.mark_dirty()
|
|
668
|
+
elif t == "ButtonPress" and event.button == 1:
|
|
669
|
+
menu = self.interp.widgets.get(o.str("-menu"))
|
|
670
|
+
if isinstance(menu, Menu):
|
|
671
|
+
if menu.viewable():
|
|
672
|
+
menu.unpost()
|
|
673
|
+
return None
|
|
674
|
+
Menu.unpost_all(self.interp)
|
|
675
|
+
rx, ry = self.interp.root_xy(self)
|
|
676
|
+
direction = o.str("-direction")
|
|
677
|
+
from ..layout import compute_requests
|
|
678
|
+
compute_requests(menu)
|
|
679
|
+
if direction == "above":
|
|
680
|
+
menu.post(rx, ry - menu.req_h, parent=self)
|
|
681
|
+
elif direction == "flush":
|
|
682
|
+
# place the selected entry on the button (OptionMenu)
|
|
683
|
+
menu.post(rx, ry, parent=self)
|
|
684
|
+
elif direction == "right":
|
|
685
|
+
menu.post(rx + self.width, ry, parent=self)
|
|
686
|
+
elif direction == "left":
|
|
687
|
+
menu.post(rx - menu.req_w, ry, parent=self)
|
|
688
|
+
else:
|
|
689
|
+
menu.post(rx, ry + self.height, parent=self)
|
|
690
|
+
self.posted = True
|
|
691
|
+
self.mark_dirty()
|
|
692
|
+
return None
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def tk_popup(interp, args):
|
|
696
|
+
menu = interp.widget(args[0])
|
|
697
|
+
x, y = get_int(args[1]), get_int(args[2])
|
|
698
|
+
if isinstance(menu, Menu):
|
|
699
|
+
Menu.unpost_all(interp)
|
|
700
|
+
if len(args) > 3 and to_str(args[3]) != "":
|
|
701
|
+
i = menu.index(args[3])
|
|
702
|
+
rows, *_ = menu.entry_metrics()
|
|
703
|
+
if i != "none" and 0 <= i < len(rows):
|
|
704
|
+
y -= rows[i][0] + rows[i][1] // 2
|
|
705
|
+
menu.post(x, y)
|
|
706
|
+
menu.active = None if i == "none" else i
|
|
707
|
+
else:
|
|
708
|
+
menu.post(x, y)
|
|
709
|
+
return ""
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def tk_option_menu(interp, args):
|
|
713
|
+
path, var = to_str(args[0]), to_str(args[1])
|
|
714
|
+
values = [to_str(v) for v in args[2:]]
|
|
715
|
+
if interp.variables.peek(var) is None and values:
|
|
716
|
+
interp.variables.set(var, values[0])
|
|
717
|
+
interp.call("menubutton", path, "-textvariable", var, "-indicatoron", 1,
|
|
718
|
+
"-menu", path + ".menu", "-relief", "raised", "-highlightthickness", 1,
|
|
719
|
+
"-anchor", "c", "-direction", "flush")
|
|
720
|
+
interp.call("menu", path + ".menu", "-tearoff", 0)
|
|
721
|
+
for v in values:
|
|
722
|
+
interp.call(path + ".menu", "add", "radiobutton", "-label", v, "-variable", var)
|
|
723
|
+
return path + ".menu"
|