# -*- coding: utf-8 -*- # Copyright (C) 2009,2010 Canonical # # Authors: # Michael Vogt, Matthew McGowan # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; version 3. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from gi.repository import GObject from gi.repository import Gtk, GdkPixbuf import logging import os from gettext import gettext as _ from softwarecenter.enums import (Icons, XapianValues) from softwarecenter.utils import ( ExecutionTime, SimpleFileDownloader, split_icon_ext, capitalize_first_word, utf8, unescape, ) from softwarecenter.backend import get_install_backend from softwarecenter.backend.reviews import get_review_loader from softwarecenter.paths import SOFTWARE_CENTER_ICON_CACHE_DIR import softwarecenter.paths from softwarecenter.db.categories import ( category_subcat, category_cat, CategoriesParser) # global cache icons to speed up rendering _app_icon_cache = {} LOG = logging.getLogger(__name__) _FREE_AS_IN_BEER = ("0.00", "") class CategoryRowReference: """ A simple container for Category properties to be displayed in a AppListStore or AppTreeStore """ def __init__(self, untranslated_name, display_name, subcats, pkg_count): self.untranslated_name = untranslated_name self.display_name = GObject.markup_escape_text(utf8(display_name)) #self.subcategories = subcats self.pkg_count = pkg_count self.vis_count = pkg_count def __repr__(self): return "[CategoryRowReference: name=%s]" % self.untranslated_name class UncategorisedRowRef(CategoryRowReference): def __init__(self, untranslated_name=None, display_name=None, pkg_count=0): if untranslated_name is None: untranslated_name = 'Uncategorised' if display_name is None: display_name = _("Uncategorized") CategoryRowReference.__init__(self, untranslated_name, display_name, None, pkg_count) def __repr__(self): return "[UncategorizedRowReference: name=%s]" % self.untranslated_name class AppPropertiesHelper(GObject.GObject): """ Baseclass that contains common functions for our liststore/treestore, only useful for subclassing """ __gsignals__ = { "needs-refresh": (GObject.SignalFlags.RUN_LAST, None, (str, ), ), } def __init__(self, db, cache, icons, icon_size=48, global_icon_cache=False): GObject.GObject.__init__(self) self.db = db self.cache = cache # get all categories cat_parser = CategoriesParser(db) self.all_categories = cat_parser.parse_applications_menu( softwarecenter.paths.APP_INSTALL_PATH) # reviews stats loader self.review_loader = get_review_loader(cache, db) # icon jazz self.icons = icons self.icon_size = icon_size self._missing_icon = None # delay this until actually needed if global_icon_cache: self.icon_cache = _app_icon_cache else: self.icon_cache = {} def _on_image_download_complete( self, downloader, image_file_path, pkgname): LOG.debug("download for '%s' complete" % image_file_path) try: pb = GdkPixbuf.Pixbuf.new_from_file_at_size(image_file_path, self.icon_size, self.icon_size) except GObject.GError as e: LOG.warn("Failed to get image file for '%s' (%s)", image_file_path, e) return # replace the icon in the icon_cache now that we've got the real # one icon_file = split_icon_ext(os.path.basename(image_file_path)) self.icon_cache[icon_file] = pb self.emit("needs-refresh", pkgname) def _download_icon_and_show_when_ready(self, url, pkgname, icon_file_name): LOG.debug("did not find the icon locally, must download %s" % icon_file_name) if url is not None: icon_file_path = os.path.join(SOFTWARE_CENTER_ICON_CACHE_DIR, icon_file_name) image_downloader = SimpleFileDownloader() image_downloader.connect('file-download-complete', self._on_image_download_complete, pkgname) image_downloader.download_file(url, icon_file_path) @property def missing_icon(self): # cache the 'missing icon' used in treeviews for apps without an icon if self._missing_icon is None: self._missing_icon = self.icons.load_icon(Icons.MISSING_APP, self.icon_size, 0) return self._missing_icon def update_availability(self, doc): doc.available = None doc.installed = None doc.purchasable = None self.is_installed(doc) def is_available(self, doc): if doc.available is None: pkgname = self.get_pkgname(doc) doc.available = ( (pkgname in self.cache and self.cache[pkgname].candidate is not None) or self.is_purchasable(doc)) return doc.available def is_installed(self, doc): if doc.installed is None: pkgname = self.get_pkgname(doc) doc.installed = (self.is_available(doc) and pkgname in self.cache and self.cache[pkgname].is_installed) return doc.installed def is_purchasable(self, doc): if doc.purchasable is None: doc.purchasable = (doc.get_value(XapianValues.PRICE) not in _FREE_AS_IN_BEER) return doc.purchasable def get_pkgname(self, doc): return self.db.get_pkgname(doc) def get_application(self, doc): return self.db.get_application(doc) def get_appname(self, doc): app = self.db.get_application(doc) return app.get_display_name(self.db, doc) def get_markup(self, doc): app = self.db.get_application(doc) # the logic is that "apps" are displayed normally # but "packages" are displayed with their summary as name if app.appname: appname = self.get_appname(doc) summary = capitalize_first_word(self.db.get_summary(doc)) else: appname = capitalize_first_word(self.db.get_summary(doc)) summary = self.get_pkgname(doc) return "%s\n%s" % ( GObject.markup_escape_text(appname), GObject.markup_escape_text(summary)) def get_price(self, doc): return doc.get_value(XapianValues.PRICE) def get_icon(self, doc): try: full_icon_file_name = self.db.get_iconname(doc) icon_file_name = split_icon_ext(full_icon_file_name) if icon_file_name: icon_name = icon_file_name if icon_name in self.icon_cache: return self.icon_cache[icon_name] # icons.load_icon takes between 0.001 to 0.01s on my # machine, this is a significant burden because get_value # is called *a lot*. caching is the only option # look for the icon on the iconpath if self.icons.has_icon(icon_name): icon = self.icons.load_icon(icon_name, self.icon_size, 0) if icon: self.icon_cache[icon_name] = icon return icon elif self.db.get_icon_download_url(doc): url = self.db.get_icon_download_url(doc) self._download_icon_and_show_when_ready( url, self.get_pkgname(doc), full_icon_file_name) # display the missing icon while the real one downloads self.icon_cache[icon_name] = self.missing_icon except GObject.GError as e: LOG.debug("get_icon returned '%s'" % e) return self.missing_icon def get_review_stats(self, doc): return self.review_loader.get_review_stats(self.get_application(doc)) def get_transaction_progress(self, doc): pkgname = self.get_pkgname(doc) if pkgname in self.backend.pending_transactions: return self.backend.pending_transactions[pkgname].progress return -1 def _category_translate(self, catname): """ helper that will look into the categories we got from the parser and returns the translated name if it find it, otherwise it resorts to plain gettext """ # look into parsed categories that use .directory translation for cat in self.all_categories: if cat.untranslated_name == catname: return cat.name # try normal translation first translated_catname = _(catname) if translated_catname == catname: # if no normal translation is found, try to find a escaped # translation (LP: #872760) translated_catname = _(GObject.markup_escape_text(catname)) # the parent expect the string unescaped translated_catname = unescape(translated_catname) return translated_catname def get_categories(self, doc): categories = doc.get_value(XapianValues.CATEGORIES).split(';') or [] if categories and categories[0].startswith('DEPARTMENT:'): return _(categories[0].split('DEPARTMENT:')[1]) for key in category_subcat: if key in categories: visible_category = category_subcat[key].split(';')[1] return self._category_translate(visible_category) for key in category_cat: if key in categories: visible_category = category_cat[key] return self._category_translate(visible_category) if categories: return _('System') else: return '' def get_icon_at_size(self, doc, width, height): pixbuf = self.get_icon(doc) pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR) return pixbuf class AppGenericStore(AppPropertiesHelper): # column types COL_TYPES = (GObject.TYPE_PYOBJECT,) # column id COL_ROW_DATA = 0 # default icon size displayed in the treeview ICON_SIZE = 32 # the amount of items to initially lo LOAD_INITIAL = 75 def __init__(self, db, cache, icons, icon_size, global_icon_cache): AppPropertiesHelper.__init__(self, db, cache, icons, icon_size, global_icon_cache) # backend stuff self.backend = get_install_backend() self.backend.connect("transaction-progress-changed", self._on_transaction_progress_changed) self.backend.connect("transaction-started", self._on_transaction_started) self.backend.connect("transaction-finished", self._on_transaction_finished) # keep track of paths for transactions in progress self.transaction_path_map = {} # active row path self.active_row = None self._in_progress = False self._break = False # other stuff self.active = False # FIXME: port from @property def installable_apps(self): return [] @property def existing_apps(self): return [] def notify_action_request(self, doc, path): pkgname = str(self.get_pkgname(doc)) self.transaction_path_map[pkgname] = (path, self.get_iter(path)) def set_from_matches(self, matches): # stub raise NotImplementedError # the following methods ensure that the contents data is refreshed # whenever a transaction potentially changes it: def _on_transaction_started(self, backend, pkgname, appname, trans_id, trans_type): #~ self._refresh_transaction_map() pass def _on_transaction_progress_changed(self, backend, pkgname, progress): if pkgname in self.transaction_path_map: path, it = self.transaction_path_map[pkgname] self.row_changed(path, it) def _on_transaction_finished(self, backend, result): pkgname = str(result.pkgname) if pkgname in self.transaction_path_map: path, it = self.transaction_path_map[pkgname] doc = self.get_value(it, self.COL_ROW_DATA) self.update_availability(doc) self.row_changed(path, it) del self.transaction_path_map[pkgname] def buffer_icons(self): def buffer_icons(): #~ print "Buffering icons ..." #t0 = GObject.get_current_time() if self.current_matches is None: return False db = self.db.xapiandb for m in self.current_matches: doc = db.get_document(m.docid) # calling get_icon is enough to cache the icon self.get_icon(doc) while Gtk.events_pending(): Gtk.main_iteration() #~ import sys #~ t_lapsed = round(GObject.get_current_time() - t0, 3) #~ print "Appstore buffered icons in %s seconds" % t_lapsed #from softwarecenter.utils import get_nice_size #~ cache_size = get_nice_size(sys.getsizeof(_app_icon_cache)) #~ print "Number of icons in cache: %s consuming: %sb" % ( #~ len(_app_icon_cache), cache_size) return False # remove from sources on completion if self.current_matches is not None: GObject.idle_add(buffer_icons) def load_range(self, indices, step): # stub pass class AppListStore(Gtk.ListStore, AppGenericStore): """ use for flat applist views. for large lists this appends rows approx three times faster than the AppTreeStore equivalent """ __gsignals__ = { "appcount-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT, ), ), # meh, this is a signal from AppPropertiesHelper "needs-refresh": (GObject.SignalFlags.RUN_LAST, None, (str, ), ), } def __init__(self, db, cache, icons, icon_size=AppGenericStore.ICON_SIZE, global_icon_cache=True): AppGenericStore.__init__( self, db, cache, icons, icon_size, global_icon_cache) Gtk.ListStore.__init__(self) self.set_column_types(self.COL_TYPES) self.current_matches = None def set_from_matches(self, matches): """ set the content of the liststore based on a list of xapian.MSetItems """ LOG.debug("set_from_matches len(matches)='%s'" % len(matches)) self.current_matches = matches n_matches = len(matches) if n_matches == 0: return extent = min(self.LOAD_INITIAL, n_matches) with ExecutionTime("store.append_initial"): for doc in [m.document for m in matches][:extent]: doc.available = doc.installed = doc.purchasable = None self.append((doc,)) if n_matches == extent: return with ExecutionTime("store.append_placeholders"): for i in range(n_matches - extent): self.append() self.emit('appcount-changed', len(matches)) self.buffer_icons() def load_range(self, indices, step): LOG.debug("load_range: %s %s" % (indices, step)) db = self.db.xapiandb matches = self.current_matches n_matches = len(matches or []) start = indices[0] end = start + step if end >= n_matches: end = n_matches for i in range(start, end): try: row_content = self[(i,)][0] except IndexError as e: LOG.warn("failed to load rows: '%s'" % e) break if row_content: continue doc = db.get_document(matches[i].docid) doc.available = doc.installed = doc.purchasable = None self[(i,)][0] = doc def clear(self): # reset the tranaction map because it will now be invalid self.transaction_path_map = {} self.current_matches = None Gtk.ListStore.clear(self) class AppTreeStore(Gtk.TreeStore, AppGenericStore): """ A treestore based application model """ __gsignals__ = { # meh, this is a signal from AppPropertiesHelper "needs-refresh": (GObject.SignalFlags.RUN_LAST, None, (str, ), ), } def __init__(self, db, cache, icons, icon_size=AppGenericStore.ICON_SIZE, global_icon_cache=True): AppGenericStore.__init__( self, db, cache, icons, icon_size, global_icon_cache) Gtk.TreeStore.__init__(self) self.set_column_types(self.COL_TYPES) def set_documents(self, parent, documents): for doc in documents: doc.available = None doc.installed = doc.purchasable = None self.append(parent, (doc,)) self.transaction_path_map = {} def set_category_documents(self, cat, documents): category = CategoryRowReference(cat.untranslated_name, cat.name, cat.subcategories, len(documents)) it = self.append(None, (category,)) self.set_documents(it, documents) return it def set_nocategory_documents(self, documents, untranslated_name=None, display_name=None): category = UncategorisedRowRef(untranslated_name, display_name, len(documents)) it = self.append(None, (category,)) self.set_documents(it, documents) return it def clear(self): # reset the tranaction map because it will now be invalid self.transaction_path_map = {} Gtk.TreeStore.clear(self)