[Deskbar] Cuemiac development
- From: Mikkel Kamstrup Erlandsen <kamstrup daimi au dk>
- To: deskbar-applet-list gnome org
- Subject: [Deskbar] Cuemiac development
- Date: Thu, 17 Nov 2005 00:37:12 +0100
Steam is picking up in the Cuemiac branch again.
I've finally put the Cuemiac in its own window, which aligns nicely with
a given widget (in our case the entry).
Also, I've nailed a tiresome bug related to the underlining of the
category labels; so I just thought I'd share my latest progress...
It's just my cuemiac.py... I won't make a real patch since it still
contains a few leftovers from a little scrollbar adventure I'm on right
now. Just stick it in a fairly recent cuemiac branch cvs checkout...
Before you flame me; be sure to check the "Known Issues" section at the
top of the file :) Also all focus-related bugs should go to /dev/null
atm :D
Cheers
Mikkel
#
# 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: Nautilus list style...
#
# - The entry loose focus sometimes when the CuemiacWindow appears,
# really annoying!
#
# - Ellipsize intelligently according to screen size
#
# - 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.
# Infact hover-expand sucks, but click-row-to-expand has serious complications
# assosiated with it (exp. lnob clicking namely).
#
# - Expanding should be possible with enter/space as well
#
# - No Multiscreen logic yet.
#
# - ** Generally focus issues all over the place! **
#
# - Should use scrolled window
from os.path import *
MYPATH = abspath(dirname(__file__))
import cgi
import sys
import gtk, gobject
import gnome
import gnome.ui, gnomeapplet
import pango
from 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 + 1
class Separator :
"""
Just a dummy to stick in the TreeStore when we have a category separator
"""
pass
class 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 beast
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,
},
#"" : {
# "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_width
class 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 ()
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.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>")
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 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 ---------------
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, ModuleList
mloader = 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"))
#mloader.load (join(MYPATH, "handlers/evolution.py"))
#mloader.load (join(MYPATH, "handlers/gtkbookmarks.py"))
#mloader.load (join(MYPATH, "handlers/programs.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)
#adjustment = gtk.Adjustment (page_size=200, upper=300,lower=100)
#scroll_win = gtk.ScrolledWindow (hadjustment=adjustment)
#scroll_win.set_policy (gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
#cwindow.add (scroll_win)
cwindow.add (cuemiac)
#scroll_win.add_with_viewport (cuemiac)
#scroll_win.get_vadjustment ().set_property ("page-size", 20)
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)
window = gtk.Window()
window.connect ("destroy", gtk.main_quit)
window.add (vbox)
window.show_all()
cuemiac.show_all()
gtk.main ()
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]