Issues (32)

dtb/gui.py (2 issues)

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)
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)
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)
116
            ttk.Entry(frame, state='readonly', textvariable=self.path_root).grid(row=0, column=1, columnspan=2, **kw_gsp)
117
            ttk.Label(frame, text="Downloads:").grid(row=1, column=0, sticky=tk.W, **kw_gp)
118
            ttk.Entry(frame, state='readonly', textvariable=self.path_downloads).grid(row=1, column=1, **kw_gsp)
119
            ttk.Button(frame, text="...", width=0, command=self.browse_downloads).grid(row=1, column=2, ipadx=5, **kw_gp)
120
121
            return frame
122
123 View Code Duplication
        def frame_incoming(master):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
124
            """Frame for incoming songs."""
125
            frame = ttk.Frame(master, **kw_f)
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)
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)
141
            ttk.Button(frame, text="Ignore Selected", command=self.do_ignore).grid(row=1, column=1, sticky=tk.SW, ipadx=5, **kw_gp)
142
            ttk.Button(frame, text="Download Selected", command=self.do_download).grid(row=1, column=2, sticky=tk.SE, ipadx=5, **kw_gp)
143
            return frame
144
145 View Code Duplication
        def frame_outgoing(master):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
146
            """Frame for outgoing songs."""
147
            frame = ttk.Frame(master, **kw_f)
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)
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)
163
            ttk.Button(frame, text="Remove Selected", command=self.do_remove).grid(row=1, column=1, sticky=tk.SW, ipadx=5, **kw_gp)
164
            ttk.Button(frame, text="Share Songs...", command=self.do_share).grid(row=1, column=2, sticky=tk.SE, ipadx=5, **kw_gp)
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)
174
        separator(frame).grid(row=1, padx=10, pady=5, **kw_gs)
175
        frame_outgoing(frame).grid(row=2, **kw_gs)
176
        separator(frame).grid(row=3, padx=10, pady=5, **kw_gs)
177
        frame_incoming(frame).grid(row=4, **kw_gs)
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)
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