## Known issues## - Expander knob should be indented or completely removed.# Note: Adding a column for the icons and putting# the expander in the second column looks bad.# Possible Fix: Create a custom cellrenderer that renders# icon+text and optionally separator lines, this way# the expander knob should align up with the icons...## - The entry loose focus sometimes when the CuemiacWindow appears,# really annoying!## - No line breaking for long titles. Do we want this anyway?# should we just ellipsize?## - Should reduce window size on row collapse. Should this be done in# CuemiacWidget or by the CuemiacWindow containing it?# Maybe CuemiacWidget should emit a signal when it collapses a row...## - "hover-expand" does not work when moving the cursor with the keyboard.## - No Multiscreen logic yet.## - ** Generally focus issues all over the place! **from os.path import *MYPATH = abspath(dirname(__file__))import cgiimport sysimport gtk, gobjectimport gnomeimport gnome.ui, gnomeappletfrom gettext import gettext as _class Nest : """ A class used to handle nested results in the CuemiacModel """ def __init__(self, category_name): self.__nest_msg = category_name self.__num_children = 0 def get_name (self, text=None): return {"count" : self.__num_children} def get_verb (self): return self.__nest_msg def set_num_children (self, num): self.__num_children = num def inc_num_children (self): self.__num_children = self.__num_children + 1class Separator : """ Just a dummy to stick in the TreeStore when we have a category separator """ passclass UnknownCategory (Exception): def __init__ (self, category_name, match): print "Unknown Category '%s' requested by %s" % (category_name, match.__class__)# See CuemiacModel for a description of this beastCATEGORIES = { "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, }, #"" : { # "name":"", # "nest":"", # "threshold":5, #},}class CuemiacModel (gtk.TreeStore): # Column name MATCHES = 0 def __init__ (self, category_infos): """ category_infos is a dict containing (case-sensitive) category names, with corresponding category information dicts. The category information dicts must contain the follwing key-value pairs: "name" : The *display name* of the category "nest" : what string to display for nested results (str) can refer to %(count)s to insert the number of childs. "threshold" : how many results before we start nesting them (int) The categories supplied in this dict are the only ones that Cuemiac model will allow. Request for other categories raise a UnknownCategory exception. IMPORTANT: The key values of the category_infos dict is NOT necesarily the same as category_infos[cat_name]["name"]. This is for internationalization reasons. Basically the keys of the dict are the only thing that uniquely determines a category. """ gtk.TreeStore.__init__ (self, gobject.TYPE_PYOBJECT) # Match object self.infos = category_infos # dict containing category descriptions self.categories = {} # a dict containing # {name : (display_name, gtk.TreeRowReference, gtk.TreeRowReference, count, threshold)) # where the tuple contains # (displayed_name, first_entry, nested_entry, total_num_hits, hits_before_hide) # for each category self.count = 0 def append (self, match): """ Appends the match to the category returned by match.get_category(). Automatically creates a new category for the match if necesary. """ if self.categories.has_key (match.get_category()): self.__append_to_category (match, self.categories [match.get_category()]) else: self.create_category_with_match (match) self.count = self.count + 1 def __append_to_category (self, match, category): cat_name, first, nest, count, threshold = category if count < threshold: pos = self.__get_row_ref_position (first) self.insert (None, pos+count, [match]) self.categories[cat_name] = (cat_name, first, nest, count + 1, threshold) elif count == threshold: # We need to nest the rest of the matches, # so append a Nest to the list nest_entry = Nest (self.infos[cat_name]["nest"]) pos = self.__get_row_ref_position (first) iter = self.insert (None, pos+count, [nest_entry]) # Create a ref to the nested match entry nest = gtk.TreeRowReference (self, self.get_path(iter)) # Now append the match nested in the NestedMatch gtk.TreeStore.append (self, iter, [match]) nest_entry.inc_num_children () self.categories[cat_name] = (cat_name, first, nest, count + 1, threshold) else: # Append nested, and increase the number of children in the # nested match iter = self.get_iter ( nest.get_path() ) gtk.TreeStore.append (self, iter, [match]) self[iter][self.MATCHES].inc_num_children () # FIXME We should show a count in the nested match self.categories[cat_name] = (cat_name, first, nest, count + 1, threshold) def create_category_with_match (self, match): """ Creates a new category with the given match as first entry. """ cat_name = match.get_category () if not self.infos.has_key (cat_name): print 'Unknown category:',cat_name raise UnknownCategory(cat_name, match) iter = gtk.TreeStore.append (self, None, [match]) gtk.TreeStore.append (self, None, [Separator()]) first = gtk.TreeRowReference (self, self.get_path(iter)) # Add the new category to our category list self.categories [cat_name] = (self.infos[cat_name]["name"], first, None, 1, self.infos[cat_name]["threshold"]) def clear (self): """ Clears this model of data. """ gtk.TreeStore.clear (self) self.categories = {} self.count = 0 def set_category_threshold (self, cat_name, threshold): display_name, first, nest, count, old_thres = self.categories[cat_name] self.categories[cat_name] = (display_name, first, nest, count, threshold) def __get_row_ref_position (self, row_ref): return int (self.get_string_from_iter (self.get_iter(row_ref.get_path()))) def __calc_max_category_width (self): max_width = 0 for cat_name in self.infos.iterkeys (): cat_width = len(self.infos[cat_name]["name"]) if cat_width > max_width: max_width = cat_width self.max_category_width = max_width def get_max_category_width (self): return self.max_category_widthclass CuemiacTreeView (gtk.TreeView): """ Shows a DeskbarCategoryModel. Used internally in the CuemiacWidget. """ __gsignals__ = { "match-selected" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT]), } def __init__ (self, cuemiac_model): gtk.TreeView.__init__ (self, cuemiac_model) icon = gtk.CellRendererPixbuf () hit_title = gtk.CellRendererText () 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.set_row_separator_func( # lambda model, iter: (model[iter][model.MATCHES].__class__ == Separator)) self.connect ("cursor-changed", self.__on_cursor_changed) self.set_property ("headers-visible", False) self.set_property ("hover-selection", True) self.set_property ("hover-expand", True) self.set_reorderable(True) self.connect ("button-press-event", self.__on_click) def __on_cursor_changed (self, view): model, iter = self.get_selection().get_selected() if model[iter][model.MATCHES].__class__ == Separator: self.get_selection().unselect_iter (iter) def __get_match_icon_for_cell (self, column, cell, model, iter, data=None): match = model[iter][model.MATCHES] if match.__class__ == Separator or match.__class__ == Nest: cell.set_property ("pixbuf", None) else: icon = match.get_icon() cell.set_property ("pixbuf", icon) def __get_match_title_for_cell (self, column, cell, model, iter, data=None): match = model[iter][model.MATCHES] if match.__class__ == Separator: cell.set_property ("markup", "") cell.set_property ("height", 20) return else: cell.set_property ("height", -1) t = "cuemiac" # 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 clear (self): self.model.clear () def __on_click (self, widget, event): model, iter = self.get_selection().get_selected() match = model[iter][model.MATCHES] # # We initially expanded on a click anywhere # within the nested row. This was a mess. # #if match.__class__ == Nest: # path = model.get_path(iter) # if self.row_expanded (path): # self.collapse_row (path) # else: # self.expand_row (path, True) #else: # self.emit ("match-selected", match) self.emit ("match-selected", match)class CuemiacWidget (gtk.HBox): """ The Cuemiac. Nothing more to say. This is what you want. The rest of this file is just code. """ __gsignals__ = { "match-selected" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT]), } def __init__ (self, cuemiac_model, cuemiac_treeview): gtk.HBox.__init__ (self) self.layout = gtk.Layout () # Place to draw category names on self.layout.set_border_width (6) self.layout.set_property ("width", 100) self.pack_start(self.layout, expand=True) self.model = cuemiac_model self.view = cuemiac_treeview self.cat_labels = {} # keys are category names and values are widgets self.pack_start(self.view, expand=True) self.view.connect_after ("size-allocate", self.align_category_labels) self.view.connect ("match-selected", lambda sender, match: self.emit ("match-selected", match)) self.layout.connect ("expose-event", self.layout_underline_categories) self.__init_category_labels () self.max_category_label_width = self.__calc_max_category_label_width () def __init_category_labels (self): """ Creates gtk.Labels for all categories in allowed by the model. Each label is realize()'ed so that we can calculate diemsnions later. Called by the constructor. """ for cat_name in self.model.infos.iterkeys(): label = gtk.Label () label.set_markup ("<b>"+self.model.infos[cat_name]["name"]+"</b>") label.realize () self.cat_labels [cat_name] = label def __calc_max_category_label_width (self): """ Returns the max width in pixels of the category labels. """ max_width = 0 for cat_label in self.cat_labels.itervalues (): w, h = cat_label.get_layout().get_pixel_size() if w > max_width: max_width = w return max_width 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): """ Appends a single match to the correct category, or creates a new category for it if needeed. """ self.model.append (match) def get_label_for_category (self, cat_name, display_name): """ Returns a gtk.Label representing the category title string. HINT: You can obtain the dimensions of a realized label (like these) with w, h = label.get_layout().get_pixel_size() The normal get_allocation() and/or get_size_request() will not necesarily work unless the label has actually benn drawn on screen. """ return self.cat_labels [cat_name] def align_category_labels (self, sender, allocation): """ Callback to postion the category "labels". """ if self.view.window is None: return column = self.view.get_column(0) pad = self.layout.get_border_width () width = self.layout.get_allocation().width - pad rect = self.view.get_allocation () self.layout.set_size_request (self.max_category_label_width + 4*pad, rect.height) self.layout.bin_window.clear () cairo = self.layout.bin_window.cairo_create () cairo.set_source_rgb (0,0,0) cairo.set_line_width (0.5) for cat_name in self.model.categories.iterkeys (): display_name, first, nest, count, threshold = self.model.categories[cat_name] path = first.get_path() area = self.view.get_background_area(path, column) x,y = self.view.tree_to_widget_coords(area.x, area.y) label = self.get_label_for_category (cat_name, display_name) if not label.parent: self.layout.put(label, 0, y + self.view.style_get_property("vertical-separator")) label.show () # Calculate dimensions from Pango Layout. # Since we might not be drawn on screen yet, # normal gtk ops might not work... w, h = label.get_layout().get_pixel_size() self.layout.move(label, width - w, y + self.view.style_get_property("vertical-separator")) y = y + area.height - 2*self.view.style_get_property("vertical-separator") cairo.move_to (pad, y) cairo.line_to (width, y) cairo.stroke () def layout_underline_categories (self, view, event): # We have to do this in response to an expose event. # Doing it in align_category_labels does *not* work # since it triggers an expose event on self.layout, # which will overwrite any drawing. width = self.layout.get_allocation().width pad = self.layout.get_border_width () column = self.view.get_column(0) self.layout.bin_window.clear () cairo = self.layout.bin_window.cairo_create () cairo.set_source_rgb (0,0,0) cairo.set_line_width (0.5) # FIXME This does not seems to have any effect for cat_name in self.model.categories.iterkeys (): display_name, first, nest, count, threshold = self.model.categories[cat_name] path = first.get_path() area = self.view.get_background_area(path, column) x,y = self.view.tree_to_widget_coords(area.x, area.y) y = y + area.height - 2*self.view.style_get_property("vertical-separator") cairo.move_to (pad, y) cairo.line_to (width, y) cairo.stroke () return False def clear (self): """ Use this to clear the entire view and model structure of the Cuemiac. """ self.model.clear () if self.layout.window: for label in self.cat_labels.itervalues (): self.layout.remove (label) self.layout.bin_window.clear ()class CuemiacWindow (gtk.Window): """ Borderless window aligning itself to a given widget """ def __init__(self, widgetToAlignWith, alignment): """ alignment should be one of gnomeapplet.ORIENT_{DOWN,UP,LEFT,RIGHT} The window will be placed accordingly relative to the given widget upon invoking .show(). """ gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) self.set_decorated(False) self.widgetToAlignWith = widgetToAlignWith self.alignment = alignment self.__is_shown = False def show (self): """ Calculates the position and shows the window """ # Get our own dimensions & position self.realize() gtk.gdk.flush() window_width = (self.window.get_geometry())[2] window_height = (self.window.get_geometry())[3] # 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) # Get the dimensions/position of the widgetToAlignWith self.widgetToAlignWith.realize() (x, y) = self.widgetToAlignWith.window.get_origin() (w, h) = self.get_size() (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 # -"Coordinates locked in captain." # -"Engage." self.move(x, y) #print "Move win to "+x+","+y self.set_gravity(gravity) gtk.Window.show (self) self.__is_shown = True def show_all (self): self.show () gtk.Window.show_all (self) def hide (self): self.__is_shown = False gtk.Window.hide (self) def is_shown (self): return self.__is_shown# --------------------BEGIN CUT-N-PASTE FROM DESKBAR-APPLET ---------------import gtkgtk.threads_init()import gnome.ui, gnomeappletimport getopt, sysfrom os.path import *# Allow to use uninstalleddef _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 appletimport deskbar, deskbar.applet, deskbar.defsimport gettext, localegettext.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 ---------------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: cuemiac.append(match))from module_list import ModuleLoader, ModuleListmloader = ModuleLoader (deskbar.MODULES_DIRS)mlist = ModuleList ()mloader.connect ("module-loaded", on_module_loaded)mloader.load_all()#mloader.load (join(MYPATH, "handlers/beagle-live.py"))#mloader.load (join(MYPATH, "handlers/google-live.py"))#mloader.load (join(MYPATH, "handlers/volumes.py"))def on_entry_changed (entry): cuemiac.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) else: cuemiac.append (ctx.module.query (text)) if not cwindow.is_shown (): cwindow.show_all ()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 cleared some text, tell async handlers to stop. for ctx in modules: if ctx.module.is_async (): ctx.module.stop_query () entry.set_text("")vbox = gtk.VBox ()entry = gtk.Entry()entry.connect ("changed", on_entry_changed)entry.connect ("key-press-event", on_entry_key_press)cmodel = CuemiacModel (CATEGORIES)cview = CuemiacTreeView (cmodel)cuemiac = CuemiacWidget (cmodel, cview)cwindow = CuemiacWindow (entry, gnomeapplet.ORIENT_DOWN)cwindow.add (cuemiac)cwindow.connect ("focus-out-event", lambda widget,event: cwindow.hide())def do_action(sender, match): if hasattr(match, 'action'): match.action(entry.get_text())cuemiac.connect ("match-selected", do_action)vbox.pack_start (entry, False)#vbox.pack_start (cuemiac, True)window = gtk.Window()window.connect ("destroy", gtk.main_quit)window.add (vbox)window.show_all()cuemiac.show_all()gtk.main ()