Am Sonntag, den 14.01.2007, 14:27 +0000 schrieb Matt Medland: > Okay, thanks. I think I'll wait for the next implementation then. > There's a link to the current script on the wiki page anyhow.
OK, I just finished the next iteration. It feels way better IMO, the UI is completely different. Feedback appreciated. -- Christian Neumair <[EMAIL PROTECTED]>
#!/usr/bin/python # * demo application for a new "default application" chooser # * GUI experiment # * started after failed GUI experiment, my experience failure is describe at # http://mail.gnome.org/archives/usability/2007-January/msg00064.html # # GPL, (C) 2007 Christian Neumair <[EMAIL PROTECTED]> import pygtk import gtk import gobject import gnomevfs from sets import Set class MimeCategory: def __init__ (self, name, combo_label, exception_label, mime_types, default_application_id_fallback): self.name = name self.combo_label = combo_label self.exception_label = exception_label self.mime_types = mime_types self.default_application_id_fallback = default_application_id_fallback self.update_from_vfs() def update_from_vfs (self): self.all_applications = {} # id->application self.applications = {} # MIME type -> application self.default_applications = {} # MIME type -> application # applications that can not handle all MIME types, TODO # these would not be available for the category combos self.exception_applications = None unhandled_apps = None # find out available apps + default app for all mime types for mime_type in self.mime_types: self.applications[mime_type] = gnomevfs.mime_get_all_applications (mime_type) self.default_applications[mime_type] = gnomevfs.mime_get_default_application (mime_type) for application in self.applications[mime_type]: self.all_applications[application[0]] = (application) # find default app FIXME this should be read out somewhere default_app = None self.default_application_id = default_app and default_app[0] or self.default_application_id_fallback class ApplicationChooserModel (gtk.ListStore): def __init__ (self, applications): gtk.ListStore.__init__ (self, gobject.TYPE_PYOBJECT, # GnomeVFSMIMEApplication gobject.TYPE_INT, # sort index gobject.TYPE_STRING) # application name self.set_default_sort_func (self.sort_func) self.set_sort_column_id (-1, gtk.SORT_ASCENDING) self.applications = applications for application in applications: # application[1] is the name self.append ([application, -1, application[1]]) # TODO #self.append ([None, 0, None]) #self.append ([None, 1, 'Custom...']) def sort_func (self, model, a, b): cmp = self.get_value (a, 1) - self.get_value (b, 1) if (cmp != 0): return cmp if self.get_value (a, 2) > self.get_value (b, 2): return 1 elif self.get_value (a, 2) < self.get_value (b, 2): return -1 else: return 0 class ApplicationChooser (gtk.ComboBox): def __init__ (self, applications, default_application_id): gtk.ComboBox.__init__ (self) self.set_row_separator_func(self.row_separator_func) self.set_model (ApplicationChooserModel (applications)) iter = self.get_model().get_iter_first() i = 0 while (iter != None): application = self.get_model().get_value (iter, 0) if application != None and application[0] == default_application_id: self.set_active (i) break i = i + 1 iter = self.get_model().iter_next (iter) cell = gtk.CellRendererText() self.pack_start(cell, True) self.add_attribute(cell, 'text', 2) def row_separator_func (self, model, iter): return model.get_value (iter, 0) == None class MIMEChooserModel (gtk.ListStore): def __init__ (self, mime_types): gtk.ListStore.__init__ (self, gobject.TYPE_STRING, # MIME Type gobject.TYPE_STRING) # MIME Type (human-readable) self.set_default_sort_func (self.sort_func) self.set_sort_column_id (0, gtk.SORT_ASCENDING) self.mime_types = mime_types for mime_type in mime_types: desc = gnomevfs.mime_get_description (mime_type) or mime_type print "appended " + mime_type self.append ([mime_type, desc]) def sort_func (self, model, a, b): cmp = self.get_value (a, 1) - self.get_value (b, 1) if (cmp != 0): return cmp if self.get_value (a, 0) > self.get_value (b, 0): return 1 elif self.get_value (a, 0) < self.get_value (b, 0): return -1 else: return 0 # Details view allowing to add MIME types that have handlers # that override the default handler # TODO proper error handling etc. # TODO implement saving/reloading/resynching with MIME data upon changes # TODO decide whether default application itself should show up class DetailsView (gtk.TreeView): def __init__(self, category): gtk.TreeView.__init__(self) self.category = category self.adding_mime_type = False liststore = gtk.ListStore(gobject.TYPE_STRING, # MIME Type gobject.TYPE_STRING, # MIME Type (human-readable) gtk.ListStore, # MIME Model gobject.TYPE_PYOBJECT, # Active Application gobject.TYPE_STRING, # Active Application (as string) gtk.ListStore) # Application Model self.set_model (liststore) self.get_model ().set_sort_column_id (1, gtk.SORT_ASCENDING) self.get_selection().set_mode (gtk.SELECTION_MULTIPLE) cell = self.file_type_renderer = gtk.CellRendererCombo () cell.set_property ('text-column', 1) cell.set_property ('editable', False) cell.set_property ('has-entry', False) column = gtk.TreeViewColumn ('File Type', cell, model=2, text=1) self.append_column (column) cell.connect ("edited", self.file_type_edited) cell.connect ("editing-canceled", self.file_type_editing_canceled) cell = self.application_renderer = gtk.CellRendererCombo () cell.set_property ('text-column', 2) cell.set_property ('editable', True) cell.set_property ('has-entry', False) column = gtk.TreeViewColumn ('Application', cell, model=5, text=4) self.append_column (column) cell.connect ("edited", self.application_edited) cell.connect ("editing-canceled", self.application_editing_canceled) self.unhandled_mime_types = [] self.handled_undisplayed_mime_types = [] self.handled_displayed_mime_types = [] # add entries for all the MIME types where we have apps for mime_type in self.category.mime_types: app = self.category.default_applications[mime_type] apps = self.category.applications[mime_type] desc = gnomevfs.mime_get_description (mime_type) if desc == None: print 'mime type "' + mime_type + '" has no description.' desc = mime_type if app == None: self.unhandled_mime_types.append (mime_type) elif app[0] != self.category.default_application_id: self.handled_displayed_mime_types.append (mime_type) liststore.append ([mime_type, desc, MIMEChooserModel ([]), \ app, app and app[1] or None, ApplicationChooserModel (apps)]) else: # handled by default application self.handled_undisplayed_mime_types.append (mime_type) continue if liststore.get_iter_first() == None: liststore.append ([None, None, MIMEChooserModel ([]), \ None, '(No exceptions defined)', ApplicationChooserModel ([]) ]) def file_type_edited (self, cell, path_string, mime_type_string): cell.set_property ('editable', False) iter = self.get_model ().get_iter_from_string (path_string) # EWW my eyes bleed! how do we figure out the MIME type from a description string? # we need the active index of the submodel, i.e. the GtkComboBox! submodel = self.get_model ().get_value (iter, 2) subiter = submodel.get_iter_first() while (subiter != None): model_mime_type_string = submodel.get_value (subiter, 1) if model_mime_type_string == mime_type_string: break subiter = submodel.iter_next (subiter) if (subiter == None): # eww should not happen! self.file_type_editing_canceled (cell) return mime_type = submodel.get_value (subiter, 0) self.get_model ().set_value (iter, 0, mime_type) self.get_model ().set_value (iter, 1, mime_type_string) model = ApplicationChooserModel (self.category.applications[mime_type]) self.get_model ().set_value (iter, 5, model) self.set_cursor_on_cell (self.get_model ().get_path (iter), self.get_column (1), self.application_renderer, True) def file_type_editing_canceled (self, cell): cell.set_property ('editable', False) if self.adding_mime_type: (liststore, iter) = self.get_selection ().get_selected () liststore.remove (iter) self.adding_mime_type = False def application_edited (self, cell, path_string, application_string): iter = self.get_model ().get_iter_from_string (path_string) mime_type = self.get_model ().get_value (iter, 0) # EWW my eyes bleed! how do we figure out the application from a its string? # we need the active index of the submodel, i.e. the GtkComboBox! submodel = self.get_model ().get_value (iter, 5) subiter = submodel.get_iter_first() while (subiter != None): model_application_string = submodel.get_value (subiter, 2) if model_application_string == application_string: break subiter = submodel.iter_next (subiter) if (subiter == None): # eww should not happen! self.application_editing_canceled (cell) return application = submodel.get_value (subiter, 0) self.get_model ().set_value (iter, 3, application) self.get_model ().set_value (iter, 4, application[1]) if self.adding_mime_type: self.adding_mime_type = False self.handled_undisplayed_mime_types.remove (mime_type) self.handled_displayed_mime_types.append (mime_type) # TODO maybe emit mime-type-added instead print 'Introduced handler ' + application[1] + ' for MIME type ' + mime_type else: print 'Changed handler to ' + application[1] + ' for MIME type ' + mime_type self.emit ("mime-type-changed", mime_type) # TODO write out changes, possibly in the handler def application_editing_canceled (self, cell): if self.adding_mime_type: (liststore, iter) = self.get_selection ().get_selected () liststore.remove (iter) def add_mime_type (self): self.adding_mime_type = True # remove dummy row if appropriate liststore = self.get_model () first_iter = liststore.get_iter_first() if first_iter != None: desc = liststore.get_value (first_iter, 1) if desc == None: liststore.remove (first_iter) # add new row iter = liststore.append ([None, '(none)', MIMEChooserModel (self.handled_undisplayed_mime_types), \ None, '', ApplicationChooserModel ([]) ]) self.file_type_renderer.set_property ('editable', True) self.set_cursor_on_cell (self.get_model ().get_path (iter), self.get_column (0), self.file_type_renderer, True) def remove_mime_type (self): (liststore, rows) = self.get_selection ().get_selected_rows () references = [] for row in rows: references.append (gtk.TreeRowReference (liststore, row)) for reference in references: path = reference.get_path () iter = liststore.get_iter (path) if iter != None: mime_type = liststore.get_value (iter, 0) liststore.remove (iter) self.handled_displayed_mime_types.remove (mime_type) self.handled_undisplayed_mime_types.append (mime_type) self.emit ("mime-type-removed", mime_type) class DetailsDialog (gtk.Dialog): def __init__ (self, category): gtk.Dialog.__init__ (self) self.category = category self.ensure_style () self.set_border_width (12) self.action_area.set_border_width (0) self.vbox.set_spacing (12) self.set_has_separator (0) self.set_title ('Details for \"%s\"' % category.name) self.connect("response", self.response) self.add_button (gtk.STOCK_ADD, gtk.RESPONSE_OK) self.add_button (gtk.STOCK_REMOVE, gtk.RESPONSE_CANCEL) self.add_button (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE) vbox = gtk.VBox () vbox.set_border_width (0) vbox.set_spacing (18) vbox.show() self.vbox.add (vbox) label_text = category.exception_label % (category.all_applications[category.default_application_id][1]) label = gtk.Label (label_text) label.set_alignment (0.0, 0.5) vbox.pack_start (label, False, False) label.show () scrolled_window = gtk.ScrolledWindow () scrolled_window.set_policy (gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) scrolled_window.set_shadow_type (gtk.SHADOW_IN) scrolled_window.set_size_request (-1, 200) vbox.add (scrolled_window) scrolled_window.show () self.details_view = DetailsView (category) scrolled_window.add (self.details_view) self.details_view.show () self.details_view.connect ("mime-type-changed", self.details_view_mime_type_changed) self.details_view.connect ("mime-type-removed", self.details_view_mime_type_removed) self.details_view.get_selection().connect ("changed", self.details_view_selection_changed) self.update_response_sensitivity () def response (self, dialog, response): if response == gtk.RESPONSE_OK: self.details_view.add_mime_type () elif response == gtk.RESPONSE_CANCEL: self.details_view.remove_mime_type () else: # FIXME handle destruction request differently? None def update_response_sensitivity (self): self.set_response_sensitive (gtk.RESPONSE_OK, len (self.details_view.handled_undisplayed_mime_types) > 0) self.set_response_sensitive (gtk.RESPONSE_CANCEL, len (self.details_view.handled_displayed_mime_types) > 0 and \ self.details_view.get_selection().count_selected_rows > 0) def details_view_mime_type_changed (self, details_view, mime_type): self.update_response_sensitivity () def details_view_mime_type_removed (self, details_view, mime_type): self.update_response_sensitivity () def details_view_selection_changed (self, selection): self.update_response_sensitivity () class ApplicationDialog (gtk.Dialog): def __init__ (self, categories): gtk.Dialog.__init__ (self) self.set_title ('Default Applications') self.ensure_style () self.set_border_width (7) self.action_area.set_border_width (0) self.vbox.set_spacing (2) self.set_has_separator (0) self.add_button (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE) table = gtk.Table (len (categories), 3, False) table.set_row_spacings (6) table.set_col_spacings (12) self.vbox.add (table) table.set_border_width (5) table.show () self.categories = categories for category in self.categories: y = categories.index (category) label = gtk.Label () label.set_text_with_mnemonic (category.combo_label) label.set_alignment (0.0, 0.5) table.attach (label, 0, 1, y, y+1, gtk.FILL, 0) label.show () chooser = ApplicationChooser (category.all_applications.values(), category.default_application_id) label.set_mnemonic_widget (chooser) table.attach (chooser, 1, 2, y, y+1, gtk.EXPAND|gtk.FILL, 0) chooser.show () if len (category.mime_types) > 1: button = gtk.Button ('Details') table.attach (button, 2, 3, y, y+1, 0, 0) button.show () button.connect("clicked", self.detailsClicked, category) # expander = gtk.Expander ('Details') # table.attach (expander, 0, 2, 1, 2, gtk.EXPAND|gtk.FILL, gtk.EXPAND|gtk.FILL) # if len (category.applications.keys()) > 1: # expander.show () # else: # expander.hide () # exception_view = DetailsView (category) # expander.add (exception_view) # exception_view.show () else: print "EWW no handler for " + str (category.name) def detailsClicked (self, button, category): print "Displaying details for " + str (category.name) details_dialog = DetailsDialog (category) details_dialog.set_transient_for (self) details_dialog.set_position (gtk.WIN_POS_CENTER_ON_PARENT) while 1: res = details_dialog.run () if res == gtk.RESPONSE_OK or \ res == gtk.RESPONSE_CANCEL: continue break details_dialog.destroy () gobject.signal_new ("mime-type-changed", DetailsView, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) gobject.signal_new ("mime-type-removed", DetailsView, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) 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' ) 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' ) 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') text_editor_category = MimeCategory ('Text Editor', '_Text Editor:', '', [ 'text/plain' ], 'gedit.desktop' ) 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' ) spreadsheet_category = MimeCategory ('Spreadsheet', '_Spreadsheet:', 'All spreadsheet files will be opened with "%s", except:', [ 'application/vnd.ms-excel', 'application/x-gnumeric' ], 'gnumeric.desktop') dialog = ApplicationDialog ([audio_category, video_category, image_category, text_editor_category, word_processing_category, spreadsheet_category]) dialog.present() dialog.run()
_______________________________________________ Usability mailing list [email protected] http://mail.gnome.org/mailman/listinfo/usability
