3
# Copyright (C) 2004 Daniel Burrows <dburrows@debian.org>
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.
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.
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
19
# Code to edit the list of directories that is contained within a
31
from warnings import warn
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):
38
if isinstance(lib, list) and reduce(operator.__and__,
39
map(lambda x:isinstance(x, basestring),
48
config.add_option('General', 'DefinedLibraries',
50
lambda x:reduce(operator.__and__,
51
map(lambda (key,val):isinstance(key, basestring) and valid_library(val),
55
# The library to load on startup. If None, load no library on startup.
56
config.add_option('General', 'DefaultLibrary',
58
lambda x:x == None or isinstance(x, basestring))
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."""
67
"""Wraps the dialog that edits the library definitions.
69
Internal use, don't touch."""
70
def __init__(self, glade_location):
71
"""Initializes the dialog from Glade and sets up bookkeeping
73
xml=gtk.glade.XML(glade_location, root='library_dialog')
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})
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')
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')
92
self.gui=xml.get_widget('library_dialog')
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)
99
# Get the shared list of libraries.
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')
110
# Used to track the history of each library that originally existed.
111
self.library_new_names={}
112
self.library_orig_names={}
114
for key in self.libraries.keys():
115
self.library_new_names[key]=key
116
self.library_orig_names[key]=key
118
self.libraries_model=gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)
119
self.directories_model=gtk.ListStore(gobject.TYPE_STRING)
121
renderer=gtk.CellRendererText()
122
renderer.set_property('editable', 1)
123
renderer.connect('edited', self.handle_library_name_edited)
124
col=gtk.TreeViewColumn('Library',
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',
135
self.libraries_list.append_column(col)
137
col=gtk.TreeViewColumn('Directories',
138
gtk.CellRendererText(),
140
self.directories_list.append_column(col)
142
self.libraries_list.set_model(self.libraries_model)
143
self.directories_list.set_model(self.directories_model)
145
def sort_by_column_data(model, iter1, iter2):
146
return cmp(model[iter1][0], model[iter2][0])
148
self.libraries_model.set_default_sort_func(sort_by_column_data)
149
self.libraries_model.set_sort_column_id(-1, gtk.SORT_ASCENDING)
151
self.directories_model.set_default_sort_func(sort_by_column_data)
152
self.directories_model.set_sort_column_id(-1, gtk.SORT_ASCENDING)
154
libraries_selection=self.libraries_list.get_selection()
155
libraries_selection.connect('changed', lambda *args:self.library_changed(libraries_selection))
157
directories_selection=self.directories_list.get_selection()
158
directories_selection.connect('changed', lambda *args:self.directory_changed(directories_selection))
160
self.update_library_list()
162
self.gui.connect('response', self.handle_response)
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."""
168
assert(dialog == self.gui)
170
if response_id == gtk.RESPONSE_OK:
171
# Trigger library edit events:
172
library_edits.call_listeners(self.library_new_names.items())
174
# Copy and convert back to lists.
176
for key,val in self.libraries.iteritems():
177
new_libraries[key]=list(val)
179
config.set_option('General', 'DefinedLibraries', new_libraries)
180
config.set_option('General', 'DefaultLibrary', self.default_library)
185
# Call this when the list of libraries needs to be regenerated
187
def update_library_list(self):
188
selected=self.selected_library
190
model=gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)
191
names=self.libraries.keys()
196
isdefault=(name == self.default_library)
197
my_iter=model.append([name, isdefault])
199
selected_iter = my_iter
201
self.libraries_list.set_model(model)
202
self.libraries_model=model
204
if selected_iter <> None:
205
self.libraries_list.get_selection().select_iter(selected_iter)
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."""
213
selected=self.selected_library
215
self.directories_model.clear()
217
self.directories_label.set_markup('<b>No library selected</b>')
218
self.add_button.set_sensitive(0)
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)
225
def library_changed(self, selection):
226
"""Handles the selection of a row in the 'library' list."""
228
if self.filesel <> None:
229
self.filesel.destroy()
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()
238
model,paths=selection.get_selected_rows()
239
assert(len(paths)==1)
241
new_selection=model[paths[0]][0]
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)
249
def directory_changed(self, selection):
250
"""Handles the selection of a row in the 'directory' list."""
252
if selection.count_selected_rows() == 0:
253
self.remove_button.set_sensitive(0)
255
self.remove_button.set_sensitive(1)
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."""
266
while self.libraries.has_key(name):
267
name='New Library %d'%x
270
# The first library you create is the default library (by default).
271
isdefault=(len(self.libraries)==0)
273
self.libraries[name]=sets.Set()
275
self.default_library=name
276
self.library_orig_names[name]=None
278
iter=self.libraries_model.append([name, isdefault])
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),
288
def delete_library(self, *args):
289
selection=self.libraries_list.get_selection()
291
assert(self.selected_library <> None)
292
assert(selection.count_selected_rows() == 1)
294
model,paths=selection.get_selected_rows()
296
curr_iter=model[paths[0]]
298
assert(self.selected_library == curr_iter[0])
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]
309
def rename_library(self, *args):
310
model,paths=self.libraries_list.get_selection().get_selected_rows()
312
assert(len(paths)==1)
314
self.libraries_list.grab_focus()
315
self.libraries_list.set_cursor(paths[0],
316
self.libraries_list.get_column(0),
319
def handle_library_name_edited(self, cell, path, new_text):
320
row=self.libraries_model[path]
324
assert(self.libraries.has_key(old_text))
326
if new_text == old_text:
327
# Do nothing to avoid unnecessary recomputation.
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())
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
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
349
del self.libraries[old_text]
350
del self.library_orig_names[old_text]
352
self.libraries_model[path]=(new_text,row[1])
354
self.libraries_model.sort_column_changed()
356
def handle_default_library_toggled(self, cell, path):
357
row=self.libraries_model[path]
362
self.default_library=None
363
self.libraries_model[path]=(row[0],0)
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
369
for tmprow in self.libraries_model:
372
# Commit the new default.
373
self.libraries_model[path]=(row[0], 1)
374
self.default_library=row[0]
376
def __do_add_directories(self, fns):
377
assert(reduce(operator.__and__,
378
map(lambda x:os.path.isdir(x), fns),
381
dirs=self.libraries[self.selected_library]
385
self.directories_model.append([d])
387
self.directories_model.sort_column_changed()
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."""
393
self.libraries_list.get_selection().select_iter(iter)
394
self.__do_add_directories(dirs)
396
def handle_add_directory(self, *args):
397
"""Prompt the user and a new directory to a library."""
399
assert(self.selected_library <> None)
401
if self.filesel <> None:
405
def on_response(dialog, response_id):
406
if response_id <> gtk.RESPONSE_OK:
408
self.filesel.destroy()
411
fns=self.filesel.get_filenames()
412
self.filesel.destroy()
414
self.__do_add_directories(fns)
416
def on_destroy(*args):
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)
429
def remove_directory(self, *args):
430
selected=self.selected_library
433
lib=self.libraries[selected]
435
selection=self.directories_list.get_selection()
437
model,paths=selection.get_selected_rows()
443
warn('Warning: trying to remove %s from library %s, but it\'s not there!'%(d, selected))
445
self.libraries[selected].remove(d)
447
# Future-proofing for the possibility of enabling multi-selections.
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)
454
self.directories_model.remove(i)
456
# Don't need to signal a sort update, since removing rows
457
# never affects the sorted order.
459
active_library_editor=None
460
"""Internal use, don't touch."""
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
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'.
473
This function prevents a situation where two editors are running
474
at once and Bad Stuff happens."""
476
global active_library_editor
478
def zap_library_editor(*args):
479
global active_library_editor
480
active_library_editor=None
482
if active_library_editor <> None:
483
active_library_editor.gui.present()
485
active_library_editor=LibraryEditor(glade_location)
486
active_library_editor.gui.connect('destroy', zap_library_editor)
489
p,i=active_library_editor.new_library(name=name, start_editing=False)
492
active_library_editor.add_directories(i, dirs)