add copyrights information
[appstream:software-center.git] / appcenter / view / appview.py
1 # Copyright (C) 2009 Canonical
2 #
3 # Authors:
4 #  Michael Vogt
5 #
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
9 # version.
10 #
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
14 # details.
15 #
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
19
20
21 import apt
22 import logging
23 import gtk
24 import gobject
25 import os
26 import sys
27 import time
28 import xapian
29
30 try:
31     from appcenter.enums import *
32 except ImportError:
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])
36     from enums import *
37
38 class ExecutionTime(object):
39     """
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"):
43         db.flush()
44     """
45     def __init__(self, info=""):
46         self.info = info
47     def __enter__(self):
48         self.now = time.time()
49     def __exit__(self, type, value, stack):
50         print "%s: %s" % (self.info, time.time() - self.now)
51
52 class AppStore(gtk.GenericTreeModel):
53     """ 
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
57     available in xapian)
58     """
59
60     (COL_NAME, 
61      COL_ICON,
62      ) = range(2)
63     column_type = (str, 
64                    gtk.gdk.Pixbuf)
65
66     def __init__(self, db, icons, search_query=None, limit=200, 
67                  sort=False, filter=None):
68         """
69         Initalize a AppStore. 
70
71         :Parameters: 
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
80         """
81         gtk.GenericTreeModel.__init__(self)
82         self.xapiandb = db
83         self.icons = icons
84         self.appnames = []
85         self.filter = filter
86         if not search_query:
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):
91                     continue
92                 self.appnames.append(doc.get_data())
93             self.appnames.sort()
94         else:
95             enquire = xapian.Enquire(db)
96             enquire.set_query(search_query)
97             enquire.set_sort_by_value_then_relevance(XAPIAN_VALUE_POPCON)
98             if limit == 0:
99                 matches = enquire.get_mset(0, db.get_doccount())
100             else:
101                 matches = enquire.get_mset(0, limit)
102             logging.debug("found ~%i matches" % matches.get_matches_estimated())
103             for m in matches:
104                 doc = m[xapian.MSET_DOCUMENT]
105                 if "APPVIEW_DEBUG_TERMS" in os.environ:
106                     print doc.get_data()
107                     for t in doc.termlist():
108                         print "'%s': %s (%s); " % (t.term, t.wdf, t.termfreq),
109                     print "\n"
110                 appname = doc.get_data()
111                 if filter and self.is_filtered_out(filter, doc):
112                     continue
113                 self.appnames.append(appname)
114             if sort:
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:
131             return None
132         index = path[0]
133         return index
134     def on_get_path(self, rowref):
135         logging.debug("on_get_path: %s" % rowref)
136         return 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:
143             try:
144                 icon_name = ""
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]
149                     break
150                 if icon_name:
151                     icon = self.icons.load_icon(icon_name, 24,0)
152                     return icon
153             except Exception, e:
154                 if not str(e).endswith("not present in theme"):
155                     logging.exception("get_icon")
156         return None
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):
161             return None
162         return new_rowref
163     def on_iter_children(self, parent):
164         if parent:
165             return None
166         return self.appnames[0]
167     def on_iter_has_child(self, rowref):
168         return False
169     def on_iter_n_children(self, rowref):
170         logging.debug("on_iter_n_children: %s (%i)" % (rowref, len(self.appnames)))
171         if rowref:
172             return 0
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))
176         if parent:
177             return 0
178         try:
179             return self.appnames[n]
180         except IndexError, e:
181             return None
182     def on_iter_parent(self, child):
183         return None
184
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)
201         # single click
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)
208         if path is None:
209             self.window.set_cursor(None)
210         else:
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)
215         if not res:
216             return
217         (path, column, wx, wy) = res
218         if event.button != 1 or path is None:
219             return
220         self.emit("row-activated", path, column)
221
222 # XXX should we use a xapian.MatchDecider instead?
223 class AppViewFilter(object):
224     """ 
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
228     """
229     def __init__(self, cache):
230         self.cache = 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):
244                 return False
245         # FIXME: add special property to the desktop file instead?
246         #        what about in the future when we support pkgs without
247         #        desktop files?
248         if self.supported_only:
249             section = doc.get_value(XAPIAN_VALUE_ARCHIVE_SECTION)
250             if section != "main" and section != "restricted":
251                 return False
252         return True
253
254 def get_query_from_search_entry(search_term):
255     # now build a query
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)
262     return query
263
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:
268     #    return
269     (db, view) = data
270     query = get_query_from_search_entry(new_text)
271     view.set_model(AppStore(db, icons, query))
272
273 if __name__ == "__main__":
274     logging.basicConfig(level=logging.DEBUG)
275
276     xapian_base_path = "/var/cache/app-install"
277     pathname = os.path.join(xapian_base_path, "xapian")
278     db = xapian.Database(pathname)
279
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
282     # with the UI)
283     #axi = xapian.Database("/var/lib/apt-xapian-index/index")
284     #db.add_database(axi)
285
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/")
289
290     # now the store
291     import apt
292     cache = apt.Cache(apt.progress.OpTextProgress())
293     filter = AppViewFilter(cache)
294     filter.set_supported_only(True)
295     store = AppStore(db, icons, filter=filter)
296
297     # gui
298     scroll = gtk.ScrolledWindow()
299     view = AppView(store)
300
301     entry = gtk.Entry()
302     entry.connect("changed", on_entry_changed, (db, view))
303
304     box = gtk.VBox()
305     box.pack_start(entry, expand=False)
306     box.pack_start(scroll)
307
308     win = gtk.Window()
309     scroll.add(view)
310     win.add(box)
311     win.set_size_request(400,400)
312     win.show_all()
313
314     gtk.main()