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