~ubuntu-branches/ubuntu/karmic/quodlibet/karmic

« back to all changes in this revision

Viewing changes to quodlibet/browsers/media.py

  • Committer: Bazaar Package Importer
  • Author(s): Luca Falavigna
  • Date: 2009-01-30 23:55:34 UTC
  • mfrom: (1.1.12 upstream)
  • Revision ID: james.westby@ubuntu.com-20090130235534-l4e72ulw0vqfo17w
Tags: 2.0-1ubuntu1
* Merge from Debian experimental (LP: #276856), remaining Ubuntu changes:
  + debian/patches/40-use-music-profile.patch:
    - Use the "Music and Movies" pipeline per default.
* Refresh the above patch for new upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# Copyright 2006 Markus Koller
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License version 2 as
 
6
# published by the Free Software Foundation
 
7
#
 
8
# $Id: media.py 4330 2008-09-14 03:19:26Z piman $
 
9
 
 
10
import os
 
11
 
 
12
import gtk
 
13
import pango
 
14
 
 
15
from quodlibet import config
 
16
from quodlibet import devices
 
17
from quodlibet import qltk
 
18
from quodlibet import stock
 
19
from quodlibet import util
 
20
 
 
21
from quodlibet.browsers._base import Browser
 
22
from quodlibet.formats._audio import AudioFile
 
23
from quodlibet.qltk.views import AllTreeView
 
24
from quodlibet.qltk.songsmenu import SongsMenu
 
25
from quodlibet.qltk.wlw import WaitLoadBar
 
26
from quodlibet.qltk.browser import LibraryBrowser
 
27
from quodlibet.qltk.delete import DeleteDialog
 
28
 
 
29
class DeviceProperties(gtk.Dialog):
 
30
    def __init__(self, parent, device):
 
31
        super(DeviceProperties, self).__init__(
 
32
            _("Device Properties"), qltk.get_top_parent(parent),
 
33
            buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
 
34
        self.set_default_size(400, -1)
 
35
        self.connect('response', self.__close)
 
36
 
 
37
        table = gtk.Table()
 
38
        table.set_border_width(8)
 
39
        table.set_row_spacings(8)
 
40
        table.set_col_spacings(8)
 
41
        self.vbox.pack_start(table, expand=False)
 
42
 
 
43
        props = []
 
44
 
 
45
        props.append((_("Device:"), device.dev, None))
 
46
        mountpoint = util.escape(
 
47
            device.mountpoint or ("<i>%s</i>" % _("Not mounted")))
 
48
        props.append((_("Mount Point:"), mountpoint, None))
 
49
 
 
50
        props.append((None, None, None))
 
51
 
 
52
        entry = gtk.Entry()
 
53
        entry.set_text(device['name'])
 
54
        props.append((_("_Name:"), entry, 'name'))
 
55
 
 
56
        y = 0
 
57
        for title, value, key in props + device.Properties():
 
58
            if title == None:
 
59
                table.attach(gtk.HSeparator(), 0, 2, y, y + 1)
 
60
            else:
 
61
                if key and isinstance(value, gtk.CheckButton):
 
62
                    value.set_label(title)
 
63
                    value.set_use_underline(True)
 
64
                    value.connect('toggled', self.__changed, key, device)
 
65
                    table.attach(value, 0, 2, y, y + 1, xoptions=gtk.FILL)
 
66
                else:
 
67
                    label = gtk.Label()
 
68
                    label.set_markup("<b>%s</b>" % util.escape(title))
 
69
                    label.set_alignment(0.0, 0.5)
 
70
                    table.attach(label, 0, 1, y, y + 1, xoptions=gtk.FILL)
 
71
                    if key and isinstance(value, gtk.Widget):
 
72
                        widget = value
 
73
                        label.set_mnemonic_widget(widget)
 
74
                        label.set_use_underline(True)
 
75
                        widget.connect('changed', self.__changed, key, device)
 
76
                    else:
 
77
                        widget = gtk.Label(value)
 
78
                        widget.set_use_markup(True)
 
79
                        widget.set_selectable(True)
 
80
                        widget.set_alignment(0.0, 0.5)
 
81
                    table.attach(widget, 1, 2, y, y + 1)
 
82
            y += 1
 
83
        self.show_all()
 
84
 
 
85
    def __changed(self, widget, key, device):
 
86
        if isinstance(widget, gtk.Entry):
 
87
            value = widget.get_text()
 
88
        elif isinstance(widget, gtk.SpinButton):
 
89
            value = widget.get_value()
 
90
        elif isinstance(widget, gtk.CheckButton):
 
91
            value = widget.get_active()
 
92
        else:
 
93
            raise NotImplementedError
 
94
        device[key] = value
 
95
 
 
96
    def __close(self, dialog, response):
 
97
        dialog.destroy()
 
98
        devices.write()
 
99
 
 
100
# This will be included in SongsMenu
 
101
class Menu(gtk.Menu):
 
102
    def __init__(self, songs, library):
 
103
        super(Menu, self).__init__()
 
104
        for device in MediaDevices.devices():
 
105
            i = qltk.MenuItem(device['name'], device.icon)
 
106
            i.set_sensitive(device.is_connected())
 
107
            i.connect_object(
 
108
                'activate', self.__copy_to_device, device, songs, library)
 
109
            self.append(i)
 
110
 
 
111
    @staticmethod
 
112
    def __copy_to_device(device, songs, library):
 
113
        if len(MediaDevices.instances()) > 0:
 
114
            browser = MediaDevices.instances()[0]
 
115
        else:
 
116
            win = LibraryBrowser(MediaDevices, library)
 
117
            browser = win.browser
 
118
        browser.select(device)
 
119
        browser.dropped(browser.get_toplevel().songlist, songs)
 
120
 
 
121
class MediaDevices(gtk.VBox, Browser, util.InstanceTracker):
 
122
    __gsignals__ = Browser.__gsignals__
 
123
 
 
124
    name = _("Media Devices")
 
125
    accelerated_name = _("_Media Devices")
 
126
    priority = 25
 
127
    replaygain_profiles = ['track']
 
128
 
 
129
    __devices = gtk.ListStore(object, str)
 
130
    __busy = False
 
131
    __last = None
 
132
 
 
133
    @staticmethod
 
134
    def cell_data(col, render, model, iter):
 
135
        device = model[iter][0]
 
136
        if device.is_connected():
 
137
            render.markup = "<b>%s</b>" % util.escape(device['name'])
 
138
        else:
 
139
            render.markup = util.escape(device['name'])
 
140
        render.set_property('markup', render.markup)
 
141
 
 
142
    @classmethod
 
143
    def init(klass, library):
 
144
        devices._hal.connect_to_signal(
 
145
            'DeviceAdded', klass.__hal_device_added)
 
146
        devices._hal.connect_to_signal(
 
147
            'DeviceRemoved', klass.__hal_device_removed)
 
148
        for udi in devices.discover():
 
149
            klass.__hal_device_added(udi)
 
150
 
 
151
    @classmethod
 
152
    def devices(klass):
 
153
        return [row[0] for row in klass.__devices]
 
154
 
 
155
    @classmethod
 
156
    def __hal_device_added(klass, udi):
 
157
        device = devices.get_by_udi(udi)
 
158
        if device != None:
 
159
            klass.__add_device(device)
 
160
 
 
161
    @classmethod
 
162
    def __hal_device_removed(klass, udi):
 
163
        for row in klass.__devices:
 
164
            if row[0].udi == udi:
 
165
                klass.__devices.remove(row.iter)
 
166
                break
 
167
 
 
168
    @classmethod
 
169
    def __add_device(klass, device):
 
170
        klass.__devices.append(row=[device, device.icon])
 
171
 
 
172
    def __init__(self, library, player):
 
173
        super(MediaDevices, self).__init__(spacing=6)
 
174
        self._register_instance()
 
175
 
 
176
        self.__cache = {}
 
177
 
 
178
        # Device list on the left pane
 
179
        swin = gtk.ScrolledWindow()
 
180
        swin.set_shadow_type(gtk.SHADOW_IN)
 
181
        swin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
 
182
        self.pack_start(swin)
 
183
 
 
184
        self.__view = view = AllTreeView()
 
185
        view.set_model(self.__devices)
 
186
        view.set_rules_hint(True)
 
187
        view.set_headers_visible(False)
 
188
        view.get_selection().set_mode(gtk.SELECTION_BROWSE)
 
189
        view.get_selection().connect_object('changed', self.__refresh, False)
 
190
        view.connect('popup-menu', self.__popup_menu, library)
 
191
        if player: view.connect('row-activated', lambda *a: player.reset())
 
192
        swin.add(view)
 
193
 
 
194
        col = gtk.TreeViewColumn("Devices")
 
195
        view.append_column(col)
 
196
 
 
197
        render = gtk.CellRendererPixbuf()
 
198
        col.pack_start(render, expand=False)
 
199
        col.add_attribute(render, 'stock-id', 1)
 
200
 
 
201
        self.__render = render = gtk.CellRendererText()
 
202
        render.set_property('ellipsize', pango.ELLIPSIZE_END)
 
203
        render.connect('edited', self.__edited)
 
204
        col.pack_start(render)
 
205
        col.set_cell_data_func(render, MediaDevices.cell_data)
 
206
 
 
207
        hbox = gtk.HBox(spacing=6)
 
208
        hbox.set_homogeneous(True)
 
209
        self.pack_start(hbox, expand=False)
 
210
 
 
211
        self.__refresh_button = refresh = gtk.Button(stock=gtk.STOCK_REFRESH)
 
212
        refresh.connect_object('clicked', self.__refresh, True)
 
213
        refresh.set_sensitive(False)
 
214
        hbox.pack_start(refresh)
 
215
 
 
216
        self.__eject_button = eject = gtk.Button(stock=stock.EJECT)
 
217
        eject.connect('clicked', self.__eject)
 
218
        eject.set_sensitive(False)
 
219
        hbox.pack_start(eject)
 
220
 
 
221
        # Device info on the right pane
 
222
        self.__header = table = gtk.Table()
 
223
        table.set_col_spacings(8)
 
224
 
 
225
        self.__device_icon = icon = gtk.Image()
 
226
        icon.set_size_request(48, 48)
 
227
        table.attach(icon, 0, 1, 0, 2, 0)
 
228
 
 
229
        self.__device_name = label = gtk.Label()
 
230
        label.set_alignment(0, 0)
 
231
        table.attach(label, 1, 3, 0, 1)
 
232
 
 
233
        self.__device_space = label = gtk.Label()
 
234
        label.set_alignment(0, 0.5)
 
235
        table.attach(label, 1, 2, 1, 2)
 
236
 
 
237
        self.__progress = progress = gtk.ProgressBar()
 
238
        progress.set_size_request(150, -1)
 
239
        table.attach(progress, 2, 3, 1, 2, xoptions=0, yoptions=0)
 
240
 
 
241
        self.accelerators = gtk.AccelGroup()
 
242
        key, mod = gtk.accelerator_parse('F2')
 
243
        self.accelerators.connect_group(key, mod, 0, self.__rename)
 
244
 
 
245
        self.__statusbar = WaitLoadBar()
 
246
 
 
247
        self.show_all()
 
248
 
 
249
    def pack(self, songpane):
 
250
        self.__vbox = vbox = gtk.VBox(spacing=6)
 
251
        vbox.pack_start(self.__header, expand=False)
 
252
        vbox.pack_start(songpane)
 
253
        vbox.pack_start(self.__statusbar, expand=False)
 
254
 
 
255
        vbox.show()
 
256
        self.__header.show_all()
 
257
        self.__header.hide()
 
258
        self.__statusbar.show_all()
 
259
        self.__statusbar.hide()
 
260
 
 
261
        self.__paned = paned = qltk.RHPaned()
 
262
        paned.pack1(self)
 
263
        paned.pack2(vbox)
 
264
        return paned
 
265
 
 
266
    def unpack(self, container, songpane):
 
267
        self.__vbox.remove(songpane)
 
268
        self.__paned.remove(self)
 
269
 
 
270
    def Menu(self, songs, songlist, library):
 
271
        model, iter = self.__view.get_selection().get_selected()
 
272
        if iter:
 
273
            device = model[iter][0]
 
274
            delete = device.delete and self.__delete_songs
 
275
        else:
 
276
            delete = False
 
277
 
 
278
        menu = SongsMenu(library, songs, delete=delete, remove=False,
 
279
            accels=songlist.accelerators)
 
280
        return menu
 
281
 
 
282
    def activate(self):
 
283
        self.__refresh()
 
284
 
 
285
    def save(self):
 
286
        selection = self.__view.get_selection()
 
287
        model, iter = selection.get_selected()
 
288
        config.set('browsers', 'media', model[iter][0]['name'])
 
289
 
 
290
    def restore(self):
 
291
        try: name = config.get('browsers', 'media')
 
292
        except config.error: pass
 
293
        else:
 
294
            for row in self.__devices:
 
295
                if row[0]['name'] == name: break
 
296
            else: return
 
297
            selection = self.__view.get_selection()
 
298
            selection.unselect_all()
 
299
            selection.select_iter(row.iter)
 
300
 
 
301
    def select(self, device):
 
302
        for row in self.__devices:
 
303
            if row[0] == device: break
 
304
        else: return
 
305
 
 
306
        # Force a full refresh
 
307
        try: del self.__cache[device.udi]
 
308
        except KeyError: pass
 
309
 
 
310
        selection = self.__view.get_selection()
 
311
        selection.unselect_all()
 
312
        selection.select_iter(row.iter)
 
313
 
 
314
    def dropped(self, songlist, songs):
 
315
        return self.__copy_songs(songlist, songs)
 
316
 
 
317
    def __popup_menu(self, view, library):
 
318
        model, iter = view.get_selection().get_selected()
 
319
        device = model[iter][0]
 
320
 
 
321
        if device.is_connected() and not self.__busy:
 
322
            songs = self.__list_songs(device)
 
323
        else:
 
324
            songs = []
 
325
        menu = SongsMenu(library, songs, playlists=False,
 
326
                         devices=False, remove=False)
 
327
 
 
328
        menu.preseparate()
 
329
 
 
330
        props = gtk.ImageMenuItem(gtk.STOCK_PROPERTIES)
 
331
        props.connect_object('activate', self.__properties, model[iter][0])
 
332
        props.set_sensitive(not self.__busy)
 
333
        menu.prepend(props)
 
334
 
 
335
        ren = gtk.ImageMenuItem(stock.RENAME)
 
336
        keyval, mod = gtk.accelerator_parse("F2")
 
337
        ren.add_accelerator(
 
338
            'activate', self.accelerators, keyval, mod, gtk.ACCEL_VISIBLE)
 
339
        def rename(path):
 
340
            self.__render.set_property('editable', True)
 
341
            view.set_cursor(path, view.get_columns()[0], start_editing=True)
 
342
        ren.connect_object('activate', rename, model.get_path(iter))
 
343
        menu.prepend(ren)
 
344
 
 
345
        menu.preseparate()
 
346
 
 
347
        eject = gtk.ImageMenuItem(stock.EJECT)
 
348
        eject.set_sensitive(
 
349
            not self.__busy and device.eject and device.is_connected())
 
350
        eject.connect_object('activate', self.__eject, None)
 
351
        menu.prepend(eject)
 
352
 
 
353
        refresh = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
 
354
        refresh.set_sensitive(device.is_connected())
 
355
        refresh.connect_object('activate', self.__refresh, True)
 
356
        menu.prepend(refresh)
 
357
 
 
358
        menu.show_all()
 
359
        menu.popup(None, None, None, 0, gtk.get_current_event_time())
 
360
        return True
 
361
 
 
362
    def __properties(self, device):
 
363
        DeviceProperties(self, device).run()
 
364
        self.__set_name(device)
 
365
 
 
366
    def __rename(self, group, acceleratable, keyval, modifier):
 
367
        model, iter = self.__view.get_selection().get_selected()
 
368
        if iter:
 
369
            self.__render.set_property('editable', True)
 
370
            self.__view.set_cursor(model.get_path(iter),
 
371
                                   self.__view.get_columns()[0],
 
372
                                   start_editing=True)
 
373
 
 
374
    def __edited(self, render, path, newname):
 
375
        self.__devices[path][0]['name'] = newname
 
376
        self.__set_name(self.__devices[path][0])
 
377
        render.set_property('editable', False)
 
378
        devices.write()
 
379
 
 
380
    def __set_name(self, device):
 
381
        self.__device_name.set_markup(
 
382
            '<span size="x-large"><b>%s</b></span>' %
 
383
                util.escape(device['name']))
 
384
 
 
385
    def __refresh(self, rescan=False):
 
386
        model, iter = self.__view.get_selection().get_selected()
 
387
        if iter:
 
388
            path = model[iter].path
 
389
            if not rescan and self.__last == path: return
 
390
            self.__last = path
 
391
 
 
392
            device = model[iter][0]
 
393
            self.__device_icon.set_from_stock(device.icon, gtk.ICON_SIZE_DIALOG)
 
394
            self.__set_name(device)
 
395
 
 
396
            songs = []
 
397
            if device.is_connected():
 
398
                self.__header.show_all()
 
399
                self.__eject_button.set_sensitive(bool(device.eject))
 
400
                self.__refresh_button.set_sensitive(True)
 
401
                self.__refresh_space(device)
 
402
 
 
403
                try: songs = self.__list_songs(device, rescan)
 
404
                except NotImplementedError: pass
 
405
            else:
 
406
                self.__eject_button.set_sensitive(False)
 
407
                self.__refresh_button.set_sensitive(False)
 
408
                self.__header.hide()
 
409
            self.emit('songs-selected', songs, device.ordered)
 
410
        else:
 
411
            self.__last = None
 
412
            self.emit('songs-selected', [], False)
 
413
 
 
414
    def __refresh_space(self, device):
 
415
        try: space, free = device.get_space()
 
416
        except NotImplementedError:
 
417
            self.__device_space.set_text("")
 
418
            self.__progress.hide()
 
419
        else:
 
420
            used = space - free
 
421
            fraction = float(used) / space
 
422
 
 
423
            self.__device_space.set_markup(
 
424
                _("<b>%s</b> used, <b>%s</b> available") %
 
425
                    (util.format_size(used), util.format_size(free)))
 
426
            self.__progress.set_fraction(fraction)
 
427
            self.__progress.set_text("%.f%%" % round(fraction * 100))
 
428
            self.__progress.show()
 
429
 
 
430
    def __list_songs(self, device, rescan=False):
 
431
        if rescan or not device.udi in self.__cache:
 
432
            self.__busy = True
 
433
            self.__cache[device.udi] = device.list(self.__statusbar)
 
434
            self.__busy = False
 
435
        return self.__cache[device.udi]
 
436
 
 
437
    def __check_device(self, device, message):
 
438
        if not device.is_connected():
 
439
            qltk.WarningMessage(
 
440
                self, message,
 
441
                _("<b>%s</b> is not connected.") % util.escape(device['name'])
 
442
            ).run()
 
443
            return False
 
444
        return True
 
445
 
 
446
    def __copy_songs(self, songlist, songs):
 
447
        model, iter = self.__view.get_selection().get_selected()
 
448
        if not iter: return False
 
449
 
 
450
        device = model[iter][0]
 
451
        if not self.__check_device(device, _("Unable to copy songs")):
 
452
            return False
 
453
 
 
454
        self.__busy = True
 
455
 
 
456
        wlb = self.__statusbar
 
457
        wlb.setup(len(songs), _("Copying <b>%(song)s</b>"), { 'song': '' })
 
458
        wlb.show()
 
459
 
 
460
        model = songlist.get_model()
 
461
        for song in songs:
 
462
            label = util.escape(song('~artist~title'))
 
463
            if wlb.step(song=label):
 
464
                wlb.hide()
 
465
                break
 
466
 
 
467
            if len(model) > 0:
 
468
                songlist.scroll_to_cell(model[-1].path)
 
469
            while gtk.events_pending(): gtk.main_iteration()
 
470
 
 
471
            space, free = device.get_space()
 
472
            if free < os.path.getsize(song['~filename']):
 
473
                wlb.hide()
 
474
                qltk.WarningMessage(
 
475
                    self, _("Unable to copy song"),
 
476
                    _("There is not enough free space for this song.")
 
477
                ).run()
 
478
                break
 
479
 
 
480
            status = device.copy(songlist, song)
 
481
            if isinstance(status, AudioFile):
 
482
                model.append([status])
 
483
                try: self.__cache[device.udi].append(song)
 
484
                except KeyError: pass
 
485
                self.__refresh_space(device)
 
486
            else:
 
487
                msg = _("<b>%s</b> could not be copied.") % label
 
488
                if type(status) == unicode:
 
489
                    msg += "\n\n" + util.escape(status)
 
490
                qltk.WarningMessage(self, _("Unable to copy song"), msg).run()
 
491
 
 
492
        if device.cleanup and not device.cleanup(wlb, 'copy'):
 
493
            pass
 
494
        else:
 
495
            wlb.hide()
 
496
 
 
497
        self.__busy = False
 
498
        return True
 
499
 
 
500
    def __delete_songs(self, songs):
 
501
        model, iter = self.__view.get_selection().get_selected()
 
502
        if not iter:
 
503
            return False
 
504
        songlist = qltk.get_top_parent(self).songlist
 
505
 
 
506
        device = model[iter][0]
 
507
        if not self.__check_device(device, _("Unable to delete songs")):
 
508
            return False
 
509
 
 
510
        song_titles = [s('~artist~title') for s in songs]
 
511
        if DeleteDialog(self, song_titles, False, True).run() != 2:
 
512
            return False
 
513
 
 
514
        self.__busy = True
 
515
 
 
516
        wlb = self.__statusbar
 
517
        wlb.setup(len(songs), _("Deleting <b>%(song)s</b>"), { 'song': '' })
 
518
        wlb.show()
 
519
 
 
520
        model = songlist.get_model()
 
521
        for song in songs:
 
522
            label = util.escape(song('~artist~title'))
 
523
            if wlb.step(song=label):
 
524
                wlb.hide()
 
525
                break
 
526
 
 
527
            status = device.delete(songlist, song)
 
528
            if status == True:
 
529
                model.remove(model.find(song))
 
530
                try: self.__cache[device.udi].remove(song)
 
531
                except (KeyError, ValueError): pass
 
532
                self.__refresh_space(device)
 
533
            else:
 
534
                msg = _("<b>%s</b> could not be deleted.") % label
 
535
                if type(status) == unicode:
 
536
                    msg += "\n\n%s" % status
 
537
                qltk.WarningMessage(
 
538
                    self, _("Unable to delete song"), msg).run()
 
539
 
 
540
        if device.cleanup and not device.cleanup(wlb, 'delete'):
 
541
            pass
 
542
        else:
 
543
            wlb.hide()
 
544
 
 
545
        self.__busy = False
 
546
 
 
547
    def __eject(self, button):
 
548
        model, iter = self.__view.get_selection().get_selected()
 
549
        if iter:
 
550
            device = model[iter][0]
 
551
            status = device.eject()
 
552
            if status == True:
 
553
                self.__refresh(True)
 
554
            else:
 
555
                msg = _("Ejecting <b>%s</b> failed.") % device['name']
 
556
                if status:
 
557
                    msg += "\n\n%s" % status
 
558
                qltk.ErrorMessage(self, _("Unable to eject device"), msg).run()
 
559
 
 
560
if devices.init():
 
561
    browsers = [MediaDevices]
 
562
else:
 
563
    print_w("Couldn't connect to HAL, disabling Media Devices browser.")
 
564
    browsers = []