diff --git a/tqdm/gui.py b/tqdm/gui.py index 2c90f413..0d8714b2 100644 --- a/tqdm/gui.py +++ b/tqdm/gui.py @@ -25,7 +25,7 @@ __all__ = ['tqdm_gui', 'tgrange', 'tqdm', 'trange'] class tqdm_gui(std_tqdm): # pragma: no cover """ - Experimental GUI version of tqdm! + Experimental Matplotlib GUI version of tqdm! """ # TODO: @classmethod: write() on GUI? @@ -40,11 +40,10 @@ class tqdm_gui(std_tqdm): # pragma: no cover colour = kwargs.pop('colour', 'g') super(tqdm_gui, self).__init__(*args, **kwargs) - # Initialize the GUI display - if self.disable or not kwargs['gui']: + if self.disable: return - warn('GUI is experimental/alpha', TqdmExperimentalWarning, stacklevel=2) + warn("GUI is experimental/alpha", TqdmExperimentalWarning, stacklevel=2) self.mpl = mpl self.plt = plt @@ -69,8 +68,8 @@ class tqdm_gui(std_tqdm): # pragma: no cover ax.set_ylim(0, 0.001) if total is not None: ax.set_xlim(0, 100) - ax.set_xlabel('percent') - self.fig.legend((self.line1, self.line2), ('cur', 'est'), + ax.set_xlabel("percent") + self.fig.legend((self.line1, self.line2), ("cur", "est"), loc='center right') # progressbar self.hspan = plt.axhspan(0, 0.001, xmin=0, xmax=0, color=colour) @@ -78,11 +77,11 @@ class tqdm_gui(std_tqdm): # pragma: no cover # ax.set_xlim(-60, 0) ax.set_xlim(0, 60) ax.invert_xaxis() - ax.set_xlabel('seconds') - ax.legend(('cur', 'est'), loc='lower left') + ax.set_xlabel("seconds") + ax.legend(("cur", "est"), loc='lower left') ax.grid() # ax.set_xlabel('seconds') - ax.set_ylabel((self.unit if self.unit else 'it') + '/s') + ax.set_ylabel((self.unit if self.unit else "it") + "/s") if self.unit_scale: plt.ticklabel_format(style='sci', axis='y', scilimits=(0, 0)) ax.yaxis.get_offset_text().set_x(-0.15) @@ -93,8 +92,6 @@ class tqdm_gui(std_tqdm): # pragma: no cover self.ax = ax def close(self): - # if not self.gui: - # return super(tqdm_gui, self).close() if self.disable: return @@ -175,7 +172,7 @@ class tqdm_gui(std_tqdm): # pragma: no cover line2.set_data(t_ago, zdata) d = self.format_dict - d["ncols"] = 0 + d['ncols'] = 0 ax.set_title(self.format_meter(**d), fontname="DejaVu Sans Mono", fontsize=11) self.plt.pause(1e-9) diff --git a/tqdm/tk.py b/tqdm/tk.py new file mode 100644 index 00000000..f8bb93e5 --- /dev/null +++ b/tqdm/tk.py @@ -0,0 +1,207 @@ +""" +GUI progressbar decorator for iterators. +Includes a default `range` iterator printing to `stderr`. + +Usage: +>>> from tqdm.gui import trange, tqdm +>>> for i in trange(10): +... ... +""" +from __future__ import absolute_import, division + +import sys +from warnings import warn + +try: + import tkinter + import tkinter.ttk as ttk +except ImportError: + import Tkinter as tkinter + import ttk as ttk + +from .std import TqdmExperimentalWarning, TqdmWarning +from .std import tqdm as std_tqdm +from .utils import _range + +__author__ = {"github.com/": ["richardsheridan", "casperdcl"]} +__all__ = ['tqdm_tk', 'ttkrange', 'tqdm', 'trange'] + + +class tqdm_tk(std_tqdm): # pragma: no cover + """ + 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. + """ + + # TODO: @classmethod: write()? + + 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['bar_format'] = kwargs.get( + 'bar_format', "{l_bar}{r_bar}").replace("{bar}", "") + # convert disable = None to False + 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(tqdm_tk, self).__init__(*args, **kwargs) + + if self.disable: + return + + if tk_parent is None: # Discover parent widget + try: + tk_parent = tkinter._default_root + except AttributeError: + raise AttributeError( + "`tk_parent` required when using `tkinter.NoDefaultRoot()`") + if tk_parent is None: # use new default root window as display + self._tk_window = tkinter.Tk() + else: # some other windows already exist + 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 leave is set but we are self-dispatching, the left window is + # totally unresponsive unless the user manually dispatches + 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['ncols'] = None + text = self.format_meter(**d) + # fixup only default bar format + self._tk_text_var.set( + text.replace("||", "", 1) if "{l_bar}{r_bar}" in self.bar_format else text) + 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(tqdm_tk, self).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): + """ + A shortcut for `tqdm.gui.tqdm_tk(xrange(*args), **kwargs)`. + On Python3+, `range` is used instead of `xrange`. + """ + return tqdm_tk(_range(*args), **kwargs) + + +# Aliases +tqdm = tqdm_tk +trange = ttkrange