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