1 # Copyright (C) 2009 Canonical
6 # This program is free software; you can redistribute it and/or modify it under
7 # the terms of the GNU General Public License as published by the Free Software
8 # Foundation; either version 2 of the License, or (at your option) any later
11 # This program is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16 # You should have received a copy of the GNU General Public License along with
17 # this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
31 from appcenter.enums import *
33 # support running from the dir too
34 d = os.path.dirname(os.path.abspath(os.path.join(os.getcwd(),__file__)))
35 sys.path.insert(0, os.path.split(d)[0])
38 class ExecutionTime(object):
40 Helper that can be used in with statements to have a simple
41 measure of the timming of a particular block of code, e.g.
42 with ExecutinTime("db flush"):
45 def __init__(self, info=""):
48 self.now = time.time()
49 def __exit__(self, type, value, stack):
50 print "%s: %s" % (self.info, time.time() - self.now)
52 class AppStore(gtk.GenericTreeModel):
54 A subclass GenericTreeModel that reads its data from a xapian
55 database. It can combined with any xapian querry and with
56 a generic filter function (that can filter on data not
66 def __init__(self, db, icons, search_query=None, limit=200,
67 sort=False, filter=None):
72 - `db`: a xapian.Database that contians the applications
73 - `icons`: a gtk.IconTheme that contains the icons
74 - `search_query`: a search as a xapian.Query
75 - `limit`: how many items the search should return (0 == unlimited)
76 - `sort`: sort alphabetically after a search
77 (default is to use relevance sort)
78 - `filter`: filter functions that can be used to filter the
79 data further. A python function that gets a pkgname
81 gtk.GenericTreeModel.__init__(self)
87 # limit to applications
88 for m in db.postlist("ATapplication"):
89 doc = db.get_document(m.docid)
90 if filter and self.is_filtered_out(filter, doc):
92 self.appnames.append(doc.get_data())
95 enquire = xapian.Enquire(db)
96 enquire.set_query(search_query)
97 enquire.set_sort_by_value_then_relevance(XAPIAN_VALUE_POPCON)
99 matches = enquire.get_mset(0, db.get_doccount())
101 matches = enquire.get_mset(0, limit)
102 logging.debug("found ~%i matches" % matches.get_matches_estimated())
104 doc = m[xapian.MSET_DOCUMENT]
105 if "APPVIEW_DEBUG_TERMS" in os.environ:
107 for t in doc.termlist():
108 print "'%s': %s (%s); " % (t.term, t.wdf, t.termfreq),
110 appname = doc.get_data()
111 if filter and self.is_filtered_out(filter, doc):
113 self.appnames.append(appname)
115 self.appnames.sort(key=str.lower)
116 def is_filtered_out(self, filter, doc):
117 """ apply filter and return True if the package is filtered out """
118 pkgname = doc.get_value(XAPIAN_VALUE_PKGNAME)
119 return not filter.filter(doc, pkgname)
120 # GtkTreeModel functions
121 def on_get_flags(self):
122 return (gtk.TREE_MODEL_LIST_ONLY|
123 gtk.TREE_MODEL_ITERS_PERSIST)
124 def on_get_n_columns(self):
125 return len(self.column_type)
126 def on_get_column_type(self, index):
127 return self.column_type[index]
128 def on_get_iter(self, path):
129 logging.debug("on_get_iter: %s" % path)
130 if len(self.appnames) == 0:
134 def on_get_path(self, rowref):
135 logging.debug("on_get_path: %s" % rowref)
137 def on_get_value(self, rowref, column):
138 #logging.debug("on_get_value: %s %s" % (rowref, column))
139 appname = self.appnames[rowref]
140 if column == self.COL_NAME:
141 return gobject.markup_escape_text(appname)
142 elif column == self.COL_ICON:
145 for post in self.xapiandb.postlist("AA"+appname):
146 doc = self.xapiandb.get_document(post.docid)
147 icon_name = doc.get_value(XAPIAN_VALUE_ICON)
148 icon_name = os.path.splitext(icon_name)[0]
151 icon = self.icons.load_icon(icon_name, 24,0)
154 if not str(e).endswith("not present in theme"):
155 logging.exception("get_icon")
157 def on_iter_next(self, rowref):
158 #logging.debug("on_iter_next: %s" % rowref)
159 new_rowref = rowref + 1
160 if new_rowref >= len(self.appnames):
163 def on_iter_children(self, parent):
166 return self.appnames[0]
167 def on_iter_has_child(self, rowref):
169 def on_iter_n_children(self, rowref):
170 logging.debug("on_iter_n_children: %s (%i)" % (rowref, len(self.appnames)))
173 return len(self.appnames)
174 def on_iter_nth_child(self, parent, n):
175 logging.debug("on_iter_nth_child: %s %i" % (parent, n))
179 return self.appnames[n]
180 except IndexError, e:
182 def on_iter_parent(self, child):
185 class AppView(gtk.TreeView):
186 """Treeview based view component that takes a AppStore and displays it"""
187 def __init__(self, store):
188 gtk.TreeView.__init__(self)
189 self.set_fixed_height_mode(True)
190 tp = gtk.CellRendererPixbuf()
191 column = gtk.TreeViewColumn("Icon", tp, pixbuf=AppStore.COL_ICON)
192 column.set_fixed_width(32)
193 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
194 self.append_column(column)
195 tr = gtk.CellRendererText()
196 column = gtk.TreeViewColumn("Name", tr, markup=AppStore.COL_NAME)
197 column.set_fixed_width(200)
198 column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
199 self.append_column(column)
200 self.set_model(store)
202 self.cursor_hand = gtk.gdk.Cursor(gtk.gdk.HAND2)
203 self.connect("motion-notify-event", self.on_motion_notify_event)
204 self.connect("button-press-event", self.on_button_press_event)
205 def on_motion_notify_event(self, widget, event):
206 #print "on_motion_notify_event: ", event
207 path = self.get_path_at_pos(event.x, event.y)
209 self.window.set_cursor(None)
211 self.window.set_cursor(self.cursor_hand)
212 def on_button_press_event(self, widget, event):
213 #print "on_button_press_event: ", event
214 res = self.get_path_at_pos(event.x, event.y)
217 (path, column, wx, wy) = res
218 if event.button != 1 or path is None:
220 self.emit("row-activated", path, column)
222 # XXX should we use a xapian.MatchDecider instead?
223 class AppViewFilter(object):
225 Filter that can be hooked into AppStore to filter for criteria that
226 are based around the package details that are not listed in xapian
227 (like installed_only) or archive section
229 def __init__(self, cache):
231 self.supported_only = False
232 self.installed_only = False
233 def set_supported_only(self, v):
234 self.supported_only = v
235 def set_installed_only(self, v):
236 self.installed_only = v
237 def filter(self, doc, pkgname):
238 """return True if the package should be displayed"""
239 #logging.debug("filter: supported_only: %s installed_only: %s '%s'" % (
240 # self.supported_only, self.installed_only, pkgname))
241 if self.installed_only:
242 if (self.cache.has_key(pkgname) and
243 not self.cache[pkgname].isInstalled):
245 # FIXME: add special property to the desktop file instead?
246 # what about in the future when we support pkgs without
248 if self.supported_only:
249 section = doc.get_value(XAPIAN_VALUE_ARCHIVE_SECTION)
250 if section != "main" and section != "restricted":
254 def get_query_from_search_entry(search_term):
256 parser = xapian.QueryParser()
257 user_query = parser.parse_query(search_term)
258 # ensure that we only search for applicatins here, even
259 # when a-x-i is loaded
260 app_query = xapian.Query("ATapplication")
261 query = xapian.Query(xapian.Query.OP_AND, app_query, user_query)
264 def on_entry_changed(widget, data):
265 new_text = widget.get_text()
266 print "on_entry_changed: ", new_text
267 #if len(new_text) < 3:
270 query = get_query_from_search_entry(new_text)
271 view.set_model(AppStore(db, icons, query))
273 if __name__ == "__main__":
274 logging.basicConfig(level=logging.DEBUG)
276 xapian_base_path = "/var/cache/app-install"
277 pathname = os.path.join(xapian_base_path, "xapian")
278 db = xapian.Database(pathname)
280 # add the apt-xapian-database for here (we don't do this
281 # for now as we do not have a good way to integrate non-apps
283 #axi = xapian.Database("/var/lib/apt-xapian-index/index")
284 #db.add_database(axi)
286 # additional icons come from app-install-data
287 icons = gtk.icon_theme_get_default()
288 icons.append_search_path("/usr/share/app-install/icons/")
292 cache = apt.Cache(apt.progress.OpTextProgress())
293 filter = AppViewFilter(cache)
294 filter.set_supported_only(True)
295 store = AppStore(db, icons, filter=filter)
298 scroll = gtk.ScrolledWindow()
299 view = AppView(store)
302 entry.connect("changed", on_entry_changed, (db, view))
305 box.pack_start(entry, expand=False)
306 box.pack_start(scroll)
311 win.set_size_request(400,400)