jpayne@68
|
1 """
|
jpayne@68
|
2 Tkinter GUI progressbar decorator for iterators.
|
jpayne@68
|
3
|
jpayne@68
|
4 Usage:
|
jpayne@68
|
5 >>> from tqdm.tk import trange, tqdm
|
jpayne@68
|
6 >>> for i in trange(10):
|
jpayne@68
|
7 ... ...
|
jpayne@68
|
8 """
|
jpayne@68
|
9 import re
|
jpayne@68
|
10 import sys
|
jpayne@68
|
11 import tkinter
|
jpayne@68
|
12 import tkinter.ttk as ttk
|
jpayne@68
|
13 from warnings import warn
|
jpayne@68
|
14
|
jpayne@68
|
15 from .std import TqdmExperimentalWarning, TqdmWarning
|
jpayne@68
|
16 from .std import tqdm as std_tqdm
|
jpayne@68
|
17
|
jpayne@68
|
18 __author__ = {"github.com/": ["richardsheridan", "casperdcl"]}
|
jpayne@68
|
19 __all__ = ['tqdm_tk', 'ttkrange', 'tqdm', 'trange']
|
jpayne@68
|
20
|
jpayne@68
|
21
|
jpayne@68
|
22 class tqdm_tk(std_tqdm): # pragma: no cover
|
jpayne@68
|
23 """
|
jpayne@68
|
24 Experimental Tkinter GUI version of tqdm!
|
jpayne@68
|
25
|
jpayne@68
|
26 Note: Window interactivity suffers if `tqdm_tk` is not running within
|
jpayne@68
|
27 a Tkinter mainloop and values are generated infrequently. In this case,
|
jpayne@68
|
28 consider calling `tqdm_tk.refresh()` frequently in the Tk thread.
|
jpayne@68
|
29 """
|
jpayne@68
|
30
|
jpayne@68
|
31 # TODO: @classmethod: write()?
|
jpayne@68
|
32
|
jpayne@68
|
33 def __init__(self, *args, **kwargs):
|
jpayne@68
|
34 """
|
jpayne@68
|
35 This class accepts the following parameters *in addition* to
|
jpayne@68
|
36 the parameters accepted by `tqdm`.
|
jpayne@68
|
37
|
jpayne@68
|
38 Parameters
|
jpayne@68
|
39 ----------
|
jpayne@68
|
40 grab : bool, optional
|
jpayne@68
|
41 Grab the input across all windows of the process.
|
jpayne@68
|
42 tk_parent : `tkinter.Wm`, optional
|
jpayne@68
|
43 Parent Tk window.
|
jpayne@68
|
44 cancel_callback : Callable, optional
|
jpayne@68
|
45 Create a cancel button and set `cancel_callback` to be called
|
jpayne@68
|
46 when the cancel or window close button is clicked.
|
jpayne@68
|
47 """
|
jpayne@68
|
48 kwargs = kwargs.copy()
|
jpayne@68
|
49 kwargs['gui'] = True
|
jpayne@68
|
50 # convert disable = None to False
|
jpayne@68
|
51 kwargs['disable'] = bool(kwargs.get('disable', False))
|
jpayne@68
|
52 self._warn_leave = 'leave' in kwargs
|
jpayne@68
|
53 grab = kwargs.pop('grab', False)
|
jpayne@68
|
54 tk_parent = kwargs.pop('tk_parent', None)
|
jpayne@68
|
55 self._cancel_callback = kwargs.pop('cancel_callback', None)
|
jpayne@68
|
56 super().__init__(*args, **kwargs)
|
jpayne@68
|
57
|
jpayne@68
|
58 if self.disable:
|
jpayne@68
|
59 return
|
jpayne@68
|
60
|
jpayne@68
|
61 if tk_parent is None: # Discover parent widget
|
jpayne@68
|
62 try:
|
jpayne@68
|
63 tk_parent = tkinter._default_root
|
jpayne@68
|
64 except AttributeError:
|
jpayne@68
|
65 raise AttributeError(
|
jpayne@68
|
66 "`tk_parent` required when using `tkinter.NoDefaultRoot()`")
|
jpayne@68
|
67 if tk_parent is None: # use new default root window as display
|
jpayne@68
|
68 self._tk_window = tkinter.Tk()
|
jpayne@68
|
69 else: # some other windows already exist
|
jpayne@68
|
70 self._tk_window = tkinter.Toplevel()
|
jpayne@68
|
71 else:
|
jpayne@68
|
72 self._tk_window = tkinter.Toplevel(tk_parent)
|
jpayne@68
|
73
|
jpayne@68
|
74 warn("GUI is experimental/alpha", TqdmExperimentalWarning, stacklevel=2)
|
jpayne@68
|
75 self._tk_dispatching = self._tk_dispatching_helper()
|
jpayne@68
|
76
|
jpayne@68
|
77 self._tk_window.protocol("WM_DELETE_WINDOW", self.cancel)
|
jpayne@68
|
78 self._tk_window.wm_title(self.desc)
|
jpayne@68
|
79 self._tk_window.wm_attributes("-topmost", 1)
|
jpayne@68
|
80 self._tk_window.after(0, lambda: self._tk_window.wm_attributes("-topmost", 0))
|
jpayne@68
|
81 self._tk_n_var = tkinter.DoubleVar(self._tk_window, value=0)
|
jpayne@68
|
82 self._tk_text_var = tkinter.StringVar(self._tk_window)
|
jpayne@68
|
83 pbar_frame = ttk.Frame(self._tk_window, padding=5)
|
jpayne@68
|
84 pbar_frame.pack()
|
jpayne@68
|
85 _tk_label = ttk.Label(pbar_frame, textvariable=self._tk_text_var,
|
jpayne@68
|
86 wraplength=600, anchor="center", justify="center")
|
jpayne@68
|
87 _tk_label.pack()
|
jpayne@68
|
88 self._tk_pbar = ttk.Progressbar(
|
jpayne@68
|
89 pbar_frame, variable=self._tk_n_var, length=450)
|
jpayne@68
|
90 if self.total is not None:
|
jpayne@68
|
91 self._tk_pbar.configure(maximum=self.total)
|
jpayne@68
|
92 else:
|
jpayne@68
|
93 self._tk_pbar.configure(mode="indeterminate")
|
jpayne@68
|
94 self._tk_pbar.pack()
|
jpayne@68
|
95 if self._cancel_callback is not None:
|
jpayne@68
|
96 _tk_button = ttk.Button(pbar_frame, text="Cancel", command=self.cancel)
|
jpayne@68
|
97 _tk_button.pack()
|
jpayne@68
|
98 if grab:
|
jpayne@68
|
99 self._tk_window.grab_set()
|
jpayne@68
|
100
|
jpayne@68
|
101 def close(self):
|
jpayne@68
|
102 if self.disable:
|
jpayne@68
|
103 return
|
jpayne@68
|
104
|
jpayne@68
|
105 self.disable = True
|
jpayne@68
|
106
|
jpayne@68
|
107 with self.get_lock():
|
jpayne@68
|
108 self._instances.remove(self)
|
jpayne@68
|
109
|
jpayne@68
|
110 def _close():
|
jpayne@68
|
111 self._tk_window.after('idle', self._tk_window.destroy)
|
jpayne@68
|
112 if not self._tk_dispatching:
|
jpayne@68
|
113 self._tk_window.update()
|
jpayne@68
|
114
|
jpayne@68
|
115 self._tk_window.protocol("WM_DELETE_WINDOW", _close)
|
jpayne@68
|
116
|
jpayne@68
|
117 # if leave is set but we are self-dispatching, the left window is
|
jpayne@68
|
118 # totally unresponsive unless the user manually dispatches
|
jpayne@68
|
119 if not self.leave:
|
jpayne@68
|
120 _close()
|
jpayne@68
|
121 elif not self._tk_dispatching:
|
jpayne@68
|
122 if self._warn_leave:
|
jpayne@68
|
123 warn("leave flag ignored if not in tkinter mainloop",
|
jpayne@68
|
124 TqdmWarning, stacklevel=2)
|
jpayne@68
|
125 _close()
|
jpayne@68
|
126
|
jpayne@68
|
127 def clear(self, *_, **__):
|
jpayne@68
|
128 pass
|
jpayne@68
|
129
|
jpayne@68
|
130 def display(self, *_, **__):
|
jpayne@68
|
131 self._tk_n_var.set(self.n)
|
jpayne@68
|
132 d = self.format_dict
|
jpayne@68
|
133 # remove {bar}
|
jpayne@68
|
134 d['bar_format'] = (d['bar_format'] or "{l_bar}<bar/>{r_bar}").replace(
|
jpayne@68
|
135 "{bar}", "<bar/>")
|
jpayne@68
|
136 msg = self.format_meter(**d)
|
jpayne@68
|
137 if '<bar/>' in msg:
|
jpayne@68
|
138 msg = "".join(re.split(r'\|?<bar/>\|?', msg, maxsplit=1))
|
jpayne@68
|
139 self._tk_text_var.set(msg)
|
jpayne@68
|
140 if not self._tk_dispatching:
|
jpayne@68
|
141 self._tk_window.update()
|
jpayne@68
|
142
|
jpayne@68
|
143 def set_description(self, desc=None, refresh=True):
|
jpayne@68
|
144 self.set_description_str(desc, refresh)
|
jpayne@68
|
145
|
jpayne@68
|
146 def set_description_str(self, desc=None, refresh=True):
|
jpayne@68
|
147 self.desc = desc
|
jpayne@68
|
148 if not self.disable:
|
jpayne@68
|
149 self._tk_window.wm_title(desc)
|
jpayne@68
|
150 if refresh and not self._tk_dispatching:
|
jpayne@68
|
151 self._tk_window.update()
|
jpayne@68
|
152
|
jpayne@68
|
153 def cancel(self):
|
jpayne@68
|
154 """
|
jpayne@68
|
155 `cancel_callback()` followed by `close()`
|
jpayne@68
|
156 when close/cancel buttons clicked.
|
jpayne@68
|
157 """
|
jpayne@68
|
158 if self._cancel_callback is not None:
|
jpayne@68
|
159 self._cancel_callback()
|
jpayne@68
|
160 self.close()
|
jpayne@68
|
161
|
jpayne@68
|
162 def reset(self, total=None):
|
jpayne@68
|
163 """
|
jpayne@68
|
164 Resets to 0 iterations for repeated use.
|
jpayne@68
|
165
|
jpayne@68
|
166 Parameters
|
jpayne@68
|
167 ----------
|
jpayne@68
|
168 total : int or float, optional. Total to use for the new bar.
|
jpayne@68
|
169 """
|
jpayne@68
|
170 if hasattr(self, '_tk_pbar'):
|
jpayne@68
|
171 if total is None:
|
jpayne@68
|
172 self._tk_pbar.configure(maximum=100, mode="indeterminate")
|
jpayne@68
|
173 else:
|
jpayne@68
|
174 self._tk_pbar.configure(maximum=total, mode="determinate")
|
jpayne@68
|
175 super().reset(total=total)
|
jpayne@68
|
176
|
jpayne@68
|
177 @staticmethod
|
jpayne@68
|
178 def _tk_dispatching_helper():
|
jpayne@68
|
179 """determine if Tkinter mainloop is dispatching events"""
|
jpayne@68
|
180 codes = {tkinter.mainloop.__code__, tkinter.Misc.mainloop.__code__}
|
jpayne@68
|
181 for frame in sys._current_frames().values():
|
jpayne@68
|
182 while frame:
|
jpayne@68
|
183 if frame.f_code in codes:
|
jpayne@68
|
184 return True
|
jpayne@68
|
185 frame = frame.f_back
|
jpayne@68
|
186 return False
|
jpayne@68
|
187
|
jpayne@68
|
188
|
jpayne@68
|
189 def ttkrange(*args, **kwargs):
|
jpayne@68
|
190 """Shortcut for `tqdm.tk.tqdm(range(*args), **kwargs)`."""
|
jpayne@68
|
191 return tqdm_tk(range(*args), **kwargs)
|
jpayne@68
|
192
|
jpayne@68
|
193
|
jpayne@68
|
194 # Aliases
|
jpayne@68
|
195 tqdm = tqdm_tk
|
jpayne@68
|
196 trange = ttkrange
|