~ubuntu-branches/ubuntu/trusty/musiclibrarian/trusty

« back to all changes in this revision

Viewing changes to build/lib/musiclibrarian/libraryeditor.py

  • Committer: Bazaar Package Importer
  • Author(s): Barry deFreese
  • Date: 2008-02-12 22:36:16 UTC
  • mfrom: (3.1.2 lenny)
  • Revision ID: james.westby@ubuntu.com-20080212223616-smcqyhbtxq6h7smw
Tags: 1.6-2.1
* Non-maintainer upload.
* Bump versioned build-deps for debhelper and python-central.
* XS-Python-Version: all. (Closes: #445399).
* Add Copyright holder to debian/copyright.
  + Link to GPL-2.
* Add Homepage field.
  + Remove upstream URL from package description.
* Bump compat to 5 to match debhelper build-dep.
* Bump Standards Version to 3.7.3.
  + Menu policy transition.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# libraryeditor.py
2
 
#
3
 
#   Copyright (C) 2004 Daniel Burrows <dburrows@debian.org>
4
 
#
5
 
#   This program is free software; you can redistribute it and/or modify
6
 
#   it under the terms of the GNU General Public License as published by
7
 
#   the Free Software Foundation; either version 2 of the License, or
8
 
#   (at your option) any later version.
9
 
#
10
 
#   This program is distributed in the hope that it will be useful,
11
 
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 
#   GNU General Public License for more details.
14
 
#
15
 
#   You should have received a copy of the GNU General Public License
16
 
#   along with this program; if not, write to the Free Software
17
 
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18
 
#
19
 
# Code to edit the list of directories that is contained within a
20
 
# library.
21
 
 
22
 
import config
23
 
import listenable
24
 
 
25
 
import gobject
26
 
import gtk
27
 
import operator
28
 
import os
29
 
import sets
30
 
 
31
 
from warnings import warn
32
 
 
33
 
def valid_library(lib):
34
 
    """Tests if lib is a valid library: a string or a list of strings."""
35
 
    if isinstance(lib, basestring):
36
 
        return True
37
 
 
38
 
    if isinstance(lib, list) and reduce(operator.__and__,
39
 
                                        map(lambda x:isinstance(x, basestring),
40
 
                                            lib),
41
 
                                        True):
42
 
        return True
43
 
 
44
 
    return False
45
 
 
46
 
 
47
 
# Defined libraries:
48
 
config.add_option('General', 'DefinedLibraries',
49
 
                  {},
50
 
                  lambda x:reduce(operator.__and__,
51
 
                                  map(lambda (key,val):isinstance(key, basestring) and valid_library(val),
52
 
                                      x.items()),
53
 
                                  True))
54
 
 
55
 
# The library to load on startup.  If None, load no library on startup.
56
 
config.add_option('General', 'DefaultLibrary',
57
 
                  None,
58
 
                  lambda x:x == None or isinstance(x, basestring))
59
 
 
60
 
library_edits=listenable.Listenable()
61
 
"""This signal is emitted when a library edit is committed.  Its
62
 
single argument is a list of tuples (old_name, new_name) where each
63
 
old_name is the name of a library, and the new_name is the library's
64
 
new name or None if it has been deleted."""
65
 
 
66
 
class LibraryEditor:
67
 
    """Wraps the dialog that edits the library definitions.
68
 
 
69
 
    Internal use, don't touch."""
70
 
    def __init__(self, glade_location):
71
 
        """Initializes the dialog from Glade and sets up bookkeeping
72
 
        information."""
73
 
        xml=gtk.glade.XML(glade_location, root='library_dialog')
74
 
 
75
 
        xml.signal_autoconnect({'new_library' : self.new_library,
76
 
                                'delete_library' : self.delete_library,
77
 
                                'rename_library' : self.rename_library,
78
 
                                'add_directory' : self.handle_add_directory,
79
 
                                'remove_directory' : self.remove_directory})
80
 
 
81
 
        # Widget extraction
82
 
        self.new_button=xml.get_widget('new')
83
 
        self.delete_button=xml.get_widget('delete')
84
 
        self.rename_button=xml.get_widget('rename')
85
 
        self.add_button=xml.get_widget('add')
86
 
        self.remove_button=xml.get_widget('remove')
87
 
 
88
 
        self.libraries_list=xml.get_widget('libraries_list')
89
 
        self.directories_list=xml.get_widget('directories_list')
90
 
        self.directories_label=xml.get_widget('directories_label')
91
 
 
92
 
        self.gui=xml.get_widget('library_dialog')
93
 
 
94
 
        self.add_button.set_sensitive(0)
95
 
        self.remove_button.set_sensitive(0)
96
 
        self.delete_button.set_sensitive(0)
97
 
        self.rename_button.set_sensitive(0)
98
 
 
99
 
        # Get the shared list of libraries.
100
 
        self.libraries={}
101
 
 
102
 
        for key,val in config.get_option('General', 'DefinedLibraries').iteritems():
103
 
            self.libraries[key]=sets.Set(val)
104
 
        # Used to "remember" what the currently selected library is,
105
 
        # to avoid unnecessary adjustments of other widgets.
106
 
        self.selected_library=None
107
 
        self.default_library=config.get_option('General', 'DefaultLibrary')
108
 
        self.filesel=None
109
 
 
110
 
        # Used to track the history of each library that originally existed.
111
 
        self.library_new_names={}
112
 
        self.library_orig_names={}
113
 
 
114
 
        for key in self.libraries.keys():
115
 
            self.library_new_names[key]=key
116
 
            self.library_orig_names[key]=key
117
 
 
118
 
        self.libraries_model=gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)
119
 
        self.directories_model=gtk.ListStore(gobject.TYPE_STRING)
120
 
 
121
 
        renderer=gtk.CellRendererText()
122
 
        renderer.set_property('editable', 1)
123
 
        renderer.connect('edited', self.handle_library_name_edited)
124
 
        col=gtk.TreeViewColumn('Library',
125
 
                               renderer,
126
 
                               text=0)
127
 
        self.libraries_list.append_column(col)
128
 
        renderer=gtk.CellRendererToggle()
129
 
        renderer.set_radio(1)
130
 
        renderer.set_property('activatable', 1)
131
 
        renderer.connect('toggled', self.handle_default_library_toggled)
132
 
        col=gtk.TreeViewColumn('Default',
133
 
                               renderer,
134
 
                               active=1)
135
 
        self.libraries_list.append_column(col)
136
 
 
137
 
        col=gtk.TreeViewColumn('Directories',
138
 
                               gtk.CellRendererText(),
139
 
                               text=0)
140
 
        self.directories_list.append_column(col)
141
 
 
142
 
        self.libraries_list.set_model(self.libraries_model)
143
 
        self.directories_list.set_model(self.directories_model)
144
 
 
145
 
        def sort_by_column_data(model, iter1, iter2):
146
 
            return cmp(model[iter1][0], model[iter2][0])
147
 
 
148
 
        self.libraries_model.set_default_sort_func(sort_by_column_data)
149
 
        self.libraries_model.set_sort_column_id(-1, gtk.SORT_ASCENDING)
150
 
 
151
 
        self.directories_model.set_default_sort_func(sort_by_column_data)
152
 
        self.directories_model.set_sort_column_id(-1, gtk.SORT_ASCENDING)
153
 
 
154
 
        libraries_selection=self.libraries_list.get_selection()
155
 
        libraries_selection.connect('changed', lambda *args:self.library_changed(libraries_selection))
156
 
 
157
 
        directories_selection=self.directories_list.get_selection()
158
 
        directories_selection.connect('changed', lambda *args:self.directory_changed(directories_selection))
159
 
 
160
 
        self.update_library_list()
161
 
 
162
 
        self.gui.connect('response', self.handle_response)
163
 
 
164
 
    def handle_response(self, dialog, response_id):
165
 
        """Deal with our own response, by pumping the changed value
166
 
        back into the configuration system."""
167
 
 
168
 
        assert(dialog == self.gui)
169
 
 
170
 
        if response_id == gtk.RESPONSE_OK:
171
 
            # Trigger library edit events:
172
 
            library_edits.call_listeners(self.library_new_names.items())
173
 
 
174
 
            # Copy and convert back to lists.
175
 
            new_libraries={}
176
 
            for key,val in self.libraries.iteritems():
177
 
                new_libraries[key]=list(val)
178
 
 
179
 
            config.set_option('General', 'DefinedLibraries', new_libraries)
180
 
            config.set_option('General', 'DefaultLibrary', self.default_library)
181
 
 
182
 
        dialog.destroy()
183
 
                         
184
 
 
185
 
    # Call this when the list of libraries needs to be regenerated
186
 
    # from scratch:
187
 
    def update_library_list(self):
188
 
        selected=self.selected_library
189
 
 
190
 
        model=gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)
191
 
        names=self.libraries.keys()
192
 
        names.sort()
193
 
        selected_iter=None
194
 
 
195
 
        for name in names:
196
 
            isdefault=(name == self.default_library)
197
 
            my_iter=model.append([name, isdefault])
198
 
            if name == selected:
199
 
                selected_iter = my_iter
200
 
 
201
 
        self.libraries_list.set_model(model)
202
 
        self.libraries_model=model
203
 
 
204
 
        if selected_iter <> None:
205
 
            self.libraries_list.get_selection().select_iter(selected_iter)
206
 
 
207
 
    # Similarly
208
 
    def update_directory_list(self):
209
 
        """Synchronizes the displayed list of directories to be
210
 
        consistent with the directories associated with
211
 
        self.selected_library."""
212
 
 
213
 
        selected=self.selected_library
214
 
 
215
 
        self.directories_model.clear()
216
 
        if selected == None:
217
 
            self.directories_label.set_markup('<b>No library selected</b>')
218
 
            self.add_button.set_sensitive(0)
219
 
        else:
220
 
            self.directories_label.set_markup('<b>Contents of %s</b>'%selected)
221
 
            for d in self.libraries[selected]:
222
 
                self.directories_model.append([d])
223
 
            self.add_button.set_sensitive(1)
224
 
 
225
 
    def library_changed(self, selection):
226
 
        """Handles the selection of a row in the 'library' list."""
227
 
 
228
 
        if self.filesel <> None:
229
 
            self.filesel.destroy()
230
 
 
231
 
        if selection.count_selected_rows() == 0:
232
 
            self.selected_library=None
233
 
            self.directories_model.clear()
234
 
            self.delete_button.set_sensitive(0)
235
 
            self.rename_button.set_sensitive(0)
236
 
            self.update_directory_list()
237
 
        else:
238
 
            model,paths=selection.get_selected_rows()
239
 
            assert(len(paths)==1)
240
 
 
241
 
            new_selection=model[paths[0]][0]
242
 
 
243
 
            if new_selection <> self.selected_library:
244
 
                self.selected_library=new_selection
245
 
                self.update_directory_list()
246
 
            self.delete_button.set_sensitive(1)
247
 
            self.rename_button.set_sensitive(1)
248
 
 
249
 
    def directory_changed(self, selection):
250
 
        """Handles the selection of a row in the 'directory' list."""
251
 
 
252
 
        if selection.count_selected_rows() == 0:
253
 
            self.remove_button.set_sensitive(0)
254
 
        else:
255
 
            self.remove_button.set_sensitive(1)
256
 
 
257
 
    def new_library(self, widget=None, name=None, start_editing=True, *args):
258
 
        """Handles clicks on the 'New Library' button by generating a
259
 
        new library.  Returns a TreeIter corresponding to the location
260
 
        of the new library in the model tree."""
261
 
 
262
 
        if name == None:
263
 
            name='New Library'
264
 
        x=2
265
 
 
266
 
        while self.libraries.has_key(name):
267
 
            name='New Library %d'%x
268
 
            x+=1
269
 
 
270
 
        # The first library you create is the default library (by default).
271
 
        isdefault=(len(self.libraries)==0)
272
 
 
273
 
        self.libraries[name]=sets.Set()
274
 
        if isdefault:
275
 
            self.default_library=name
276
 
        self.library_orig_names[name]=None
277
 
 
278
 
        iter=self.libraries_model.append([name, isdefault])
279
 
 
280
 
        self.libraries_list.get_selection().select_iter(iter)
281
 
        path=self.libraries_model.get_path(iter)
282
 
        self.libraries_list.set_cursor(path,
283
 
                                       self.libraries_list.get_column(0),
284
 
                                       start_editing)
285
 
 
286
 
        return path,iter
287
 
 
288
 
    def delete_library(self, *args):
289
 
        selection=self.libraries_list.get_selection()
290
 
 
291
 
        assert(self.selected_library <> None)
292
 
        assert(selection.count_selected_rows() == 1)
293
 
 
294
 
        model,paths=selection.get_selected_rows()
295
 
 
296
 
        curr_iter=model[paths[0]]
297
 
 
298
 
        assert(self.selected_library == curr_iter[0])
299
 
 
300
 
        if self.selected_library == self.default_library:
301
 
            self.default_library = None
302
 
        if self.library_new_names.has_key(self.selected_library):
303
 
            self.library_new_names[self.selected_library]=None
304
 
        del self.library_orig_names[self.selected_library]
305
 
        del self.libraries[self.selected_library]
306
 
        del curr_iter
307
 
        del model[paths[0]]
308
 
 
309
 
    def rename_library(self, *args):
310
 
        model,paths=self.libraries_list.get_selection().get_selected_rows()
311
 
 
312
 
        assert(len(paths)==1)
313
 
 
314
 
        self.libraries_list.grab_focus()
315
 
        self.libraries_list.set_cursor(paths[0],
316
 
                                       self.libraries_list.get_column(0),
317
 
                                       True)
318
 
 
319
 
    def handle_library_name_edited(self, cell, path, new_text):
320
 
        row=self.libraries_model[path]
321
 
 
322
 
        old_text=row[0]
323
 
 
324
 
        assert(self.libraries.has_key(old_text))
325
 
 
326
 
        if new_text == old_text:
327
 
           # Do nothing to avoid unnecessary recomputation.
328
 
            return
329
 
 
330
 
        if self.libraries.has_key(new_text):
331
 
            m=gtk.MessageDialog(type=gtk.MESSAGE_ERROR,
332
 
                                buttons=gtk.BUTTONS_OK,
333
 
                                message_format='The library "%s" already exists.'%new_text)
334
 
            m.connect('response', lambda *args:m.destroy())
335
 
            m.show()
336
 
            return
337
 
 
338
 
        self.libraries[new_text]=self.libraries[old_text]
339
 
        self.library_orig_names[new_text]=self.library_orig_names[old_text]
340
 
        orig_name=self.library_orig_names[new_text]
341
 
        if orig_name <> None:
342
 
            self.library_new_names[orig_name]=new_text
343
 
 
344
 
        if old_text == self.selected_library:
345
 
            self.selected_library=new_text
346
 
        if old_text == self.default_library:
347
 
            self.default_library=new_text
348
 
 
349
 
        del self.libraries[old_text]
350
 
        del self.library_orig_names[old_text]
351
 
 
352
 
        self.libraries_model[path]=(new_text,row[1])
353
 
 
354
 
        self.libraries_model.sort_column_changed()
355
 
 
356
 
    def handle_default_library_toggled(self, cell, path):
357
 
        row=self.libraries_model[path]
358
 
 
359
 
        old_value=row[1]
360
 
 
361
 
        if old_value:
362
 
            self.default_library=None
363
 
            self.libraries_model[path]=(row[0],0)
364
 
        else:
365
 
            if self.default_library <> None:
366
 
                # Just wipe out all default settings.  Inefficient but
367
 
                # safe, and should be OK if only a few libraries
368
 
                # exist.
369
 
                for tmprow in self.libraries_model:
370
 
                    tmprow[1]=0
371
 
 
372
 
            # Commit the new default.
373
 
            self.libraries_model[path]=(row[0], 1)
374
 
            self.default_library=row[0]
375
 
 
376
 
    def __do_add_directories(self, fns):
377
 
        assert(reduce(operator.__and__,
378
 
                      map(lambda x:os.path.isdir(x), fns),
379
 
                      True))
380
 
 
381
 
        dirs=self.libraries[self.selected_library]
382
 
        for d in fns:
383
 
            if d not in dirs:
384
 
                dirs.add(d)
385
 
                self.directories_model.append([d])
386
 
 
387
 
        self.directories_model.sort_column_changed()
388
 
 
389
 
    def add_directories(self, iter, dirs):
390
 
        """Add some directories to a library.  iter is the TreeIter of
391
 
        the library to be modified, in the library list TreeView."""
392
 
 
393
 
        self.libraries_list.get_selection().select_iter(iter)
394
 
        self.__do_add_directories(dirs)
395
 
 
396
 
    def handle_add_directory(self, *args):
397
 
        """Prompt the user and a new directory to a library."""
398
 
 
399
 
        assert(self.selected_library <> None)
400
 
 
401
 
        if self.filesel <> None:
402
 
            self.filesel.show()
403
 
            return
404
 
 
405
 
        def on_response(dialog, response_id):
406
 
            if response_id <> gtk.RESPONSE_OK:
407
 
                # Do nothing.
408
 
                self.filesel.destroy()
409
 
                return
410
 
 
411
 
            fns=self.filesel.get_filenames()
412
 
            self.filesel.destroy()
413
 
 
414
 
            self.__do_add_directories(fns)
415
 
 
416
 
        def on_destroy(*args):
417
 
            self.filesel=None
418
 
 
419
 
        self.filesel=gtk.FileChooserDialog(
420
 
            title='Choose a directory to add to %s'%self.selected_library,
421
 
            action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
422
 
            buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK,
423
 
                     gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
424
 
        self.filesel.connect('response', on_response)
425
 
        self.filesel.connect('destroy', on_destroy)
426
 
        self.filesel.set_select_multiple(True)
427
 
        self.filesel.show()
428
 
 
429
 
    def remove_directory(self, *args):
430
 
        selected=self.selected_library
431
 
        assert(selected)
432
 
 
433
 
        lib=self.libraries[selected]
434
 
 
435
 
        selection=self.directories_list.get_selection()
436
 
 
437
 
        model,paths=selection.get_selected_rows()
438
 
 
439
 
        for p in paths:
440
 
            d=model[p][0]
441
 
 
442
 
            if d not in lib:
443
 
                warn('Warning: trying to remove %s from library %s, but it\'s not there!'%(d, selected))
444
 
 
445
 
            self.libraries[selected].remove(d)
446
 
 
447
 
        # Future-proofing for the possibility of enabling multi-selections.
448
 
        #
449
 
        # We need to get all iterators first, because iterators don't
450
 
        # go bad when you delete them.
451
 
        iters=map(lambda x:self.directories_model.get_iter(x), paths)
452
 
 
453
 
        for i in iters:
454
 
            self.directories_model.remove(i)
455
 
 
456
 
        # Don't need to signal a sort update, since removing rows
457
 
        # never affects the sorted order.
458
 
 
459
 
active_library_editor=None
460
 
"""Internal use, don't touch."""
461
 
 
462
 
def show_library_editor(glade_location, name=None, dirs=None):
463
 
    """Create and display the dialog that edits library definitions.
464
 
    If a dialog is already open, just bring it to the front.
465
 
    glade_location is the location from which the glade file should be
466
 
    loaded.
467
 
 
468
 
    If 'name' is not None, a new library named 'name' (possibly
469
 
    mangled to make it unique) will be created.  If, in addition,
470
 
    'dirs' is not None, the new library will initially contain the
471
 
    directories specified in 'dirs'.
472
 
 
473
 
    This function prevents a situation where two editors are running
474
 
    at once and Bad Stuff happens."""
475
 
 
476
 
    global active_library_editor
477
 
 
478
 
    def zap_library_editor(*args):
479
 
        global active_library_editor
480
 
        active_library_editor=None
481
 
 
482
 
    if active_library_editor <> None:
483
 
        active_library_editor.gui.present()
484
 
    else:
485
 
        active_library_editor=LibraryEditor(glade_location)
486
 
        active_library_editor.gui.connect('destroy', zap_library_editor)
487
 
 
488
 
    if name <> None:
489
 
        p,i=active_library_editor.new_library(name=name, start_editing=False)
490
 
 
491
 
        if dirs <> None:
492
 
            active_library_editor.add_directories(i, dirs)