simba-uw-tf-dev 4.7.8__py3-none-any.whl → 4.7.9__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.

Potentially problematic release.


This version of simba-uw-tf-dev might be problematic. Click here for more details.

@@ -1,1091 +1,1091 @@
1
- __author__ = "Simon Nilsson; sronilsson@gmail.com"
2
-
3
- import os.path
4
- import platform
5
- import threading
6
- import tkinter
7
- import tkinter as tk
8
- import webbrowser
9
- from copy import copy, deepcopy
10
- from tkinter import *
11
- from tkinter.filedialog import askdirectory, askopenfilename
12
- from tkinter.ttk import Combobox
13
- from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
14
-
15
- try:
16
- from typing import Literal
17
- except:
18
- from typing_extensions import Literal
19
-
20
- import numpy as np
21
- import PIL.Image
22
- from PIL import ImageTk
23
-
24
- from simba.utils.enums import Defaults, Formats, TkBinds
25
- from simba.utils.lookups import get_icons_paths, get_tooltips
26
- from simba.utils.read_write import get_fn_ext
27
- from simba.utils.checks import check_if_valid_img
28
-
29
- MENU_ICONS = get_icons_paths()
30
- TOOLTIPS = get_tooltips()
31
-
32
- def onMousewheel(event, canvas):
33
- try:
34
- scrollSpeed = event.delta
35
- if platform.system() == "Darwin":
36
- scrollSpeed = event.delta
37
- elif platform.system() == "Windows":
38
- scrollSpeed = int(event.delta / 120)
39
- canvas.yview_scroll(-1 * (scrollSpeed), "units")
40
- except:
41
- pass
42
-
43
-
44
- def bindToMousewheel(event, canvas):
45
- canvas.bind_all("<MouseWheel>", lambda event: onMousewheel(event, canvas))
46
-
47
-
48
- def unbindToMousewheel(event, canvas):
49
- canvas.unbind_all("<MouseWheel>")
50
-
51
-
52
- def onFrameConfigure(canvas):
53
- """Reset the scroll region to encompass the inner frame"""
54
- canvas.configure(scrollregion=canvas.bbox("all"))
55
-
56
-
57
- def on_mouse_scroll(event, canvas):
58
- if event.delta:
59
- canvas.yview_scroll(-1 * (event.delta // 120), "units")
60
- elif event.num == 4:
61
- canvas.yview_scroll(-1, "units")
62
- elif event.num == 5:
63
- canvas.yview_scroll(1, "units")
64
-
65
- def hxtScrollbar(master):
66
- canvas = tk.Canvas(master, borderwidth=0, bg="#f0f0f0", relief="flat")
67
- frame = tk.Frame(canvas, bg="#f0f0f0", bd=0)
68
-
69
- vsb = tk.Scrollbar(master, orient="vertical", command=canvas.yview, bg="#cccccc", width=20)
70
- hsb = tk.Scrollbar(master, orient="horizontal", command=canvas.xview, bg="#cccccc")
71
-
72
- canvas.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
73
-
74
- canvas.pack(side="left", fill="both", expand=True, padx=0, pady=0)
75
- vsb.pack(side="right", fill="y", padx=(0, 0))
76
- hsb.pack(side="bottom", fill="x", pady=(0, 0))
77
-
78
- canvas.create_window((0, 0), window=frame, anchor="nw")
79
-
80
- def on_frame_configure(event):
81
- canvas.configure(scrollregion=canvas.bbox("all"))
82
-
83
- frame.bind("<Configure>", on_frame_configure)
84
-
85
- def _on_mousewheel(event):
86
- canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
87
-
88
- def _on_key(event):
89
- if event.keysym == "Up":
90
- canvas.yview_scroll(-1, "units")
91
- elif event.keysym == "Down":
92
- canvas.yview_scroll(1, "units")
93
- elif event.keysym == "Page_Up":
94
- canvas.yview_scroll(-1, "pages")
95
- elif event.keysym == "Page_Down":
96
- canvas.yview_scroll(1, "pages")
97
-
98
- # Focus & bindings when mouse enters
99
- def _on_enter(_):
100
- frame.focus_set()
101
- frame.bind_all("<MouseWheel>", _on_mousewheel)
102
- frame.bind_all("<Key>", _on_key)
103
-
104
- # Clean up when mouse leaves
105
- # def _on_leave(_):
106
- # frame.unbind_all("<MouseWheel>")
107
- # frame.unbind_all("<Key>")
108
-
109
- frame.bind("<Enter>", _on_enter)
110
- #frame.bind("<Leave>", _on_leave)
111
-
112
- return frame
113
-
114
- def form_validator_is_numeric(inStr, acttyp):
115
- if acttyp == "1": # insert
116
- if not inStr.isdigit():
117
- return False
118
- return True
119
-
120
-
121
- class DropDownMenu(Frame):
122
- """
123
- Legacy, use :func:`simba.ui.tkinter_functions.SimBADropDown`.
124
- """
125
- def __init__(self,
126
- parent=None,
127
- dropdownLabel="",
128
- choice_dict=None,
129
- labelwidth="",
130
- com=None,
131
- val: Optional[Any] = None,
132
- **kw):
133
-
134
- Frame.__init__(self, master=parent, **kw)
135
- self.dropdownvar = StringVar()
136
- self.lblName = Label(self, text=dropdownLabel, width=labelwidth, anchor=W, font=Formats.FONT_REGULAR.value)
137
- self.lblName.grid(row=0, column=0)
138
- self.choices = choice_dict
139
- self.popupMenu = OptionMenu(self, self.dropdownvar, *self.choices, command=com)
140
- self.popupMenu.grid(row=0, column=1)
141
- if val is not None:
142
- self.setChoices(val)
143
-
144
- def getChoices(self):
145
- return self.dropdownvar.get()
146
-
147
- def setChoices(self, choice):
148
- self.dropdownvar.set(choice)
149
-
150
- def enable(self):
151
- self.popupMenu.configure(state="normal")
152
-
153
- def disable(self):
154
- self.popupMenu.configure(state="disable")
155
-
156
-
157
- class SimBAScaleBar(Frame):
158
- def __init__(self,
159
- parent: Union[Frame, Canvas, LabelFrame, Toplevel, Tk],
160
- label: Optional[str] = None,
161
- label_width: Optional[int] = None,
162
- orient: Literal['horizontal', 'vertical'] = HORIZONTAL,
163
- length: int = 200,
164
- value: Optional[int] = 95,
165
- showvalue: bool = True,
166
- label_clr: str = 'black',
167
- lbl_font: tuple = Formats.FONT_REGULAR.value,
168
- scale_font: tuple = Formats.FONT_REGULAR_ITALICS.value,
169
- lbl_img: Optional[str] = None,
170
- from_: int = -1,
171
- resolution: int = 1,
172
- to: int = 100,
173
- tickinterval: Optional[int] = None,
174
- troughcolor: Optional[str] = None,
175
- activebackground: Optional[str] = None,
176
- sliderrelief: Literal["raised", "sunken", "flat", "ridge", "solid", "groove"] = 'flat'):
177
-
178
- super().__init__(master=parent)
179
- self.columnconfigure(0, weight=0)
180
- self.columnconfigure(1, weight=0)
181
- if lbl_img is not None:
182
- self.columnconfigure(2, weight=0)
183
- self.lbl_lbl = SimBALabel(parent=self, txt='', txt_clr='black', bg_clr=None, font=lbl_font, width=None, anchor='w', img=lbl_img, compound=None)
184
- self.lbl_lbl.grid(row=0, column=0, sticky=SW)
185
- else:
186
- self.lbl_lbl = None
187
-
188
- self.scale = Scale(self,
189
- from_=from_,
190
- to=to,
191
- orient=orient,
192
- length=length,
193
- font=scale_font,
194
- sliderrelief=sliderrelief,
195
- troughcolor=troughcolor,
196
- tickinterval=tickinterval,
197
- resolution=resolution,
198
- showvalue=showvalue,
199
- activebackground=activebackground)
200
-
201
- if label is not None:
202
- self.lbl = SimBALabel(parent=self, txt=label, font=lbl_font, txt_clr=label_clr, width=label_width)
203
- self.lbl.grid(row=0, column=1, sticky=SW)
204
-
205
- self.scale.grid(row=0, column=2, sticky=NW)
206
- if value is not None:
207
- self.set_value(value=value)
208
-
209
- def set_value(self, value: int):
210
- self.scale.set(value)
211
-
212
- def get_value(self) -> Union[int, float]:
213
- return self.scale.get()
214
-
215
- def get(self) -> Union[int, float]:
216
- ## Alternative for ``get_value`` for legacy reasons.
217
- return self.scale.get()
218
-
219
-
220
-
221
-
222
- class Entry_Box(Frame):
223
- def __init__(self,
224
- parent: Union[Frame, Canvas, LabelFrame, Toplevel, Tk],
225
- fileDescription: Optional[str] = "",
226
- labelwidth: Union[int, str] = None,
227
- label_bg_clr: Optional[str] = None,
228
- status: Literal["normal", "disabled", "readonly"] = 'normal',
229
- validation: Optional[Union[Callable, str]] = None,
230
- entry_box_width: Union[int, str] = None,
231
- entry_box_clr: Optional[str] = None,
232
- img: Optional[str] = None,
233
- value: Optional[Any] = None,
234
- label_font: tuple = Formats.FONT_REGULAR.value,
235
- entry_font: tuple = Formats.FONT_REGULAR.value,
236
- tooltip_key: Optional[str] = None,
237
- justify: Literal["left", "center", "right"] = 'left',
238
- cmd: Optional[Callable] = None,
239
- **kw):
240
-
241
- super(Entry_Box, self).__init__(master=parent)
242
- self.validation_methods = {"numeric": (self.register(form_validator_is_numeric), "%P", "%d")}
243
- self.status = status if status is not None else NORMAL
244
- self.labelname = fileDescription
245
- Frame.__init__(self, master=parent, **kw)
246
- self.columnconfigure(0, weight=0)
247
- self.columnconfigure(1, weight=0)
248
- if img is not None:
249
- self.columnconfigure(2, weight=0)
250
- self.entrybox_img_lbl = SimBALabel(parent=self, txt='', txt_clr='black', bg_clr=label_bg_clr, font=label_font, width=None, anchor='w', img=img, compound=None)
251
- self.entrybox_img_lbl.grid(row=0, column=0, sticky="w")
252
- else:
253
- self.entrybox_img_lbl = None
254
- self.filePath = StringVar()
255
- self.lblName = Label(self, text=fileDescription, width=labelwidth, anchor=W, font=label_font, bg=label_bg_clr)
256
- self.lblName.grid(row=0, column=1)
257
- if tooltip_key in TOOLTIPS.keys():
258
- CreateToolTip(widget=self.lblName, text=TOOLTIPS[tooltip_key])
259
- if not entry_box_width:
260
- self.entPath = Entry(self, textvariable=self.filePath, state=self.status, validate="key", validatecommand=self.validation_methods.get(validation, None), font=entry_font, justify=justify, bg=entry_box_clr)
261
- else:
262
- self.entPath = Entry(self, textvariable=self.filePath, state=self.status, width=entry_box_width, validate="key", font=entry_font, justify=justify, validatecommand=self.validation_methods.get(validation, None), bg=entry_box_clr)
263
- self.entPath.grid(row=0, column=2)
264
- if value is not None:
265
- self.entry_set(val=value)
266
- if cmd is not None:
267
- self.bind_combobox_keys()
268
- self.cmd = cmd
269
-
270
- def bind_combobox_keys(self):
271
- self.entPath.bind("<KeyRelease>", self.run_cmd)
272
-
273
- @property
274
- def entry_get(self):
275
- return self.entPath.get()
276
-
277
- def entry_set(self, val):
278
- self.filePath.set(val)
279
-
280
- def set_state(self, setstatus):
281
- self.entPath.config(state=setstatus)
282
-
283
- def destroy(self):
284
- try:
285
- if self.entrybox_img_lbl is not None:
286
- self.entrybox_img_lbl.destroy()
287
- self.lblName.destroy()
288
- self.entPath.destroy()
289
- except:
290
- pass
291
-
292
- def run_cmd(self, x):
293
- self.cmd(self.entry_get.strip())
294
-
295
-
296
-
297
- class FolderSelect(Frame):
298
- def __init__(self,
299
- parent: Union[Frame, LabelFrame, Canvas, Toplevel],
300
- folderDescription: Optional[str] = "",
301
- color: Optional[str] = None,
302
- label_bg_clr: Optional[str] = None,
303
- font: Tuple = Formats.FONT_REGULAR.value,
304
- title: Optional[str] = "",
305
- lblwidth: Optional[int] = 0,
306
- bg_clr: Optional[str] = 'white',
307
- entry_width: Optional[int] = 20,
308
- lbl_icon: Optional[str] = None,
309
- initialdir: Optional[Union[str, os.PathLike]] = None,
310
- tooltip_txt: Optional[str] = None,
311
- tooltip_key: Optional[str] = None,
312
- **kw):
313
-
314
- self.title, self.initialdir = title, initialdir
315
- self.color = color if color is not None else "black"
316
- self.lblwidth = lblwidth if lblwidth is not None else 0
317
- self.parent = parent
318
- Frame.__init__(self, master=parent, **kw)
319
- browse_icon = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS["browse_small"]["icon_path"]))
320
- self.columnconfigure(0, weight=0)
321
- self.columnconfigure(1, weight=0)
322
- self.columnconfigure(2, weight=0)
323
- if lbl_icon is not None:
324
- self.columnconfigure(3, weight=0)
325
- self.lbl_icon = SimBALabel(parent=self, txt='', txt_clr='black', bg_clr=label_bg_clr, font=font, width=None, anchor='w', img=lbl_icon, compound=None)
326
- self.lbl_icon.grid(row=0, column=0, sticky="w")
327
- else:
328
- self.lbl_icon = None
329
- self.folderPath = StringVar()
330
- self.lblName = Label(self, text=folderDescription, fg=str(self.color), width=str(self.lblwidth), anchor=W, font=font, bg=label_bg_clr)
331
- self.lblName.grid(row=0, column=1, sticky=NW)
332
- self.entPath = Label(self, textvariable=self.folderPath, relief=SUNKEN, font=font, bg=bg_clr, width=entry_width)
333
- self.entPath.grid(row=0, column=2, sticky=NW)
334
- self.btnFind = SimbaButton(parent=self, txt=Defaults.BROWSE_FOLDER_BTN_TEXT.value, font=font, cmd=self.setFolderPath, img=browse_icon)
335
- self.btnFind.image = browse_icon
336
- self.btnFind.grid(row=0, column=3, sticky=NW)
337
- self.folderPath.set("No folder selected")
338
- if tooltip_txt is not None and isinstance(tooltip_txt, str):
339
- CreateToolTip(widget=self.lblName, text=tooltip_txt)
340
- elif tooltip_key in TOOLTIPS.keys():
341
- CreateToolTip(widget=self.lblName, text=TOOLTIPS[tooltip_key])
342
-
343
- def setFolderPath(self):
344
- if self.initialdir is not None:
345
- if not os.path.isdir(self.initialdir):
346
- self.initialdir = None
347
- else:
348
- pass
349
- folder_selected = askdirectory(title=str(self.title), parent=self.parent, initialdir=self.initialdir)
350
- if folder_selected:
351
- self.folderPath.set(folder_selected)
352
- self.entPath.configure(width=len(folder_selected)+10)
353
- else:
354
- self.folderPath.set("No folder selected")
355
- self.entPath.configure(width=len("No folder selected")+10)
356
-
357
- @property
358
- def folder_path(self):
359
- return self.folderPath.get()
360
-
361
-
362
- class ToolTip(object):
363
-
364
- def __init__(self, widget):
365
- self.widget = widget
366
- self.tipwindow = None
367
- self.id = None
368
- self.x = self.y = 0
369
-
370
- def showtip(self, text):
371
- "Display text in tooltip window"
372
- self.text = text
373
- if self.tipwindow or not self.text:
374
- return
375
- x, y, cx, cy = self.widget.bbox("insert")
376
- x = x + self.widget.winfo_rootx() + 20
377
- y = y + cy + self.widget.winfo_rooty() + 20
378
- self.tipwindow = tw = Toplevel(self.widget)
379
- tw.wm_overrideredirect(1)
380
- tw.wm_geometry("+%d+%d" % (x, y))
381
- label = Label(tw, text=self.text, justify=LEFT, background="#ffffe0", relief=SOLID, borderwidth=1, font=Formats.FONT_REGULAR.value)
382
- label.pack(ipadx=1)
383
-
384
- def hidetip(self):
385
- tw = self.tipwindow
386
- self.tipwindow = None
387
- if tw:
388
- tw.destroy()
389
-
390
-
391
- def CreateToolTip(widget, text):
392
- toolTip = ToolTip(widget)
393
- def enter(event):
394
- toolTip.showtip(text)
395
-
396
- def leave(event):
397
- toolTip.hidetip()
398
-
399
- widget.bind("<Enter>", enter)
400
- widget.bind("<Leave>", leave)
401
-
402
-
403
- def CreateLabelFrameWithIcon(parent: Union[Toplevel, LabelFrame, Canvas, Frame],
404
- header: str,
405
- icon_name: str,
406
- padx: Optional[int] = None,
407
- pady: Optional[int] = None,
408
- relief: str = 'solid',
409
- width: Optional[int] = None,
410
- bg: Optional[str] = None,
411
- font: tuple = Formats.FONT_HEADER.value,
412
- icon_link: Optional[Union[str, None]] = LabelFrame,
413
- tooltip_key: Optional[str] = None):
414
-
415
- if icon_name in MENU_ICONS.keys():
416
- icon = PIL.Image.open(MENU_ICONS[icon_name]["icon_path"])
417
- icon = ImageTk.PhotoImage(icon)
418
- else:
419
- icon = None
420
- frm = Frame(parent, bg=bg)
421
-
422
- label_image = Label(frm, image=icon, bg=bg)
423
- label_image.image = icon
424
- if icon_link:
425
- label_image.bind("<Button-1>", lambda e: callback(icon_link))
426
- label_image.grid(row=0, column=0)
427
-
428
-
429
- label_text = Label(frm, text=header, font=font, bg=bg)
430
- label_text.grid(row=0, column=1)
431
-
432
-
433
-
434
- lbl_frm = LabelFrame(parent, labelwidget=frm, relief=relief, width=width, padx=padx or 0, pady=pady or 0)
435
-
436
- if tooltip_key in TOOLTIPS.keys():
437
- CreateToolTip(widget=label_text, text=TOOLTIPS[tooltip_key])
438
-
439
- if bg is not None:
440
- lbl_frm.configure(bg=bg)
441
- return lbl_frm
442
-
443
- def callback(url):
444
- webbrowser.open_new(url)
445
-
446
-
447
- def create_scalebar(
448
- parent: Frame, name: str, min: int, max: int, cmd: object or None = None
449
- ):
450
-
451
- scale = Scale(
452
- parent,
453
- from_=min,
454
- to=max,
455
- orient=HORIZONTAL,
456
- length=200,
457
- label=name,
458
- command=cmd,
459
- )
460
- scale.set(0)
461
- return scale
462
-
463
-
464
- class TwoOptionQuestionPopUp(object):
465
- """
466
- Helpe to create a two-option question tkinter pop up window (e.g., YES/NO).
467
-
468
- :parameter str question: Question to present to the user. E.g., ``DO YOU WANT TO PROCEED?``.
469
- :parameter str option_one: The first user option. E.g., ``YES``.
470
- :parameter str option_one: The second user option. E.g., ``NO``.
471
- :parameter str title: The window titel in the top banner of the pop-up. E.g., ``A QUESTION FOR YOU!``.
472
- :parameter Optional[str] link: If not None, then a link to documentation presenting background info about the user choices.
473
- """
474
-
475
- def __init__(self,
476
- question: str,
477
- option_one: str,
478
- option_two: str,
479
- title: str,
480
- link: Optional[str] = None):
481
-
482
- self.main_frm = Toplevel()
483
- self.main_frm.geometry("600x200")
484
- self.main_frm.title(title)
485
- menu_icons = get_menu_icons()
486
- self.main_frm.iconphoto(False, menu_icons['question_mark']["img"])
487
-
488
- #img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS['question_mark']["icon_path"]))
489
-
490
- question_frm = Frame(self.main_frm)
491
- question_frm.pack(expand=True, fill="both")
492
- Label(question_frm, text=question, font=Formats.LABELFRAME_HEADER_FORMAT.value).pack()
493
-
494
- button_one = SimbaButton(parent=question_frm, txt=option_one, txt_clr="blue", bg_clr="lightgrey", img='check_blue', cmd=self.run, cmd_kwargs={'selected_option': lambda: option_one}, font=Formats.FONT_LARGE.value, hover_font=Formats.FONT_LARGE_BOLD.value)
495
- button_two = SimbaButton(parent=question_frm, txt=option_two, txt_clr="red", bg_clr="lightgrey", img='close', cmd=self.run, cmd_kwargs={'selected_option': lambda: option_two}, font=Formats.FONT_LARGE.value, hover_font=Formats.FONT_LARGE_BOLD.value)
496
- if link:
497
- link_lbl = Label(question_frm, text="Click here for more information.", cursor="hand2", fg="blue")
498
- link_lbl.bind("<Button-1>", lambda e: callback(link))
499
- link_lbl.place(relx=0.5, rely=0.30, anchor=CENTER)
500
- button_one.place(relx=0.5, rely=0.50, anchor=CENTER)
501
- button_two.place(relx=0.5, rely=0.80, anchor=CENTER)
502
- self.main_frm.wait_window()
503
-
504
- def run(self, selected_option):
505
- self.selected_option = selected_option
506
- self.main_frm.destroy()
507
-
508
- def SimbaButton(parent: Union[Frame, Canvas, LabelFrame, Toplevel],
509
- txt: str,
510
- txt_clr: Optional[str] = 'black',
511
- bg_clr: Optional[str] = '#f0f0f0',
512
- active_bg_clr: Optional[str] = None,
513
- hover_bg_clr: Optional[str] = Formats.BTN_HOVER_CLR.value,
514
- hover_font: Optional[Tuple] = Formats.FONT_REGULAR_BOLD.value,
515
- font: Optional[Tuple] = Formats.FONT_REGULAR.value,
516
- width: Optional[int] = None,
517
- height: Optional[int] = None,
518
- compound: Optional[str] = 'left',
519
- img: Optional[Union[ImageTk.PhotoImage, str]] = None,
520
- cmd: Optional[Callable] = None,
521
- cmd_kwargs: Optional[Dict[Any, Any]] = None,
522
- enabled: Optional[bool] = True,
523
- anchor: str = 'w',
524
- thread: Optional[bool] = False,
525
- tooltip_txt: Optional[str] = None) -> Button:
526
- def on_enter(e):
527
- e.widget.config(bg=hover_bg_clr, font=hover_font)
528
-
529
- def on_leave(e):
530
- e.widget.config(bg=bg_clr, font=font)
531
-
532
- # if hover_font != font:
533
- # hover_font = copy(font)
534
-
535
-
536
-
537
- if isinstance(img, str):
538
- img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS[img]["icon_path"]))
539
-
540
- if cmd_kwargs is None:
541
- cmd_kwargs = {}
542
-
543
- def execute_command():
544
- if cmd:
545
- evaluated_kwargs = {k: (v() if callable(v) else v) for k, v in cmd_kwargs.items()}
546
- cmd(**evaluated_kwargs)
547
-
548
- if cmd is not None:
549
- if thread:
550
- command = lambda: threading.Thread(target=execute_command).start()
551
- else:
552
- command = execute_command
553
- else:
554
- command = None
555
-
556
- btn = Button(master=parent,
557
- text=txt,
558
- compound=compound,
559
- image=img,
560
- relief=RAISED,
561
- fg=txt_clr,
562
- activebackground=active_bg_clr,
563
- font=font,
564
- bg=bg_clr,
565
- anchor=anchor,
566
- command=command)
567
-
568
- if img is not None:
569
- btn.image = img
570
- if width is not None:
571
- btn.config(width=width)
572
- if height is not None:
573
- btn.config(height=height)
574
- if not enabled:
575
- btn.config(state=DISABLED)
576
-
577
- if hover_bg_clr is not None:
578
- btn.bind("<Enter>", on_enter)
579
- btn.bind("<Leave>", on_leave)
580
-
581
- if tooltip_txt is not None:
582
- CreateToolTip(widget=btn, text=tooltip_txt)
583
-
584
- return btn
585
-
586
-
587
- def SimbaCheckbox(parent: Union[Frame, Toplevel, LabelFrame, Canvas],
588
- txt: str,
589
- txt_clr: Optional[str] = 'black',
590
- bg_clr: Optional[str] = None,
591
- txt_img: Optional[str] = None,
592
- txt_img_location: Literal['left', 'right', 'top', 'bottom'] = RIGHT,
593
- font: Optional[Tuple[str, str, int]] = Formats.FONT_REGULAR.value,
594
- val: Optional[bool] = False,
595
- state: Literal["disabled", 'normal'] = NORMAL,
596
- indicatoron: bool = True,
597
- cmd: Optional[Callable] = None,
598
- tooltip_txt: Optional[str] = None,
599
- tooltip_key: Optional[str] = None):
600
-
601
- var = BooleanVar(value=False)
602
- if val: var.set(True)
603
- if isinstance(txt_img, str):
604
- txt_img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS[txt_img]["icon_path"]))
605
- if cmd is None:
606
- cb = Checkbutton(master=parent, font=font, fg=txt_clr, image=txt_img, text=txt, compound=txt_img_location, variable=var, indicatoron=indicatoron, bg=bg_clr)
607
- else:
608
- cb = Checkbutton(master=parent, font=font, fg=txt_clr, image=txt_img, text=txt, compound=txt_img_location, variable=var, command=cmd, indicatoron=indicatoron, bg=bg_clr)
609
- if txt_img is not None:
610
- cb.image = txt_img
611
- if state == DISABLED:
612
- cb.configure(state=DISABLED)
613
-
614
- if isinstance(tooltip_txt, str):
615
- CreateToolTip(widget=cb, text=tooltip_txt)
616
- elif isinstance(tooltip_key, str) and tooltip_key in TOOLTIPS.keys():
617
- CreateToolTip(widget=cb, text=TOOLTIPS[tooltip_key])
618
-
619
- return cb, var
620
-
621
- def SimBALabel(parent: Union[Frame, Canvas, LabelFrame, Toplevel],
622
- txt: str,
623
- txt_clr: str = 'black',
624
- bg_clr: Optional[str] = None,
625
- hover_fg_clr: Optional[str] = None,
626
- font: tuple = Formats.FONT_REGULAR.value,
627
- hover_font: Optional[Tuple] = None,
628
- relief: str = FLAT,
629
- compound: Optional[Literal['left', 'right', 'top', 'bottom', 'center']] = 'left',
630
- justify: Optional[str] = None,
631
- link: Optional[str] = None,
632
- width: Optional[int] = None,
633
- padx: Optional[int] = None,
634
- pady: Optional[int] = None,
635
- cursor: Optional[str] = None,
636
- img: Optional[str] = None,
637
- anchor: Optional[str] = None,
638
- tooltip_key: Optional[str] = None,
639
- hover_img: Optional[np.ndarray] = None):
640
-
641
-
642
- def _hover_enter(e):
643
- w = e.widget
644
- if hover_img is not None and check_if_valid_img(data=hover_img, raise_error=False):
645
- arr = np.asarray(hover_img, dtype=np.uint8)
646
- if arr.ndim == 3 and arr.shape[2] == 3:
647
- arr = arr[:, :, ::-1]
648
- pil_img = PIL.Image.fromarray(arr)
649
- photo = ImageTk.PhotoImage(pil_img)
650
- tw = Toplevel(w)
651
- tw.wm_overrideredirect(True)
652
- tw.attributes("-topmost", True)
653
- tw.wm_geometry("+%d+%d" % (w.winfo_rootx() + w.winfo_width() + 4, w.winfo_rooty()))
654
- frm = Frame(tw, relief="solid", bd=2, bg="#f0f0f0")
655
- frm.pack(padx=2, pady=2)
656
- lbl_hover = Label(frm, image=photo, bg="#f0f0f0")
657
- lbl_hover.image = photo
658
- lbl_hover.pack(padx=4, pady=(4, 2))
659
- caption_lbl = Label(frm, text=txt, font=font, fg=txt_clr, bg=bg_clr)
660
- caption_lbl.pack(pady=(0, 4))
661
- tw.lift()
662
- w._hover_toplevel = tw
663
- elif hover_fg_clr is not None or hover_font is not None:
664
- w.config(fg=hover_fg_clr, font=hover_font)
665
-
666
- def _hover_leave(e):
667
- w = e.widget
668
- if getattr(w, "_hover_toplevel", None) is not None:
669
- try:
670
- w._hover_toplevel.destroy()
671
- except tkinter.TclError:
672
- pass
673
- w._hover_toplevel = None
674
- if hover_fg_clr is not None or hover_font is not None:
675
- w.config(fg=txt_clr, bg=bg_clr, font=font)
676
-
677
- def on_enter(e):
678
- if hover_img is not None:
679
- _hover_enter(e)
680
- else:
681
- e.widget.config(fg=hover_fg_clr, font=hover_font)
682
-
683
- def on_leave(e):
684
- if hover_img is not None:
685
- _hover_leave(e)
686
- else:
687
- e.widget.config(fg=txt_clr, bg=bg_clr, font=font)
688
-
689
- anchor = 'w' if anchor is None else anchor
690
- if isinstance(img, str) and img in MENU_ICONS.keys():
691
- img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS[img]["icon_path"]))
692
- else:
693
- img = None
694
- if img is not None and compound is None:
695
- compound = 'left'
696
-
697
- lbl = Label(parent,
698
- text=txt,
699
- font=font,
700
- fg=txt_clr,
701
- bg=bg_clr,
702
- justify=justify,
703
- relief=relief,
704
- cursor=cursor,
705
- anchor=anchor,
706
- image=img,
707
- compound=compound,
708
- padx=padx,
709
- pady=pady)
710
-
711
- if img is not None:
712
- lbl.image = img
713
-
714
- if width is not None:
715
- lbl.configure(width=width)
716
-
717
- if link is not None:
718
- lbl.bind("<Button-1>", lambda e: callback(link))
719
-
720
- elif tooltip_key in TOOLTIPS.keys():
721
- CreateToolTip(widget=lbl, text=TOOLTIPS[tooltip_key])
722
-
723
- if hover_font is not None or hover_fg_clr is not None or hover_img is not None:
724
- lbl.bind(TkBinds.ENTER.value, on_enter)
725
- lbl.bind(TkBinds.LEAVE.value, on_leave)
726
-
727
- return lbl
728
-
729
-
730
- def get_menu_icons():
731
- menu_icons = copy(MENU_ICONS)
732
- for k in menu_icons.keys():
733
- menu_icons[k]["img"] = ImageTk.PhotoImage(image=PIL.Image.open(os.path.join(os.path.dirname(__file__), menu_icons[k]["icon_path"])))
734
- return menu_icons
735
-
736
-
737
-
738
- class SimBASeperator(Frame):
739
-
740
- def __init__(self,
741
- parent: Union[Frame, Canvas, LabelFrame, Toplevel, Tk],
742
- color: Optional[str] = 'black',
743
- orient: Literal['horizontal', 'vertical'] = 'horizontal',
744
- cursor: Optional[str] = None,
745
- borderwidth: Optional[int] = None,
746
- takefocus: Optional[int] = 0,
747
- height: int = 1,
748
- relief: Literal["raised", "sunken", "flat", "ridge", "solid", "groove"] = "flat"):
749
-
750
- super().__init__(master=parent, height=height, bg=color if color is not None else "black", relief=relief, bd=borderwidth if borderwidth is not None else None)
751
- style = tk.ttk.Style()
752
- style_name = "SimBA.TSeparator"
753
-
754
- style.configure(style_name,
755
- background=color if color is not None else "black")
756
-
757
- seperator = tk.ttk.Separator(self,
758
- orient=orient,
759
- style=style_name,
760
- cursor=cursor,
761
- takefocus=takefocus)
762
-
763
- if orient == "horizontal":
764
- seperator.pack(fill="x", expand=True)
765
- else:
766
- seperator.pack(fill="y", expand=True)
767
-
768
-
769
- class SimBADropDown(Frame):
770
- """
771
- Create a dropdown menu widget with optional searchable functionality.
772
-
773
- This class creates a ttk.Combobox dropdown menu with a label, supporting both readonly and searchable modes.
774
- When searchable mode is enabled, users can type to filter the dropdown options.
775
-
776
- :param parent (Frame | Canvas | LabelFrame | Toplevel | Tk): The parent widget container.
777
- :param dropdown_options (Iterable[Any] | List[Any] | Tuple[Any]): List of options to display in the dropdown menu.
778
- :param label (str, optional): Text label displayed next to the dropdown. Default: None.
779
- :param label_width (int, optional): Width of the label in characters. Default: None.
780
- :param label_font (tuple, optional): Font tuple for the label. Default: Formats.FONT_REGULAR.value.
781
- :param label_bg_clr (str, optional): Background color for the label. Default: None.
782
- :param dropdown_font_size (int, optional): Font size for the dropdown text. Default: None.
783
- :param justify (str): Text justification in the dropdown ('left', 'center', 'right'). Default: 'center'.
784
- :param dropdown_width (int, optional): Width of the dropdown widget in characters. Default: None.
785
- :param command (Callable, optional): Callback function to execute when an option is selected. Default: None.
786
- :param value (Any, optional): Initial selected value for the dropdown. Default: None.
787
- :param state (str, optional): Initial state of the dropdown ('normal', 'disabled'). Default: None.
788
- :param searchable (bool): If True, allows typing to filter dropdown options. Default: False.
789
- :param tooltip_txt (str, optional): Tooltip text to display on hover. Default: None.
790
- :param tooltip_key (str, optional): Key for tooltip lookup in TOOLTIPS dictionary. For dictionary, see `simba.assets.lookups.tooptips.json`. Default: None.
791
-
792
- :example:
793
- >>> dropdown = SimBADropDown(parent=parent_frm, dropdown_options=['Option 1', 'Option 2', 'Option 3'], label='Select option:', searchable=True)
794
- >>> selected = dropdown.get_value()
795
- """
796
- def __init__(self,
797
- parent: Union[Frame, Canvas, LabelFrame, Toplevel, Tk],
798
- dropdown_options: Union[Iterable[Any], List[Any], Tuple[Any]],
799
- label: Optional[str] = None,
800
- label_width: Optional[int] = None,
801
- label_font: tuple = Formats.FONT_REGULAR.value,
802
- label_bg_clr: Optional[str] = None,
803
- dropdown_font_size: Optional[int] = None,
804
- justify: Literal['left', 'right', 'center'] = 'center',
805
- img: Optional[str] = None,
806
- dropdown_width: Optional[int] = None,
807
- command: Callable = None,
808
- value: Optional[Any] = None,
809
- state: Optional[str] = None,
810
- searchable: bool = False,
811
- tooltip_txt: Optional[str] = None,
812
- tooltip_key: Optional[str] = None):
813
-
814
-
815
- super().__init__(master=parent)
816
- self.dropdown_var = StringVar()
817
- self.columnconfigure(0, weight=0)
818
- self.columnconfigure(1, weight=0)
819
- if img is not None:
820
- self.columnconfigure(2, weight=0)
821
- self.dropdown_img_lbl = SimBALabel(parent=self, txt='', txt_clr='black', bg_clr=label_bg_clr, font=label_font, width=None, anchor='w', img=img, compound='left')
822
- self.dropdown_img_lbl.grid(row=0, column=0, sticky="w")
823
- else:
824
- self.dropdown_img_lbl = None
825
- self.dropdown_lbl = SimBALabel(parent=self, txt=label, txt_clr='black', bg_clr=label_bg_clr, font=label_font, width=label_width, anchor='w')
826
- self.dropdown_lbl.grid(row=0, column=1, sticky=NW)
827
- self.dropdown_options = dropdown_options
828
- self.original_options = deepcopy(self.dropdown_options)
829
- self.command, self.searchable = command, searchable
830
- if dropdown_font_size is None:
831
- drop_down_font = None
832
- else:
833
- drop_down_font = ("Poppins", dropdown_font_size)
834
- self.combobox_state = 'normal' if searchable else "readonly"
835
- self.dropdown = Combobox(self, textvariable=self.dropdown_var, font=drop_down_font, values=self.dropdown_options, state=self.combobox_state, width=dropdown_width, justify=justify)
836
- if searchable: self.bind_combobox_keys()
837
- self.dropdown.grid(row=0, column=2, sticky="w")
838
- if value is not None: self.set_value(value=value)
839
- if command is not None:
840
- self.command = command
841
- self.dropdown.bind("<<ComboboxSelected>>", self.on_select)
842
- if state == 'disabled':
843
- self.disable()
844
- if isinstance(tooltip_txt, str):
845
- CreateToolTip(widget=self.dropdown_lbl, text=tooltip_txt)
846
- elif tooltip_key in TOOLTIPS.keys():
847
- CreateToolTip(widget=self.dropdown_lbl, text=TOOLTIPS[tooltip_key])
848
- if img is not None:
849
- CreateToolTip(widget=self.dropdown_img_lbl, text=TOOLTIPS[tooltip_key])
850
-
851
-
852
- def set_value(self, value: Any):
853
- self.dropdown_var.set(value)
854
-
855
- def get_value(self):
856
- return self.dropdown_var.get()
857
-
858
- def enable(self):
859
- self.dropdown.configure(state="normal")
860
-
861
- def disable(self):
862
- self.dropdown.configure(state="disabled")
863
-
864
- def getChoices(self):
865
- return self.dropdown_var.get()
866
-
867
- def setChoices(self, choice):
868
- self.dropdown_var.set(choice)
869
-
870
- def on_select(self, event):
871
- selected_value = self.dropdown_var.get()
872
- self.command(selected_value)
873
- if self.searchable: self.dropdown['values'] = self.original_options
874
-
875
- def set_width(self, width: int):
876
- self.dropdown.configure(width=width)
877
-
878
- def change_options(self, values: List[str], set_index: Optional[int] = None, set_str: Optional[str] = None, auto_change_width: Optional[bool] = True):
879
- self.dropdown_var.set('')
880
- self.dropdown['values'] = values
881
- if isinstance(set_index, int) and (0 <= set_index <= len(values) - 1):
882
- self.dropdown_var.set(values[set_index])
883
- elif (set_str is not None) and (set_str in values):
884
- self.dropdown_var.set(set_str)
885
- elif self.searchable and (set_str is not None):
886
- self.dropdown_var.set(set_str)
887
- else:
888
- self.dropdown_var.set(values[0])
889
- if auto_change_width: self.set_width(width=max(5, max(len(s) for s in values)))
890
- if self.dropdown['values'] == ('',) or len(values) == 0 and not self.searchable:
891
- self.disable()
892
- else:
893
- self.enable()
894
- self.dropdown.configure(state='normal' if self.searchable else "readonly")
895
-
896
- def bind_combobox_keys(self):
897
- self.dropdown.bind("<KeyRelease>", self._on_key_release)
898
- if self.searchable:
899
- self.dropdown.bind("<Button-1>", lambda e: self._reset_to_all_options())
900
-
901
- def _on_key_release(self, event):
902
- if event.keysym in ['Up', 'Down', 'Left', 'Right', 'Tab', 'Return']:
903
- return
904
- try:
905
- cursor_pos = self.dropdown.index(INSERT)
906
- except:
907
- cursor_pos = len(self.dropdown_var.get())
908
- current_text = self.dropdown_var.get()
909
- filtered_options = [x for x in self.original_options if current_text.lower() in x.lower()]
910
- if current_text != '':
911
- self.dropdown['values'] = filtered_options
912
- # Restore the text and cursor position
913
- self.dropdown_var.set(current_text)
914
- self.dropdown.icursor(cursor_pos)
915
- else:
916
- self.dropdown['values'] = self.original_options
917
- self.dropdown_var.set('')
918
- self.dropdown.icursor(0)
919
-
920
- def _reset_to_all_options(self):
921
- if self.searchable and self.dropdown_var.get() == '':
922
- self.dropdown['values'] = self.original_options
923
-
924
- class DropDownMenu(Frame):
925
-
926
- """
927
- Legacy, use :func:`simba.ui.tkinter_functions.SimBADropDown`.
928
- """
929
- def __init__(self,
930
- parent=None,
931
- dropdownLabel="",
932
- choice_dict=None,
933
- labelwidth="",
934
- com=None,
935
- val: Optional[Any] = None,
936
- **kw):
937
- Frame.__init__(self, master=parent, **kw)
938
- self.dropdownvar = StringVar()
939
- self.lblName = Label(self, text=dropdownLabel, width=labelwidth, anchor=W, font=Formats.FONT_REGULAR.value)
940
- self.lblName.grid(row=0, column=0)
941
- self.choices = choice_dict
942
- self.popupMenu = OptionMenu(self, self.dropdownvar, *self.choices, command=com)
943
- self.popupMenu.grid(row=0, column=1)
944
- if val is not None:
945
- self.setChoices(val)
946
- def getChoices(self):
947
- return self.dropdownvar.get()
948
- def setChoices(self, choice):
949
- self.dropdownvar.set(choice)
950
- def enable(self):
951
- self.popupMenu.configure(state="normal")
952
- def disable(self):
953
- self.popupMenu.configure(state="disable")
954
-
955
-
956
-
957
- class FileSelect(Frame):
958
- def __init__(self,
959
- parent=None,
960
- fileDescription="",
961
- color=None,
962
- title: Optional[str] = None,
963
- lblwidth: Optional[int] = None,
964
- file_types=None,
965
- bg_clr: Optional[str] = 'white',
966
- dropdown: Union[DropDownMenu, SimBADropDown] = None,
967
- entry_width: Optional[int] = 20,
968
- status: Optional[str] = None,
969
- lbl_icon: Optional[str] = None,
970
- font: Tuple = Formats.FONT_REGULAR.value,
971
- initialdir: Optional[Union[str, os.PathLike]] = None,
972
- initial_path: Optional[Union[str, os.PathLike]] = None,
973
- tooltip_txt: Optional[str] = None,
974
- tooltip_key: Optional[str] = None,
975
- **kw):
976
-
977
- self.title, self.dropdown, self.initialdir = title, dropdown, initialdir
978
- self.file_type = file_types
979
- self.color = color if color is not None else "black"
980
- self.lblwidth = lblwidth if lblwidth is not None else 0
981
- self.parent = parent
982
- Frame.__init__(self, master=parent, **kw)
983
- browse_icon = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS["browse"]["icon_path"]))
984
- self.columnconfigure(0, weight=0)
985
- self.columnconfigure(1, weight=0)
986
- self.columnconfigure(2, weight=0)
987
- if lbl_icon is not None:
988
- self.columnconfigure(3, weight=0)
989
- self.lbl_icon = SimBALabel(parent=self, txt='', txt_clr='black', font=font, width=None, anchor='w', img=lbl_icon, compound=None)
990
- self.lbl_icon.grid(row=0, column=0, sticky="w")
991
- else:
992
- self.lbl_icon = None
993
- self.filePath = StringVar()
994
- self.lblName = Label(self, text=fileDescription, fg=str(self.color), width=str(self.lblwidth), anchor=W, font=Formats.FONT_REGULAR.value)
995
- self.lblName.grid(row=0, column=1, sticky=W)
996
- self.entPath = Label(self, textvariable=self.filePath, relief=SUNKEN, font=Formats.FONT_REGULAR.value, bg=bg_clr, width=entry_width)
997
- self.entPath.grid(row=0, column=2)
998
- self.btnFind = SimbaButton(parent=self, txt=Defaults.BROWSE_FILE_BTN_TEXT.value, font=font, cmd=self.setFilePath, img=browse_icon)
999
- self.btnFind.grid(row=0, column=3)
1000
- self.filePath.set(Defaults.NO_FILE_SELECTED_TEXT.value)
1001
- if initial_path is not None:
1002
- self.filePath.set(initial_path)
1003
- if status is not None:
1004
- self.set_state(setstatus=status)
1005
- if tooltip_txt is not None and isinstance(tooltip_txt, str):
1006
- CreateToolTip(widget=self.lblName, text=tooltip_txt)
1007
- elif tooltip_key in TOOLTIPS.keys():
1008
- CreateToolTip(widget=self.lblName, text=TOOLTIPS[tooltip_key])
1009
-
1010
- def setFilePath(self):
1011
- if self.initialdir is not None:
1012
- if not os.path.isdir(self.initialdir):
1013
- self.initialdir = None
1014
- else:
1015
- pass
1016
-
1017
- if self.file_type:
1018
- file_selected = askopenfilename(title=self.title, parent=self.parent, filetypes=self.file_type, initialdir=self.initialdir)
1019
- else:
1020
- file_selected = askopenfilename(title=self.title, parent=self.parent, initialdir=self.initialdir)
1021
- if file_selected:
1022
- if self.dropdown is not None:
1023
- _, name, _ = get_fn_ext(filepath=file_selected)
1024
- self.dropdown.setChoices(name)
1025
- self.filePath.set(name)
1026
- else:
1027
- self.filePath.set(file_selected)
1028
- self.entPath.configure(width=len(file_selected)+10)
1029
-
1030
- else:
1031
- self.filePath.set(Defaults.NO_FILE_SELECTED_TEXT.value)
1032
- self.entPath.configure(width=len(Defaults.NO_FILE_SELECTED_TEXT.value) + 10)
1033
-
1034
- @property
1035
- def file_path(self):
1036
- return self.filePath.get()
1037
-
1038
- def set_state(self, setstatus):
1039
- self.entPath.config(state=setstatus)
1040
- self.btnFind["state"] = setstatus
1041
-
1042
-
1043
- def SimBARadioButton(parent: Union[Frame, Canvas, LabelFrame, Toplevel],
1044
- txt: str,
1045
- variable: Union[BooleanVar, StringVar],
1046
- txt_clr: Optional[str] = 'black',
1047
- font: Optional[Tuple] = Formats.FONT_REGULAR.value,
1048
- compound: Optional[str] = 'left',
1049
- img: Optional[Union[ImageTk.PhotoImage, str]] = None,
1050
- enabled: Optional[bool] = True,
1051
- tooltip_txt: Optional[str] = None,
1052
- value: bool = False,
1053
- cmd: Optional[Callable] = None,
1054
- cmd_kwargs: Optional[Dict[Any, Any]] = None) -> Radiobutton:
1055
-
1056
- if isinstance(img, str):
1057
- img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS[img]["icon_path"]))
1058
-
1059
- if cmd_kwargs is None:
1060
- cmd_kwargs = {}
1061
-
1062
- def execute_command():
1063
- if cmd:
1064
- evaluated_kwargs = {k: (v() if callable(v) else v) for k, v in cmd_kwargs.items()}
1065
- cmd(**evaluated_kwargs)
1066
-
1067
- if cmd is not None:
1068
- command = execute_command
1069
- else:
1070
- command = None
1071
-
1072
- btn = Radiobutton(parent,
1073
- text=txt,
1074
- font=font,
1075
- image=img,
1076
- fg=txt_clr,
1077
- variable=variable,
1078
- value=value,
1079
- compound=compound,
1080
- command=command)
1081
-
1082
- if img is not None:
1083
- btn.image = img
1084
-
1085
- if not enabled:
1086
- btn.config(state=DISABLED)
1087
-
1088
- if tooltip_txt is not None:
1089
- CreateToolTip(widget=btn, text=tooltip_txt)
1090
-
1091
- return btn
1
+ __author__ = "Simon Nilsson; sronilsson@gmail.com"
2
+
3
+ import os.path
4
+ import platform
5
+ import threading
6
+ import tkinter
7
+ import tkinter as tk
8
+ import webbrowser
9
+ from copy import copy, deepcopy
10
+ from tkinter import *
11
+ from tkinter.filedialog import askdirectory, askopenfilename
12
+ from tkinter.ttk import Combobox
13
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
14
+
15
+ try:
16
+ from typing import Literal
17
+ except:
18
+ from typing_extensions import Literal
19
+
20
+ import numpy as np
21
+ import PIL.Image
22
+ from PIL import ImageTk
23
+
24
+ from simba.utils.checks import check_if_valid_img
25
+ from simba.utils.enums import Defaults, Formats, TkBinds
26
+ from simba.utils.lookups import get_icons_paths, get_tooltips
27
+ from simba.utils.read_write import get_fn_ext
28
+
29
+ MENU_ICONS = get_icons_paths()
30
+ TOOLTIPS = get_tooltips()
31
+
32
+ def onMousewheel(event, canvas):
33
+ try:
34
+ scrollSpeed = event.delta
35
+ if platform.system() == "Darwin":
36
+ scrollSpeed = event.delta
37
+ elif platform.system() == "Windows":
38
+ scrollSpeed = int(event.delta / 120)
39
+ canvas.yview_scroll(-1 * (scrollSpeed), "units")
40
+ except:
41
+ pass
42
+
43
+
44
+ def bindToMousewheel(event, canvas):
45
+ canvas.bind_all("<MouseWheel>", lambda event: onMousewheel(event, canvas))
46
+
47
+
48
+ def unbindToMousewheel(event, canvas):
49
+ canvas.unbind_all("<MouseWheel>")
50
+
51
+
52
+ def onFrameConfigure(canvas):
53
+ """Reset the scroll region to encompass the inner frame"""
54
+ canvas.configure(scrollregion=canvas.bbox("all"))
55
+
56
+
57
+ def on_mouse_scroll(event, canvas):
58
+ if event.delta:
59
+ canvas.yview_scroll(-1 * (event.delta // 120), "units")
60
+ elif event.num == 4:
61
+ canvas.yview_scroll(-1, "units")
62
+ elif event.num == 5:
63
+ canvas.yview_scroll(1, "units")
64
+
65
+ def hxtScrollbar(master):
66
+ canvas = tk.Canvas(master, borderwidth=0, bg="#f0f0f0", relief="flat")
67
+ frame = tk.Frame(canvas, bg="#f0f0f0", bd=0)
68
+
69
+ vsb = tk.Scrollbar(master, orient="vertical", command=canvas.yview, bg="#cccccc", width=20)
70
+ hsb = tk.Scrollbar(master, orient="horizontal", command=canvas.xview, bg="#cccccc")
71
+
72
+ canvas.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
73
+
74
+ canvas.pack(side="left", fill="both", expand=True, padx=0, pady=0)
75
+ vsb.pack(side="right", fill="y", padx=(0, 0))
76
+ hsb.pack(side="bottom", fill="x", pady=(0, 0))
77
+
78
+ canvas.create_window((0, 0), window=frame, anchor="nw")
79
+
80
+ def on_frame_configure(event):
81
+ canvas.configure(scrollregion=canvas.bbox("all"))
82
+
83
+ frame.bind("<Configure>", on_frame_configure)
84
+
85
+ def _on_mousewheel(event):
86
+ canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
87
+
88
+ def _on_key(event):
89
+ if event.keysym == "Up":
90
+ canvas.yview_scroll(-1, "units")
91
+ elif event.keysym == "Down":
92
+ canvas.yview_scroll(1, "units")
93
+ elif event.keysym == "Page_Up":
94
+ canvas.yview_scroll(-1, "pages")
95
+ elif event.keysym == "Page_Down":
96
+ canvas.yview_scroll(1, "pages")
97
+
98
+ # Focus & bindings when mouse enters
99
+ def _on_enter(_):
100
+ frame.focus_set()
101
+ frame.bind_all("<MouseWheel>", _on_mousewheel)
102
+ frame.bind_all("<Key>", _on_key)
103
+
104
+ # Clean up when mouse leaves
105
+ # def _on_leave(_):
106
+ # frame.unbind_all("<MouseWheel>")
107
+ # frame.unbind_all("<Key>")
108
+
109
+ frame.bind("<Enter>", _on_enter)
110
+ #frame.bind("<Leave>", _on_leave)
111
+
112
+ return frame
113
+
114
+ def form_validator_is_numeric(inStr, acttyp):
115
+ if acttyp == "1": # insert
116
+ if not inStr.isdigit():
117
+ return False
118
+ return True
119
+
120
+
121
+ class DropDownMenu(Frame):
122
+ """
123
+ Legacy, use :func:`simba.ui.tkinter_functions.SimBADropDown`.
124
+ """
125
+ def __init__(self,
126
+ parent=None,
127
+ dropdownLabel="",
128
+ choice_dict=None,
129
+ labelwidth="",
130
+ com=None,
131
+ val: Optional[Any] = None,
132
+ **kw):
133
+
134
+ Frame.__init__(self, master=parent, **kw)
135
+ self.dropdownvar = StringVar()
136
+ self.lblName = Label(self, text=dropdownLabel, width=labelwidth, anchor=W, font=Formats.FONT_REGULAR.value)
137
+ self.lblName.grid(row=0, column=0)
138
+ self.choices = choice_dict
139
+ self.popupMenu = OptionMenu(self, self.dropdownvar, *self.choices, command=com)
140
+ self.popupMenu.grid(row=0, column=1)
141
+ if val is not None:
142
+ self.setChoices(val)
143
+
144
+ def getChoices(self):
145
+ return self.dropdownvar.get()
146
+
147
+ def setChoices(self, choice):
148
+ self.dropdownvar.set(choice)
149
+
150
+ def enable(self):
151
+ self.popupMenu.configure(state="normal")
152
+
153
+ def disable(self):
154
+ self.popupMenu.configure(state="disable")
155
+
156
+
157
+ class SimBAScaleBar(Frame):
158
+ def __init__(self,
159
+ parent: Union[Frame, Canvas, LabelFrame, Toplevel, Tk],
160
+ label: Optional[str] = None,
161
+ label_width: Optional[int] = None,
162
+ orient: Literal['horizontal', 'vertical'] = HORIZONTAL,
163
+ length: int = 200,
164
+ value: Optional[int] = 95,
165
+ showvalue: bool = True,
166
+ label_clr: str = 'black',
167
+ lbl_font: tuple = Formats.FONT_REGULAR.value,
168
+ scale_font: tuple = Formats.FONT_REGULAR_ITALICS.value,
169
+ lbl_img: Optional[str] = None,
170
+ from_: int = -1,
171
+ resolution: int = 1,
172
+ to: int = 100,
173
+ tickinterval: Optional[int] = None,
174
+ troughcolor: Optional[str] = None,
175
+ activebackground: Optional[str] = None,
176
+ sliderrelief: Literal["raised", "sunken", "flat", "ridge", "solid", "groove"] = 'flat'):
177
+
178
+ super().__init__(master=parent)
179
+ self.columnconfigure(0, weight=0)
180
+ self.columnconfigure(1, weight=0)
181
+ if lbl_img is not None:
182
+ self.columnconfigure(2, weight=0)
183
+ self.lbl_lbl = SimBALabel(parent=self, txt='', txt_clr='black', bg_clr=None, font=lbl_font, width=None, anchor='w', img=lbl_img, compound=None)
184
+ self.lbl_lbl.grid(row=0, column=0, sticky=SW)
185
+ else:
186
+ self.lbl_lbl = None
187
+
188
+ self.scale = Scale(self,
189
+ from_=from_,
190
+ to=to,
191
+ orient=orient,
192
+ length=length,
193
+ font=scale_font,
194
+ sliderrelief=sliderrelief,
195
+ troughcolor=troughcolor,
196
+ tickinterval=tickinterval,
197
+ resolution=resolution,
198
+ showvalue=showvalue,
199
+ activebackground=activebackground)
200
+
201
+ if label is not None:
202
+ self.lbl = SimBALabel(parent=self, txt=label, font=lbl_font, txt_clr=label_clr, width=label_width)
203
+ self.lbl.grid(row=0, column=1, sticky=SW)
204
+
205
+ self.scale.grid(row=0, column=2, sticky=NW)
206
+ if value is not None:
207
+ self.set_value(value=value)
208
+
209
+ def set_value(self, value: int):
210
+ self.scale.set(value)
211
+
212
+ def get_value(self) -> Union[int, float]:
213
+ return self.scale.get()
214
+
215
+ def get(self) -> Union[int, float]:
216
+ ## Alternative for ``get_value`` for legacy reasons.
217
+ return self.scale.get()
218
+
219
+
220
+
221
+
222
+ class Entry_Box(Frame):
223
+ def __init__(self,
224
+ parent: Union[Frame, Canvas, LabelFrame, Toplevel, Tk],
225
+ fileDescription: Optional[str] = "",
226
+ labelwidth: Union[int, str] = None,
227
+ label_bg_clr: Optional[str] = None,
228
+ status: Literal["normal", "disabled", "readonly"] = 'normal',
229
+ validation: Optional[Union[Callable, str]] = None,
230
+ entry_box_width: Union[int, str] = None,
231
+ entry_box_clr: Optional[str] = None,
232
+ img: Optional[str] = None,
233
+ value: Optional[Any] = None,
234
+ label_font: tuple = Formats.FONT_REGULAR.value,
235
+ entry_font: tuple = Formats.FONT_REGULAR.value,
236
+ tooltip_key: Optional[str] = None,
237
+ justify: Literal["left", "center", "right"] = 'left',
238
+ cmd: Optional[Callable] = None,
239
+ **kw):
240
+
241
+ super(Entry_Box, self).__init__(master=parent)
242
+ self.validation_methods = {"numeric": (self.register(form_validator_is_numeric), "%P", "%d")}
243
+ self.status = status if status is not None else NORMAL
244
+ self.labelname = fileDescription
245
+ Frame.__init__(self, master=parent, **kw)
246
+ self.columnconfigure(0, weight=0)
247
+ self.columnconfigure(1, weight=0)
248
+ if img is not None:
249
+ self.columnconfigure(2, weight=0)
250
+ self.entrybox_img_lbl = SimBALabel(parent=self, txt='', txt_clr='black', bg_clr=label_bg_clr, font=label_font, width=None, anchor='w', img=img, compound=None)
251
+ self.entrybox_img_lbl.grid(row=0, column=0, sticky="w")
252
+ else:
253
+ self.entrybox_img_lbl = None
254
+ self.filePath = StringVar()
255
+ self.lblName = Label(self, text=fileDescription, width=labelwidth, anchor=W, font=label_font, bg=label_bg_clr)
256
+ self.lblName.grid(row=0, column=1)
257
+ if tooltip_key in TOOLTIPS.keys():
258
+ CreateToolTip(widget=self.lblName, text=TOOLTIPS[tooltip_key])
259
+ if not entry_box_width:
260
+ self.entPath = Entry(self, textvariable=self.filePath, state=self.status, validate="key", validatecommand=self.validation_methods.get(validation, None), font=entry_font, justify=justify, bg=entry_box_clr)
261
+ else:
262
+ self.entPath = Entry(self, textvariable=self.filePath, state=self.status, width=entry_box_width, validate="key", font=entry_font, justify=justify, validatecommand=self.validation_methods.get(validation, None), bg=entry_box_clr)
263
+ self.entPath.grid(row=0, column=2)
264
+ if value is not None:
265
+ self.entry_set(val=value)
266
+ if cmd is not None:
267
+ self.bind_combobox_keys()
268
+ self.cmd = cmd
269
+
270
+ def bind_combobox_keys(self):
271
+ self.entPath.bind("<KeyRelease>", self.run_cmd)
272
+
273
+ @property
274
+ def entry_get(self):
275
+ return self.entPath.get()
276
+
277
+ def entry_set(self, val):
278
+ self.filePath.set(val)
279
+
280
+ def set_state(self, setstatus):
281
+ self.entPath.config(state=setstatus)
282
+
283
+ def destroy(self):
284
+ try:
285
+ if self.entrybox_img_lbl is not None:
286
+ self.entrybox_img_lbl.destroy()
287
+ self.lblName.destroy()
288
+ self.entPath.destroy()
289
+ except:
290
+ pass
291
+
292
+ def run_cmd(self, x):
293
+ self.cmd(self.entry_get.strip())
294
+
295
+
296
+
297
+ class FolderSelect(Frame):
298
+ def __init__(self,
299
+ parent: Union[Frame, LabelFrame, Canvas, Toplevel],
300
+ folderDescription: Optional[str] = "",
301
+ color: Optional[str] = None,
302
+ label_bg_clr: Optional[str] = None,
303
+ font: Tuple = Formats.FONT_REGULAR.value,
304
+ title: Optional[str] = "",
305
+ lblwidth: Optional[int] = 0,
306
+ bg_clr: Optional[str] = 'white',
307
+ entry_width: Optional[int] = 20,
308
+ lbl_icon: Optional[str] = None,
309
+ initialdir: Optional[Union[str, os.PathLike]] = None,
310
+ tooltip_txt: Optional[str] = None,
311
+ tooltip_key: Optional[str] = None,
312
+ **kw):
313
+
314
+ self.title, self.initialdir = title, initialdir
315
+ self.color = color if color is not None else "black"
316
+ self.lblwidth = lblwidth if lblwidth is not None else 0
317
+ self.parent = parent
318
+ Frame.__init__(self, master=parent, **kw)
319
+ browse_icon = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS["browse_small"]["icon_path"]))
320
+ self.columnconfigure(0, weight=0)
321
+ self.columnconfigure(1, weight=0)
322
+ self.columnconfigure(2, weight=0)
323
+ if lbl_icon is not None:
324
+ self.columnconfigure(3, weight=0)
325
+ self.lbl_icon = SimBALabel(parent=self, txt='', txt_clr='black', bg_clr=label_bg_clr, font=font, width=None, anchor='w', img=lbl_icon, compound=None)
326
+ self.lbl_icon.grid(row=0, column=0, sticky="w")
327
+ else:
328
+ self.lbl_icon = None
329
+ self.folderPath = StringVar()
330
+ self.lblName = Label(self, text=folderDescription, fg=str(self.color), width=str(self.lblwidth), anchor=W, font=font, bg=label_bg_clr)
331
+ self.lblName.grid(row=0, column=1, sticky=NW)
332
+ self.entPath = Label(self, textvariable=self.folderPath, relief=SUNKEN, font=font, bg=bg_clr, width=entry_width)
333
+ self.entPath.grid(row=0, column=2, sticky=NW)
334
+ self.btnFind = SimbaButton(parent=self, txt=Defaults.BROWSE_FOLDER_BTN_TEXT.value, font=font, cmd=self.setFolderPath, img=browse_icon)
335
+ self.btnFind.image = browse_icon
336
+ self.btnFind.grid(row=0, column=3, sticky=NW)
337
+ self.folderPath.set("No folder selected")
338
+ if tooltip_txt is not None and isinstance(tooltip_txt, str):
339
+ CreateToolTip(widget=self.lblName, text=tooltip_txt)
340
+ elif tooltip_key in TOOLTIPS.keys():
341
+ CreateToolTip(widget=self.lblName, text=TOOLTIPS[tooltip_key])
342
+
343
+ def setFolderPath(self):
344
+ if self.initialdir is not None:
345
+ if not os.path.isdir(self.initialdir):
346
+ self.initialdir = None
347
+ else:
348
+ pass
349
+ folder_selected = askdirectory(title=str(self.title), parent=self.parent, initialdir=self.initialdir)
350
+ if folder_selected:
351
+ self.folderPath.set(folder_selected)
352
+ self.entPath.configure(width=len(folder_selected)+10)
353
+ else:
354
+ self.folderPath.set("No folder selected")
355
+ self.entPath.configure(width=len("No folder selected")+10)
356
+
357
+ @property
358
+ def folder_path(self):
359
+ return self.folderPath.get()
360
+
361
+
362
+ class ToolTip(object):
363
+
364
+ def __init__(self, widget):
365
+ self.widget = widget
366
+ self.tipwindow = None
367
+ self.id = None
368
+ self.x = self.y = 0
369
+
370
+ def showtip(self, text):
371
+ "Display text in tooltip window"
372
+ self.text = text
373
+ if self.tipwindow or not self.text:
374
+ return
375
+ x, y, cx, cy = self.widget.bbox("insert")
376
+ x = x + self.widget.winfo_rootx() + 20
377
+ y = y + cy + self.widget.winfo_rooty() + 20
378
+ self.tipwindow = tw = Toplevel(self.widget)
379
+ tw.wm_overrideredirect(1)
380
+ tw.wm_geometry("+%d+%d" % (x, y))
381
+ label = Label(tw, text=self.text, justify=LEFT, background="#ffffe0", relief=SOLID, borderwidth=1, font=Formats.FONT_REGULAR.value)
382
+ label.pack(ipadx=1)
383
+
384
+ def hidetip(self):
385
+ tw = self.tipwindow
386
+ self.tipwindow = None
387
+ if tw:
388
+ tw.destroy()
389
+
390
+
391
+ def CreateToolTip(widget, text):
392
+ toolTip = ToolTip(widget)
393
+ def enter(event):
394
+ toolTip.showtip(text)
395
+
396
+ def leave(event):
397
+ toolTip.hidetip()
398
+
399
+ widget.bind("<Enter>", enter)
400
+ widget.bind("<Leave>", leave)
401
+
402
+
403
+ def CreateLabelFrameWithIcon(parent: Union[Toplevel, LabelFrame, Canvas, Frame],
404
+ header: str,
405
+ icon_name: str,
406
+ padx: Optional[int] = None,
407
+ pady: Optional[int] = None,
408
+ relief: str = 'solid',
409
+ width: Optional[int] = None,
410
+ bg: Optional[str] = None,
411
+ font: tuple = Formats.FONT_HEADER.value,
412
+ icon_link: Optional[Union[str, None]] = LabelFrame,
413
+ tooltip_key: Optional[str] = None):
414
+
415
+ if icon_name in MENU_ICONS.keys():
416
+ icon = PIL.Image.open(MENU_ICONS[icon_name]["icon_path"])
417
+ icon = ImageTk.PhotoImage(icon)
418
+ else:
419
+ icon = None
420
+ frm = Frame(parent, bg=bg)
421
+
422
+ label_image = Label(frm, image=icon, bg=bg)
423
+ label_image.image = icon
424
+ if icon_link:
425
+ label_image.bind("<Button-1>", lambda e: callback(icon_link))
426
+ label_image.grid(row=0, column=0)
427
+
428
+
429
+ label_text = Label(frm, text=header, font=font, bg=bg)
430
+ label_text.grid(row=0, column=1)
431
+
432
+
433
+
434
+ lbl_frm = LabelFrame(parent, labelwidget=frm, relief=relief, width=width, padx=padx or 0, pady=pady or 0)
435
+
436
+ if tooltip_key in TOOLTIPS.keys():
437
+ CreateToolTip(widget=label_text, text=TOOLTIPS[tooltip_key])
438
+
439
+ if bg is not None:
440
+ lbl_frm.configure(bg=bg)
441
+ return lbl_frm
442
+
443
+ def callback(url):
444
+ webbrowser.open_new(url)
445
+
446
+
447
+ def create_scalebar(
448
+ parent: Frame, name: str, min: int, max: int, cmd: object or None = None
449
+ ):
450
+
451
+ scale = Scale(
452
+ parent,
453
+ from_=min,
454
+ to=max,
455
+ orient=HORIZONTAL,
456
+ length=200,
457
+ label=name,
458
+ command=cmd,
459
+ )
460
+ scale.set(0)
461
+ return scale
462
+
463
+
464
+ class TwoOptionQuestionPopUp(object):
465
+ """
466
+ Helpe to create a two-option question tkinter pop up window (e.g., YES/NO).
467
+
468
+ :parameter str question: Question to present to the user. E.g., ``DO YOU WANT TO PROCEED?``.
469
+ :parameter str option_one: The first user option. E.g., ``YES``.
470
+ :parameter str option_one: The second user option. E.g., ``NO``.
471
+ :parameter str title: The window titel in the top banner of the pop-up. E.g., ``A QUESTION FOR YOU!``.
472
+ :parameter Optional[str] link: If not None, then a link to documentation presenting background info about the user choices.
473
+ """
474
+
475
+ def __init__(self,
476
+ question: str,
477
+ option_one: str,
478
+ option_two: str,
479
+ title: str,
480
+ link: Optional[str] = None):
481
+
482
+ self.main_frm = Toplevel()
483
+ self.main_frm.geometry("600x200")
484
+ self.main_frm.title(title)
485
+ menu_icons = get_menu_icons()
486
+ self.main_frm.iconphoto(False, menu_icons['question_mark']["img"])
487
+
488
+ #img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS['question_mark']["icon_path"]))
489
+
490
+ question_frm = Frame(self.main_frm)
491
+ question_frm.pack(expand=True, fill="both")
492
+ Label(question_frm, text=question, font=Formats.LABELFRAME_HEADER_FORMAT.value).pack()
493
+
494
+ button_one = SimbaButton(parent=question_frm, txt=option_one, txt_clr="blue", bg_clr="lightgrey", img='check_blue', cmd=self.run, cmd_kwargs={'selected_option': lambda: option_one}, font=Formats.FONT_LARGE.value, hover_font=Formats.FONT_LARGE_BOLD.value)
495
+ button_two = SimbaButton(parent=question_frm, txt=option_two, txt_clr="red", bg_clr="lightgrey", img='close', cmd=self.run, cmd_kwargs={'selected_option': lambda: option_two}, font=Formats.FONT_LARGE.value, hover_font=Formats.FONT_LARGE_BOLD.value)
496
+ if link:
497
+ link_lbl = Label(question_frm, text="Click here for more information.", cursor="hand2", fg="blue")
498
+ link_lbl.bind("<Button-1>", lambda e: callback(link))
499
+ link_lbl.place(relx=0.5, rely=0.30, anchor=CENTER)
500
+ button_one.place(relx=0.5, rely=0.50, anchor=CENTER)
501
+ button_two.place(relx=0.5, rely=0.80, anchor=CENTER)
502
+ self.main_frm.wait_window()
503
+
504
+ def run(self, selected_option):
505
+ self.selected_option = selected_option
506
+ self.main_frm.destroy()
507
+
508
+ def SimbaButton(parent: Union[Frame, Canvas, LabelFrame, Toplevel],
509
+ txt: str,
510
+ txt_clr: Optional[str] = 'black',
511
+ bg_clr: Optional[str] = '#f0f0f0',
512
+ active_bg_clr: Optional[str] = None,
513
+ hover_bg_clr: Optional[str] = Formats.BTN_HOVER_CLR.value,
514
+ hover_font: Optional[Tuple] = Formats.FONT_REGULAR_BOLD.value,
515
+ font: Optional[Tuple] = Formats.FONT_REGULAR.value,
516
+ width: Optional[int] = None,
517
+ height: Optional[int] = None,
518
+ compound: Optional[str] = 'left',
519
+ img: Optional[Union[ImageTk.PhotoImage, str]] = None,
520
+ cmd: Optional[Callable] = None,
521
+ cmd_kwargs: Optional[Dict[Any, Any]] = None,
522
+ enabled: Optional[bool] = True,
523
+ anchor: str = 'w',
524
+ thread: Optional[bool] = False,
525
+ tooltip_txt: Optional[str] = None) -> Button:
526
+ def on_enter(e):
527
+ e.widget.config(bg=hover_bg_clr, font=hover_font)
528
+
529
+ def on_leave(e):
530
+ e.widget.config(bg=bg_clr, font=font)
531
+
532
+ # if hover_font != font:
533
+ # hover_font = copy(font)
534
+
535
+
536
+
537
+ if isinstance(img, str):
538
+ img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS[img]["icon_path"]))
539
+
540
+ if cmd_kwargs is None:
541
+ cmd_kwargs = {}
542
+
543
+ def execute_command():
544
+ if cmd:
545
+ evaluated_kwargs = {k: (v() if callable(v) else v) for k, v in cmd_kwargs.items()}
546
+ cmd(**evaluated_kwargs)
547
+
548
+ if cmd is not None:
549
+ if thread:
550
+ command = lambda: threading.Thread(target=execute_command).start()
551
+ else:
552
+ command = execute_command
553
+ else:
554
+ command = None
555
+
556
+ btn = Button(master=parent,
557
+ text=txt,
558
+ compound=compound,
559
+ image=img,
560
+ relief=RAISED,
561
+ fg=txt_clr,
562
+ activebackground=active_bg_clr,
563
+ font=font,
564
+ bg=bg_clr,
565
+ anchor=anchor,
566
+ command=command)
567
+
568
+ if img is not None:
569
+ btn.image = img
570
+ if width is not None:
571
+ btn.config(width=width)
572
+ if height is not None:
573
+ btn.config(height=height)
574
+ if not enabled:
575
+ btn.config(state=DISABLED)
576
+
577
+ if hover_bg_clr is not None:
578
+ btn.bind("<Enter>", on_enter)
579
+ btn.bind("<Leave>", on_leave)
580
+
581
+ if tooltip_txt is not None:
582
+ CreateToolTip(widget=btn, text=tooltip_txt)
583
+
584
+ return btn
585
+
586
+
587
+ def SimbaCheckbox(parent: Union[Frame, Toplevel, LabelFrame, Canvas],
588
+ txt: str,
589
+ txt_clr: Optional[str] = 'black',
590
+ bg_clr: Optional[str] = None,
591
+ txt_img: Optional[str] = None,
592
+ txt_img_location: Literal['left', 'right', 'top', 'bottom'] = RIGHT,
593
+ font: Optional[Tuple[str, str, int]] = Formats.FONT_REGULAR.value,
594
+ val: Optional[bool] = False,
595
+ state: Literal["disabled", 'normal'] = NORMAL,
596
+ indicatoron: bool = True,
597
+ cmd: Optional[Callable] = None,
598
+ tooltip_txt: Optional[str] = None,
599
+ tooltip_key: Optional[str] = None):
600
+
601
+ var = BooleanVar(value=False)
602
+ if val: var.set(True)
603
+ if isinstance(txt_img, str):
604
+ txt_img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS[txt_img]["icon_path"]))
605
+ if cmd is None:
606
+ cb = Checkbutton(master=parent, font=font, fg=txt_clr, image=txt_img, text=txt, compound=txt_img_location, variable=var, indicatoron=indicatoron, bg=bg_clr)
607
+ else:
608
+ cb = Checkbutton(master=parent, font=font, fg=txt_clr, image=txt_img, text=txt, compound=txt_img_location, variable=var, command=cmd, indicatoron=indicatoron, bg=bg_clr)
609
+ if txt_img is not None:
610
+ cb.image = txt_img
611
+ if state == DISABLED:
612
+ cb.configure(state=DISABLED)
613
+
614
+ if isinstance(tooltip_txt, str):
615
+ CreateToolTip(widget=cb, text=tooltip_txt)
616
+ elif isinstance(tooltip_key, str) and tooltip_key in TOOLTIPS.keys():
617
+ CreateToolTip(widget=cb, text=TOOLTIPS[tooltip_key])
618
+
619
+ return cb, var
620
+
621
+ def SimBALabel(parent: Union[Frame, Canvas, LabelFrame, Toplevel],
622
+ txt: str,
623
+ txt_clr: str = 'black',
624
+ bg_clr: Optional[str] = None,
625
+ hover_fg_clr: Optional[str] = None,
626
+ font: tuple = Formats.FONT_REGULAR.value,
627
+ hover_font: Optional[Tuple] = None,
628
+ relief: str = FLAT,
629
+ compound: Optional[Literal['left', 'right', 'top', 'bottom', 'center']] = 'left',
630
+ justify: Optional[str] = None,
631
+ link: Optional[str] = None,
632
+ width: Optional[int] = None,
633
+ padx: Optional[int] = None,
634
+ pady: Optional[int] = None,
635
+ cursor: Optional[str] = None,
636
+ img: Optional[str] = None,
637
+ anchor: Optional[str] = None,
638
+ tooltip_key: Optional[str] = None,
639
+ hover_img: Optional[np.ndarray] = None):
640
+
641
+
642
+ def _hover_enter(e):
643
+ w = e.widget
644
+ if hover_img is not None and check_if_valid_img(data=hover_img, raise_error=False):
645
+ arr = np.asarray(hover_img, dtype=np.uint8)
646
+ if arr.ndim == 3 and arr.shape[2] == 3:
647
+ arr = arr[:, :, ::-1]
648
+ pil_img = PIL.Image.fromarray(arr)
649
+ photo = ImageTk.PhotoImage(pil_img)
650
+ tw = Toplevel(w)
651
+ tw.wm_overrideredirect(True)
652
+ tw.attributes("-topmost", True)
653
+ tw.wm_geometry("+%d+%d" % (w.winfo_rootx() + w.winfo_width() + 4, w.winfo_rooty()))
654
+ frm = Frame(tw, relief="solid", bd=2, bg="#f0f0f0")
655
+ frm.pack(padx=2, pady=2)
656
+ lbl_hover = Label(frm, image=photo, bg="#f0f0f0")
657
+ lbl_hover.image = photo
658
+ lbl_hover.pack(padx=4, pady=(4, 2))
659
+ caption_lbl = Label(frm, text=txt, font=font, fg=txt_clr, bg=bg_clr)
660
+ caption_lbl.pack(pady=(0, 4))
661
+ tw.lift()
662
+ w._hover_toplevel = tw
663
+ elif hover_fg_clr is not None or hover_font is not None:
664
+ w.config(fg=hover_fg_clr, font=hover_font)
665
+
666
+ def _hover_leave(e):
667
+ w = e.widget
668
+ if getattr(w, "_hover_toplevel", None) is not None:
669
+ try:
670
+ w._hover_toplevel.destroy()
671
+ except tkinter.TclError:
672
+ pass
673
+ w._hover_toplevel = None
674
+ if hover_fg_clr is not None or hover_font is not None:
675
+ w.config(fg=txt_clr, bg=bg_clr, font=font)
676
+
677
+ def on_enter(e):
678
+ if hover_img is not None:
679
+ _hover_enter(e)
680
+ else:
681
+ e.widget.config(fg=hover_fg_clr, font=hover_font)
682
+
683
+ def on_leave(e):
684
+ if hover_img is not None:
685
+ _hover_leave(e)
686
+ else:
687
+ e.widget.config(fg=txt_clr, bg=bg_clr, font=font)
688
+
689
+ anchor = 'w' if anchor is None else anchor
690
+ if isinstance(img, str) and img in MENU_ICONS.keys():
691
+ img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS[img]["icon_path"]))
692
+ else:
693
+ img = None
694
+ if img is not None and compound is None:
695
+ compound = 'left'
696
+
697
+ lbl = Label(parent,
698
+ text=txt,
699
+ font=font,
700
+ fg=txt_clr,
701
+ bg=bg_clr,
702
+ justify=justify,
703
+ relief=relief,
704
+ cursor=cursor,
705
+ anchor=anchor,
706
+ image=img,
707
+ compound=compound,
708
+ padx=padx,
709
+ pady=pady)
710
+
711
+ if img is not None:
712
+ lbl.image = img
713
+
714
+ if width is not None:
715
+ lbl.configure(width=width)
716
+
717
+ if link is not None:
718
+ lbl.bind("<Button-1>", lambda e: callback(link))
719
+
720
+ elif tooltip_key in TOOLTIPS.keys():
721
+ CreateToolTip(widget=lbl, text=TOOLTIPS[tooltip_key])
722
+
723
+ if hover_font is not None or hover_fg_clr is not None or hover_img is not None:
724
+ lbl.bind(TkBinds.ENTER.value, on_enter)
725
+ lbl.bind(TkBinds.LEAVE.value, on_leave)
726
+
727
+ return lbl
728
+
729
+
730
+ def get_menu_icons():
731
+ menu_icons = copy(MENU_ICONS)
732
+ for k in menu_icons.keys():
733
+ menu_icons[k]["img"] = ImageTk.PhotoImage(image=PIL.Image.open(os.path.join(os.path.dirname(__file__), menu_icons[k]["icon_path"])))
734
+ return menu_icons
735
+
736
+
737
+
738
+ class SimBASeperator(Frame):
739
+
740
+ def __init__(self,
741
+ parent: Union[Frame, Canvas, LabelFrame, Toplevel, Tk],
742
+ color: Optional[str] = 'black',
743
+ orient: Literal['horizontal', 'vertical'] = 'horizontal',
744
+ cursor: Optional[str] = None,
745
+ borderwidth: Optional[int] = None,
746
+ takefocus: Optional[int] = 0,
747
+ height: int = 1,
748
+ relief: Literal["raised", "sunken", "flat", "ridge", "solid", "groove"] = "flat"):
749
+
750
+ super().__init__(master=parent, height=height, bg=color if color is not None else "black", relief=relief, bd=borderwidth if borderwidth is not None else None)
751
+ style = tk.ttk.Style()
752
+ style_name = "SimBA.TSeparator"
753
+
754
+ style.configure(style_name,
755
+ background=color if color is not None else "black")
756
+
757
+ seperator = tk.ttk.Separator(self,
758
+ orient=orient,
759
+ style=style_name,
760
+ cursor=cursor,
761
+ takefocus=takefocus)
762
+
763
+ if orient == "horizontal":
764
+ seperator.pack(fill="x", expand=True)
765
+ else:
766
+ seperator.pack(fill="y", expand=True)
767
+
768
+
769
+ class SimBADropDown(Frame):
770
+ """
771
+ Create a dropdown menu widget with optional searchable functionality.
772
+
773
+ This class creates a ttk.Combobox dropdown menu with a label, supporting both readonly and searchable modes.
774
+ When searchable mode is enabled, users can type to filter the dropdown options.
775
+
776
+ :param parent (Frame | Canvas | LabelFrame | Toplevel | Tk): The parent widget container.
777
+ :param dropdown_options (Iterable[Any] | List[Any] | Tuple[Any]): List of options to display in the dropdown menu.
778
+ :param label (str, optional): Text label displayed next to the dropdown. Default: None.
779
+ :param label_width (int, optional): Width of the label in characters. Default: None.
780
+ :param label_font (tuple, optional): Font tuple for the label. Default: Formats.FONT_REGULAR.value.
781
+ :param label_bg_clr (str, optional): Background color for the label. Default: None.
782
+ :param dropdown_font_size (int, optional): Font size for the dropdown text. Default: None.
783
+ :param justify (str): Text justification in the dropdown ('left', 'center', 'right'). Default: 'center'.
784
+ :param dropdown_width (int, optional): Width of the dropdown widget in characters. Default: None.
785
+ :param command (Callable, optional): Callback function to execute when an option is selected. Default: None.
786
+ :param value (Any, optional): Initial selected value for the dropdown. Default: None.
787
+ :param state (str, optional): Initial state of the dropdown ('normal', 'disabled'). Default: None.
788
+ :param searchable (bool): If True, allows typing to filter dropdown options. Default: False.
789
+ :param tooltip_txt (str, optional): Tooltip text to display on hover. Default: None.
790
+ :param tooltip_key (str, optional): Key for tooltip lookup in TOOLTIPS dictionary. For dictionary, see `simba.assets.lookups.tooptips.json`. Default: None.
791
+
792
+ :example:
793
+ >>> dropdown = SimBADropDown(parent=parent_frm, dropdown_options=['Option 1', 'Option 2', 'Option 3'], label='Select option:', searchable=True)
794
+ >>> selected = dropdown.get_value()
795
+ """
796
+ def __init__(self,
797
+ parent: Union[Frame, Canvas, LabelFrame, Toplevel, Tk],
798
+ dropdown_options: Union[Iterable[Any], List[Any], Tuple[Any]],
799
+ label: Optional[str] = None,
800
+ label_width: Optional[int] = None,
801
+ label_font: tuple = Formats.FONT_REGULAR.value,
802
+ label_bg_clr: Optional[str] = None,
803
+ dropdown_font_size: Optional[int] = None,
804
+ justify: Literal['left', 'right', 'center'] = 'center',
805
+ img: Optional[str] = None,
806
+ dropdown_width: Optional[int] = None,
807
+ command: Callable = None,
808
+ value: Optional[Any] = None,
809
+ state: Optional[str] = None,
810
+ searchable: bool = False,
811
+ tooltip_txt: Optional[str] = None,
812
+ tooltip_key: Optional[str] = None):
813
+
814
+
815
+ super().__init__(master=parent)
816
+ self.dropdown_var = StringVar()
817
+ self.columnconfigure(0, weight=0)
818
+ self.columnconfigure(1, weight=0)
819
+ if img is not None:
820
+ self.columnconfigure(2, weight=0)
821
+ self.dropdown_img_lbl = SimBALabel(parent=self, txt='', txt_clr='black', bg_clr=label_bg_clr, font=label_font, width=None, anchor='w', img=img, compound='left')
822
+ self.dropdown_img_lbl.grid(row=0, column=0, sticky="w")
823
+ else:
824
+ self.dropdown_img_lbl = None
825
+ self.dropdown_lbl = SimBALabel(parent=self, txt=label, txt_clr='black', bg_clr=label_bg_clr, font=label_font, width=label_width, anchor='w')
826
+ self.dropdown_lbl.grid(row=0, column=1, sticky=NW)
827
+ self.dropdown_options = dropdown_options
828
+ self.original_options = deepcopy(self.dropdown_options)
829
+ self.command, self.searchable = command, searchable
830
+ if dropdown_font_size is None:
831
+ drop_down_font = None
832
+ else:
833
+ drop_down_font = ("Poppins", dropdown_font_size)
834
+ self.combobox_state = 'normal' if searchable else "readonly"
835
+ self.dropdown = Combobox(self, textvariable=self.dropdown_var, font=drop_down_font, values=self.dropdown_options, state=self.combobox_state, width=dropdown_width, justify=justify)
836
+ if searchable: self.bind_combobox_keys()
837
+ self.dropdown.grid(row=0, column=2, sticky="w")
838
+ if value is not None: self.set_value(value=value)
839
+ if command is not None:
840
+ self.command = command
841
+ self.dropdown.bind("<<ComboboxSelected>>", self.on_select)
842
+ if state == 'disabled':
843
+ self.disable()
844
+ if isinstance(tooltip_txt, str):
845
+ CreateToolTip(widget=self.dropdown_lbl, text=tooltip_txt)
846
+ elif tooltip_key in TOOLTIPS.keys():
847
+ CreateToolTip(widget=self.dropdown_lbl, text=TOOLTIPS[tooltip_key])
848
+ if img is not None:
849
+ CreateToolTip(widget=self.dropdown_img_lbl, text=TOOLTIPS[tooltip_key])
850
+
851
+
852
+ def set_value(self, value: Any):
853
+ self.dropdown_var.set(value)
854
+
855
+ def get_value(self):
856
+ return self.dropdown_var.get()
857
+
858
+ def enable(self):
859
+ self.dropdown.configure(state="normal")
860
+
861
+ def disable(self):
862
+ self.dropdown.configure(state="disabled")
863
+
864
+ def getChoices(self):
865
+ return self.dropdown_var.get()
866
+
867
+ def setChoices(self, choice):
868
+ self.dropdown_var.set(choice)
869
+
870
+ def on_select(self, event):
871
+ selected_value = self.dropdown_var.get()
872
+ self.command(selected_value)
873
+ if self.searchable: self.dropdown['values'] = self.original_options
874
+
875
+ def set_width(self, width: int):
876
+ self.dropdown.configure(width=width)
877
+
878
+ def change_options(self, values: List[str], set_index: Optional[int] = None, set_str: Optional[str] = None, auto_change_width: Optional[bool] = True):
879
+ self.dropdown_var.set('')
880
+ self.dropdown['values'] = values
881
+ if isinstance(set_index, int) and (0 <= set_index <= len(values) - 1):
882
+ self.dropdown_var.set(values[set_index])
883
+ elif (set_str is not None) and (set_str in values):
884
+ self.dropdown_var.set(set_str)
885
+ elif self.searchable and (set_str is not None):
886
+ self.dropdown_var.set(set_str)
887
+ else:
888
+ self.dropdown_var.set(values[0])
889
+ if auto_change_width: self.set_width(width=max(5, max(len(s) for s in values)))
890
+ if self.dropdown['values'] == ('',) or len(values) == 0 and not self.searchable:
891
+ self.disable()
892
+ else:
893
+ self.enable()
894
+ self.dropdown.configure(state='normal' if self.searchable else "readonly")
895
+
896
+ def bind_combobox_keys(self):
897
+ self.dropdown.bind("<KeyRelease>", self._on_key_release)
898
+ if self.searchable:
899
+ self.dropdown.bind("<Button-1>", lambda e: self._reset_to_all_options())
900
+
901
+ def _on_key_release(self, event):
902
+ if event.keysym in ['Up', 'Down', 'Left', 'Right', 'Tab', 'Return']:
903
+ return
904
+ try:
905
+ cursor_pos = self.dropdown.index(INSERT)
906
+ except:
907
+ cursor_pos = len(self.dropdown_var.get())
908
+ current_text = self.dropdown_var.get()
909
+ filtered_options = [x for x in self.original_options if current_text.lower() in x.lower()]
910
+ if current_text != '':
911
+ self.dropdown['values'] = filtered_options
912
+ # Restore the text and cursor position
913
+ self.dropdown_var.set(current_text)
914
+ self.dropdown.icursor(cursor_pos)
915
+ else:
916
+ self.dropdown['values'] = self.original_options
917
+ self.dropdown_var.set('')
918
+ self.dropdown.icursor(0)
919
+
920
+ def _reset_to_all_options(self):
921
+ if self.searchable and self.dropdown_var.get() == '':
922
+ self.dropdown['values'] = self.original_options
923
+
924
+ class DropDownMenu(Frame):
925
+
926
+ """
927
+ Legacy, use :func:`simba.ui.tkinter_functions.SimBADropDown`.
928
+ """
929
+ def __init__(self,
930
+ parent=None,
931
+ dropdownLabel="",
932
+ choice_dict=None,
933
+ labelwidth="",
934
+ com=None,
935
+ val: Optional[Any] = None,
936
+ **kw):
937
+ Frame.__init__(self, master=parent, **kw)
938
+ self.dropdownvar = StringVar()
939
+ self.lblName = Label(self, text=dropdownLabel, width=labelwidth, anchor=W, font=Formats.FONT_REGULAR.value)
940
+ self.lblName.grid(row=0, column=0)
941
+ self.choices = choice_dict
942
+ self.popupMenu = OptionMenu(self, self.dropdownvar, *self.choices, command=com)
943
+ self.popupMenu.grid(row=0, column=1)
944
+ if val is not None:
945
+ self.setChoices(val)
946
+ def getChoices(self):
947
+ return self.dropdownvar.get()
948
+ def setChoices(self, choice):
949
+ self.dropdownvar.set(choice)
950
+ def enable(self):
951
+ self.popupMenu.configure(state="normal")
952
+ def disable(self):
953
+ self.popupMenu.configure(state="disable")
954
+
955
+
956
+
957
+ class FileSelect(Frame):
958
+ def __init__(self,
959
+ parent=None,
960
+ fileDescription="",
961
+ color=None,
962
+ title: Optional[str] = None,
963
+ lblwidth: Optional[int] = None,
964
+ file_types=None,
965
+ bg_clr: Optional[str] = 'white',
966
+ dropdown: Union[DropDownMenu, SimBADropDown] = None,
967
+ entry_width: Optional[int] = 20,
968
+ status: Optional[str] = None,
969
+ lbl_icon: Optional[str] = None,
970
+ font: Tuple = Formats.FONT_REGULAR.value,
971
+ initialdir: Optional[Union[str, os.PathLike]] = None,
972
+ initial_path: Optional[Union[str, os.PathLike]] = None,
973
+ tooltip_txt: Optional[str] = None,
974
+ tooltip_key: Optional[str] = None,
975
+ **kw):
976
+
977
+ self.title, self.dropdown, self.initialdir = title, dropdown, initialdir
978
+ self.file_type = file_types
979
+ self.color = color if color is not None else "black"
980
+ self.lblwidth = lblwidth if lblwidth is not None else 0
981
+ self.parent = parent
982
+ Frame.__init__(self, master=parent, **kw)
983
+ browse_icon = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS["browse"]["icon_path"]))
984
+ self.columnconfigure(0, weight=0)
985
+ self.columnconfigure(1, weight=0)
986
+ self.columnconfigure(2, weight=0)
987
+ if lbl_icon is not None:
988
+ self.columnconfigure(3, weight=0)
989
+ self.lbl_icon = SimBALabel(parent=self, txt='', txt_clr='black', font=font, width=None, anchor='w', img=lbl_icon, compound=None)
990
+ self.lbl_icon.grid(row=0, column=0, sticky="w")
991
+ else:
992
+ self.lbl_icon = None
993
+ self.filePath = StringVar()
994
+ self.lblName = Label(self, text=fileDescription, fg=str(self.color), width=str(self.lblwidth), anchor=W, font=Formats.FONT_REGULAR.value)
995
+ self.lblName.grid(row=0, column=1, sticky=W)
996
+ self.entPath = Label(self, textvariable=self.filePath, relief=SUNKEN, font=Formats.FONT_REGULAR.value, bg=bg_clr, width=entry_width)
997
+ self.entPath.grid(row=0, column=2)
998
+ self.btnFind = SimbaButton(parent=self, txt=Defaults.BROWSE_FILE_BTN_TEXT.value, font=font, cmd=self.setFilePath, img=browse_icon)
999
+ self.btnFind.grid(row=0, column=3)
1000
+ self.filePath.set(Defaults.NO_FILE_SELECTED_TEXT.value)
1001
+ if initial_path is not None:
1002
+ self.filePath.set(initial_path)
1003
+ if status is not None:
1004
+ self.set_state(setstatus=status)
1005
+ if tooltip_txt is not None and isinstance(tooltip_txt, str):
1006
+ CreateToolTip(widget=self.lblName, text=tooltip_txt)
1007
+ elif tooltip_key in TOOLTIPS.keys():
1008
+ CreateToolTip(widget=self.lblName, text=TOOLTIPS[tooltip_key])
1009
+
1010
+ def setFilePath(self):
1011
+ if self.initialdir is not None:
1012
+ if not os.path.isdir(self.initialdir):
1013
+ self.initialdir = None
1014
+ else:
1015
+ pass
1016
+
1017
+ if self.file_type:
1018
+ file_selected = askopenfilename(title=self.title, parent=self.parent, filetypes=self.file_type, initialdir=self.initialdir)
1019
+ else:
1020
+ file_selected = askopenfilename(title=self.title, parent=self.parent, initialdir=self.initialdir)
1021
+ if file_selected:
1022
+ if self.dropdown is not None:
1023
+ _, name, _ = get_fn_ext(filepath=file_selected)
1024
+ self.dropdown.setChoices(name)
1025
+ self.filePath.set(name)
1026
+ else:
1027
+ self.filePath.set(file_selected)
1028
+ self.entPath.configure(width=len(file_selected)+10)
1029
+
1030
+ else:
1031
+ self.filePath.set(Defaults.NO_FILE_SELECTED_TEXT.value)
1032
+ self.entPath.configure(width=len(Defaults.NO_FILE_SELECTED_TEXT.value) + 10)
1033
+
1034
+ @property
1035
+ def file_path(self):
1036
+ return self.filePath.get()
1037
+
1038
+ def set_state(self, setstatus):
1039
+ self.entPath.config(state=setstatus)
1040
+ self.btnFind["state"] = setstatus
1041
+
1042
+
1043
+ def SimBARadioButton(parent: Union[Frame, Canvas, LabelFrame, Toplevel],
1044
+ txt: str,
1045
+ variable: Union[BooleanVar, StringVar],
1046
+ txt_clr: Optional[str] = 'black',
1047
+ font: Optional[Tuple] = Formats.FONT_REGULAR.value,
1048
+ compound: Optional[str] = 'left',
1049
+ img: Optional[Union[ImageTk.PhotoImage, str]] = None,
1050
+ enabled: Optional[bool] = True,
1051
+ tooltip_txt: Optional[str] = None,
1052
+ value: bool = False,
1053
+ cmd: Optional[Callable] = None,
1054
+ cmd_kwargs: Optional[Dict[Any, Any]] = None) -> Radiobutton:
1055
+
1056
+ if isinstance(img, str):
1057
+ img = ImageTk.PhotoImage(image=PIL.Image.open(MENU_ICONS[img]["icon_path"]))
1058
+
1059
+ if cmd_kwargs is None:
1060
+ cmd_kwargs = {}
1061
+
1062
+ def execute_command():
1063
+ if cmd:
1064
+ evaluated_kwargs = {k: (v() if callable(v) else v) for k, v in cmd_kwargs.items()}
1065
+ cmd(**evaluated_kwargs)
1066
+
1067
+ if cmd is not None:
1068
+ command = execute_command
1069
+ else:
1070
+ command = None
1071
+
1072
+ btn = Radiobutton(parent,
1073
+ text=txt,
1074
+ font=font,
1075
+ image=img,
1076
+ fg=txt_clr,
1077
+ variable=variable,
1078
+ value=value,
1079
+ compound=compound,
1080
+ command=command)
1081
+
1082
+ if img is not None:
1083
+ btn.image = img
1084
+
1085
+ if not enabled:
1086
+ btn.config(state=DISABLED)
1087
+
1088
+ if tooltip_txt is not None:
1089
+ CreateToolTip(widget=btn, text=tooltip_txt)
1090
+
1091
+ return btn