-
Notifications
You must be signed in to change notification settings - Fork 352
/
dialogs.py
1879 lines (1544 loc) · 60.9 KB
/
dialogs.py
1
2
3
4
5
6
7
8
9
10
11
12
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
This module contains various base dialog base classes that can be
used to create custom dialogs for the end user.
These classes serve as the basis for the pre-defined static helper
methods in the `Messagebox`, and `Querybox` container classes.
"""
import calendar
import textwrap
import locale
from datetime import datetime
from tkinter import font
import ttkbootstrap as ttk
from ttkbootstrap import utility
from ttkbootstrap.icons import Icon
from ttkbootstrap.constants import *
from tkinter import BaseWidget
from ttkbootstrap.localization import MessageCatalog
class Dialog(BaseWidget):
"""A simple dialog base class."""
def __init__(self, parent=None, title="", alert=False):
"""
Parameters:
parent (Widget):
Makes the window the logical parent of the message box.
The messagebox is displayed on top of its parent window.
title (str):
The string displayed as the title of the message box.
This option is ignored on Mac OS X, where platform
guidelines forbid the use of a title on this kind of
dialog.
alert (bool):
Ring the display's bell when the dialog is shown.
"""
BaseWidget._setup(self, parent, {})
self._winsys = self.master.tk.call("tk", "windowingsystem")
self._parent = parent
self._toplevel = None
self._title = title or " "
self._result = None
self._alert = alert
self._initial_focus = None
def _locate(self):
toplevel = self._toplevel
if self._parent is None:
master = toplevel.master
else:
master = self._parent
x = master.winfo_rootx()
y = master.winfo_rooty()
toplevel.geometry(f"+{x}+{y}")
def show(self, position=None):
"""Show the popup dialog
Parameters:
position: Tuple[int, int]
The x and y coordinates used to position the dialog. By
default the dialog will anchor at the NW corner of the
parent window.
"""
self._result = None
self.build()
if position is None:
self._locate()
else:
try:
x, y = position
self._toplevel.geometry(f'+{x}+{y}')
except:
self._locate()
self._toplevel.deiconify()
if self._alert:
self._toplevel.bell()
if self._initial_focus:
self._initial_focus.focus_force()
self._toplevel.grab_set()
self._toplevel.wait_window()
def create_body(self, master):
"""Create the dialog body.
This method should be overridden and is called by the `build`
method. Set the `self._initial_focus` for the widget that
should receive the initial focus.
Parameters:
master (Widget):
The parent widget.
"""
raise NotImplementedError
def create_buttonbox(self, master):
"""Create the dialog button box.
This method should be overridden and is called by the `build`
method. Set the `self._initial_focus` for the button that
should receive the intial focus.
Parameters:
master (Widget):
The parent widget.
"""
raise NotImplementedError
def build(self):
"""Build the dialog from settings"""
# setup toplevel based on widowing system
if self._winsys == "win32":
self._toplevel = ttk.Toplevel(
transient=self.master,
title=self._title,
resizable=(0, 0),
minsize=(250, 15),
iconify=True,
)
else:
self._toplevel = ttk.Toplevel(
transient=self.master,
title=self._title,
resizable=(0, 0),
windowtype="dialog",
iconify=True,
)
self._toplevel.withdraw() # reset the iconify state
# bind <Escape> event to window close
self._toplevel.bind("<Escape>", lambda _: self._toplevel.destroy())
# set position of popup from parent window
# self._locate()
# create widgets
self.create_body(self._toplevel)
self.create_buttonbox(self._toplevel)
# update the window before showing
self._toplevel.update_idletasks()
@property
def result(self):
"""Returns the result of the dialog."""
return self._result
class MessageDialog(Dialog):
"""A simple modal dialog class that can be used to build simple
message dialogs.
Displays a message and a set of buttons. Each of the buttons in the
message window is identified by a unique symbolic name. After the
message window is popped up, the message box awaits for the user to
select one of the buttons. Then it returns the symbolic name of the
selected button. Use a `Toplevel` widget for more advanced modal
dialog designs.
"""
def __init__(
self,
message,
title=" ",
buttons=None,
command=None,
width=50,
parent=None,
alert=False,
default=None,
padding=(20, 20),
icon=None,
**kwargs,
):
"""
Parameters:
message (str):
A message to display in the message box.
title (str):
The string displayed as the title of the message box.
This option is ignored on Mac OS X, where platform
guidelines forbid the use of a title on this kind of
dialog.
buttons (List[str]):
A list of buttons to appear at the bottom of the popup
messagebox. The buttons can be a list of strings which
will define the symbolic name and the button text.
`['OK', 'Cancel']`. Alternatively, you can assign a
bootstyle to each button by using the colon to separate the
button text and the bootstyle. If no colon is found, then
the style is set to 'primary' by default.
`['OK:success','Cancel:danger']`.
command (Tuple[Callable, str]):
The function to invoke when the user closes the dialog.
The actual command is a tuple that consists of the
function to call and the symbolic name of the button that
closes the dialog.
width (int):
The maximum number of characters per line in the message.
If the text stretches beyond the limit, the line will break
at the word.
parent (Widget):
Makes the window the logical parent of the message box.
The messagebox is displayed on top of its parent window.
alert (bool):
Ring the display's bell when the dialog is shown.
default (str):
The symbolic name of the default button. The default
button is invoked when the the <Return> key is pressed.
If no default is provided, the right-most button in the
button list will be set as the default.,
padding (Union[int, Tuple[int]]):
The amount of space between the border and the widget
contents.
icon (str):
An image path, path-like object or image data to be
displayed to the left of the text.
**kwargs (Dict):
Other optional keyword arguments.
Example:
```python
root = tk.Tk()
md = MessageDialog("Displays a message with buttons.")
md.show()
```
"""
super().__init__(parent, title, alert)
self._message = message
self._command = command
self._width = width
self._alert = alert
self._default = default
self._padding = padding
self._icon = icon
self._localize = kwargs.get("localize")
if buttons is None:
self._buttons = [
f"{MessageCatalog.translate('Cancel')}:secondary",
f"{MessageCatalog.translate('OK')}:primary",
]
else:
self._buttons = buttons
def create_body(self, master):
"""Overrides the parent method; adds the message section."""
container = ttk.Frame(master, padding=self._padding)
if self._icon:
try:
# assume this is image data
self._img = ttk.PhotoImage(data=self._icon)
icon_lbl = ttk.Label(container, image=self._img)
icon_lbl.pack(side=LEFT, padx=5)
except:
try:
# assume this is a file path
self._img = ttk.PhotoImage(file=self._icon)
icon_lbl = ttk.Label(container, image=self._img)
icon_lbl.pack(side=LEFT, padx=5)
except:
# icon is neither data nor a valid file path
print("MessageDialog icon is invalid")
if self._message:
for msg in self._message.split("\n"):
message = "\n".join(textwrap.wrap(msg, width=self._width))
message_label = ttk.Label(container, text=message)
message_label.pack(pady=(0, 3), fill=X, anchor=N)
container.pack(fill=X, expand=True)
def create_buttonbox(self, master):
"""Overrides the parent method; adds the message buttonbox"""
frame = ttk.Frame(master, padding=(5, 5))
button_list = []
for i, button in enumerate(self._buttons[::-1]):
cnf = button.split(":")
if len(cnf) == 2:
text, bootstyle = cnf
else:
text = cnf[0]
bootstyle = "secondary"
if self._localize == True:
text = MessageCatalog.translate(text)
btn = ttk.Button(frame, bootstyle=bootstyle, text=text)
btn.configure(command=lambda b=btn: self.on_button_press(b))
btn.pack(padx=2, side=RIGHT)
btn.lower() # set focus traversal left-to-right
button_list.append(btn)
if self._default is not None and text == self._default:
self._initial_focus = btn
elif self._default is None and i == 0:
self._initial_focus = btn
# bind default button to return key press and set focus
self._toplevel.bind("<Return>", lambda _, b=btn: b.invoke())
self._toplevel.bind("<KP_Enter>", lambda _, b=btn: b.invoke())
ttk.Separator(self._toplevel).pack(fill=X)
frame.pack(side=BOTTOM, fill=X, anchor=S)
if not self._initial_focus:
self._initial_focus = button_list[0]
def on_button_press(self, button):
"""Save result, destroy the toplevel, and execute command."""
self._result = button["text"]
command = self._command
if command is not None:
command()
self._toplevel.destroy()
def show(self, position=None):
"""Create and display the popup messagebox."""
super().show(position)
class QueryDialog(Dialog):
"""A simple modal dialog class that can be used to build simple
data input dialogs. Displays a prompt, and input box, and a set of
buttons. Additional data manipulation can be performed on the
user input post-hoc by overriding the `apply` method.
Use a `Toplevel` widget for more advanced modal dialog designs.
"""
def __init__(
self,
prompt,
title=" ",
initialvalue="",
minvalue=None,
maxvalue=None,
width=65,
datatype=str,
padding=(20, 20),
parent=None,
):
"""
Parameters:
prompt (str):
A message to display in the message box above the entry
widget.
title (str):
The string displayed as the title of the message box.
This option is ignored on Mac OS X, where platform
guidelines forbid the use of a title on this kind of
dialog.
initialvalue (Any):
The initial value in the entry widget.
minvalue (Any):
The minimum allowed value. Only valid for int and float
data types.
maxvalue (Any):
The maximum allowed value. Only valid for int and float
data types.
width (int):
The maximum number of characters per line in the
message. If the text stretches beyond the limit, the
line will break at the word.
parent (Widget):
Makes the window the logical parent of the message box.
The messagebox is displayed on top of its parent
window.
padding (Union[int, Tuple[int]]):
The amount of space between the border and the widget
contents.
datatype (Union[int, str, float]):
The data type used to validate the entry value.
"""
super().__init__(parent, title)
self._prompt = prompt
self._initialvalue = initialvalue
self._minvalue = minvalue
self._maxvalue = maxvalue
self._width = width
self._datatype = datatype
self._padding = padding
self._result = None
def create_body(self, master):
"""Overrides the parent method; adds the message and input
section."""
frame = ttk.Frame(master, padding=self._padding)
if self._prompt:
for p in self._prompt.split("\n"):
prompt = "\n".join(textwrap.wrap(p, width=self._width))
prompt_label = ttk.Label(frame, text=prompt)
prompt_label.pack(pady=(0, 5), fill=X, anchor=N)
entry = ttk.Entry(master=frame)
entry.insert(END, self._initialvalue)
entry.pack(pady=(0, 5), fill=X)
entry.bind("<Return>", self.on_submit)
entry.bind("<KP_Enter>", self.on_submit)
entry.bind("<Escape>", self.on_cancel)
frame.pack(fill=X, expand=True)
self._initial_focus = entry
def create_buttonbox(self, master):
"""Overrides the parent method; adds the message buttonbox"""
frame = ttk.Frame(master, padding=(5, 10))
submit = ttk.Button(
master=frame,
bootstyle="primary",
text=MessageCatalog.translate("Submit"),
command=self.on_submit,
)
submit.pack(padx=5, side=RIGHT)
submit.lower() # set focus traversal left-to-right
cancel = ttk.Button(
master=frame,
bootstyle="secondary",
text=MessageCatalog.translate("Cancel"),
command=self.on_cancel,
)
cancel.pack(padx=5, side=RIGHT)
cancel.lower() # set focus traversal left-to-right
ttk.Separator(self._toplevel).pack(fill=X)
frame.pack(side=BOTTOM, fill=X, anchor=S)
def on_submit(self, *_):
"""Save result, destroy the toplevel, and apply any post-hoc
data manipulations."""
self._result = self._initial_focus.get()
valid_result = self.validate()
if not valid_result:
return # keep toplevel open for valid response
self._toplevel.destroy()
self.apply()
def on_cancel(self, *_):
"""Close the toplevel and return empty."""
self._toplevel.destroy()
return
def validate(self):
"""Validate the data
This method is called automatically to validate the data before
the dialog is destroyed. Can be subclassed and overridden.
"""
# no default checks required for string data types
if self._datatype not in [float, int, complex]:
return True
# convert result to appropriate data type
try:
self._result = self._datatype(self._result)
except ValueError:
msg = MessageCatalog.translate("Should be of data type")
Messagebox.ok(
message=f"{msg} `{self._datatype}`",
title=MessageCatalog.translate("Invalid data type"),
parent=self._toplevel
)
return False
# max value range
if self._maxvalue is not None:
if self._result > self._maxvalue:
msg = MessageCatalog.translate("Number cannot be greater than")
Messagebox.ok(
message=f"{msg} {self._maxvalue}",
title=MessageCatalog.translate("Out of range"),
parent=self._toplevel
)
return False
# min value range
if self._minvalue is not None:
if self._result < self._minvalue:
msg = MessageCatalog.translate("Number cannot be less than")
Messagebox.ok(
message=f"{msg} {self._minvalue}",
title=MessageCatalog.translate("Out of range"),
parent=self._toplevel
)
return False
# valid result
return True
def apply(self):
"""Process the data.
This method is called automatically to process the data after
the dialog is destroyed. By default, it does nothing.
"""
pass # override
class DatePickerDialog:
"""A dialog that displays a calendar popup and returns the
selected date as a datetime object.
The current date is displayed by default unless the `startdate`
parameter is provided.
The month can be changed by clicking the chevrons to the left
and right of the month-year title.
Left-click the arrow to move the calendar by one month.
Right-click the arrow to move the calendar by one year.
Right-click the title to reset the calendar to the start date.
The starting weekday can be changed with the `firstweekday`
parameter for geographies that do not start the calendar on
Sunday, which is the default.
The widget grabs focus and all screen events until released.
If you want to cancel a date selection, click the 'X' button
at the top-right corner of the widget.
The bootstyle api may be used to change the style of the widget.
The available colors include -> primary, secondary, success,
info, warning, danger, light, dark.
![](../../assets/dialogs/date-picker-dialog.png)
"""
locale.setlocale(locale.LC_ALL, locale.setlocale(locale.LC_TIME, ""))
def __init__(
self,
parent=None,
title=" ",
firstweekday=6,
startdate=None,
bootstyle=PRIMARY,
):
"""
Parameters:
parent (Widget):
The parent widget; the popup will appear to the
bottom-right of the parent widget. If no parent is
provided, the widget is centered on the screen.
title (str):
The text that appears on the titlebar.
firstweekday (int):
Specifies the first day of the week. 0=Monday,
1=Tuesday, etc...
startdate (datetime):
The date to be in focus when the widget is
displayed.
bootstyle (str):
The following colors can be used to change the color of
the title and hover / pressed color -> primary,
secondary, info, warning, success, danger, light, dark.
"""
self.parent = parent
self.root = ttk.Toplevel(
title=title,
transient=self.parent,
resizable=(False, False),
topmost=True,
minsize=(226, 1),
iconify=True,
)
self.firstweekday = firstweekday
self.startdate = startdate or datetime.today().date()
self.bootstyle = bootstyle or PRIMARY
self.date_selected = self.startdate
self.date = startdate or self.date_selected
self.calendar = calendar.Calendar(firstweekday=firstweekday)
self.titlevar = ttk.StringVar()
self.datevar = ttk.IntVar()
self._setup_calendar()
self.root.grab_set()
self.root.wait_window()
def _setup_calendar(self):
"""Setup the calendar widget"""
# create the widget containers
self.frm_calendar = ttk.Frame(
master=self.root, padding=0, borderwidth=0, relief=FLAT
)
self.frm_calendar.pack(fill=BOTH, expand=YES)
self.frm_title = ttk.Frame(self.frm_calendar, padding=(3, 3))
self.frm_title.pack(fill=X)
self.frm_header = ttk.Frame(self.frm_calendar, bootstyle=SECONDARY)
self.frm_header.pack(fill=X)
# setup the toplevel widget
self.root.withdraw() # reset the iconify state
self.frm_calendar.update_idletasks() # actualize geometry
# create visual components
self._draw_titlebar()
self._draw_calendar()
# make toplevel visible
self._set_window_position()
self.root.deiconify()
def _update_widget_bootstyle(self):
self.frm_title.configure(bootstyle=self.bootstyle)
self.title.configure(bootstyle=f"{self.bootstyle}-inverse")
self.prev_period.configure(style=f"Chevron.{self.bootstyle}.TButton")
self.next_period.configure(style=f"Chevron.{self.bootstyle}.TButton")
def _draw_calendar(self):
self._update_widget_bootstyle()
self._set_title()
self._current_month_days()
self.frm_dates = ttk.Frame(self.frm_calendar)
self.frm_dates.pack(fill=BOTH, expand=YES)
for row, weekday_list in enumerate(self.monthdays):
for col, day in enumerate(weekday_list):
self.frm_dates.columnconfigure(col, weight=1)
if day == 0:
ttk.Label(
master=self.frm_dates,
text=self.monthdates[row][col].day,
anchor=CENTER,
padding=5,
bootstyle=SECONDARY,
).grid(row=row, column=col, sticky=NSEW)
else:
if all(
[
day == self.date_selected.day,
self.date.month == self.date_selected.month,
self.date.year == self.date_selected.year,
]
):
day_style = "secondary-toolbutton"
else:
day_style = f"{self.bootstyle}-calendar"
def selected(x=row, y=col):
self._on_date_selected(x, y)
btn = ttk.Radiobutton(
master=self.frm_dates,
variable=self.datevar,
value=day,
text=day,
bootstyle=day_style,
padding=5,
command=selected,
)
btn.grid(row=row, column=col, sticky=NSEW)
def _draw_titlebar(self):
"""Draw the calendar title bar which includes the month title
and the buttons that increment and decrement the selected
month.
In addition to the previous and next MONTH commands that are
assigned to the button press, a "right-click" event is assigned
to each button that causes the calendar to move to the previous
and next YEAR.
"""
# create and pack the title and action buttons
self.prev_period = ttk.Button(
master=self.frm_title, text="«", command=self.on_prev_month
)
self.prev_period.pack(side=LEFT)
self.title = ttk.Label(
master=self.frm_title,
textvariable=self.titlevar,
anchor=CENTER,
font="-weight bold",
)
self.title.pack(side=LEFT, fill=X, expand=YES)
self.next_period = ttk.Button(
master=self.frm_title,
text="»",
command=self.on_next_month,
)
self.next_period.pack(side=LEFT)
# bind "year" callbacks to action buttons
self.prev_period.bind("<Button-3>", self.on_prev_year, "+")
self.next_period.bind("<Button-3>", self.on_next_year, "+")
self.title.bind("<Button-1>", self.on_reset_date)
# create and pack days of the week header
for col in self._header_columns():
ttk.Label(
master=self.frm_header,
text=col,
anchor=CENTER,
padding=5,
bootstyle=(SECONDARY, INVERSE),
).pack(side=LEFT, fill=X, expand=YES)
def _set_title(self):
_titledate = f'{self.date.strftime("%B %Y")}'
self.titlevar.set(value=_titledate.capitalize())
def _current_month_days(self):
"""Fetch the day numbers and dates for all days in the current
month. `monthdays` is a list of days as integers, and
`monthdates` is a list of `datetime` objects.
"""
self.monthdays = self.calendar.monthdayscalendar(
year=self.date.year, month=self.date.month
)
self.monthdates = self.calendar.monthdatescalendar(
year=self.date.year, month=self.date.month
)
def _header_columns(self):
"""Create and return a list of weekdays to be used as a header
in the calendar. The order of the weekdays is based on the
`firstweekday` property.
Returns:
List[str]:
A list of weekday column names for the calendar header.
"""
weekdays = [
MessageCatalog.translate("Mo"),
MessageCatalog.translate("Tu"),
MessageCatalog.translate("We"),
MessageCatalog.translate("Th"),
MessageCatalog.translate("Fr"),
MessageCatalog.translate("Sa"),
MessageCatalog.translate("Su"),
]
header = weekdays[self.firstweekday :] + weekdays[: self.firstweekday]
return header
def _on_date_selected(self, row, col):
"""Callback for selecting a date.
An index is assigned to each date button that corresponds to
the dates in the `monthdates` matrix. When the user clicks a
button to select a date, the index from this button is used
to lookup the date value of the button based on the row and
column index reference. This value is saved in the
`date_selected` property and the `Toplevel` is destroyed.
Parameters:
index (Tuple[int, int]):
A row and column index of the date selected; to be
found in the `monthdates` matrix.
Returns:
datetime:
The date selected
"""
self.date_selected = self.monthdates[row][col]
self.root.destroy()
def _selection_callback(func):
"""Calls the decorated `func` and redraws the calendar."""
def inner(self, *args):
func(self, *args)
self.frm_dates.destroy()
self._draw_calendar()
return inner
@_selection_callback
def on_next_month(self):
"""Increment the calendar data to the next month"""
year, month = self._nextmonth(self.date.year, self.date.month)
self.date = datetime(year=year, month=month, day=1).date()
@_selection_callback
def on_next_year(self, *_):
"""Increment the calendar data to the next year"""
year = self.date.year + 1
month = self.date.month
self.date = datetime(year=year, month=month, day=1).date()
@_selection_callback
def on_prev_month(self):
"""Decrement the calendar to the previous year"""
year, month = self._prevmonth(self.date.year, self.date.month)
self.date = datetime(year=year, month=month, day=1).date()
@_selection_callback
def on_prev_year(self, *_):
year = self.date.year - 1
month = self.date.month
self.date = datetime(year=year, month=month, day=1).date()
@_selection_callback
def on_reset_date(self, *_):
"""Set the calendar to the start date"""
self.date = self.startdate
def _set_window_position(self):
"""Move the window the to bottom-right of the parent widget, or
the top-left corner of the master window if no parent is
provided.
"""
if self.parent:
xpos = self.parent.winfo_rootx() + self.parent.winfo_width()
ypos = self.parent.winfo_rooty() + self.parent.winfo_height()
self.root.geometry(f"+{xpos}+{ypos}")
else:
xpos = self.root.master.winfo_rootx()
ypos = self.root.master.winfo_rooty()
self.root.geometry(f"+{xpos}+{ypos}")
@staticmethod
def _nextmonth(year, month):
if month == 12:
return year + 1, 1
else:
return year, month + 1
@staticmethod
def _prevmonth(year, month):
if month == 1:
return year - 1, 12
else:
return year, month - 1
class FontDialog(Dialog):
"""A dialog that displays a variety of options for choosing a font.
This dialog constructs and returns a `Font` object based on the
options selected by the user. The initial font is based on OS
settings and will vary.
The font object is returned when the **Ok** button is pressed and
can be passed to any widget that accepts a _font_ configuration
option.
![](../../assets/dialogs/querybox-get-font.png)
"""
def __init__(self, title="Font Selector", parent=None):
title = MessageCatalog.translate(title)
super().__init__(parent=parent, title=title)
self._style = ttk.Style()
self._default = font.nametofont("TkDefaultFont")
self._actual = self._default.actual()
self._size = ttk.Variable(value=self._actual["size"])
self._family = ttk.Variable(value=self._actual["family"])
self._slant = ttk.Variable(value=self._actual["slant"])
self._weight = ttk.Variable(value=self._actual["weight"])
self._overstrike = ttk.Variable(value=self._actual["overstrike"])
self._underline = ttk.Variable(value=self._actual["underline"])
self._preview_font = font.Font()
self._slant.trace_add("write", self._update_font_preview)
self._weight.trace_add("write", self._update_font_preview)
self._overstrike.trace_add("write", self._update_font_preview)
self._underline.trace_add("write", self._update_font_preview)
_headingfont = font.nametofont("TkHeadingFont")
_headingfont.configure(weight="bold")
self._update_font_preview()
self._families = set([self._family.get()])
for f in font.families():
if all([f, not f.startswith("@"), "emoji" not in f.lower()]):
self._families.add(f)
def create_body(self, master):
width = utility.scale_size(master, 600)
height = utility.scale_size(master, 500)
self._toplevel.geometry(f"{width}x{height}")
family_size_frame = ttk.Frame(master, padding=10)
family_size_frame.pack(fill=X, anchor=N)
self._initial_focus = self._font_families_selector(family_size_frame)
self._font_size_selector(family_size_frame)
self._font_options_selectors(master, padding=10)
self._font_preview(master, padding=10)
def create_buttonbox(self, master):
container = ttk.Frame(master, padding=(5, 10))
container.pack(fill=X)
ok_btn = ttk.Button(
master=container,
bootstyle="primary",
text=MessageCatalog.translate("OK"),
command=self._on_submit,
)
ok_btn.pack(side=RIGHT, padx=5)
ok_btn.bind("<Return>", lambda _: ok_btn.invoke())
cancel_btn = ttk.Button(
master=container,
bootstyle="secondary",
text=MessageCatalog.translate("Cancel"),
command=self._on_cancel,
)
cancel_btn.pack(side=RIGHT, padx=5)
cancel_btn.bind("<Return>", lambda _: cancel_btn.invoke())
def _font_families_selector(self, master):
container = ttk.Frame(master)
container.pack(fill=BOTH, expand=YES, side=LEFT)
header = ttk.Label(
container,
text=MessageCatalog.translate("Family"),
font="TkHeadingFont",
)
header.pack(fill=X, pady=(0, 2), anchor=N)
listbox = ttk.Treeview(
master=container,
height=5,
show="",
columns=[0],
)
listbox.column(0, width=utility.scale_size(listbox, 250))
listbox.pack(side=LEFT, fill=BOTH, expand=YES)
listbox_vbar = ttk.Scrollbar(
container,
command=listbox.yview,
orient=VERTICAL,
bootstyle="rounded",
)
listbox_vbar.pack(side=RIGHT, fill=Y)
listbox.configure(yscrollcommand=listbox_vbar.set)
for f in self._families:
listbox.insert("", iid=f, index=END, tags=[f], values=[f])
listbox.tag_configure(f, font=(f, self._size.get()))
iid = self._family.get()
listbox.selection_set(iid) # select default value
listbox.see(iid) # ensure default is visible
listbox.bind(
"<<TreeviewSelect>>", lambda e: self._on_select_font_family(e)
)
return listbox
def _font_size_selector(self, master):
container = ttk.Frame(master)
container.pack(side=LEFT, fill=Y, padx=(10, 0))
header = ttk.Label(
container,
text=MessageCatalog.translate("Size"),
font="TkHeadingFont",
)