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.
Files changed (84) hide show
  1. tkinter_web/__init__.py +89 -0
  2. tkinter_web/__main__.py +69 -0
  3. tkinter_web/_tkinter.py +321 -0
  4. tkinter_web/_vendor/LICENSE-Python.txt +277 -0
  5. tkinter_web/_vendor/README.md +9 -0
  6. tkinter_web/_vendor/tkinter/__init__.py +4988 -0
  7. tkinter_web/_vendor/tkinter/__main__.py +7 -0
  8. tkinter_web/_vendor/tkinter/colorchooser.py +86 -0
  9. tkinter_web/_vendor/tkinter/commondialog.py +53 -0
  10. tkinter_web/_vendor/tkinter/constants.py +110 -0
  11. tkinter_web/_vendor/tkinter/dialog.py +49 -0
  12. tkinter_web/_vendor/tkinter/dnd.py +324 -0
  13. tkinter_web/_vendor/tkinter/filedialog.py +492 -0
  14. tkinter_web/_vendor/tkinter/font.py +239 -0
  15. tkinter_web/_vendor/tkinter/messagebox.py +146 -0
  16. tkinter_web/_vendor/tkinter/scrolledtext.py +56 -0
  17. tkinter_web/_vendor/tkinter/simpledialog.py +441 -0
  18. tkinter_web/_vendor/tkinter/ttk.py +1647 -0
  19. tkinter_web/app.py +358 -0
  20. tkinter_web/client/fonts/LICENSE-DejaVu.txt +47 -0
  21. tkinter_web/client/fonts/mono-bold-italic.woff2 +0 -0
  22. tkinter_web/client/fonts/mono-bold.woff2 +0 -0
  23. tkinter_web/client/fonts/mono-italic.woff2 +0 -0
  24. tkinter_web/client/fonts/mono.woff2 +0 -0
  25. tkinter_web/client/fonts/sans-bold-italic.woff2 +0 -0
  26. tkinter_web/client/fonts/sans-bold.woff2 +0 -0
  27. tkinter_web/client/fonts/sans-italic.woff2 +0 -0
  28. tkinter_web/client/fonts/sans.woff2 +0 -0
  29. tkinter_web/client/fonts/serif-bold-italic.woff2 +0 -0
  30. tkinter_web/client/fonts/serif-bold.woff2 +0 -0
  31. tkinter_web/client/fonts/serif-italic.woff2 +0 -0
  32. tkinter_web/client/fonts/serif.woff2 +0 -0
  33. tkinter_web/client/index.html +17 -0
  34. tkinter_web/client/js/bitmaps.js +82 -0
  35. tkinter_web/client/js/main.js +1427 -0
  36. tkinter_web/client/js/paint.js +694 -0
  37. tkinter_web/client/tkweb.css +342 -0
  38. tkinter_web/embed.py +204 -0
  39. tkinter_web/page.py +49 -0
  40. tkinter_web/tk/LICENSE-Tk.txt +40 -0
  41. tkinter_web/tk/__init__.py +11 -0
  42. tkinter_web/tk/_keysyms.py +547 -0
  43. tkinter_web/tk/_specs.py +1137 -0
  44. tkinter_web/tk/colors.json +1 -0
  45. tkinter_web/tk/dialogs.py +554 -0
  46. tkinter_web/tk/events.py +458 -0
  47. tkinter_web/tk/fontmetrics.json +1 -0
  48. tkinter_web/tk/fonts.py +467 -0
  49. tkinter_web/tk/geometry.py +1170 -0
  50. tkinter_web/tk/images.py +875 -0
  51. tkinter_web/tk/input.py +361 -0
  52. tkinter_web/tk/interp.py +1287 -0
  53. tkinter_web/tk/layout.py +192 -0
  54. tkinter_web/tk/options.py +368 -0
  55. tkinter_web/tk/render.py +258 -0
  56. tkinter_web/tk/tcl.py +262 -0
  57. tkinter_web/tk/ttk/__init__.py +16 -0
  58. tkinter_web/tk/ttk/style.py +357 -0
  59. tkinter_web/tk/ttk/treeview.py +817 -0
  60. tkinter_web/tk/ttk/widgets.py +1818 -0
  61. tkinter_web/tk/variables.py +152 -0
  62. tkinter_web/tk/widgets/__init__.py +89 -0
  63. tkinter_web/tk/widgets/base.py +255 -0
  64. tkinter_web/tk/widgets/buttons.py +452 -0
  65. tkinter_web/tk/widgets/canvas.py +1643 -0
  66. tkinter_web/tk/widgets/entry.py +752 -0
  67. tkinter_web/tk/widgets/frame.py +189 -0
  68. tkinter_web/tk/widgets/listbox.py +450 -0
  69. tkinter_web/tk/widgets/menu.py +723 -0
  70. tkinter_web/tk/widgets/message.py +85 -0
  71. tkinter_web/tk/widgets/panedwindow.py +249 -0
  72. tkinter_web/tk/widgets/scale.py +423 -0
  73. tkinter_web/tk/widgets/scrollbar.py +224 -0
  74. tkinter_web/tk/widgets/text.py +1546 -0
  75. tkinter_web/tk/winfo.py +147 -0
  76. tkinter_web/tk/wm.py +236 -0
  77. tkinter_web/transport/__init__.py +1 -0
  78. tkinter_web/transport/base.py +34 -0
  79. tkinter_web/transport/port.py +88 -0
  80. tkinter_web/transport/websocket.py +142 -0
  81. tkinter_web_backend-0.1.0.dist-info/METADATA +247 -0
  82. tkinter_web_backend-0.1.0.dist-info/RECORD +84 -0
  83. tkinter_web_backend-0.1.0.dist-info/WHEEL +4 -0
  84. 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"