1 | #!/usr/bin/env python |
||
2 | |||
3 | 1 | """Graphical interface for DropTheBeat.""" |
|
4 | |||
5 | 1 | import sys |
|
6 | 1 | from unittest.mock import Mock |
|
7 | 1 | try: |
|
8 | 1 | import tkinter as tk |
|
9 | 1 | from tkinter import ttk |
|
10 | 1 | from tkinter import messagebox, simpledialog, filedialog |
|
11 | except ImportError as err: |
||
12 | sys.stderr.write("WARNING: {}\n".format(err)) |
||
13 | tk = Mock() |
||
14 | ttk = Mock() |
||
15 | |||
16 | 1 | import os |
|
17 | 1 | import argparse |
|
18 | 1 | from itertools import chain |
|
19 | 1 | import logging |
|
20 | |||
21 | 1 | from dtb import GUI, __version__ |
|
22 | 1 | from dtb import share, user |
|
23 | 1 | from dtb.common import SHARED, WarningFormatter |
|
24 | 1 | from dtb import settings |
|
25 | |||
26 | 1 | _LAUNCH = True |
|
27 | |||
28 | |||
29 | 1 | class Application(ttk.Frame): # pylint: disable=too-many-instance-attributes |
|
30 | """Tkinter application for DropTheBeat.""" |
||
31 | |||
32 | 1 | def __init__(self, master=None, root=None, home=None, name=None): |
|
33 | ttk.Frame.__init__(self, master) |
||
34 | |||
35 | # Load the root sharing directory |
||
36 | self.root = root or share.find(home) |
||
37 | |||
38 | # Load the user |
||
39 | self.user = user.User(os.path.join(self.root, name)) if name else None |
||
40 | try: |
||
41 | self.user = self.user or user.get_current(self.root) |
||
42 | except EnvironmentError: |
||
43 | |||
44 | while True: |
||
45 | |||
46 | msg = "Enter your name in the form 'First Last':" |
||
47 | text = simpledialog.askstring("Create a User", msg) |
||
48 | logging.debug("text: {}".format(repr(text))) |
||
49 | name = text.strip(" '") if text else None |
||
50 | if not name: |
||
51 | raise KeyboardInterrupt("no user specified") |
||
52 | try: |
||
53 | self.user = user.User.new(self.root, name) |
||
54 | except EnvironmentError: |
||
55 | existing = user.User(os.path.join(self.root, name)) |
||
56 | msg = "Is this you:" |
||
57 | for info in existing.info: |
||
58 | msg += "\n\n'{}' on '{}'".format(info[1], info[0]) |
||
59 | if not existing.info or \ |
||
60 | messagebox.askyesno("Add to Existing User", msg): |
||
61 | self.user = user.User.add(self.root, name) |
||
62 | break |
||
63 | else: |
||
64 | break |
||
65 | |||
66 | # Create variables |
||
67 | self.path_root = tk.StringVar(value=self.root) |
||
68 | self.path_downloads = tk.StringVar(value=self.user.path_downloads) |
||
69 | self.outgoing = [] |
||
70 | self.incoming = [] |
||
71 | |||
72 | # Initialize the GUI |
||
73 | self.listbox_outgoing = None |
||
74 | self.listbox_incoming = None |
||
75 | frame = self.init(master) |
||
76 | frame.pack(fill=tk.BOTH, expand=1) |
||
77 | |||
78 | # Show the GUI |
||
79 | master.deiconify() |
||
80 | self.update() |
||
81 | |||
82 | 1 | def init(self, root): |
|
83 | """Initialize frames and widgets.""" |
||
84 | |||
85 | # pylint: disable=line-too-long |
||
86 | |||
87 | mac = sys.platform == 'darwin' |
||
88 | |||
89 | # Shared keyword arguments |
||
90 | kw_f = {'padding': 5} # constructor arguments for frames |
||
91 | kw_gp = {'padx': 5, 'pady': 5} # grid arguments for padded widgets |
||
92 | kw_gs = {'sticky': tk.NSEW} # grid arguments for sticky widgets |
||
93 | kw_gsp = dict(chain(kw_gs.items(), kw_gp.items())) # grid arguments for sticky padded widgets |
||
94 | |||
95 | # Configure grid |
||
96 | frame = ttk.Frame(root, **kw_f) |
||
0 ignored issues
–
show
|
|||
97 | frame.rowconfigure(0, weight=0) |
||
98 | frame.rowconfigure(2, weight=1) |
||
99 | frame.rowconfigure(4, weight=1) |
||
100 | frame.columnconfigure(0, weight=1) |
||
101 | |||
102 | # Create widgets |
||
103 | def frame_settings(master): |
||
104 | """Frame for the settings.""" |
||
105 | frame = ttk.Frame(master, **kw_f) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
106 | |||
107 | # Configure grid |
||
108 | frame.rowconfigure(0, weight=1) |
||
109 | frame.rowconfigure(1, weight=1) |
||
110 | frame.columnconfigure(0, weight=0) |
||
111 | frame.columnconfigure(1, weight=1) |
||
112 | frame.columnconfigure(2, weight=0) |
||
113 | |||
114 | # Place widgets |
||
115 | ttk.Label(frame, text="Shared:").grid(row=0, column=0, sticky=tk.W, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
116 | ttk.Entry(frame, state='readonly', textvariable=self.path_root).grid(row=0, column=1, columnspan=2, **kw_gsp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
117 | ttk.Label(frame, text="Downloads:").grid(row=1, column=0, sticky=tk.W, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
118 | ttk.Entry(frame, state='readonly', textvariable=self.path_downloads).grid(row=1, column=1, **kw_gsp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
119 | ttk.Button(frame, text="...", width=0, command=self.browse_downloads).grid(row=1, column=2, ipadx=5, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
120 | |||
121 | return frame |
||
122 | |||
123 | View Code Duplication | def frame_incoming(master): |
|
0 ignored issues
–
show
|
|||
124 | """Frame for incoming songs.""" |
||
125 | frame = ttk.Frame(master, **kw_f) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
126 | |||
127 | # Configure grid |
||
128 | frame.rowconfigure(0, weight=1) |
||
129 | frame.rowconfigure(1, weight=0) |
||
130 | frame.columnconfigure(0, weight=0) |
||
131 | frame.columnconfigure(1, weight=1) |
||
132 | frame.columnconfigure(2, weight=1) |
||
133 | |||
134 | # Place widgets |
||
135 | self.listbox_incoming = tk.Listbox(frame, selectmode=tk.EXTENDED if mac else tk.MULTIPLE) |
||
136 | self.listbox_incoming.grid(row=0, column=0, columnspan=3, **kw_gsp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
137 | scroll_incoming = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.listbox_incoming.yview) |
||
138 | self.listbox_incoming.configure(yscrollcommand=scroll_incoming.set) |
||
139 | scroll_incoming.grid(row=0, column=2, sticky=(tk.N, tk.E, tk.S)) |
||
140 | ttk.Button(frame, text="\u21BB", width=0, command=self.update).grid(row=1, column=0, sticky=tk.SW, ipadx=5, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
141 | ttk.Button(frame, text="Ignore Selected", command=self.do_ignore).grid(row=1, column=1, sticky=tk.SW, ipadx=5, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
142 | ttk.Button(frame, text="Download Selected", command=self.do_download).grid(row=1, column=2, sticky=tk.SE, ipadx=5, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
143 | return frame |
||
144 | |||
145 | View Code Duplication | def frame_outgoing(master): |
|
0 ignored issues
–
show
|
|||
146 | """Frame for outgoing songs.""" |
||
147 | frame = ttk.Frame(master, **kw_f) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
148 | |||
149 | # Configure grid |
||
150 | frame.rowconfigure(0, weight=1) |
||
151 | frame.rowconfigure(1, weight=0) |
||
152 | frame.columnconfigure(0, weight=0) |
||
153 | frame.columnconfigure(1, weight=1) |
||
154 | frame.columnconfigure(2, weight=1) |
||
155 | |||
156 | # Place widgets |
||
157 | self.listbox_outgoing = tk.Listbox(frame, selectmode=tk.EXTENDED if mac else tk.MULTIPLE) |
||
158 | self.listbox_outgoing.grid(row=0, column=0, columnspan=3, **kw_gsp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
159 | scroll_outgoing = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.listbox_outgoing.yview) |
||
160 | self.listbox_outgoing.configure(yscrollcommand=scroll_outgoing.set) |
||
161 | scroll_outgoing.grid(row=0, column=2, sticky=(tk.N, tk.E, tk.S)) |
||
162 | ttk.Button(frame, text="\u21BB", width=0, command=self.update).grid(row=1, column=0, sticky=tk.SW, ipadx=5, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
163 | ttk.Button(frame, text="Remove Selected", command=self.do_remove).grid(row=1, column=1, sticky=tk.SW, ipadx=5, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
164 | ttk.Button(frame, text="Share Songs...", command=self.do_share).grid(row=1, column=2, sticky=tk.SE, ipadx=5, **kw_gp) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
165 | |||
166 | return frame |
||
167 | |||
168 | def separator(master): |
||
169 | """Widget to separate frames.""" |
||
170 | return ttk.Separator(master) |
||
171 | |||
172 | # Place widgets |
||
173 | frame_settings(frame).grid(row=0, **kw_gs) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
174 | separator(frame).grid(row=1, padx=10, pady=5, **kw_gs) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
175 | frame_outgoing(frame).grid(row=2, **kw_gs) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
176 | separator(frame).grid(row=3, padx=10, pady=5, **kw_gs) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
177 | frame_incoming(frame).grid(row=4, **kw_gs) |
||
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
178 | |||
179 | return frame |
||
180 | |||
181 | 1 | def browse_downloads(self): |
|
182 | """Browser for a new downloads directory.""" |
||
183 | path = filedialog.askdirectory() |
||
184 | logging.debug("path: {}".format(path)) |
||
185 | if path: |
||
186 | self.user.path_downloads = path |
||
187 | self.path_downloads.set(self.user.path_downloads) |
||
188 | |||
189 | 1 | def do_remove(self): |
|
190 | """Remove selected songs.""" |
||
191 | for index in (int(s) for s in self.listbox_outgoing.curselection()): |
||
192 | self.outgoing[index].ignore() |
||
193 | self.update() |
||
194 | |||
195 | 1 | def do_share(self): |
|
196 | """Share songs.""" |
||
197 | paths = filedialog.askopenfilenames() |
||
198 | if isinstance(paths, str): # http://bugs.python.org/issue5712 |
||
199 | paths = self.master.tk.splitlist(paths) |
||
200 | logging.debug("paths: {}".format(paths)) |
||
201 | for path in paths: |
||
202 | self.user.recommend(path) |
||
203 | self.update() |
||
204 | |||
205 | 1 | def do_ignore(self): |
|
206 | """Ignore selected songs.""" |
||
207 | for index in (int(s) for s in self.listbox_incoming.curselection()): |
||
208 | song = self.incoming[index] |
||
209 | song.ignore() |
||
210 | self.update() |
||
211 | |||
212 | 1 | def do_download(self): |
|
213 | """Download selected songs.""" |
||
214 | indicies = (int(s) for s in self.listbox_incoming.curselection()) |
||
215 | try: |
||
216 | for index in indicies: |
||
217 | song = self.incoming[index] |
||
218 | song.download(catch=False) |
||
219 | except IOError as exc: |
||
220 | self.show_error_from_exception(exc, "Download Error") |
||
221 | self.update() |
||
222 | |||
223 | 1 | def update(self): |
|
224 | """Update the list of outgoing and incoming songs.""" |
||
225 | |||
226 | # Cleanup outgoing songs |
||
227 | self.user.cleanup() |
||
228 | |||
229 | # Update outgoing songs list |
||
230 | logging.info("updating outgoing songs...") |
||
231 | self.outgoing = list(self.user.outgoing) |
||
232 | self.listbox_outgoing.delete(0, tk.END) |
||
233 | for song in self.outgoing: |
||
234 | self.listbox_outgoing.insert(tk.END, song.out_string) |
||
235 | # Update incoming songs list |
||
236 | |||
237 | logging.info("updating incoming songs...") |
||
238 | self.incoming = list(self.user.incoming) |
||
239 | self.listbox_incoming.delete(0, tk.END) |
||
240 | for song in self.incoming: |
||
241 | self.listbox_incoming.insert(tk.END, song.in_string) |
||
242 | |||
243 | 1 | @staticmethod |
|
244 | 1 | def show_error_from_exception(exception, title="Error"): |
|
245 | """Convert an exception to an error dialog.""" |
||
246 | message = str(exception) |
||
247 | message = message[0].upper() + message[1:] |
||
248 | if ": " in message: |
||
249 | message.replace(": ", ":\n\n") |
||
250 | else: |
||
251 | message += "." |
||
252 | messagebox.showerror(title, message) |
||
253 | |||
254 | |||
255 | 1 | def main(args=None): |
|
256 | """Process command-line arguments and run the program.""" |
||
257 | |||
258 | # Main parser |
||
259 | 1 | parser = argparse.ArgumentParser(prog=GUI, description=__doc__, **SHARED) |
|
0 ignored issues
–
show
Usage of
* or ** arguments should usually be done with care.
Generally, there is nothing wrong with usage of For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect. ![]() |
|||
260 | 1 | parser.add_argument('--home', metavar='PATH', help="path to home directory") |
|
261 | # Hidden argument to override the root sharing directory path |
||
262 | 1 | parser.add_argument('--root', metavar="PATH", help=argparse.SUPPRESS) |
|
263 | # Hidden argument to run the program as a different user |
||
264 | 1 | parser.add_argument('--test', metavar='"First Last"', |
|
265 | help=argparse.SUPPRESS) |
||
266 | |||
267 | # Parse arguments |
||
268 | 1 | args = parser.parse_args(args=args) |
|
269 | |||
270 | # Configure logging |
||
271 | 1 | _configure_logging(args.verbose) |
|
272 | |||
273 | # Run the program |
||
274 | 1 | try: |
|
275 | 1 | success = run(args) |
|
276 | except KeyboardInterrupt: |
||
277 | logging.debug("program manually closed") |
||
278 | else: |
||
279 | 1 | if success: |
|
280 | 1 | logging.debug("program exited") |
|
281 | else: |
||
282 | logging.debug("program exited with error") |
||
283 | sys.exit(1) |
||
284 | |||
285 | |||
286 | 1 | def _configure_logging(verbosity=0): |
|
287 | """Configure logging using the provided verbosity level (0+).""" |
||
288 | |||
289 | # Configure the logging level and format |
||
290 | 1 | if verbosity == 0: |
|
291 | 1 | level = settings.VERBOSE_LOGGING_LEVEL |
|
292 | 1 | default_format = settings.DEFAULT_LOGGING_FORMAT |
|
293 | 1 | verbose_format = settings.VERBOSE_LOGGING_FORMAT |
|
294 | else: |
||
295 | 1 | level = settings.VERBOSE2_LOGGING_LEVEL |
|
296 | 1 | default_format = verbose_format = settings.VERBOSE_LOGGING_FORMAT |
|
297 | |||
298 | # Set a custom formatter |
||
299 | 1 | logging.basicConfig(level=level) |
|
300 | 1 | formatter = WarningFormatter(default_format, verbose_format) |
|
301 | 1 | logging.root.handlers[0].setFormatter(formatter) |
|
302 | |||
303 | |||
304 | 1 | def run(args): |
|
305 | """Start the GUI.""" |
||
306 | |||
307 | # Exit if tkinter is not available |
||
308 | 1 | if isinstance(tk, Mock) or isinstance(ttk, Mock): |
|
309 | 1 | logging.error("tkinter is not available") |
|
310 | 1 | return False |
|
311 | |||
312 | root = tk.Tk() |
||
313 | root.title("{} (v{})".format(GUI, __version__)) |
||
314 | root.minsize(500, 500) |
||
315 | |||
316 | # Map the Mac 'command' key to 'control' |
||
317 | if sys.platform == 'darwin': |
||
318 | root.bind_class('Listbox', '<Command-Button-1>', |
||
319 | root.bind_class('Listbox', '<Control-Button-1>')) |
||
320 | |||
321 | # Temporarily hide the window for other dialogs |
||
322 | root.withdraw() |
||
323 | |||
324 | # Start the application |
||
325 | try: |
||
326 | app = Application(master=root, home=args.home, |
||
327 | root=args.root, name=args.test) |
||
328 | if _LAUNCH: |
||
329 | app.mainloop() |
||
330 | except Exception as e: # pylint: disable=broad-except |
||
331 | Application.show_error_from_exception(e) |
||
332 | return False |
||
333 | |||
334 | return True |
||
335 | |||
336 | |||
337 | if __name__ == '__main__': # pragma: no cover (manual test) |
||
338 | main() |
||
339 |
Generally, there is nothing wrong with usage of
*
or**
arguments. For readability of the code base, we suggest to not over-use these language constructs though.For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.