Default repository for tkpane.py.
Revisión | 9d80a794577eac1794f919592b75aff4b0e419fa (tree) |
---|---|
Tiempo | 2018-01-30 14:24:16 |
Autor | ![]() |
Commiter | Dreas Nielsen |
First commit. Working TkPane and several general pane types in lib.py.
@@ -0,0 +1,124 @@ | ||
1 | +#!/usr/bin/python | |
2 | +# test_1.py | |
3 | +# | |
4 | +# PURPOSE | |
5 | +# | |
6 | +# | |
7 | +# NOTES | |
8 | +# 1. | |
9 | +# | |
10 | +# AUTHOR | |
11 | +# Dreas Nielsen | |
12 | +# | |
13 | +# HISTORY | |
14 | +# Date Remarks | |
15 | +# ---------- ----------------------------------------------------------- | |
16 | +# 2018-01-26 Created. RDN. | |
17 | +# 2018-01-27 Added button and message panes. RDN. | |
18 | +# 2018-01-28 Added application frame for additional padding. RDN. | |
19 | +# ========================================================================= | |
20 | + | |
21 | +try: | |
22 | + import Tkinter as tk | |
23 | +except: | |
24 | + import tkinter as tk | |
25 | + | |
26 | +import sys | |
27 | +sys.path.append("..") | |
28 | + | |
29 | +from tkpane import * | |
30 | +import tkpane.lib | |
31 | +import tklayout | |
32 | + | |
33 | +# Add a method to the AppLayout class to get a pane: the first child of | |
34 | +# a frame's widgets. | |
35 | +def layout_pane(self, pane_name): | |
36 | + return self.frame_widgets(pane_name)[0] | |
37 | + | |
38 | +tklayout.AppLayout.pane = layout_pane | |
39 | + | |
40 | + | |
41 | +# Create simple sets of data to display in the TableDisplayPane. | |
42 | +ds1_headers = ["column_1", "column_2", "column_3"] | |
43 | +ds1_data = [["abcde", 25.1, 5], | |
44 | + ["fghij", 87.3, 8], | |
45 | + ["klmno", 12.8, 3]] | |
46 | + | |
47 | +ds2_headers = ["row_id","row_number","long_text","some_date","some_number"] | |
48 | +ds2_data = [ | |
49 | + ["Row 4",4,"Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.","1951-03-19",61.9917173461], | |
50 | + ["Row 8",8,"Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus.","1977-07-21",34.5729855806], | |
51 | + ["Row 12",12,"DJs flock by when MTV ax quiz prog.","1983-10-12",2.3773967111] | |
52 | + ] | |
53 | + | |
54 | +def build_message_pane(parent): | |
55 | + tkpane.lib.MessagePane(parent, "Enter user credentials and an output directory to enable data operations.") | |
56 | + | |
57 | +def build_table_pane(parent): | |
58 | + tkpane.lib.TableDisplayPane(parent, "This is a demo with a message that is long enough that it should wrap when the window is resized to a relatively small size.", ds1_headers, ds1_data) | |
59 | + | |
60 | +def build_button_pane(parent): | |
61 | + def no_action(): | |
62 | + pass | |
63 | + tkpane.lib.OkCancelPane(parent, no_action, no_action) | |
64 | + | |
65 | +# Lay out the panes | |
66 | +lo = tklayout.AppLayout() | |
67 | +inp_panes = lo.row_elements(["user_pane", "output_pane"], row_weight=0) | |
68 | +app = lo.column_elements(["message_pane", inp_panes, "table_pane", "button_pane", "status_pane"], row_weights=[0,0,1,0,0]) | |
69 | + | |
70 | +root = tk.Tk() | |
71 | + | |
72 | +# Use an extra frame within the root element with padding to add extra space | |
73 | +# around the outermost app widgets. | |
74 | +appframe = tk.Frame(root, padx=11, pady=11) | |
75 | +appframe.pack(expand=True, fill=tk.BOTH) | |
76 | + | |
77 | +lo.create_layout(appframe, app) | |
78 | + | |
79 | +lo.build_elements({"message_pane": build_message_pane, | |
80 | + "user_pane": tkpane.lib.UserPane, | |
81 | + "output_pane": tkpane.lib.OutputDirPane, | |
82 | + "table_pane": build_table_pane, | |
83 | + "button_pane": build_button_pane, | |
84 | + "status_pane": tkpane.lib.StatusProgressPane | |
85 | + }) | |
86 | + | |
87 | + | |
88 | +# Start the demo application with the 'OK' button disabled. | |
89 | +button_pane = lo.pane("button_pane") | |
90 | +button_pane.disable([]) | |
91 | + | |
92 | +# Require a user name and output directory to be entered for the 'OK' button | |
93 | +# to be enabled. | |
94 | +button_pane.keys_to_enable = ["name", "output_dir"] | |
95 | + | |
96 | +user_pane = lo.pane("user_pane") | |
97 | +user_pane.enable_callbacks = [button_pane.enable] | |
98 | +output_pane = lo.pane("output_pane") | |
99 | +output_pane.enable_callbacks = [button_pane.enable] | |
100 | +output_pane.clear_callbacks = [button_pane.disable] | |
101 | +output_pane.disable_callbacks = [button_pane.disable] | |
102 | + | |
103 | +# Make the buttons change the data and the status bar. | |
104 | +status_pane = lo.pane("status_pane") | |
105 | +table_pane = lo.pane("table_pane") | |
106 | +def ok_click(*args): | |
107 | + # When this is bound, it will receive event arguments, which should be ignored. | |
108 | + status_pane.set_status("OK button clicked.") | |
109 | + status_pane.set_value(50) | |
110 | + table_pane.display_data(ds2_headers, ds2_data) | |
111 | +button_pane.set_ok_action(ok_click) | |
112 | + | |
113 | +def cancel_click(*args): | |
114 | + # When this is bound, it will receive event arguments, which should be ignored. | |
115 | + status_pane.clear([]) | |
116 | +button_pane.set_cancel_action(cancel_click) | |
117 | + | |
118 | +# Bind <Enter> and <Esc> to the buttons. | |
119 | +root.bind("<Return>", ok_click) | |
120 | +root.bind("<Escape>", cancel_click) | |
121 | + | |
122 | + | |
123 | +# Run the application | |
124 | +root.mainloop() |
@@ -0,0 +1,1 @@ | ||
1 | +from tkpane import * |
@@ -0,0 +1,57 @@ | ||
1 | + | |
2 | +try: | |
3 | + import Tkinter as tk | |
4 | +except: | |
5 | + import tkinter as tk | |
6 | + | |
7 | +class Dialog(tk.Toplevel): | |
8 | + # From effbot: http://effbot.org/tkinterbook/tkinter-dialog-windows.htm | |
9 | + def __init__(self, parent, title = None): | |
10 | + tk.Toplevel.__init__(self, parent) | |
11 | + self.transient(parent) | |
12 | + if title: | |
13 | + self.title(title) | |
14 | + self.parent = parent | |
15 | + self.result = None | |
16 | + body = tk.Frame(self) | |
17 | + self.initial_focus = self.makebody(body) | |
18 | + body.pack(padx=5, pady=5) | |
19 | + self.buttonbox() | |
20 | + self.grab_set() | |
21 | + if not self.initial_focus: | |
22 | + self.initial_focus = self | |
23 | + self.protocol("WM_DELETE_WINDOW", self.cancel) | |
24 | + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, | |
25 | + parent.winfo_rooty()+50)) | |
26 | + self.initial_focus.focus_set() | |
27 | + self.wait_window(self) | |
28 | + def makebody(self, master): | |
29 | + # create dialog body. return widget that should have | |
30 | + # initial focus. this method should be overridden | |
31 | + pass | |
32 | + def buttonbox(self): | |
33 | + # add standard button box. override if you don't want the | |
34 | + # standard buttons | |
35 | + box = tk.Frame(self) | |
36 | + w = tk.Button(box, text="Ok", width=10, command=self.ok, default=tk.ACTIVE) | |
37 | + w.pack(side=tk.LEFT, padx=5, pady=5) | |
38 | + w = tk.Button(box, text="Cancel", width=10, command=self.cancel) | |
39 | + w.pack(side=tk.LEFT, padx=5, pady=5) | |
40 | + self.bind("<Return>", self.ok) | |
41 | + self.bind("<Escape>", self.cancel) | |
42 | + box.pack() | |
43 | + def ok(self, event=None): | |
44 | + if not self.validate(): | |
45 | + self.initial_focus.focus_set() # put focus back | |
46 | + return | |
47 | + self.withdraw() | |
48 | + self.update_idletasks() | |
49 | + self.apply() | |
50 | + self.cancel() | |
51 | + def cancel(self, event=None): | |
52 | + self.parent.focus_set() | |
53 | + self.destroy() | |
54 | + def validate(self): | |
55 | + return 1 # override | |
56 | + def apply(self): | |
57 | + pass # override |
@@ -0,0 +1,722 @@ | ||
1 | +# panelib.py | |
2 | +# | |
3 | +# PURPOSE | |
4 | +# A collection of TkPane subclasses. | |
5 | +# | |
6 | +# NOTES | |
7 | +# 1. | |
8 | +# | |
9 | +# AUTHORS | |
10 | +# Dreas Nielsen (RDN) | |
11 | +# | |
12 | +# HISTORY | |
13 | +# Date Remarks | |
14 | +# ---------- ---------------------------------------------------------------- | |
15 | +# 2018-01-15 Created. RDN. | |
16 | +# 2018-01-26 Added OkCancelPane, StatusProgressPane, and TableDisplayPane. RDN. | |
17 | +# 2018-01-27 Added MessagePane. RDN. | |
18 | +# 2018-01-28 Added pane styles. RDN. | |
19 | +# 2018-01-29 Added OutputFilePane, InputFilePane, and TextPane. RDN. | |
20 | +#=============================================================================== | |
21 | + | |
22 | +try: | |
23 | + import Tkinter as tk | |
24 | +except: | |
25 | + import tkinter as tk | |
26 | +try: | |
27 | + import ttk | |
28 | +except: | |
29 | + from tkinter import ttk | |
30 | +try: | |
31 | + import tkFileDialog as tk_file | |
32 | +except: | |
33 | + from tkinter import filedialog as tk_file | |
34 | +try: | |
35 | + import tkFont as tkfont | |
36 | +except: | |
37 | + from tkinter import tkfont | |
38 | + | |
39 | + | |
40 | +from tkpane import TkPane | |
41 | + | |
42 | + | |
43 | + | |
44 | +#=============================================================================== | |
45 | +# Pane styles | |
46 | +# A style is a set of options for pane (frame) configuration and gridding. | |
47 | +#------------------------------------------------------------------------------- | |
48 | +panestyles = {} | |
49 | + | |
50 | +class PaneStyle(object): | |
51 | + # Default spacing inside pane borders should be 18 pixels per the GNOME HIG | |
52 | + # (https://developer.gnome.org/hig/stable/visual-layout.html.en). | |
53 | + # Assuming a 3-pixel padding around interior widgets and that frames (panes) | |
54 | + # will be adjacent, default internal padding for frames should be set to | |
55 | + # (18 - 3)/2 ~= 7 pixels, | |
56 | + # This leaves the border around the outermost widgets in the app window | |
57 | + # smaller than desired. A frame around the entire application can be created | |
58 | + # to add this padding. | |
59 | + """A set of configuration options for frames and for widgets.""" | |
60 | + #def __init__(self, stylename, frame_config_dict, frame_grid_dict={}): | |
61 | + def __init__(self, stylename, frame_config_dict={"padx": 7, "pady":7}, frame_grid_dict={}): | |
62 | + self.config = frame_config_dict | |
63 | + self.grid = frame_grid_dict | |
64 | + panestyles[stylename] = self | |
65 | + | |
66 | +# Default grid styles are used for all of these. | |
67 | +PaneStyle("plain") | |
68 | +PaneStyle("ridged", {"padx": 7, "pady": 7, "borderwidth": 2, "relief": tk.RIDGE}) | |
69 | +PaneStyle("grooved", {"padx": 7, "pady": 7, "borderwidth": 2, "relief": tk.GROOVE}) | |
70 | +PaneStyle("sunken", {"padx": 7, "pady": 7, "borderwidth": 2, "relief": tk.SUNKEN}) | |
71 | +PaneStyle("statusbar", {"padx": 7, "pady": 2, "borderwidth": 2, "relief": tk.SUNKEN}) | |
72 | + | |
73 | +current_panestyle = "plain" | |
74 | +dialog_style = "plain" | |
75 | + | |
76 | +def frame_config_opts(style=None): | |
77 | + return panestyles[style or current_panestyle].config | |
78 | + | |
79 | +def frame_grid_opts(style=None): | |
80 | + return panestyles[style or current_panestyle].grid | |
81 | + | |
82 | + | |
83 | + | |
84 | + | |
85 | +#=============================================================================== | |
86 | +# Dialog class | |
87 | +# Used by some panes. Also usable for other purposes. | |
88 | +# Adapted from effbot: http://effbot.org/tkinterbook/tkinter-dialog-windows.htm. | |
89 | +#------------------------------------------------------------------------------- | |
90 | + | |
91 | +class Dialog(tk.Toplevel): | |
92 | + def __init__(self, parent, title = None): | |
93 | + tk.Toplevel.__init__(self, parent, **frame_config_opts(dialog_style)) | |
94 | + self.transient(parent) | |
95 | + if title: | |
96 | + self.title(title) | |
97 | + self.parent = parent | |
98 | + self.result = None | |
99 | + body = tk.Frame(self) | |
100 | + self.initial_focus = self.makebody(body) | |
101 | + body.pack(padx=3, pady=3) | |
102 | + self.buttonbox() | |
103 | + self.grab_set() | |
104 | + if not self.initial_focus: | |
105 | + self.initial_focus = self | |
106 | + self.protocol("WM_DELETE_WINDOW", self.cancel) | |
107 | + self.geometry("+%d+%d" % (parent.winfo_rootx()+50, | |
108 | + parent.winfo_rooty()+50)) | |
109 | + self.initial_focus.focus_set() | |
110 | + self.wait_window(self) | |
111 | + def makebody(self, master): | |
112 | + # Create the dialog body. Return the widget that should have | |
113 | + # the initial focus. This method should be overridden. | |
114 | + pass | |
115 | + def buttonbox(self): | |
116 | + # Add a standard button box. This method should be overriden | |
117 | + # if no buttons, or some other buttons, are to be shown. | |
118 | + box = tk.Frame(self) | |
119 | + w = tk.Button(box, text="Cancel", width=8, command=self.cancel) | |
120 | + w.pack(side=tk.RIGHT, padx=3, pady=3) | |
121 | + w = tk.Button(box, text="OK", width=8, command=self.ok) | |
122 | + w.pack(side=tk.RIGHT, padx=3, pady=3) | |
123 | + self.bind("<Return>", self.ok) | |
124 | + self.bind("<Escape>", self.cancel) | |
125 | + box.pack() | |
126 | + def ok(self, event=None): | |
127 | + if not self.validate(): | |
128 | + self.initial_focus.focus_set() | |
129 | + return | |
130 | + self.withdraw() | |
131 | + self.update_idletasks() | |
132 | + self.apply() | |
133 | + self.cancel() | |
134 | + def cancel(self, event=None): | |
135 | + self.parent.focus_set() | |
136 | + self.destroy() | |
137 | + def validate(self): | |
138 | + # Override this method as necessary. | |
139 | + return 1 | |
140 | + def apply(self): | |
141 | + # Override this method as necessary. | |
142 | + pass | |
143 | + | |
144 | + | |
145 | + | |
146 | +#=============================================================================== | |
147 | +# Pane classes | |
148 | +#------------------------------------------------------------------------------- | |
149 | + | |
150 | +class MessagePane(TkPane): | |
151 | + """Pane to display a text message. | |
152 | + | |
153 | + This pane does not manage any data. | |
154 | + | |
155 | + Overridden methods: none. | |
156 | + | |
157 | + Custom methods: | |
158 | + * set_message | |
159 | + """ | |
160 | + def __init__(self, parent, message): | |
161 | + TkPane.__init__(self, parent, frame_config_opts(), frame_grid_opts()) | |
162 | + self.msg_label = None | |
163 | + def wrap_mp_msg(event): | |
164 | + self.msg_label.configure(wraplength=event.width - 5) | |
165 | + msgframe = ttk.Frame(master=parent) | |
166 | + self.msg_label = ttk.Label(msgframe, text=message) | |
167 | + self.msg_label.bind("<Configure>", wrap_mp_msg) | |
168 | + self.msg_label.grid(column=0, row=0, sticky=tk.EW, padx=6, pady=6) | |
169 | + msgframe.rowconfigure(0, weight=0) | |
170 | + msgframe.columnconfigure(0, weight=1) | |
171 | + msgframe.grid(row=0, column=0, sticky=tk.EW) | |
172 | + parent.rowconfigure(0, weight=0) | |
173 | + parent.columnconfigure(0, weight=1) | |
174 | + def set_message(self, message): | |
175 | + """Change the message displayed in the pane.""" | |
176 | + self.msg_label.configure(text=message) | |
177 | + | |
178 | + | |
179 | +class UserPane(TkPane): | |
180 | + """Pane to display a user's name, and to prompt for a user's name and password. | |
181 | + | |
182 | + Data keys managed by this pane are "name" and "password," | |
183 | + | |
184 | + Overridden methods: | |
185 | + * clear_pane | |
186 | + * values | |
187 | + """ | |
188 | + | |
189 | + class GetUserDialog(Dialog): | |
190 | + def makebody(self, master): | |
191 | + tk.Label(master, text="User name:", width=12, anchor=tk.E).grid(row=0, column=0, sticky=tk.E, padx=3, pady=3) | |
192 | + tk.Label(master, text="Password:", width=12, anchor=tk.E).grid(row=1, column=0, sticky=tk.E, padx=3, pady=3) | |
193 | + self.e1 = tk.Entry(master, width=36) | |
194 | + self.e2 = tk.Entry(master, width=36, show="*") | |
195 | + self.e1.grid(row=0, column=1, sticky=tk.W, padx=3, pady=3) | |
196 | + self.e2.grid(row=1, column=1, sticky=tk.W, padx=3, pady=3) | |
197 | + return self.e1 | |
198 | + def validate(self): | |
199 | + return self.e1.get() != u'' and self.e2.get() != u'' | |
200 | + def apply(self): | |
201 | + self.result = {u"name": self.e1.get(), u"password": self.e2.get()} | |
202 | + | |
203 | + def __init__(self, parent): | |
204 | + TkPane.__init__(self, parent, config_opts=frame_config_opts(), grid_opts=frame_grid_opts()) | |
205 | + user_label = tk.Label(self, text='User name:', width=10, anchor=tk.E) | |
206 | + self.user_var = tk.StringVar() | |
207 | + self.user_pw = None | |
208 | + user_display = tk.Entry(self, textvariable=self.user_var) | |
209 | + user_display.config(state='readonly') | |
210 | + user_button = tk.Button(self, text='Change', width=8, command=self.set_user) | |
211 | + user_label.grid(row=0, column=0, padx=6, pady=3, sticky=tk.EW) | |
212 | + user_display.grid(row=0, column=1, padx=6, pady=3, sticky=tk.EW) | |
213 | + user_button.grid(row=1, column=1, padx=6, pady=1, sticky=tk.W) | |
214 | + self.columnconfigure(0, weight=0) | |
215 | + self.columnconfigure(1, weight=1) | |
216 | + self.rowconfigure(1, weight=1) | |
217 | + parent.rowconfigure(0, weight=0) | |
218 | + parent.columnconfigure(0, weight=1) | |
219 | + | |
220 | + def values(self): | |
221 | + return { u"name": self.user_var.get(), u"password": self.user_pw } | |
222 | + | |
223 | + def clear_pane(self): | |
224 | + self.user_var.set(u'') | |
225 | + self.user_pw = None | |
226 | + | |
227 | + def set_user(self): | |
228 | + # Open a dialog box to prompt for the user's name and password. | |
229 | + dlg = self.GetUserDialog(self) | |
230 | + if dlg.result is not None: | |
231 | + self.user_var.set(dlg.result[u"name"]) | |
232 | + self.user_pw = dlg.result[u"password"] | |
233 | + self.call_enables() | |
234 | + else: | |
235 | + self.call_clears() | |
236 | + self.call_disables() | |
237 | + | |
238 | + | |
239 | +class OutputDirPane(TkPane): | |
240 | + """ Pane to get and display an output directory. | |
241 | + | |
242 | + The data key managed by this pane is "output_dir," | |
243 | + | |
244 | + Overridden methods: | |
245 | + * clear_pane | |
246 | + * disable_pane | |
247 | + * enable_pane | |
248 | + * values | |
249 | + """ | |
250 | + | |
251 | + def __init__(self, parent): | |
252 | + TkPane.__init__(self, parent, frame_config_opts(), frame_grid_opts()) | |
253 | + dir_label = tk.Label(self, text='Output directory:', anchor=tk.E) | |
254 | + self.dir_var = tk.StringVar() | |
255 | + self.dir_var.trace('w', self.dir_changed) | |
256 | + self.dir_display = tk.Entry(self, textvariable=self.dir_var) | |
257 | + self.dir_button = tk.Button(self, text='Browse', width=8, command=self.set_outputdir) | |
258 | + dir_label.grid(row=0, column=0, padx=3, pady=3, sticky=tk.EW) | |
259 | + self.dir_display.grid(row=0, column=1, padx=3, pady=3, sticky=tk.EW) | |
260 | + self.dir_button.grid(row=1, column=1, padx=3, pady=1, sticky=tk.W) | |
261 | + self.columnconfigure(0, weight=0) | |
262 | + self.columnconfigure(1, weight=1) | |
263 | + self.rowconfigure(1, weight=1) | |
264 | + parent.rowconfigure(0, weight=0) | |
265 | + parent.columnconfigure(0, weight=1) | |
266 | + | |
267 | + def values(self): | |
268 | + return { u"output_dir": self.dir_var.get() } | |
269 | + | |
270 | + def clear_pane(self): | |
271 | + self.dir_var.set(u'') | |
272 | + | |
273 | + def enable_pane(self): | |
274 | + self.dir_display.configure(state='!readonly') | |
275 | + self.dir_button.configure(state=['!disabled']) | |
276 | + | |
277 | + def disable_pane(self): | |
278 | + self.dir_display.configure(state='readonly') | |
279 | + self.dir_button.configure(state=['disabled']) | |
280 | + | |
281 | + def dir_changed(self, var, index, mode): | |
282 | + if mode == "w": | |
283 | + self.validate() | |
284 | + | |
285 | + def validate(self): | |
286 | + import os.path | |
287 | + if os.path.isdir(self.dir_var.get() or ""): | |
288 | + self.call_enables() | |
289 | + else: | |
290 | + self.call_disables() | |
291 | + | |
292 | + def set_outputdir(self): | |
293 | + self.dir_var.set(u'') | |
294 | + dir = tk_file.askdirectory() | |
295 | + if dir: | |
296 | + self.dir_var.set(dir) | |
297 | + else: | |
298 | + self.call_clears() | |
299 | + self.validate() | |
300 | + | |
301 | + | |
302 | +class OutputFilePane(TkPane): | |
303 | + """ Pane to get and display an output filename. | |
304 | + | |
305 | + The data key managed by this pane is "output_filename," | |
306 | + | |
307 | + Overridden methods: | |
308 | + * clear_pane | |
309 | + * disable_pane | |
310 | + * enable_pane | |
311 | + * values | |
312 | + """ | |
313 | + | |
314 | + def __init__(self, parent, optiondict): | |
315 | + """:param optiondict: a dictionary of option names and values for the | |
316 | + Tkinter 'asksaveasfilename' method. | |
317 | + """ | |
318 | + self.optiondict = optiondict | |
319 | + TkPane.__init__(self, parent, frame_config_opts(), frame_grid_opts()) | |
320 | + dir_label = tk.Label(self, text='Output directory:', anchor=tk.E) | |
321 | + self.file_var = tk.StringVar() | |
322 | + self.file_var.trace('w', self.file_changed) | |
323 | + self.file_display = tk.Entry(self, textvariable=self.file_var) | |
324 | + self.dir_button = tk.Button(self, text='Browse', width=8, command=self.set_outputfile) | |
325 | + dir_label.grid(row=0, column=0, padx=3, pady=3, sticky=tk.EW) | |
326 | + self.file_display.grid(row=0, column=1, padx=3, pady=3, sticky=tk.EW) | |
327 | + self.dir_button.grid(row=1, column=1, padx=3, pady=1, sticky=tk.W) | |
328 | + self.columnconfigure(0, weight=0) | |
329 | + self.columnconfigure(1, weight=1) | |
330 | + self.rowconfigure(1, weight=1) | |
331 | + parent.rowconfigure(0, weight=0) | |
332 | + parent.columnconfigure(0, weight=1) | |
333 | + | |
334 | + def values(self): | |
335 | + return { u"output_filename": self.file_var.get() } | |
336 | + | |
337 | + def clear_pane(self): | |
338 | + self.file_var.set(u'') | |
339 | + | |
340 | + def enable_pane(self): | |
341 | + self.file_display.configure(state='!readonly') | |
342 | + self.dir_button.configure(state=['!disabled']) | |
343 | + | |
344 | + def disable_pane(self): | |
345 | + self.file_display.configure(state='readonly') | |
346 | + self.dir_button.configure(state=['disabled']) | |
347 | + | |
348 | + def file_changed(self, var, index, mode): | |
349 | + if mode == "w": | |
350 | + self.validate() | |
351 | + | |
352 | + def validate(self): | |
353 | + import os.path | |
354 | + fn = self.file_var.get() or "" | |
355 | + if os.path.isdir(os.path.dirname(fn)): | |
356 | + self.call_enables() | |
357 | + else: | |
358 | + self.call_disables() | |
359 | + | |
360 | + def set_outputfile(self): | |
361 | + self.file_var.set(u'') | |
362 | + fn = tk_file.asksaveasfilename(**self.optiondict) | |
363 | + if fn: | |
364 | + self.file_var.set(dir) | |
365 | + else: | |
366 | + self.call_clears() | |
367 | + self.validate() | |
368 | + | |
369 | + | |
370 | +class InputFilePane(TkPane): | |
371 | + """ Pane to get and display an input filename. | |
372 | + | |
373 | + The data key managed by this pane is "input_filename," | |
374 | + | |
375 | + Overridden methods: | |
376 | + * clear_pane | |
377 | + * disable_pane | |
378 | + * enable_pane | |
379 | + * values | |
380 | + """ | |
381 | + | |
382 | + def __init__(self, parent, optiondict): | |
383 | + """:param optiondict: a dictionary of option names and values for the | |
384 | + Tkinter 'askopenfilename' method. | |
385 | + """ | |
386 | + self.optiondict = optiondict | |
387 | + TkPane.__init__(self, parent, frame_config_opts(), frame_grid_opts()) | |
388 | + dir_label = tk.Label(self, text='Output directory:', anchor=tk.E) | |
389 | + self.file_var = tk.StringVar() | |
390 | + self.file_var.trace('w', self.file_changed) | |
391 | + self.file_display = tk.Entry(self, textvariable=self.file_var) | |
392 | + self.dir_button = tk.Button(self, text='Browse', width=8, command=self.set_inputfile) | |
393 | + dir_label.grid(row=0, column=0, padx=3, pady=3, sticky=tk.EW) | |
394 | + self.file_display.grid(row=0, column=1, padx=3, pady=3, sticky=tk.EW) | |
395 | + self.dir_button.grid(row=1, column=1, padx=3, pady=1, sticky=tk.W) | |
396 | + self.columnconfigure(0, weight=0) | |
397 | + self.columnconfigure(1, weight=1) | |
398 | + self.rowconfigure(1, weight=1) | |
399 | + parent.rowconfigure(0, weight=0) | |
400 | + parent.columnconfigure(0, weight=1) | |
401 | + | |
402 | + def values(self): | |
403 | + return { u"input_filename": self.file_var.get() } | |
404 | + | |
405 | + def clear_pane(self): | |
406 | + self.file_var.set(u'') | |
407 | + | |
408 | + def enable_pane(self): | |
409 | + self.file_display.configure(state='!readonly') | |
410 | + self.dir_button.configure(state=['!disabled']) | |
411 | + | |
412 | + def disable_pane(self): | |
413 | + self.file_display.configure(state='readonly') | |
414 | + self.dir_button.configure(state=['disabled']) | |
415 | + | |
416 | + def file_changed(self, var, index, mode): | |
417 | + if mode == "w": | |
418 | + self.validate() | |
419 | + | |
420 | + def validate(self): | |
421 | + import os.path | |
422 | + if os.path.isfile(self.file_var.get() or ""): | |
423 | + self.call_enables() | |
424 | + else: | |
425 | + self.call_disables() | |
426 | + | |
427 | + def set_inputfile(self): | |
428 | + self.file_var.set(u'') | |
429 | + dir = tk_file.askopenfilename(**self.optiondict) | |
430 | + if dir: | |
431 | + self.file_var.set(dir) | |
432 | + else: | |
433 | + self.call_clears() | |
434 | + self.validate() | |
435 | + | |
436 | + | |
437 | +class TextPane(TkPane): | |
438 | + """Pane to display a Tkinter Text widget. | |
439 | + | |
440 | + Because of the large number of uses of the Text widget, this pane | |
441 | + provides direct access to the Text widget via the 'textwidget' method. | |
442 | + To simplify use, this pane also provides direct methods for appending to, | |
443 | + replacing, and clearing the contents of the Text widget. | |
444 | + The custom methods 'set_status' and 'clear_status' allow a TextPane to | |
445 | + be used as a status_reporter callback for any other type of pane. | |
446 | + | |
447 | + Data keys managed by this pane: "text". | |
448 | + | |
449 | + Overridden methods: | |
450 | + * values | |
451 | + * clear_pane | |
452 | + * enable_pane | |
453 | + * disable_pane | |
454 | + | |
455 | + Custom methods: | |
456 | + * textwidget | |
457 | + * replace_all | |
458 | + * append | |
459 | + * set_status | |
460 | + * clear_status | |
461 | + """ | |
462 | + def __init__(self, parent, optiondict, initial_text=None): | |
463 | + """:param optiondict: Initial options for the Text widget, as a dictionary.""" | |
464 | + TkPane.__init__(self, parent, frame_config_opts(), frame_grid_opts()) | |
465 | + self.textwidget = tk.Text(self, **optiondict) | |
466 | + if initial_text is not None: | |
467 | + self.replace_all(initial_text) | |
468 | + self.textwidget.grid(row=0, column=0, padx=3, pady=3, sticky=tk.NSEW) | |
469 | + self.rowconfigure(0, weight=1) | |
470 | + self.columnconfigure(0, weight=1) | |
471 | + parent.rowconfigure(0, weight=1) | |
472 | + parent.columnconfigure(0, weight=1) | |
473 | + | |
474 | + def textwidget(self): | |
475 | + """Return the text widget object, to allow direct manipulation.""" | |
476 | + return self.textwidget | |
477 | + | |
478 | + def values(self): | |
479 | + return {"text": self.textwidget.get("1.0", tk.END)} | |
480 | + | |
481 | + def clear_pane(self): | |
482 | + self.textwidget.delete("1.0", tk.END) | |
483 | + | |
484 | + def enable_pane(self): | |
485 | + self.textwidget.configure(state=tk.NORMAL) | |
486 | + | |
487 | + def disable_pane(self): | |
488 | + self.textwidget.configure(state=tk.DISABLED) | |
489 | + | |
490 | + def replace_all(self, new_contents): | |
491 | + self.clear_pane() | |
492 | + self.textwidget.insert(tk.END, new_contents) | |
493 | + | |
494 | + def append(self, more_text): | |
495 | + """Inserts the given text at the end of the Text widget's contents.""" | |
496 | + self.textwidget.insert(tk.END, more_text) | |
497 | + | |
498 | + def set_status(self, status_msg): | |
499 | + """Inserts the status message at the end of the Text widget's contents.""" | |
500 | + self.append(status_msg) | |
501 | + | |
502 | + def clear_status(self): | |
503 | + """Clear the entire widget.""" | |
504 | + self.clear() | |
505 | + | |
506 | + | |
507 | + | |
508 | +class StatusProgressPane(TkPane): | |
509 | + """Pane to display a status bar and progress bar. | |
510 | + | |
511 | + There are no data keys managed by this pane. | |
512 | + | |
513 | + Overridden methods: | |
514 | + * clear_pane | |
515 | + * values | |
516 | + | |
517 | + Custom methods: | |
518 | + * set_status(message): Sets the status bar message. | |
519 | + * set_determinate(): Sets the progress bar to determinate mode. | |
520 | + * set_indeterminate(): Sets the progress bar to indeterminate mode. | |
521 | + * set_value(value): Sets a determinate progress bar to the specified value (0-100). | |
522 | + * start(): Starts an indefinite progress bar. | |
523 | + * stop(): Stops an indefinite progress bar. | |
524 | + """ | |
525 | + def __init__(self, parent): | |
526 | + TkPane.__init__(self, parent, frame_config_opts("statusbar"), frame_grid_opts("statusbar")) | |
527 | + self.status_msg = tk.StringVar() | |
528 | + self.status_msg.set('') | |
529 | + self.ctrvalue = tk.DoubleVar() | |
530 | + self.ctrvalue.set(0) | |
531 | + statusbar = ttk.Label(parent, text='', textvariable=self.status_msg, relief=tk.RIDGE, anchor=tk.W) | |
532 | + self.progressmode = 'determinate' | |
533 | + ctrprogress = ttk.Progressbar(parent, mode=self.progressmode, maximum=100, | |
534 | + orient='horizontal', length=150, variable=self.ctrvalue) | |
535 | + statusbar.grid(row=0, column=0, sticky=tk.EW) | |
536 | + ctrprogress.grid(row=0, column=1, sticky=tk.EW) | |
537 | + self.columnconfigure(0, weight=1) | |
538 | + self.columnconfigure(1, weight=0) | |
539 | + self.rowconfigure(1, weight=1) | |
540 | + parent.rowconfigure(0, weight=0) | |
541 | + parent.columnconfigure(0, weight=1) | |
542 | + | |
543 | + def values(self): | |
544 | + return {'status': self.satus_msg.get(), 'progress': self.ctrvalue.get()} | |
545 | + | |
546 | + def clear_pane(self): | |
547 | + """Clears the status bar and progress bar.""" | |
548 | + self.status_msg.set('') | |
549 | + self.ctrvalue.set(0) | |
550 | + self.stop() | |
551 | + | |
552 | + def set_status(self, message): | |
553 | + """Sets the status bar message.""" | |
554 | + self.status_msg.set(message) | |
555 | + | |
556 | + def set_determinate(self): | |
557 | + """Sets the progress bar to definite mode.""" | |
558 | + self.progressmode = "indeterminate" | |
559 | + self.ctrprogress.configure(mode=self.progressmode) | |
560 | + | |
561 | + def set_indeterminate(self): | |
562 | + """Sets the progress bar to indefinite mode.""" | |
563 | + self.progressmode = "indeterminate" | |
564 | + self.ctrprogress.configure(mode=self.progressmode) | |
565 | + | |
566 | + def set_value(self, value): | |
567 | + """Sets the progress bar indicator. | |
568 | + | |
569 | + The 'value' argument should be between 0 and 100, and will be trimmed to | |
570 | + this range if it is not. | |
571 | + """ | |
572 | + if self.progressmode == "determinate": | |
573 | + self.ctrvalue.set(max(min(float(value), 100.0), 0.0)) | |
574 | + | |
575 | + def start(self): | |
576 | + """Start an indefinite progress bar running.""" | |
577 | + if self.progressmode == "indeterminate": | |
578 | + self.ctrprogress.start() | |
579 | + | |
580 | + def stop(self): | |
581 | + """Stop an indefinite progress bar.""" | |
582 | + if self.progressmode == "determinate": | |
583 | + pass | |
584 | + | |
585 | + | |
586 | +class TableDisplayPane(TkPane): | |
587 | + """Pane to display a specified data table.""" | |
588 | + def __init__(self, parent, message, column_headers, rowset): | |
589 | + """Create the table display. | |
590 | + | |
591 | + :param message: A message to display above the data table. | |
592 | + :param column_headers: A list of the column names for the data table. | |
593 | + :param rowset: An iterable that yields rows for the data table. | |
594 | + | |
595 | + There are no data keys managed by this pane. | |
596 | + | |
597 | + Overridden methods: | |
598 | + * clear_pane | |
599 | + | |
600 | + Custom methods: | |
601 | + * display_data | |
602 | + """ | |
603 | + TkPane.__init__(self, parent, frame_config_opts(), frame_grid_opts()) | |
604 | + # Message frame and control. | |
605 | + self.msg_label = None | |
606 | + def wrap_msg(event): | |
607 | + self.msg_label.configure(wraplength=event.width - 5) | |
608 | + if message is not None: | |
609 | + msgframe = ttk.Frame(master=self, padding="3 3 3 3") | |
610 | + self.msg_label = ttk.Label(msgframe, text=message) | |
611 | + self.msg_label.bind("<Configure>", wrap_msg) | |
612 | + self.msg_label.grid(column=0, row=0, sticky=tk.EW) | |
613 | + msgframe.rowconfigure(0, weight=0) | |
614 | + msgframe.columnconfigure(0, weight=1) | |
615 | + msgframe.grid(row=0, column=0, pady=3, sticky=tk.EW) | |
616 | + tableframe = ttk.Frame(master=self, padding="3 3 3 3") | |
617 | + # Create and configure the Treeview table widget and scrollbars. | |
618 | + self.tbl = ttk.Treeview(tableframe, columns=column_headers, selectmode="none", show="headings") | |
619 | + ysb = ttk.Scrollbar(tableframe, orient='vertical', command=self.tbl.yview) | |
620 | + xsb = ttk.Scrollbar(tableframe, orient='horizontal', command=self.tbl.xview) | |
621 | + self.tbl.configure(yscrollcommand=ysb.set, xscrollcommand=xsb.set) | |
622 | + tableframe.grid(column=0, row=1 if message is not None else 0, sticky=tk.NSEW) | |
623 | + self.tbl.grid(column=0, row=0, sticky=tk.NSEW) | |
624 | + ysb.grid(column=1, row=0, sticky=tk.NS) | |
625 | + xsb.grid(column=0, row=1, sticky=tk.EW) | |
626 | + tableframe.columnconfigure(0, weight=1) | |
627 | + tableframe.rowconfigure(0, weight=1) | |
628 | + # Display the data | |
629 | + self.display_data(column_headers, rowset) | |
630 | + # Make the table resizeable | |
631 | + if message is not None: | |
632 | + self.rowconfigure(0, weight=0) | |
633 | + self.rowconfigure(1, weight=1) | |
634 | + else: | |
635 | + self.rowconfigure(0, weight=1) | |
636 | + parent.rowconfigure(0, weight=1) | |
637 | + parent.columnconfigure(0, weight=1) | |
638 | + def clear_pane(self): | |
639 | + for item in self.tbl.get_children(): | |
640 | + self.tbl.delete(item) | |
641 | + self.tbl.configure(columns=[""]) | |
642 | + def display_data(self, column_headers, rowset): | |
643 | + """Display a new data set on the pane. | |
644 | + | |
645 | + :param column_headers: A list of strings for the headers of the data columns. | |
646 | + :param rowset: A list of lists of data values to display. The outer list is rows, the inner lists are columns. | |
647 | + """ | |
648 | + self.clear_pane() | |
649 | + # Reconfigure TreeView columns | |
650 | + self.tbl.configure(columns=column_headers) | |
651 | + # Get the data to display. | |
652 | + nrows = range(len(rowset)) | |
653 | + ncols = range(len(column_headers)) | |
654 | + hdrwidths = [len(column_headers[j]) for j in ncols] | |
655 | + datawidthtbl = [[len(rowset[i][j] if isinstance(rowset[i][j], basestring) else unicode(rowset[i][j])) for i in nrows] for j in ncols] | |
656 | + datawidths = [max(cwidths) for cwidths in datawidthtbl] | |
657 | + colwidths = [max(hdrwidths[i], datawidths[i]) for i in ncols] | |
658 | + # Set the font. | |
659 | + ff = tkfont.nametofont("TkFixedFont") | |
660 | + tblstyle = ttk.Style() | |
661 | + tblstyle.configure('tblstyle', font=ff) | |
662 | + self.tbl.configure()["style"] = tblstyle | |
663 | + charpixels = int(1.3 * ff.measure(u"0")) | |
664 | + pixwidths = [charpixels * col for col in colwidths] | |
665 | + # Fill the Treeview table widget with data | |
666 | + for i in range(len(column_headers)): | |
667 | + self.tbl.column(column_headers[i], width=pixwidths[i]) | |
668 | + self.tbl.heading(column_headers[i], text=column_headers[i]) | |
669 | + for i, row in enumerate(rowset): | |
670 | + enc_row = [c if c is not None else '' for c in row] | |
671 | + self.tbl.insert(parent='', index='end', iid=str(i), values=enc_row) | |
672 | + | |
673 | + | |
674 | +class OkCancelPane(TkPane): | |
675 | + """Pane to display Ok and Cancel buttons. | |
676 | + | |
677 | + There are no data keys managed by this pane. | |
678 | + | |
679 | + Overridden methods: | |
680 | + * disable_pane | |
681 | + * enable_pane | |
682 | + | |
683 | + Custom methods: | |
684 | + * set_cancel_action | |
685 | + * set_ok_action | |
686 | + * ok | |
687 | + * cancal | |
688 | + """ | |
689 | + def __init__(self, parent, ok_action, cancel_action): | |
690 | + def do_nothing(): | |
691 | + pass | |
692 | + self.ok_action = ok_action if ok_action is not None else do_nothing | |
693 | + self.cancel_action = cancel_action if cancel_action is not None else do_nothing | |
694 | + TkPane.__init__(self, parent, frame_config_opts(), frame_grid_opts()) | |
695 | + self.cancel_btn = ttk.Button(self, text="Cancel", command=self.cancel_action) | |
696 | + self.cancel_btn.grid(row=0, column=1, padx=3, sticky=tk.E) | |
697 | + self.ok_btn = ttk.Button(self, text="OK", command=self.ok_action) | |
698 | + self.ok_btn.grid(row=0, column=0, padx=3, sticky=tk.E) | |
699 | + self.columnconfigure(0, weight=1) | |
700 | + self.columnconfigure(1, weight=0) | |
701 | + self.rowconfigure(0, weight=1) | |
702 | + parent.rowconfigure(0, weight=0) | |
703 | + parent.columnconfigure(0, weight=1) | |
704 | + def enable_pane(self): | |
705 | + self.ok_btn.state(['!disabled']) | |
706 | + def disable_pane(self): | |
707 | + self.ok_btn.state(['disabled']) | |
708 | + def set_ok_action(self, ok_action): | |
709 | + """Specify the callback function to be called when the 'OK' button is clicked.""" | |
710 | + self.ok_action = ok_action if ok_action is not None else do_nothing | |
711 | + self.ok_btn.configure(command=self.ok_action) | |
712 | + def set_cancel_action(self, cancel_action): | |
713 | + """Specify the callback function to be called when the 'Cancel' button is clicked.""" | |
714 | + self.cancel_action = cancel_action if cancel_action is not None else do_nothing | |
715 | + self.cancel_btn.configure(command=self.cancel_action) | |
716 | + def ok(self): | |
717 | + """Trigger this pane's 'OK' action.""" | |
718 | + self.ok_action() | |
719 | + def cancel(self): | |
720 | + """Trigger this pane's 'Cancel' action.""" | |
721 | + self.cancel_action() | |
722 | + |