|
""" |
|
Tkinter GUI progressbar decorator for iterators. |
|
|
|
Usage: |
|
>>> from tqdm.tk import trange, tqdm |
|
>>> for i in trange(10): |
|
... ... |
|
""" |
|
import re |
|
import sys |
|
import tkinter |
|
import tkinter.ttk as ttk |
|
from warnings import warn |
|
|
|
from .std import TqdmExperimentalWarning, TqdmWarning |
|
from .std import tqdm as std_tqdm |
|
|
|
__author__ = {"github.com/": ["richardsheridan", "casperdcl"]} |
|
__all__ = ['tqdm_tk', 'ttkrange', 'tqdm', 'trange'] |
|
|
|
|
|
class tqdm_tk(std_tqdm): |
|
""" |
|
Experimental Tkinter GUI version of tqdm! |
|
|
|
Note: Window interactivity suffers if `tqdm_tk` is not running within |
|
a Tkinter mainloop and values are generated infrequently. In this case, |
|
consider calling `tqdm_tk.refresh()` frequently in the Tk thread. |
|
""" |
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs): |
|
""" |
|
This class accepts the following parameters *in addition* to |
|
the parameters accepted by `tqdm`. |
|
|
|
Parameters |
|
---------- |
|
grab : bool, optional |
|
Grab the input across all windows of the process. |
|
tk_parent : `tkinter.Wm`, optional |
|
Parent Tk window. |
|
cancel_callback : Callable, optional |
|
Create a cancel button and set `cancel_callback` to be called |
|
when the cancel or window close button is clicked. |
|
""" |
|
kwargs = kwargs.copy() |
|
kwargs['gui'] = True |
|
|
|
kwargs['disable'] = bool(kwargs.get('disable', False)) |
|
self._warn_leave = 'leave' in kwargs |
|
grab = kwargs.pop('grab', False) |
|
tk_parent = kwargs.pop('tk_parent', None) |
|
self._cancel_callback = kwargs.pop('cancel_callback', None) |
|
super().__init__(*args, **kwargs) |
|
|
|
if self.disable: |
|
return |
|
|
|
if tk_parent is None: |
|
try: |
|
tk_parent = tkinter._default_root |
|
except AttributeError: |
|
raise AttributeError( |
|
"`tk_parent` required when using `tkinter.NoDefaultRoot()`") |
|
if tk_parent is None: |
|
self._tk_window = tkinter.Tk() |
|
else: |
|
self._tk_window = tkinter.Toplevel() |
|
else: |
|
self._tk_window = tkinter.Toplevel(tk_parent) |
|
|
|
warn("GUI is experimental/alpha", TqdmExperimentalWarning, stacklevel=2) |
|
self._tk_dispatching = self._tk_dispatching_helper() |
|
|
|
self._tk_window.protocol("WM_DELETE_WINDOW", self.cancel) |
|
self._tk_window.wm_title(self.desc) |
|
self._tk_window.wm_attributes("-topmost", 1) |
|
self._tk_window.after(0, lambda: self._tk_window.wm_attributes("-topmost", 0)) |
|
self._tk_n_var = tkinter.DoubleVar(self._tk_window, value=0) |
|
self._tk_text_var = tkinter.StringVar(self._tk_window) |
|
pbar_frame = ttk.Frame(self._tk_window, padding=5) |
|
pbar_frame.pack() |
|
_tk_label = ttk.Label(pbar_frame, textvariable=self._tk_text_var, |
|
wraplength=600, anchor="center", justify="center") |
|
_tk_label.pack() |
|
self._tk_pbar = ttk.Progressbar( |
|
pbar_frame, variable=self._tk_n_var, length=450) |
|
if self.total is not None: |
|
self._tk_pbar.configure(maximum=self.total) |
|
else: |
|
self._tk_pbar.configure(mode="indeterminate") |
|
self._tk_pbar.pack() |
|
if self._cancel_callback is not None: |
|
_tk_button = ttk.Button(pbar_frame, text="Cancel", command=self.cancel) |
|
_tk_button.pack() |
|
if grab: |
|
self._tk_window.grab_set() |
|
|
|
def close(self): |
|
if self.disable: |
|
return |
|
|
|
self.disable = True |
|
|
|
with self.get_lock(): |
|
self._instances.remove(self) |
|
|
|
def _close(): |
|
self._tk_window.after('idle', self._tk_window.destroy) |
|
if not self._tk_dispatching: |
|
self._tk_window.update() |
|
|
|
self._tk_window.protocol("WM_DELETE_WINDOW", _close) |
|
|
|
|
|
|
|
if not self.leave: |
|
_close() |
|
elif not self._tk_dispatching: |
|
if self._warn_leave: |
|
warn("leave flag ignored if not in tkinter mainloop", |
|
TqdmWarning, stacklevel=2) |
|
_close() |
|
|
|
def clear(self, *_, **__): |
|
pass |
|
|
|
def display(self, *_, **__): |
|
self._tk_n_var.set(self.n) |
|
d = self.format_dict |
|
|
|
d['bar_format'] = (d['bar_format'] or "{l_bar}<bar/>{r_bar}").replace( |
|
"{bar}", "<bar/>") |
|
msg = self.format_meter(**d) |
|
if '<bar/>' in msg: |
|
msg = "".join(re.split(r'\|?<bar/>\|?', msg, maxsplit=1)) |
|
self._tk_text_var.set(msg) |
|
if not self._tk_dispatching: |
|
self._tk_window.update() |
|
|
|
def set_description(self, desc=None, refresh=True): |
|
self.set_description_str(desc, refresh) |
|
|
|
def set_description_str(self, desc=None, refresh=True): |
|
self.desc = desc |
|
if not self.disable: |
|
self._tk_window.wm_title(desc) |
|
if refresh and not self._tk_dispatching: |
|
self._tk_window.update() |
|
|
|
def cancel(self): |
|
""" |
|
`cancel_callback()` followed by `close()` |
|
when close/cancel buttons clicked. |
|
""" |
|
if self._cancel_callback is not None: |
|
self._cancel_callback() |
|
self.close() |
|
|
|
def reset(self, total=None): |
|
""" |
|
Resets to 0 iterations for repeated use. |
|
|
|
Parameters |
|
---------- |
|
total : int or float, optional. Total to use for the new bar. |
|
""" |
|
if hasattr(self, '_tk_pbar'): |
|
if total is None: |
|
self._tk_pbar.configure(maximum=100, mode="indeterminate") |
|
else: |
|
self._tk_pbar.configure(maximum=total, mode="determinate") |
|
super().reset(total=total) |
|
|
|
@staticmethod |
|
def _tk_dispatching_helper(): |
|
"""determine if Tkinter mainloop is dispatching events""" |
|
codes = {tkinter.mainloop.__code__, tkinter.Misc.mainloop.__code__} |
|
for frame in sys._current_frames().values(): |
|
while frame: |
|
if frame.f_code in codes: |
|
return True |
|
frame = frame.f_back |
|
return False |
|
|
|
|
|
def ttkrange(*args, **kwargs): |
|
"""Shortcut for `tqdm.tk.tqdm(range(*args), **kwargs)`.""" |
|
return tqdm_tk(range(*args), **kwargs) |
|
|
|
|
|
|
|
tqdm = tqdm_tk |
|
trange = ttkrange |
|
|