SpecEnhancedPreferredApps

Attachment 'christians-script-v2.py'

Download

   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.
  • [get | view] (2007-01-19 17:29:44, 28.3 KB) [[attachment:c1.jpg]]
  • [get | view] (2007-01-19 17:29:55, 28.4 KB) [[attachment:c2.jpg]]
  • [get | view] (2007-01-14 19:50:37, 18.0 KB) [[attachment:christians-script-v2.py]]
  • [get | view] (2007-01-11 18:54:41, 37.6 KB) [[attachment:mockup0.2.png]]
 All files | Selected Files: delete move to page

You are not allowed to attach a file to this page.