[Deskbar] second pearl/cuemiac technology preview



Here's another monster patch (applying against HEAD). You will need to
copy the attached deskbar.svg to deskbar/data/art/ before you run
"python pearl.py" (and go nuts from the iNzanE level of eye candy).

NOTE:
Please don't report bugs on this yet. It is still to early for that,
this is just a preview. Basically I just took a nine inch nail and
hammered through the deskbar internals to nail it onto the view.

Comments on layout, design, implementation, usability are welcome.

What we have in this version of the Pearl:
	- Beginnings of keyboard navigation
	- Semi stateful expandedness (no gconf storage yet, but that's easy)
	- No match activation (I'm too lazy)
	- Loads of bugs and misbehaviors
	- Orientation aware expander/button/popup
	- A new deskbar focus mechanism (button pops an entry up)

Cheers
Mikkel

diff -ruN ../deskbar-applet/deskbar/categories.py deskbar/categories.py
--- ../deskbar-applet/deskbar/categories.py	1970-01-01 01:00:00.000000000 +0100
+++ deskbar/categories.py	2006-01-12 14:58:50.000000000 +0100
@@ -0,0 +1,73 @@
+
+class UnknownCategory (Exception):
+	def __init__ (self, category_name, match):
+		print "** Unknown Category '%s' requested by %s" % (category_name, match.__class__)
+
+
+CATEGORIES = {
+	"Files"		: {	
+				"name": "Files",
+				"nest": "<b>%(count)s</b> <i>more files</i>", 
+				"threshold": 3
+			},
+	"Actions"	: {
+				"name": "Actions",
+				"nest": "<b>%(count)s</b> <i>more actions</i>", 
+				"threshold": 1
+			},
+	"News"		: {
+				"name": "News",
+				"nest": "<b>%(count)s</b> <i>more news items</i>", 
+				"threshold": 3
+			},
+	"Contacts"	: {
+				"name": "Contacts",
+				"nest": "<b>%(count)s</b> <i>more contacts</i>",
+				"threshold": 3
+			},
+	"Emails"	: {
+				"name": "Emails",
+				"nest": "<b>%(count)s</b> <i>more emails</i>", 
+				"threshold": 3
+			},
+	"Notes"	: {
+				"name": "Notes",
+				"nest": "<b>%(count)s</b> <i>more notes</i>", 
+				"threshold": 3
+			},
+	"Volumes"	: {
+				"name": "Volumes",
+				"nest": "<b>%(count)s</b> <i>more volumes</i>", 
+				"threshold": 3
+			},
+	"Google Search"	: {
+				"name": "Google Search",
+				"nest": "<b>%(count)s</b> <i>more online hits</i>", 
+				"threshold": 2
+			},
+	"Calendar"	: {
+				"name": "Calendar",
+				"nest": "<b>%(count)s</b> <i>more calendar items</i>", 
+				"threshold": 1
+			},
+	"Conversation"	: {
+				"name": "Conversation",
+				"nest": "<b>%(count)s</b> <i>more conversations</i>", 
+				"threshold": 1
+			},
+	"Web Browser" : {
+		"name":"Web Browser",
+		"nest":"<b>%(count)s</b> <i>more items</i>",
+		"threshold":5,
+	},
+	"Programs" : {
+		"name":"Programs",
+		"nest":"<b>%(count)s</b> <i>more programs</i>",
+		"threshold":3,
+	},
+	"Debug" : {
+		"name":"Debug",
+		"nest":"<b>%(count)s</b> <i>more debugging handlers</i>",
+		"threshold":2,
+	},
+}
diff -ruN ../deskbar-applet/deskbar/deskbar_applet_button.py deskbar/deskbar_applet_button.py
--- ../deskbar-applet/deskbar/deskbar_applet_button.py	1970-01-01 01:00:00.000000000 +0100
+++ deskbar/deskbar_applet_button.py	2006-01-12 14:58:44.000000000 +0100
@@ -0,0 +1,116 @@
+import gtk
+import gnomeapplet
+import gobject
+
+class DeskbarAppletButton (gtk.HBox):
+	"""
+	Button consisting of two toggle buttons. A "main" with and image, and an "arrow"
+	with a gtk.Arrow.
+	
+	It automatically arranges itself according to one of 
+	gnomeapplet.ORIENT_UP,gnomeapplet.ORIENT_{UP,DOWN,LEFT,RIGHT}.
+	
+	Signals:
+		toggled-main: The main button has been toggled
+		toggle-arrow: the arrow button has been toggled
+		
+	The widget implements an interface like the gtk.ToggleButton, with _main or _arrow
+	appended to method names for each button.
+	"""
+	__gsignals__ = {
+		"toggled-main" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT]),
+		"toggled-arrow" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT])
+	}
+
+	def __init__ (self, popup_dir, image=None):
+		"""
+		popup_dir: gnomeapplet.ORIENT_UP,gnomeapplet.ORIENT_{UP,DOWN,LEFT,RIGHT}
+		image: gtk.Image to pack in the main button
+		"""
+		gtk.HBox.__init__ (self)
+		if popup_dir in [gnomeapplet.ORIENT_UP,gnomeapplet.ORIENT_DOWN]:
+			self.box = gtk.HBox ()
+			print "H"
+		else:
+			self.box = gtk.VBox ()
+			print "V"
+			
+		self.popup_dir = popup_dir
+		
+		self.button_main = gtk.ToggleButton ()
+		self.button_main.set_relief (gtk.RELIEF_NONE)
+		if image is None:
+			self.image = gtk.Image ()
+		else:
+			self.image = image
+		self.button_main.add (self.image)
+		self.button_main.connect ("toggled", lambda widget: self.emit ("toggled-main", widget))
+		
+		self.button_arrow = gtk.ToggleButton ()
+		self.button_arrow.set_relief (gtk.RELIEF_NONE)
+		self.arrow = gtk.Arrow (self.gnomeapplet_dir_to_arrow_dir(popup_dir), gtk.SHADOW_IN)
+		self.button_arrow.add (self.arrow)
+		self.button_arrow.connect ("toggled", lambda widget: self.emit ("toggled-arrow", widget))
+				
+		if popup_dir in [gnomeapplet.ORIENT_UP,gnomeapplet.ORIENT_DOWN]:
+			self.separator = gtk.VSeparator ()
+		else:
+			self.separator = gtk.HSeparator ()
+		
+		self.box.pack_start (self.button_arrow, False)
+		self.box.pack_start (self.separator, False)
+		self.box.pack_start (self.button_main)
+		self.add (self.box)
+	
+	def toggle_main (self):
+		self.button_main.toggled ()
+		
+	def toggle_arrow (self):
+		self.button_arrow.toggled ()
+	
+	def get_active_main (self):
+		return self.button_main.get_active ()
+	
+	def set_active_main (self, is_active):
+		self.button_main.set_active (is_active)
+	
+	def get_active_arrow (self):
+		return self.button_arrow.get_active ()
+
+	def set_active_arrow (self, is_active):
+		self.button_arrow.set_active (is_active)
+			
+	def set_button_image_from_file (self, filename):
+		# FIXME: Take self.popup_dir into account. Rotate img if necesary
+
+		# We use an intermediate pixbuf to scale the image
+		pixbuf = gtk.gdk.pixbuf_new_from_file_at_size (filename, 40, 18) # FIXME: Detect proper icon size
+		self.image.set_from_pixbuf (pixbuf)
+		
+	def gnomeapplet_dir_to_arrow_dir (self, gnomeapplet_dir):
+		"""
+		Returns the appropriate gtk.ARROW_{UP,DOWN,LEFT,RIGHT} corresponding
+		to gnomeapplet_dir; which can be one of
+		gnomeapplet.ORIENT_{UP,DOWN,LEFT,RIGHT}
+		"""
+		if gnomeapplet_dir == gnomeapplet.ORIENT_DOWN:
+			return gtk.ARROW_DOWN
+		elif gnomeapplet_dir == gnomeapplet.ORIENT_UP:
+			return gtk.ARROW_UP
+		elif gnomeapplet_dir == gnomeapplet.ORIENT_LEFT:
+			print "l"
+			return gtk.ARROW_LEFT
+		else:
+			return gtk.ARROW_RIGHT
+		
+if __name__ == "__main__":
+	button = DeskbarAppletButton (gnomeapplet.ORIENT_RIGHT)
+	
+	win = gtk.Window ()
+	win.connect ("destroy", gtk.main_quit)
+	win.add (button)
+	win.show_all ()
+		
+	button.set_button_image_from_file ("/home/mikkel/Documents/deskbar.svg")
+	
+	gtk.main ()
diff -ruN ../deskbar-applet/deskbar/handlers/beagle-live.py deskbar/handlers/beagle-live.py
--- ../deskbar-applet/deskbar/handlers/beagle-live.py	2005-12-21 17:17:49.000000000 +0100
+++ deskbar/handlers/beagle-live.py	2006-01-12 14:58:10.000000000 +0100
@@ -122,6 +122,16 @@
 		
 		self.__result = result
 	
+	def get_category (self):
+		t = self.__result["type"]
+		if t == "MailMessage" : return "Emails"
+		elif t == "Contact": return "Contacts"
+		elif t == "File": return "Files"
+		elif t == "FeedItem": return "News"
+		elif t == "Note": return "Notes"
+		elif t == "IMLog": return "Conversation"
+		elif t == "Calendar": return "Calendar"
+	
 	def get_name (self, text=None):
 		# We use the result dict itself to look up words
 		return self.__result
diff -ruN ../deskbar-applet/deskbar/handlers/beagle.py deskbar/handlers/beagle.py
--- ../deskbar-applet/deskbar/handlers/beagle.py	2005-12-26 18:37:06.000000000 +0100
+++ deskbar/handlers/beagle.py	2006-01-12 14:58:10.000000000 +0100
@@ -29,6 +29,9 @@
 	def action(self, text=None):
 		gobject.spawn_async(["best", '--no-tray', '--show-window', self._name], flags=gobject.SPAWN_SEARCH_PATH)
 	
+	def get_category(self):
+		return "Actions"
+	
 	def get_verb(self):
 		return _("Search for %s using Beagle") % "<b>%(name)s</b>"
 	
diff -ruN ../deskbar-applet/deskbar/handlers/debug-async_handler.py deskbar/handlers/debug-async_handler.py
--- ../deskbar-applet/deskbar/handlers/debug-async_handler.py	2005-11-15 05:27:26.000000000 +0100
+++ deskbar/handlers/debug-async_handler.py	2006-01-12 14:58:10.000000000 +0100
@@ -22,6 +22,9 @@
 		
 	def action(self, text=None):
 		print str(self.__class__) + " : action triggered"
+		
+	def get_category (self):
+		return "Debug"
 
 class AsyncDebugHandler (AsyncHandler): 
 
diff -ruN ../deskbar-applet/deskbar/handlers/debug-requirements.py deskbar/handlers/debug-requirements.py
--- ../deskbar-applet/deskbar/handlers/debug-requirements.py	2005-11-28 21:16:27.000000000 +0100
+++ deskbar/handlers/debug-requirements.py	2006-01-12 14:58:10.000000000 +0100
@@ -33,6 +33,9 @@
 		
 	def action(self, text=None):
 		pass
+		
+	def get_category (self):
+		return "Debug"
 
 class DebugRequirementsModule(deskbar.handler.Handler):
 	def __init__ (self):
diff -ruN ../deskbar-applet/deskbar/handlers/debug-signal_handler.py deskbar/handlers/debug-signal_handler.py
--- ../deskbar-applet/deskbar/handlers/debug-signal_handler.py	2005-11-15 05:27:26.000000000 +0100
+++ deskbar/handlers/debug-signal_handler.py	2006-01-12 14:58:10.000000000 +0100
@@ -19,6 +19,9 @@
 		
 	def action(self, text=None):
 		print str(self.__class__) + " : action triggered"
+		
+	def get_category (self):
+		return "Debug"
 
 
 class SignallingDebugHandler(SignallingHandler):
diff -ruN ../deskbar-applet/deskbar/handlers/email_address.py deskbar/handlers/email_address.py
--- ../deskbar-applet/deskbar/handlers/email_address.py	2005-11-15 05:27:27.000000000 +0100
+++ deskbar/handlers/email_address.py	2006-01-12 14:58:10.000000000 +0100
@@ -21,6 +21,9 @@
 		
 	def action(self, text=None):
 		gnomevfs.url_show("mailto:"+self._email)
+		
+	def get_category(self):
+		return "Emails"
 	
 	def get_verb(self):
 		return _("Send Email to %s") % "<b>%(name)s</b>"
diff -ruN ../deskbar-applet/deskbar/handlers/evolution.py deskbar/handlers/evolution.py
--- ../deskbar-applet/deskbar/handlers/evolution.py	2006-01-02 01:57:54.000000000 +0100
+++ deskbar/handlers/evolution.py	2006-01-12 14:58:10.000000000 +0100
@@ -29,6 +29,9 @@
 		
 	def action(self, text=None):
 		gnomevfs.url_show("mailto:"+self._email)
+		
+	def get_category(self):
+		return "Contacts"
 	
 	def get_name(self, text=None):
 		return {
diff -ruN ../deskbar-applet/deskbar/handlers/files.py deskbar/handlers/files.py
--- ../deskbar-applet/deskbar/handlers/files.py	2005-11-18 15:14:25.000000000 +0100
+++ deskbar/handlers/files.py	2006-01-12 14:58:10.000000000 +0100
@@ -27,6 +27,9 @@
 	def action(self, text=None):
 		gobject.spawn_async(["gnome-open", self._filename], flags=gobject.SPAWN_SEARCH_PATH)
 		
+	def get_category(self):
+		return "Files"
+		
 	def get_verb(self):
 		return _("Open %s") % "<b>%(name)s</b>"
 	
diff -ruN ../deskbar-applet/deskbar/handlers/galago.py deskbar/handlers/galago.py
--- ../deskbar-applet/deskbar/handlers/galago.py	2006-01-02 01:57:54.000000000 +0100
+++ deskbar/handlers/galago.py	2006-01-12 14:58:10.000000000 +0100
@@ -21,6 +21,9 @@
 		
 	def action(self, text=None):
 		gnomevfs.url_show("mailto:"+self._email)
+		
+	def get_category(self):
+		return "Contacts"
 	
 	def get_verb(self):
 		return _("Send Email to %s") % "<b>%(name)s</b>"
diff -ruN ../deskbar-applet/deskbar/handlers/google-live.py deskbar/handlers/google-live.py
--- ../deskbar-applet/deskbar/handlers/google-live.py	2005-11-28 21:16:27.000000000 +0100
+++ deskbar/handlers/google-live.py	2006-01-12 14:58:10.000000000 +0100
@@ -62,6 +62,9 @@
 		
 	def action(self, text=None):
 		gnomevfs.url_show(self.__url)
+		
+	def get_category(self):
+		return "Google Search"
 	
 	def get_hash(self, text=None):
 		return self.__url
diff -ruN ../deskbar-applet/deskbar/handlers/gtkbookmarks.py deskbar/handlers/gtkbookmarks.py
--- ../deskbar-applet/deskbar/handlers/gtkbookmarks.py	2006-01-02 01:57:54.000000000 +0100
+++ deskbar/handlers/gtkbookmarks.py	2006-01-12 14:58:10.000000000 +0100
@@ -23,6 +23,9 @@
 		
 	def action(self, text=None):
 		gobject.spawn_async(["nautilus", self._path], flags=gobject.SPAWN_SEARCH_PATH)
+		
+	def get_category(self):
+		return "Files"
 	
 	def get_verb(self):
 		return _("Open location %s") % "<b>%(name)s</b>"
diff -ruN ../deskbar-applet/deskbar/handlers/pathprograms.py deskbar/handlers/pathprograms.py
--- ../deskbar-applet/deskbar/handlers/pathprograms.py	2006-01-02 01:57:54.000000000 +0100
+++ deskbar/handlers/pathprograms.py	2006-01-12 14:58:10.000000000 +0100
@@ -25,6 +25,9 @@
 	def action(self, text=None):
 		gobject.spawn_async(text.split(" "), flags=gobject.SPAWN_SEARCH_PATH)
 	
+	def get_category(self):
+		return "Programs"
+	
 	def get_verb(self):
 		return _("Execute %s") % "<b>%(text)s</b>"
 
diff -ruN ../deskbar-applet/deskbar/handlers/programs.py deskbar/handlers/programs.py
--- ../deskbar-applet/deskbar/handlers/programs.py	2006-01-02 01:57:54.000000000 +0100
+++ deskbar/handlers/programs.py	2006-01-12 14:58:10.000000000 +0100
@@ -58,6 +58,9 @@
 		else:
 			self._desktop.launch([])
 	
+	def get_category(self):
+		return "Programs"
+	
 	def get_verb(self):
 		#translators: First %s is the programs full name, second is the executable name
 		#translators: For example: Launch Text Editor (gedit)
diff -ruN ../deskbar-applet/deskbar/handlers/volumes.py deskbar/handlers/volumes.py
--- ../deskbar-applet/deskbar/handlers/volumes.py	2006-01-02 01:57:54.000000000 +0100
+++ deskbar/handlers/volumes.py	2006-01-12 14:58:10.000000000 +0100
@@ -28,6 +28,9 @@
 	
 	def action(self, text=None):
 		gobject.spawn_async(["nautilus", self.__drive.get_activation_uri()], flags=gobject.SPAWN_SEARCH_PATH)
+	
+	def get_category(self):
+		return "Files"
 	 
 	def get_verb(self):
 		activation = self.__drive.get_activation_uri()
diff -ruN ../deskbar-applet/deskbar/handlers/web_address.py deskbar/handlers/web_address.py
--- ../deskbar-applet/deskbar/handlers/web_address.py	2006-01-02 01:57:54.000000000 +0100
+++ deskbar/handlers/web_address.py	2006-01-12 14:58:10.000000000 +0100
@@ -28,6 +28,9 @@
 			gnomevfs.url_show(self._url)
 		else:
 			gobject.spawn_async(["nautilus", self._url], flags=gobject.SPAWN_SEARCH_PATH)
+			
+	def get_category(self):
+		return "Actions"
 	
 	def get_verb(self):
 		if not self._has_method:
diff -ruN ../deskbar-applet/deskbar/handlers_browsers.py deskbar/handlers_browsers.py
--- ../deskbar-applet/deskbar/handlers_browsers.py	2006-01-02 01:57:53.000000000 +0100
+++ deskbar/handlers_browsers.py	2006-01-12 14:58:33.000000000 +0100
@@ -24,6 +24,9 @@
 	def action(self, text=None):
 		gnomevfs.url_show(self._url)
 		
+	def get_category(self):
+		return "Web Browser"
+		
 	def get_verb(self):
 		if self._is_history:
 			return _("Open History Item %s") % "<b>%(name)s</b>"
diff -ruN ../deskbar-applet/deskbar/pearl.py deskbar/pearl.py
--- ../deskbar-applet/deskbar/pearl.py	1970-01-01 01:00:00.000000000 +0100
+++ deskbar/pearl.py	2006-01-12 14:58:59.000000000 +0100
@@ -0,0 +1,728 @@
+#
+# Release dependant:
+#
+# - (EASY/MEDIUM) Some way of marking the default match to be executed.
+#   Hint: Use cairo. gtk.TreeView.get_bin_window() to obtain a gtk.gdk.Drawable.
+#
+# - (HARD [Partially solved]) Pass focus properly from entry to results window.
+#   Note: You can't change window focus. Only focus the treeview in the inactive window.
+#   Must be done manually as it is done in gtk.EntryCompletion.
+#
+# - (EASY) Expanding should be possible with enter/space as well
+#
+# - (EASY [Hard part solved]) Store expandedness state of categories (DONE)
+#   Missing: Store states in Gconf. Hint: Just store CuemiacTreeView.__expansion_states
+#
+# - (?) Take category/handler ordering into account
+#   Idea: Use no category sorting per default, then let the treeview be reoderable.
+#         store sorting in gconf.
+#
+# - (TRIVIAL but THINK) Trim category names to as few as possible, and really get the names right
+#
+# - Handlers don't need "prefixes" like "Google Live:" or "Open news item"
+#
+# Would be really really nice:
+#
+# - (MEDIUM) User defined (non-static) categories *WITHOUT PERFOMANCE HIT*
+#
+# - (?) Optimize memory and speed
+#
+# - (?) Multiscreen logic.
+#
+# - (MEDIUM/HARD) Type letters when treeview has focus (Do we want this?)
+# 
+# Bonus features/Ideas
+#
+# - (HARD) Detach the search window to "save" the search
+#
+# - (MEDIUM) Drag hits onto desktop/nautilus (likely to require additional Match api)
+#
+
+
+from os.path import *
+from gettext import gettext as _
+
+import cgi
+import sys
+
+import gtk
+incompatible = gtk.check_version (2,8,0)
+if incompatible:
+	print _("You need Gtk+ version 2.8.0 or higher to use this module")
+	print incompatible
+	sys.exit (1) # FIXME: Throw an appropriate exception instead, so we can use a gui notification
+del incompatible
+
+import gnome, gobject
+import gnome.ui, gnomeapplet
+import pango
+
+from categories import CATEGORIES
+
+class Nest :
+	"""
+	A class used to handle nested results in the CuemiacModel.
+	"""
+	def __init__(self, category_name, parent):
+		self.__nest_msg = category_name
+		self.__parent = parent # The CuemiacCategory to which this nest belongs
+	
+	def get_name (self, text=None):
+		return {"count" : self.__parent.get_count () - self.__parent.get_threshold ()}
+	
+	def get_verb (self):
+		return self.__nest_msg
+		
+	def get_count (self):
+		return self.__parent.get_count () - self.__parent.get_threshold ()
+		
+	def get_id (self):
+		"""id used to store expansion state"""
+		return self.__parent.get_name () + "::nest"
+
+class CuemiacCategory :
+	"""
+	A class representing a root node in the cuemiac model/view.
+	"""
+	def __init__ (self, name, parent, threshold=5):
+		"""
+		name: i18n'ed name for the category
+		parent: CuemiacTreeStore in which this category belongs
+		threshold: max number of hits before nesting
+		"""
+		self.__category_row_ref = None
+		self.__nest_row_ref = None
+		self.__parent = parent
+
+		self.__name = name
+		self.__threshold = threshold
+		self.__count = 0
+
+	def get_category_row_path (self):
+		if self.__category_row_ref is None:
+			return None
+		return self.__category_row_ref.get_path ()
+		
+	def get_nest_row_path (self):
+		if self.__nest_row_ref is None:
+			return None
+		return self.__nest_row_ref.get_path ()
+
+	def set_category_iter (self, iter):
+		self.__category_row_ref = gtk.TreeRowReference (self.__parent, self.__parent.get_path(iter))
+		
+	def get_category_iter (self):
+		"""Returns a gtk.TreeIter pointing at the category"""
+		if self.__category_row_ref is None:
+			return None
+		return self.__parent.get_iter (self.__category_row_ref.get_path())
+		
+	def set_nest_iter (self, iter):
+		self.__nest_row_ref = gtk.TreeRowReference (self.__parent, self.__parent.get_path(iter))	
+		
+	def get_nest_iter (self):
+		"""Returns a gtk.TreeIter pointing at the nested row"""
+		if self.__nest_row_ref is None:
+			return None
+		return self.__parent.get_iter (self.__nest_row_ref.get_path())
+	
+	def get_name (self):
+		return self.__name
+		
+	def get_id (self):
+		"""id used to store expansion state"""
+		return self.__name
+	
+	def inc_count (self):
+		"""increase total number of hits in this category"""
+		self.__count = self.__count + 1
+	
+	def get_count (self):
+		"""return the total number of hits in this category"""
+		return self.__count
+	
+	def get_threshold (self):
+		return self.__threshold
+
+class CuemiacModel (gtk.TreeStore):
+	"""
+	A tree model to store hits sorted by categories. CuemiacCategory's are root nodes,
+	with each child representing a hit or a "nest" containing additional hits.
+	Schematically this looks like:
+	
+	CuemiacCategory->
+		-> deskbar.handler.Match
+		-> deskbar.handler.Match
+		...
+		-> deskbar.handler.Match
+		-> Nest
+			-> deskbar.handler.Match
+			-> deskbar.handler.Match
+			...
+	CuemiacCategory->
+		...
+	...
+	
+	Signal arguments:
+		"category-added" : CuemiacCategory, gtk.TreePath
+		"nest-added" : CuemiacCategory, gtk.TreePath
+	"""
+	# Column name
+	MATCHES = 0
+	
+	__gsignals__ = {
+		"category-added" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT]),
+		"nest-added" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT])
+	}
+	
+	def __init__ (self):
+		gtk.TreeStore.__init__ (self, gobject.TYPE_PYOBJECT)
+		self.__categories = {}
+		
+	def append (self, match):
+		"""
+		Automagically append a match or list of matches 
+		to correct category(s), or create a new one(s) if needed.
+		"""
+		if type (match) == list:
+			for hit in match:
+				self.__append (hit)
+		else:
+			self.__append (match)
+		
+	def __append (self, match):
+		if self.__categories.has_key (match.get_category()):
+			self.__append_to_category (match)
+		else:
+			self.__create_category_with_match (match)
+			
+			
+	def __create_category_with_match (self, match):
+		"""
+		Assumes that the category for the match does not exist.
+		"""
+		#FIXME: Check validity of category name and use  proper i18n
+		# Set up a new category
+		cat = CuemiacCategory (match.get_category(), self)
+		iter = gtk.TreeStore.append (self, None, [cat])
+		cat.set_category_iter (iter)
+		self.__categories [match.get_category()] = cat
+
+		# Append the match to the category		
+		gtk.TreeStore.append (self, iter, [match])
+		cat.inc_count ()
+		self.emit ("category-added", cat, cat.get_category_row_path ())
+		
+	
+	def __append_to_category (self, match):
+
+		cat = self.__categories [match.get_category ()]
+		row_iter = None
+		
+		if cat.get_count() < cat.get_threshold() :
+			# We havent reached threshold, append normally
+			cat.inc_count ()
+			gtk.TreeStore.append (self, cat.get_category_iter(), [match])
+			
+		elif cat.get_count() == cat.get_threshold():
+			# We reached the threshold with this match
+			# Set up a Nest, and append the match to that
+			nest = Nest (CATEGORIES[match.get_category ()]["nest"], cat)
+			nest_iter = gtk.TreeStore.append (self, cat.get_category_iter(), [nest])
+			cat.set_nest_iter (nest_iter)
+			
+			cat.inc_count ()
+			gtk.TreeStore.append (self, nest_iter, [match])
+			self.emit ("nest-added", nest, cat.get_nest_row_path ())
+		else:
+			# We've already passed the threshold. Append the match in the nest.
+			cat.inc_count ()
+			gtk.TreeStore.append (self, cat.get_nest_iter(), [match])
+			# Update the nested count in the nest row:
+			self.row_changed (cat.get_nest_row_path(), cat.get_nest_iter())
+			
+		# Update the row count in the view:
+		self.row_changed (cat.get_category_row_path(), cat.get_category_iter())
+		
+	def clear (self):
+		"""Clears this model of data."""
+		gtk.TreeStore.clear (self)
+		self.__categories = {}
+
+class CellRendererCuemiacCategory (gtk.CellRendererText):
+	"""
+	Special cell renderer for the CuemiacTreeView.
+	If the cell to be rendered is a normal Match, it falls back to the normal
+	gtk.CellRendererText render method.
+	If the cell is a CuemiacCategory it takes the icon column of the view into
+	consideration and correctly left justifies the category title.
+	
+	This renderer also creates a small space between category headers. This is
+	to ensure that they don't appear as one solid block when collapsed.
+	"""
+	__gproperties__ = {
+        		'category-header' : (gobject.TYPE_STRING, 'markup for category title string',
+                  	'markup for category title string, None if this is not a category header',
+                 	 None, gobject.PARAM_READWRITE),
+                 	 
+                 'match-count' : (gobject.TYPE_INT, 'number of hits in the category',
+                  	'the number of hits for the CuemiacCategory to be rendered',
+                 	 0,1000,0, gobject.PARAM_READWRITE)
+        }
+	
+	def __init__ (self):
+		gtk.CellRendererText.__init__ (self)
+		self.__category_header = None
+		self.__match_count = 0
+		
+		# Obtain theme font and set it to bold and decrease size 2 points
+		style = gtk.Style ()
+		self.header_font_desc = style.font_desc
+		self.header_font_desc.set_weight (pango.WEIGHT_BOLD)
+		self.header_font_desc.set_size (self.header_font_desc.get_size () - pango.SCALE *2)
+		self.header_bg = style.base [gtk.STATE_NORMAL]
+	
+	def do_render (self, window, widget, background_area, cell_area, expose_area, flags):
+		if not self.get_property ("category-header"):
+			gtk.CellRendererText.do_render (self, window, widget, background_area, cell_area, expose_area, flags)
+		else:
+			self.render_category (window, widget, background_area, cell_area, expose_area, flags)
+	
+	def render_category (self, window, widget, background_area, cell_area, expose_area, flag):
+		"""
+		Renders the category title from the "category-header" property and displays a rigth aligned
+		hit count (read from the "match-count" property).
+		"""
+		ctx = window.cairo_create ()
+		
+		# Set up a pango.Layout for the category title
+		cat_layout = ctx.create_layout ()
+		cat_layout.set_text (self.get_property("category-header"))
+		cat_layout.set_font_description (self.header_font_desc)
+		
+		# Set up a pango.Layout for the hit count
+		count_layout = ctx.create_layout ()
+		count_layout.set_text ("(" + str(self.get_property("match-count")) + ")")
+		count_layout.set_font_description (self.header_font_desc)
+		
+		# Position and draw the layouts
+		ctx.move_to (18, background_area.y + 6)
+		ctx.show_layout (cat_layout)
+		w, h = count_layout.get_pixel_size()
+		ctx.move_to (background_area.width - w + 10, background_area.y + 6)
+		ctx.show_layout (count_layout)
+		
+		# Draw a line in the normal background color in the top of the header,
+		# to separate rows a bit.
+		ctx.set_source_color (self.header_bg)
+		ctx.move_to (0, background_area.y + 1)
+		ctx.line_to (background_area.width + 100, background_area.y + 1) #FIXME: This 100 should really be the icon column width
+		ctx.stroke ()
+		
+	def do_get_property(self, property):
+		if property.name == 'category-header':
+			return self.__category_header
+		elif property.name == 'match-count':
+			return self.__match_count
+		else:
+			raise AttributeError, 'unknown property %s' % property.name
+
+	def do_set_property(self, property, value):
+		if property.name == 'category-header':
+			self.__category_header = value
+		elif property.name == 'match-count':
+			self.__match_count = value
+		else:
+			raise AttributeError, 'unknown property %s' % property.name
+		
+
+class CuemiacTreeView (gtk.TreeView):
+	"""
+	Shows a DeskbarCategoryModel. Sets the background of the root nodes (category headers)
+	to gtk.Style().bg[gtk.STATE_NORMAL].
+	"""
+	
+	__gsignals__ = {
+		"match-selected" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT]),
+	}
+	
+	def __init__ (self, model):
+		gtk.TreeView.__init__ (self, model)
+				
+		icon = gtk.CellRendererPixbuf ()
+		hit_title = CellRendererCuemiacCategory ()
+		hit_title.set_property ("ellipsize", pango.ELLIPSIZE_END)
+		hit_title.set_property ("width-chars", 50) #FIXME: Pick width according to screen size
+		hits = gtk.TreeViewColumn ("Hits")
+		hits.pack_start (icon)
+		hits.pack_start (hit_title)
+		hits.set_cell_data_func(hit_title, self.__get_match_title_for_cell)			
+		hits.set_cell_data_func(icon, self.__get_match_icon_for_cell)
+		self.append_column (hits)
+		
+		self.connect ("cursor-changed", self.__on_cursor_changed)
+		self.set_property ("headers-visible", False)
+		self.set_property ("hover-selection", True)
+		self.connect ("button-press-event", self.__on_click)
+				
+		self.set_enable_search (False)
+		self.set_reorderable(True)
+		# FIXME: Make it so that categories *only* can be reordered by dragging
+		# gtkTreeView does not use normal gtk DnD api.
+		# it uses the api from hell
+		
+		gtk_style = gtk.Style ()
+		self.header_bg = gtk_style.bg[gtk.STATE_NORMAL]
+		self.match_bg = gtk_style.base [gtk.STATE_NORMAL]
+		
+		# Stuff to handle persistant expansion states
+		self.__expansion_states = []
+		self.connect ("row-expanded", self.__on_row_expanded, model)
+		self.connect ("row-collapsed", self.__on_row_collapsed, model)
+		model.connect ("category-added", self.__on_category_added)
+		model.connect ("nest-added", self.__on_nest_added)
+
+	def clear (self):
+		self.model.clear ()
+	
+	def __on_row_expanded (self, widget, iter, path, model):
+		idx = model[iter][model.MATCHES].get_id ()
+		self.__expansion_states.append (idx)
+		
+	def __on_row_collapsed (self, widget, iter, path, model):
+		idx = model[iter][model.MATCHES].get_id ()
+		self.__expansion_states.remove (idx)
+	
+	def __on_category_added (self, widget, cat, path):
+		if cat.get_id() in self.__expansion_states:
+			self.expand_row (path, False)
+		
+	def __on_nest_added (self, widget, cat, path):
+		if cat.get_id() in self.__expansion_states:
+			self.expand_row (path, False)
+		
+	def __on_cursor_changed (self, view):
+		model, iter = self.get_selection().get_selected()
+	
+	def __get_match_icon_for_cell (self, column, cell, model, iter, data=None):
+		
+		match = model[iter][model.MATCHES]
+		
+		if match.__class__ == CuemiacCategory:
+			cell.set_property ("pixbuf", None)
+			cell.set_property ("cell-background-gdk", self.header_bg)
+			
+		else:
+			cell.set_property ("cell-background-gdk", self.match_bg)
+			if match.__class__ == Nest:
+				cell.set_property ("pixbuf", None)		
+			else:
+				cell.set_property ("pixbuf", match.get_icon())
+
+		
+	def __get_match_title_for_cell (self, column, cell, model, iter, data=None):
+
+		match = model[iter][model.MATCHES]
+		
+		if match.__class__ == CuemiacCategory:
+			# Look up i18n category name
+			cell.set_property ("cell-background-gdk", self.header_bg)
+			#cell.set_property ("height", 20)
+			cell.set_property ("category-header", match.get_name())
+			cell.set_property ("match-count", match.get_count ())
+			return
+		else:
+			cell.set_property ("category-header", None)
+			cell.set_property ("height", -1)
+			cell.set_property ("cell-background-gdk", self.match_bg)
+		
+		t = entry.get_text().strip () # FIXME: This will have to be changed in a proper implementation
+		# Pass unescaped query to the matches
+		verbs = {"text" : t}
+		verbs.update(match.get_name(t))
+		# Escape the query now for display
+		verbs["text"] = cgi.escape(verbs["text"])
+		
+		cell.set_property ("markup", match.get_verb () % verbs)
+
+	def __on_click (self, widget, event):
+		model, iter = self.get_selection().get_selected()
+		match = model[iter][model.MATCHES]
+		self.emit ("match-selected", match)
+	
+class CuemiacWindow (gtk.Window):
+	"""
+	Borderless window aligning itself to a given widget.
+	Use CuemiacWindow.update_position() to align it.
+	"""
+	def __init__(self, widgetToAlignWith, alignment):
+		"""
+		alignment should be one of
+			gnomeapplet.ORIENT_{DOWN,UP,LEFT,RIGHT}
+		
+		Call CuemiacWindow.update_position () to position the window.
+		
+		The child widget is automatically put in a scrolled window. The window
+		expands until it reaches a height of 0.6 * screen_height.
+		"""
+		gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
+		self.set_decorated (False)
+		#self.set_focus_on_map (False) # don't grab focus when popping up
+		
+		# Skip the taskbar, and the pager, stick and stay on top
+		self.stick()
+		self.set_keep_above(True)
+		self.set_skip_pager_hint(True)
+		self.set_skip_taskbar_hint(True)
+		#self.set_type_hint (gtk.gdk.WINDOW_TYPE_HINT_DOCK) # This line makes me unable to focus the window
+		
+		self.widgetToAlignWith = widgetToAlignWith
+		self.alignment = alignment
+		self.__is_shown = False
+		
+		self.scroll_win = gtk.ScrolledWindow ()
+		self.scroll_win.set_policy (gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+		gtk.Window.add (self, self.scroll_win)
+		
+		self.screen_height = self.get_screen().get_height ()
+		self.max_window_height = int (0.6 * self.screen_height)
+		
+		self.realize()
+		gtk.gdk.flush()
+		
+	def add (self, child):
+		"""Adds a widget to the internal scroll window"""
+		# Register a callback to respond to resizing of the child
+		self.scroll_win.add_with_viewport (child)
+		child.connect ("size-request", self.adjust_size)
+
+	def adjust_size (self, child, event):
+		"""Callback to adjust the size on child resizing"""
+		# FIXME: SHould we handle width intelligently also?
+		w, h = child.size_request ()
+		h = h + 4 # To ensure we don't always show scrollbars
+		h = min (h, self.max_window_height)
+		self.resize (w, h)
+
+	def update_position (self):
+		"""
+		Calculates the position and moves the window to it.
+		IMPORATNT: widgetToAlignWith should be realized!
+		"""
+		# Get our own dimensions & position
+		window_width  = (self.window.get_geometry())[2]
+	   	window_height = (self.window.get_geometry())[3]
+
+		# Get the dimensions/position of the widgetToAlignWith
+		(x, y) = self.widgetToAlignWith.window.get_origin()
+
+		(w, h) = self.size_request()
+
+		target_w = self.widgetToAlignWith.allocation.width
+		target_h = self.widgetToAlignWith.allocation.height
+
+		screen = self.get_screen()
+
+		found_monitor = False
+		n = screen.get_n_monitors()
+		for i in range(0, n):
+				monitor = screen.get_monitor_geometry(i)
+				if (x >= monitor.x and x <= monitor.x + monitor.width and \
+					y >= monitor.y and y <= monitor.y + monitor.height):
+						found_monitor = True
+						break
+		
+		if not found_monitor:
+				monitor = gtk.gdk.Rectangle(0, 0, screen.get_width(), screen.get_width())
+		
+		self.alignment
+		if self.alignment == gnomeapplet.ORIENT_RIGHT:
+				x += target_w
+
+				if ((y + h) > monitor.y + monitor.height):
+						y -= (y + h) - (monitor.y + monitor.height)
+				
+				if ((y + h) > (monitor.height / 2)):
+						gravity = gtk.gdk.GRAVITY_SOUTH_WEST	
+				else:
+						gravity = gtk.gdk.GRAVITY_NORTH_WEST
+		elif self.alignment == gnomeapplet.ORIENT_LEFT:
+				x -= w
+
+				if ((y + h) > monitor.y + monitor.height):
+						y -= (y + h) - (monitor.y + monitor.height)
+				
+				if ((y + h) > (monitor.height / 2)):
+						gravity = gtk.gdk.GRAVITY_SOUTH_EAST
+				else:
+						gravity = gtk.gdk.GRAVITY_NORTH_EAST
+		elif self.alignment == gnomeapplet.ORIENT_DOWN:
+				y += target_h
+
+				if ((x + w) > monitor.x + monitor.width):
+						x -= (x + w) - (monitor.x + monitor.width)
+
+				gravity = gtk.gdk.GRAVITY_NORTH_WEST
+		elif self.alignment == gnomeapplet.ORIENT_UP:
+				y -= h
+
+				if ((x + w) > monitor.x + monitor.width):
+						x -= (x + w) - (monitor.x + monitor.width)
+
+				gravity = gtk.gdk.GRAVITY_SOUTH_WEST
+		
+		self.move(x, y)
+		self.set_gravity(gravity)
+		
+		
+# --------------------BEGIN CUT-N-PASTE FROM DESKBAR-APPLET ---------------
+import gtk
+gtk.threads_init()
+import gnome.ui, gnomeapplet
+
+import getopt, sys
+from os.path import *
+
+# Allow to use uninstalled
+def _check(path):
+	return exists(path) and isdir(path) and isfile(path+"/AUTHORS")
+
+name = join(dirname(__file__), "..")
+if _check(name):
+	print 'Running uninstalled deskbar, modifying PYTHONPATH'
+	sys.path.insert(0, abspath(name))
+else:
+	print "Running installed deskbar, using normal PYTHONPATH"
+
+# Now the path is set, import our applet
+import deskbar, deskbar.applet, deskbar.defs
+
+import gettext, locale
+gettext.bindtextdomain('deskbar-applet', abspath(join(deskbar.defs.DATA_DIR, "locale")))
+gettext.textdomain('deskbar-applet')
+
+locale.bindtextdomain('deskbar-applet', abspath(join(deskbar.defs.DATA_DIR, "locale")))
+locale.textdomain('deskbar-applet')
+# --------------------END CUT-N-PASTE FROM DESKBAR-APPLET ---------------
+
+#
+# BEWARE: The following is all a gross hack to get something on the screen
+# in the fewest lines of code.
+#
+
+#
+# Load modules and set up async handling of matches
+#
+modules = []
+def on_module_loaded (loader, ctx):
+	loader.initialize_module (ctx)
+	modules.append (ctx)
+	if ctx.module.is_async ():
+		ctx.module.connect ("query-ready", lambda sender, match: cmodel.append(match))
+
+from module_list import ModuleLoader, ModuleList
+mloader = ModuleLoader (deskbar.MODULES_DIRS)
+mlist = ModuleList ()
+mloader.connect ("module-loaded", on_module_loaded)
+
+mloader.load_all()
+
+#
+# Callback when somehting is typed into the entry
+#
+def on_entry_changed (entry):
+	# FIXME: We should store the expandedness state of the categories somehow
+	cmodel.clear()
+	text = entry.get_text().strip()
+	
+	if text == "":
+		cwindow.hide ()
+		return
+	
+	for ctx in modules:
+		if not ctx.enabled:
+			continue
+			
+		if ctx.module.is_async ():
+			ctx.module.query_async (text, 10)
+		else:
+			cmodel.append (ctx.module.query (text, 10))
+
+#
+# Callback to monitor special keys (like Esc)
+#
+def on_entry_key_press (entry, event):
+	
+	if event.keyval == gtk.keysyms.Escape:
+			# bind Escape to clear the GtkEntry
+			if not entry.get_text().strip() == "":
+				# If we clear some text, tell async handlers to stop.
+				for ctx in modules:
+					if ctx.module.is_async ():
+						ctx.module.stop_query ()
+			entry.set_text("")
+			return True
+	
+	if event.keyval == 65364 or \
+	   event.keyval == 65362 or \
+	   event.keyval == 43 or \
+	   event.keyval == 45 or \
+	   event.keyval ==  65293:	# Down, Up, +, -, Enter
+		cview.grab_focus ()
+		cview.event (event)
+		gobject.timeout_add(5, lambda: entry.grab_focus()) # FIXME: Add a restore_entry() method that does it all
+		gobject.timeout_add(7, lambda: entry.set_position (len (entry.get_text ())))
+		return True
+	
+	return False
+
+def do_action(sender, match):
+	print "Action triggered"
+	if hasattr(match, 'action'):
+		match.action(entry.get_text())
+
+def show_entry (dba_button, toggle_button):
+	if dba_button.get_active_main ():
+		cwindow.update_position ()
+		cwindow.show_all ()
+		entry.grab_focus ()
+	else:
+		cwindow.hide ()
+
+#
+# Setup up the UI and create the Cuemiac
+#
+orientation = gnomeapplet.ORIENT_RIGHT
+
+from deskbar_applet_button import DeskbarAppletButton
+dba_button = DeskbarAppletButton (orientation)
+dba_button.connect ("toggled-main", show_entry)
+dba_button.set_button_image_from_file (join(deskbar.ART_DATA_DIR, "deskbar.svg"))
+
+vbox = gtk.VBox ()
+
+entry = gtk.Entry()
+entry.connect ("changed", on_entry_changed)
+entry.connect ("key-press-event", on_entry_key_press)
+
+cmodel = CuemiacModel ()
+cview = CuemiacTreeView (cmodel)
+cview.connect ("match-selected", do_action)
+cwindow = CuemiacWindow (dba_button, orientation)
+#cwindow = gtk.Window ()
+
+vbox.pack_start (entry)
+vbox.pack_start (cview)
+cwindow.add (vbox)
+#cwindow.connect ("focus-out-event", lambda widget,event: cwindow.hide())
+
+
+#
+# Setup final GUI stuff
+#	
+window = gtk.Window()
+window.connect ("destroy", gtk.main_quit)
+window.add (dba_button)
+window.show_all()
+
+gtk.main ()

Attachment: deskbar.svg
Description: image/svg



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]