SpecEnhancedPreferredApps
Attachment 'christians-script-v2.py'
Download
Toggle line numbers
1 #!/usr/bin/python
2 # * demo application for a new "default application" chooser
3 # * GUI experiment
4 # * started after failed GUI experiment, my experience failure is describe at
5 # http://mail.gnome.org/archives/usability/2007-January/msg00064.html
6 #
7 # GPL, (C) 2007 Christian Neumair <chris@gnome-de.org>
8
9 import pygtk
10 import gtk
11 import gobject
12 import gnomevfs
13 from sets import Set
14
15 class MimeCategory:
16 def __init__ (self, name, combo_label, exception_label, mime_types, default_application_id_fallback):
17 self.name = name
18 self.combo_label = combo_label
19 self.exception_label = exception_label
20 self.mime_types = mime_types
21 self.default_application_id_fallback = default_application_id_fallback
22 self.update_from_vfs()
23
24 def update_from_vfs (self):
25 self.all_applications = {} # id->application
26 self.applications = {} # MIME type -> application
27 self.default_applications = {} # MIME type -> application
28
29 # applications that can not handle all MIME types, TODO
30 # these would not be available for the category combos
31 self.exception_applications = None
32
33 unhandled_apps = None
34
35 # find out available apps + default app for all mime types
36 for mime_type in self.mime_types:
37 self.applications[mime_type] = gnomevfs.mime_get_all_applications (mime_type)
38 self.default_applications[mime_type] = gnomevfs.mime_get_default_application (mime_type)
39
40 for application in self.applications[mime_type]:
41 self.all_applications[application[0]] = (application)
42
43 # find default app FIXME this should be read out somewhere
44 default_app = None
45
46 self.default_application_id = default_app and default_app[0] or self.default_application_id_fallback
47
48 class ApplicationChooserModel (gtk.ListStore):
49 def __init__ (self, applications):
50 gtk.ListStore.__init__ (self,
51 gobject.TYPE_PYOBJECT, # GnomeVFSMIMEApplication
52 gobject.TYPE_INT, # sort index
53 gobject.TYPE_STRING) # application name
54 self.set_default_sort_func (self.sort_func)
55 self.set_sort_column_id (-1, gtk.SORT_ASCENDING)
56
57 self.applications = applications
58 for application in applications:
59 # application[1] is the name
60 self.append ([application, -1, application[1]])
61
62 # TODO
63 #self.append ([None, 0, None])
64 #self.append ([None, 1, 'Custom...'])
65
66 def sort_func (self, model, a, b):
67 cmp = self.get_value (a, 1) - self.get_value (b, 1)
68 if (cmp != 0):
69 return cmp
70
71 if self.get_value (a, 2) > self.get_value (b, 2):
72 return 1
73 elif self.get_value (a, 2) < self.get_value (b, 2):
74 return -1
75 else:
76 return 0
77
78 class ApplicationChooser (gtk.ComboBox):
79 def __init__ (self, applications, default_application_id):
80 gtk.ComboBox.__init__ (self)
81 self.set_row_separator_func(self.row_separator_func)
82
83 self.set_model (ApplicationChooserModel (applications))
84
85 iter = self.get_model().get_iter_first()
86 i = 0
87 while (iter != None):
88 application = self.get_model().get_value (iter, 0)
89 if application != None and application[0] == default_application_id:
90 self.set_active (i)
91 break
92 i = i + 1
93 iter = self.get_model().iter_next (iter)
94
95
96 cell = gtk.CellRendererText()
97 self.pack_start(cell, True)
98 self.add_attribute(cell, 'text', 2)
99
100 def row_separator_func (self, model, iter):
101 return model.get_value (iter, 0) == None
102
103 class MIMEChooserModel (gtk.ListStore):
104 def __init__ (self, mime_types):
105 gtk.ListStore.__init__ (self,
106 gobject.TYPE_STRING, # MIME Type
107 gobject.TYPE_STRING) # MIME Type (human-readable)
108 self.set_default_sort_func (self.sort_func)
109 self.set_sort_column_id (0, gtk.SORT_ASCENDING)
110
111 self.mime_types = mime_types
112 for mime_type in mime_types:
113 desc = gnomevfs.mime_get_description (mime_type) or mime_type
114 print "appended " + mime_type
115 self.append ([mime_type, desc])
116
117 def sort_func (self, model, a, b):
118 cmp = self.get_value (a, 1) - self.get_value (b, 1)
119 if (cmp != 0):
120 return cmp
121
122 if self.get_value (a, 0) > self.get_value (b, 0):
123 return 1
124 elif self.get_value (a, 0) < self.get_value (b, 0):
125 return -1
126 else:
127 return 0
128
129 # Details view allowing to add MIME types that have handlers
130 # that override the default handler
131 # TODO proper error handling etc.
132 # TODO implement saving/reloading/resynching with MIME data upon changes
133 # TODO decide whether default application itself should show up
134 class DetailsView (gtk.TreeView):
135 def __init__(self, category):
136 gtk.TreeView.__init__(self)
137
138 self.category = category
139 self.adding_mime_type = False
140
141 liststore = gtk.ListStore(gobject.TYPE_STRING, # MIME Type
142 gobject.TYPE_STRING, # MIME Type (human-readable)
143 gtk.ListStore, # MIME Model
144 gobject.TYPE_PYOBJECT, # Active Application
145 gobject.TYPE_STRING, # Active Application (as string)
146 gtk.ListStore) # Application Model
147 self.set_model (liststore)
148 self.get_model ().set_sort_column_id (1, gtk.SORT_ASCENDING)
149
150 self.get_selection().set_mode (gtk.SELECTION_MULTIPLE)
151
152 cell = self.file_type_renderer = gtk.CellRendererCombo ()
153 cell.set_property ('text-column', 1)
154 cell.set_property ('editable', False)
155 cell.set_property ('has-entry', False)
156 column = gtk.TreeViewColumn ('File Type', cell, model=2, text=1)
157 self.append_column (column)
158
159 cell.connect ("edited", self.file_type_edited)
160 cell.connect ("editing-canceled", self.file_type_editing_canceled)
161
162 cell = self.application_renderer = gtk.CellRendererCombo ()
163 cell.set_property ('text-column', 2)
164 cell.set_property ('editable', True)
165 cell.set_property ('has-entry', False)
166 column = gtk.TreeViewColumn ('Application', cell, model=5, text=4)
167 self.append_column (column)
168
169 cell.connect ("edited", self.application_edited)
170 cell.connect ("editing-canceled", self.application_editing_canceled)
171
172 self.unhandled_mime_types = []
173 self.handled_undisplayed_mime_types = []
174 self.handled_displayed_mime_types = []
175
176 # add entries for all the MIME types where we have apps
177 for mime_type in self.category.mime_types:
178 app = self.category.default_applications[mime_type]
179 apps = self.category.applications[mime_type]
180 desc = gnomevfs.mime_get_description (mime_type)
181 if desc == None:
182 print 'mime type "' + mime_type + '" has no description.'
183 desc = mime_type
184
185 if app == None:
186 self.unhandled_mime_types.append (mime_type)
187 elif app[0] != self.category.default_application_id:
188 self.handled_displayed_mime_types.append (mime_type)
189 liststore.append ([mime_type, desc, MIMEChooserModel ([]), \
190 app, app and app[1] or None, ApplicationChooserModel (apps)])
191 else:
192 # handled by default application
193 self.handled_undisplayed_mime_types.append (mime_type)
194 continue
195
196 if liststore.get_iter_first() == None:
197 liststore.append ([None, None, MIMEChooserModel ([]), \
198 None, '(No exceptions defined)', ApplicationChooserModel ([]) ])
199
200 def file_type_edited (self, cell, path_string, mime_type_string):
201 cell.set_property ('editable', False)
202
203 iter = self.get_model ().get_iter_from_string (path_string)
204
205 # EWW my eyes bleed! how do we figure out the MIME type from a description string?
206 # we need the active index of the submodel, i.e. the GtkComboBox!
207 submodel = self.get_model ().get_value (iter, 2)
208
209 subiter = submodel.get_iter_first()
210 while (subiter != None):
211 model_mime_type_string = submodel.get_value (subiter, 1)
212 if model_mime_type_string == mime_type_string:
213 break
214 subiter = submodel.iter_next (subiter)
215
216 if (subiter == None): # eww should not happen!
217 self.file_type_editing_canceled (cell)
218 return
219
220 mime_type = submodel.get_value (subiter, 0)
221
222 self.get_model ().set_value (iter, 0, mime_type)
223 self.get_model ().set_value (iter, 1, mime_type_string)
224
225 model = ApplicationChooserModel (self.category.applications[mime_type])
226 self.get_model ().set_value (iter, 5, model)
227 self.set_cursor_on_cell (self.get_model ().get_path (iter), self.get_column (1), self.application_renderer, True)
228
229 def file_type_editing_canceled (self, cell):
230 cell.set_property ('editable', False)
231
232 if self.adding_mime_type:
233 (liststore, iter) = self.get_selection ().get_selected ()
234 liststore.remove (iter)
235 self.adding_mime_type = False
236
237 def application_edited (self, cell, path_string, application_string):
238 iter = self.get_model ().get_iter_from_string (path_string)
239
240 mime_type = self.get_model ().get_value (iter, 0)
241
242 # EWW my eyes bleed! how do we figure out the application from a its string?
243 # we need the active index of the submodel, i.e. the GtkComboBox!
244 submodel = self.get_model ().get_value (iter, 5)
245
246 subiter = submodel.get_iter_first()
247 while (subiter != None):
248 model_application_string = submodel.get_value (subiter, 2)
249 if model_application_string == application_string:
250 break
251 subiter = submodel.iter_next (subiter)
252
253 if (subiter == None): # eww should not happen!
254 self.application_editing_canceled (cell)
255 return
256
257 application = submodel.get_value (subiter, 0)
258
259 self.get_model ().set_value (iter, 3, application)
260 self.get_model ().set_value (iter, 4, application[1])
261
262 if self.adding_mime_type:
263 self.adding_mime_type = False
264
265 self.handled_undisplayed_mime_types.remove (mime_type)
266 self.handled_displayed_mime_types.append (mime_type)
267
268 # TODO maybe emit mime-type-added instead
269 print 'Introduced handler ' + application[1] + ' for MIME type ' + mime_type
270 else:
271 print 'Changed handler to ' + application[1] + ' for MIME type ' + mime_type
272
273 self.emit ("mime-type-changed", mime_type)
274 # TODO write out changes, possibly in the handler
275
276 def application_editing_canceled (self, cell):
277 if self.adding_mime_type:
278 (liststore, iter) = self.get_selection ().get_selected ()
279 liststore.remove (iter)
280
281 def add_mime_type (self):
282 self.adding_mime_type = True
283
284 # remove dummy row if appropriate
285 liststore = self.get_model ()
286 first_iter = liststore.get_iter_first()
287 if first_iter != None:
288 desc = liststore.get_value (first_iter, 1)
289 if desc == None:
290 liststore.remove (first_iter)
291
292 # add new row
293 iter = liststore.append ([None, '(none)', MIMEChooserModel (self.handled_undisplayed_mime_types), \
294 None, '', ApplicationChooserModel ([]) ])
295 self.file_type_renderer.set_property ('editable', True)
296 self.set_cursor_on_cell (self.get_model ().get_path (iter), self.get_column (0), self.file_type_renderer, True)
297
298 def remove_mime_type (self):
299 (liststore, rows) = self.get_selection ().get_selected_rows ()
300
301 references = []
302 for row in rows:
303 references.append (gtk.TreeRowReference (liststore, row))
304
305 for reference in references:
306 path = reference.get_path ()
307 iter = liststore.get_iter (path)
308 if iter != None:
309 mime_type = liststore.get_value (iter, 0)
310 liststore.remove (iter)
311 self.handled_displayed_mime_types.remove (mime_type)
312 self.handled_undisplayed_mime_types.append (mime_type)
313 self.emit ("mime-type-removed", mime_type)
314
315
316 class DetailsDialog (gtk.Dialog):
317 def __init__ (self, category):
318 gtk.Dialog.__init__ (self)
319
320 self.category = category
321
322 self.ensure_style ()
323 self.set_border_width (12)
324 self.action_area.set_border_width (0)
325 self.vbox.set_spacing (12)
326 self.set_has_separator (0)
327
328 self.set_title ('Details for \"%s\"' % category.name)
329
330 self.connect("response", self.response)
331
332 self.add_button (gtk.STOCK_ADD, gtk.RESPONSE_OK)
333 self.add_button (gtk.STOCK_REMOVE, gtk.RESPONSE_CANCEL)
334 self.add_button (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
335
336
337 vbox = gtk.VBox ()
338 vbox.set_border_width (0)
339 vbox.set_spacing (18)
340 vbox.show()
341 self.vbox.add (vbox)
342
343 label_text = category.exception_label % (category.all_applications[category.default_application_id][1])
344
345 label = gtk.Label (label_text)
346 label.set_alignment (0.0, 0.5)
347 vbox.pack_start (label, False, False)
348 label.show ()
349
350 scrolled_window = gtk.ScrolledWindow ()
351 scrolled_window.set_policy (gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
352 scrolled_window.set_shadow_type (gtk.SHADOW_IN)
353 scrolled_window.set_size_request (-1, 200)
354 vbox.add (scrolled_window)
355 scrolled_window.show ()
356
357 self.details_view = DetailsView (category)
358 scrolled_window.add (self.details_view)
359 self.details_view.show ()
360 self.details_view.connect ("mime-type-changed", self.details_view_mime_type_changed)
361 self.details_view.connect ("mime-type-removed", self.details_view_mime_type_removed)
362 self.details_view.get_selection().connect ("changed", self.details_view_selection_changed)
363
364 self.update_response_sensitivity ()
365
366 def response (self, dialog, response):
367 if response == gtk.RESPONSE_OK:
368 self.details_view.add_mime_type ()
369 elif response == gtk.RESPONSE_CANCEL:
370 self.details_view.remove_mime_type ()
371 else: # FIXME handle destruction request differently?
372 None
373
374 def update_response_sensitivity (self):
375 self.set_response_sensitive (gtk.RESPONSE_OK, len (self.details_view.handled_undisplayed_mime_types) > 0)
376 self.set_response_sensitive (gtk.RESPONSE_CANCEL, len (self.details_view.handled_displayed_mime_types) > 0 and \
377 self.details_view.get_selection().count_selected_rows > 0)
378
379 def details_view_mime_type_changed (self, details_view, mime_type):
380 self.update_response_sensitivity ()
381
382 def details_view_mime_type_removed (self, details_view, mime_type):
383 self.update_response_sensitivity ()
384
385 def details_view_selection_changed (self, selection):
386 self.update_response_sensitivity ()
387
388
389 class ApplicationDialog (gtk.Dialog):
390 def __init__ (self, categories):
391 gtk.Dialog.__init__ (self)
392
393 self.set_title ('Default Applications')
394
395 self.ensure_style ()
396 self.set_border_width (7)
397 self.action_area.set_border_width (0)
398 self.vbox.set_spacing (2)
399 self.set_has_separator (0)
400
401 self.add_button (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
402
403 table = gtk.Table (len (categories), 3, False)
404 table.set_row_spacings (6)
405 table.set_col_spacings (12)
406 self.vbox.add (table)
407 table.set_border_width (5)
408 table.show ()
409
410 self.categories = categories
411 for category in self.categories:
412 y = categories.index (category)
413
414 label = gtk.Label ()
415 label.set_text_with_mnemonic (category.combo_label)
416 label.set_alignment (0.0, 0.5)
417 table.attach (label, 0, 1, y, y+1, gtk.FILL, 0)
418 label.show ()
419
420 chooser = ApplicationChooser (category.all_applications.values(),
421 category.default_application_id)
422 label.set_mnemonic_widget (chooser)
423 table.attach (chooser, 1, 2, y, y+1, gtk.EXPAND|gtk.FILL, 0)
424 chooser.show ()
425
426 if len (category.mime_types) > 1:
427 button = gtk.Button ('Details')
428 table.attach (button, 2, 3, y, y+1, 0, 0)
429 button.show ()
430 button.connect("clicked", self.detailsClicked, category)
431
432 # expander = gtk.Expander ('Details')
433 # table.attach (expander, 0, 2, 1, 2, gtk.EXPAND|gtk.FILL, gtk.EXPAND|gtk.FILL)
434 # if len (category.applications.keys()) > 1:
435 # expander.show ()
436 # else:
437 # expander.hide ()
438
439 # exception_view = DetailsView (category)
440 # expander.add (exception_view)
441 # exception_view.show ()
442 else:
443 print "EWW no handler for " + str (category.name)
444
445 def detailsClicked (self, button, category):
446 print "Displaying details for " + str (category.name)
447
448 details_dialog = DetailsDialog (category)
449 details_dialog.set_transient_for (self)
450 details_dialog.set_position (gtk.WIN_POS_CENTER_ON_PARENT)
451
452 while 1:
453 res = details_dialog.run ()
454 if res == gtk.RESPONSE_OK or \
455 res == gtk.RESPONSE_CANCEL:
456 continue
457 break
458
459 details_dialog.destroy ()
460
461 gobject.signal_new ("mime-type-changed", DetailsView,
462 gobject.SIGNAL_RUN_FIRST,
463 gobject.TYPE_NONE,
464 (gobject.TYPE_STRING,))
465 gobject.signal_new ("mime-type-removed", DetailsView,
466 gobject.SIGNAL_RUN_FIRST,
467 gobject.TYPE_NONE,
468 (gobject.TYPE_STRING,))
469
470
471 audio_category = MimeCategory ('Audio', '_Audio Player:', 'All audio files will be played with "%s" by default, except:', [ 'audio/x-wav', 'audio/x-vorbis+ogg', 'audio/x-flac+ogg', 'audio/x-speex+ogg', 'audio/mpeg' ], 'totem.desktop' )
472 video_category = MimeCategory ('Video', '_Video Player:', 'All video files will be played with "%s" by default, except:', [ 'video/mpeg', 'video/x-ms-wmv', 'video/x-msvideo', 'video/x-nsv', 'video/x-sgi-movie', 'video/wavelet', 'video/quicktime', 'video/isivideo', 'video/dv', 'audio/vnd.rn-realvideo', 'video/mp4', 'application/x-matroska', 'application/x-flash-video' ], 'totem.desktop' )
473 image_category = MimeCategory ('Image', '_Image Viewer:', 'All image files will be opened with "%s" by default, except:', [ 'image/vnd.rn-realpix', 'image/bmp', 'image/cgm', 'image/fax-g3', 'image/g3fax', 'image/gif', 'image/ief', 'image/jpeg', 'image/jpeg2000', 'image/x-pict', 'image/png', 'image/rle', 'image/svg+xml', 'image/tiff', 'image/vnd.dwg', 'image/vnd.dxf', 'image/x-3ds', 'image/x-applix-graphics', 'image/x-cmu-raster', 'image/x-compressed-xcf', 'image/x-dib', 'image/vnd.djvu', 'image/dpx', 'image/x-eps', 'image/x-fits', 'image/x-fpx', 'image/x-ico', 'image/x-iff', 'image/x-ilbm', 'image/x-jng', 'image/x-lwo', 'image/x-lws', 'image/x-msod', 'image/x-niff', 'image/x-pcx', 'image/x-photo-cd', 'image/x-portable-anymap', 'image/x-portable-bitmap', 'image/x-portable-graymap', 'image/x-portable-pixmap', 'image/x-psd', 'image/x-rgb', 'image/x-sgi', 'image/x-sun-raster', 'image/x-tga', 'image/x-win-bitmap', 'image/x-wmf', 'image/x-xbitmap', 'image/x-xcf', 'image/x-xfig', 'image/x-xpixmap', 'image/x-xwindowdump' ], 'eog.desktop')
474 text_editor_category = MimeCategory ('Text Editor', '_Text Editor:', '', [ 'text/plain' ], 'gedit.desktop' )
475 word_processing_category = MimeCategory ('Word Processing', '_Word Processor:', 'All text documents will be opened with "%s" by default, except:', [ 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.text-template','application/vnd.oasis.opendocument.text-master', 'application/vnd.sun.xml.writer', 'application/vnd.sun.xml.writer.template', 'application/vnd.sun.xml.writer.global', 'application/vnd.stardivision.writer', 'application/msword', 'application/rtf' ], 'abiword.desktop' )
476 spreadsheet_category = MimeCategory ('Spreadsheet', '_Spreadsheet:', 'All spreadsheet files will be opened with "%s", except:', [ 'application/vnd.ms-excel', 'application/x-gnumeric' ], 'gnumeric.desktop')
477
478 dialog = ApplicationDialog ([audio_category, video_category, image_category, text_editor_category, word_processing_category, spreadsheet_category])
479 dialog.present()
480 dialog.run()
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.