home *** CD-ROM | disk | FTP | other *** search
- # (c) 2005 Canonical, GPL
-
- import pygtk
- pygtk.require("2.0")
- import gtk
- import gtk.gdk
- import gobject
- import xdg.Menu
- import sys
- import os
- import gettext
- import common
-
- from warnings import warn
- from gettext import gettext as _
-
- from Util import *
-
- # possible filter states for the application list
- ( SHOW_ALL,
- SHOW_ALL_SUPPORTED,
- SHOW_ALL_FREE,
- SHOW_ONLY_MAIN,
- SHOW_ONLY_PROPRIETARY,
- SHOW_ONLY_THIRD_PARTY,
- ) = range(6)
-
- class MenuItem(object):
- " base class for a object in the menu "
- def __init__(self, name, iconname=None):
- # the name that is displayed
- self.name = name
- # the icon that is displayed
- self.iconname = iconname
- self.icontheme = None
- def __repr__(self):
- return "MenuItem: %s" % self.name
-
- class Category(MenuItem):
- """ represents a category """
- def __init__(self, parent, name, iconname=None):
- MenuItem.__init__(self, name, iconname)
- def initListStores(self, parent):
- self.icontheme = parent.icons
- # if that category has applications, add them to the
- # store here
- self.all_applications = gtk.ListStore(gobject.TYPE_STRING,
- gobject.TYPE_PYOBJECT,
- gobject.TYPE_INT)
- # set the visible filter
- self.filtered_applications = self.all_applications.filter_new()
- self.filtered_applications.set_visible_func(parent._visible_filter)
- self.applications = gtk.TreeModelSort(self.filtered_applications)
- #self.applications.set_sort_func(SORT_RANK, parent._ranking_sort_func)
-
- class Application(MenuItem):
- """ this class represents a application """
- def __init__(self, name, iconname=None):
- MenuItem.__init__(self, name, iconname)
- # the apt-pkg name that belongs to the app
- self.pkgname = None
- # the description
- self.description = ""
- # a html page
- self.html = None
- # mime type
- self.mime = None
- # exec
- self.execCmd = None
- # needsTerminal
- self.needsTerminal = False
- # we have it right now
- self.available = False
- # component the package is in (main, universe, multiverse, restricted)
- self.component = None
- # channel the pacakge is in (e.g. skype)
- self.channel = None
- # states
- self.isInstalled = False
- self.toInstall = False
- self.popcon = 1 # the raw popcon data
- self.rank = 1 # used by the ranking algo
- # licence and support
- self.free = False
- self.licenseUri = None
- self.supported = False
- self.thirdparty = False
- self.architectures = []
- # textual menu path
- self.menupath = ""
-
- class ApplicationMenu(object):
- """ this represents the application menu, the interessting bits are:
- - store that can be attached to a TreeView
- - pkg_to_app a dictionary that maps the apt pkgname to the application
- items
- """
-
- debug = 0
-
- def __init__(self, datadir, cachedir, cache, treeview_categories,
- treeview_packages, progress, filter=SHOW_ONLY_MAIN,
- dontPopulate=False):
- self.menudir = datadir+"/desktop"
- self.cache = cache
- self.treeview_categories = treeview_categories
- self.treeview_packages = treeview_packages
- self.popcon_max = 1
-
- # cache
- self.pickle = {}
-
- # icon theme
- self.icons = common.ToughIconTheme()
- self.icons.prepend_search_path(os.path.join(datadir, "icons"))
- # some icon-themes (kde) don't support this icon
- try:
- gtk.window_set_default_icon(self.icons.load_icon("gnome-settings-default-applications", 32, 0))
- except gobject.GError:
- pass
-
- # search
- self.searchTerms = []
- self.mimeSearch = None
-
- # properties for the view
- self.filter = filter
-
- # a dictonary that provides a mapping from a pkg to the
- # application names it provides
- self.pkg_to_app = {}
-
- # a set of seen desktop entries
- self.desktopEntriesSeen = set()
-
- # the categories
- self.real_categories_store = gtk.ListStore(gobject.TYPE_STRING,
- gobject.TYPE_PYOBJECT)
-
- if dontPopulate:
- return
-
- # populate the tree
- # use cached self.pickle (should be renamed to self.categories)
- # and cache self.pkgs_to_app
- if os.path.exists("%s/menu.p" % cachedir):
- print "using existing cache"
- import cPickle
- self.pickle = cPickle.load(open("%s/menu.p" % cachedir))
- else:
- print "no cache found"
- self.desktopEntriesSeen.clear()
- menu = xdg.Menu.parse(os.path.join(self.menudir, "applications.menu"))
- self._populateFromEntry(menu)
-
- # refresh based on the pickled information
- self.refresh(progress)
- self.store = self.real_categories_store
-
- # default is the tree
- self.treeview_categories.set_model(self.real_categories_store)
- self.treeview_packages.set_model(None)
-
-
- # helpers
- def _refilter(self):
- # we need to disconnect the model from the view when we
- # do a refilter, otherwise we get random crashes in the search
- # (to reproduce:
- # 1. open "accessability" 2. unselect "show unsupported"
- # 3. search for "apt" 4. turn "show unsupported" on/off -> BOOM
- model = self.treeview_packages.get_model()
-
- # save the cursor position (or rather, the name of the app selected)
- name = None
- (path, colum) = self.treeview_packages.get_cursor()
- if path:
- name = model.get_value(model.get_iter(path), COL_NAME)
- #print "found: %s (%s) " % (name, path)
-
- # this is the actual refiltering
- self.treeview_packages.set_model(None)
- if model != None:
- model.get_model().refilter()
- self.treeview_packages.set_model(model)
-
- # recalculate the width of the treeview to avoid useless
- # scrollbars
- self.treeview_packages.columns_autosize()
-
- # redo the cursor
- if name != None:
- for it in iterate_list_store(model, model.get_iter_first()):
- aname = model.get_value(it, COL_NAME)
- if name == aname:
- #print "selecting: %s (%s)" % (name, model.get_path(it))
- #self.treeview_packages.expand_to_path(model.get_path(it))
- self.treeview_packages.set_cursor(model.get_path(it))
- return
- elif len(model) > 0:
- self.treeview_packages.set_cursor(0)
-
- def _ranking_sort_func(self, model, iter1, iter2):
- """
- Sort by the search result rank
- """
- #print "_sort_func()"
- item1 = model.get_value(iter1, COL_ITEM)
- item2 = model.get_value(iter2, COL_ITEM)
- if item1 == None or item2 == None:
- return 0
- if item1.rank < item2.rank: return 1
- elif item1.rank > item2.rank: return -1
- else: return 0
-
- def _visible_filter(self, model, iter):
- item = model.get_value(iter, COL_ITEM)
- if item:
- # check for the various view settings
- if self.mimeSearch and not self.mimeSearch.approved(
- item.component, item.pkgname):
- return False
- if self.filter == SHOW_ONLY_MAIN and item.component != "main":
- return False
- if self.filter == SHOW_ALL_SUPPORTED and item.supported != True:
- return False
- if self.filter == SHOW_ALL_FREE and item.free == False:
- return False
- if self.filter == SHOW_ONLY_PROPRIETARY and item.free == True:
- return False
- if self.filter == SHOW_ONLY_THIRD_PARTY and item.thirdparty != True:
- return False
- # if we search, do the ranking updates
- if len(self.searchTerms) > 0:
- rank = self._filterAndRank(item)
- if rank == None:
- return False
- else:
- item.rank = rank
- return True
-
- def _filterAndRank(self, item):
- """
- Watch out, Google!
- """
- trigger = ""
- rank = item.popcon
-
- # special case the mime-search
- if self.mimeSearch:
- if self._mimeFilter(item):
- return rank
- else:
- return None
-
- # the normal case
- for term in self.searchTerms:
- hit = False
- if term == item.name.lower() or \
- term == item.pkgname.lower():
- # FIXME: we should probably not use a hardcoded number here
- # because the popcon numbers will change over time
- # so rather calculate it based on the available numbers
- rank += 1500
- hit = True
- if term in item.name.lower():
- rank += item.popcon * 3
- trigger += " name"
- hit = True
- if term in item.pkgname.lower():
- rank += item.popcon * 3
- trigger += " pkg_name"
- hit = True
- if self._mimeMatch(item, term, fuzzy=True):
- rank += item.popcon * 2
- trigger += " mime"
- hit = True
- if term in item.description.lower():
- rank += item.popcon
- trigger += " desc"
- hit = True
- if self.cache.has_key(item.pkgname) and \
- term in self.cache[item.pkgname].description.lower():
- rank += item.popcon
- trigger += " pkg_desc"
- hit = True
- if hit == False:
- return None
- #print "found %s (%s/%s): %s" % (item.name, item.popcon, rank, trigger)
- return rank
-
- def _mimeMatch(self, item, term, fuzzy=False):
- for re_pattern in item.mime:
- # mvo: we get a list of regexp from
- # pyxdg.DesktopEntry.getMimeType, but it does not
- # use any special pattern at all, so we use the plain
- # pattern (e.g. text/html, audio/mp3 here)
- pattern = re_pattern.pattern
- if fuzzy and term in pattern:
- return True
- elif not fuzzy and re_pattern.match(term):
- return True
- return False
-
- def _mimeFilter(self, item):
- for mime_type in self.searchTerms:
- if self._mimeMatch(item, mime_type):
- return True
- return False
-
- def doMimeSearch(self, mime_type, fuzzy=False):
- res = set()
- model = self.real_categories_store.get_value(self.all_category_iter, COL_ITEM).all_applications
- for it in iterate_list_store(model, model.get_iter_first()):
- item = model.get_value(it, COL_ITEM)
- for re_pattern in item.mime:
- # mvo: we get a list of regexp from
- # pyxdg.DesktopEntry.getMimeType, but it does not
- # use any special pattern at all, so we use the plain
- # pattern (e.g. text/html, audio/mp3 here)
- pattern = re_pattern.pattern
- if fuzzy and mime_type in pattern:
- res.add(item)
- elif not fuzzy and re_pattern.match(mime_type):
- res.add(item)
- return res
-
- def createMenuCache(self, targetdir):
- self.desktopEntriesSeen.clear()
- self.pkg_to_app.clear()
- menu = xdg.Menu.parse(os.path.abspath(os.path.join(self.menudir, "applications.menu")))
- self._populateFromEntry(menu)
- import pickle
- pickle.dump(self.pickle, open('%s/menu.p' % targetdir,'w'))
-
- def refresh(self, progress):
- self.real_categories_store.clear()
-
- # add "All" category
- self.all_category_iter = self.real_categories_store.append()
- item = Category(self, "<b>%s</b>" % _("All"), "distributor-logo")
- item.initListStores(self)
- self.real_categories_store.set(self.all_category_iter,
- COL_NAME, "<b>%s</b>" % _("All"),
- COL_ITEM, item)
-
- # now go for the categories
- i=0
- lenx=len(self.pickle.keys())
- keys = self.pickle.keys()
- keys.sort(cmp=lambda x,y: cmp(x.name.lower(), y.name.lower()))
- for category in keys:
- category.initListStores(self)
- self.real_categories_store.set(self.real_categories_store.append(),
- COL_NAME, category.name,
- COL_ITEM, category,)
- i += 1
- progress.update(i/float(lenx)*100.0)
- for item in self.pickle[category]:
- # get name/description again to make sure that i18n is honored
- item.name = xmlescape(item.desktop_entry.getName())
- item.description = xmlescape(item.desktop_entry.getComment())
- # kde uses generic name *grumpf*
- if not item.description:
- item.description = xmlescape(item.desktop_entry.get('GenericName'))
- item.icontheme = self.icons
-
- # add to category
- category.all_applications.set(category.all_applications.append(),
- COL_NAME, item.name,
- COL_ITEM, item,
- COL_POPCON, item.popcon)
- # add to all
- store = self.real_categories_store.get_value(self.all_category_iter, COL_ITEM).all_applications
- store.set(store.append(),
- COL_NAME, item.name,
- COL_ITEM, item,
- COL_POPCON, item.popcon)
-
- # do the popcon_max calculation
- if item.popcon > self.popcon_max:
- self.popcon_max = item.popcon
-
- # populate the pkg_to_app data structure
- pkgname = item.pkgname
- if self.pkg_to_app.has_key(pkgname):
- if not item.name in [pkg.name for pkg in self.pkg_to_app[pkgname]]:
- self.pkg_to_app[pkgname].append(item)
- else:
- self.pkg_to_app[pkgname] = [item]
-
- # update the installed information
- if self.cache.has_key(pkgname):
- item.isInstalled = self.cache[pkgname].isInstalled
- else:
- item.isInstalled = False
- item.toInstall = item.isInstalled
-
-
- def getChanges(self, get_paths=False):
- """ return the selected changes in the tree
- TODO: what is get_paths?
- """
- to_inst = set()
- to_rm = set()
- for (name, item) in self.store:
- for (name,item,popcon) in item.all_applications:
- if item.isInstalled and not item.toInstall:
- to_rm.add(item)
- if not item.isInstalled and item.toInstall:
- to_inst.add(item)
- #print "to_add: %s" % to_inst
- #print "to_rm: %s" % to_rm
- return (to_inst, to_rm)
-
- def isChanged(self):
- """ check if there are changes at all """
- for (cat_name, cat) in self.store:
- for (name,item,popcon) in cat.all_applications:
- if item.toInstall != item.isInstalled:
- return True
- return False
-
- def _populateFromEntry(self, node, parent = None, progress=None):
- #progress.update((len(self.real_categories_store)/float(progress.allItems))*100)
- # for some reason xdg hiddes some entries, but we don't like that
- for entry in node.getEntries(hidden=True):
- self._dbg(2, "entry: %s" % (entry))
- if isinstance(entry, xdg.Menu.Menu):
- # we found a toplevel menu
- name = xmlescape(entry.getName())
- self._dbg(1, "we have a sub-menu %s " % name)
- item = Category(self, name, entry.getIcon())
- #print "adding: %s" % name
- self.pickle[item] = []
- self._populateFromEntry(entry, item, progress=progress)
- elif isinstance(entry, xdg.Menu.MenuEntry):
- # more debug output
- self._dbg(3, node.getPath() + "/\t" + entry.DesktopFileID + "\t" + entry.DesktopEntry.getFileName())
-
- # we found a application
- name = xmlescape(entry.DesktopEntry.getName())
- self._dbg(1, "we have a application %s (%s) " % (name,entry.DesktopFileID))
- if name and entry.DesktopEntry.hasKey("X-AppInstall-Package"):
- self._dbg(2,"parent is %s" % parent.name)
-
- # check for duplicates, caused by e.g. scribus that has:
- # Categories=Application;Graphics;Qt;Office;
- # so it appears in Graphics and Office
- if name in self.desktopEntriesSeen:
- #print "already seen %s (%s)" % (name, entry)
- continue
- self.desktopEntriesSeen.add(name)
-
- item = Application(name)
- # save the desktop entry to get the translations back later
- item.desktop_entry = entry.DesktopEntry
- pkgname = entry.DesktopEntry.get("X-AppInstall-Package")
- item.pkgname = pkgname
- # figure component and support status
- item.component = entry.DesktopEntry.get("X-AppInstall-Section")
- supported = entry.DesktopEntry.get("X-AppInstall-Supported")
- if supported != "":
- item.supported = bool(supported)
- else:
- if item.component == "main" or \
- item.component == "restricted":
- item.supported = True
- # check for free software
- if item.component == "main" or item.component == "universe":
- item.free = True
- else:
- item.free = False
- # check for third party apps
- item.channel = entry.DesktopEntry.get("X-AppInstall-Channel")
- thirdparty = entry.DesktopEntry.get("X-AppInstall-Proprietary")
- if thirdparty != "":
- item.thirdparty = bool(thirdparty)
- item.licenseUri = entry.DesktopEntry.get("X-AppInstall-LicenseUri")
- if self.cache.has_key(item.pkgname):
- item.available = True
- else:
- item.available = False
- # Supported architectures
- archs = entry.DesktopEntry.get("X-AppInstall-Architectures", list=True)
- if archs:
- item.architectures.extend(archs)
- # Icon
- item.iconname = entry.DesktopEntry.get("X-AppInstall-Icon", "") or entry.DesktopEntry.getIcon()
- if self.cache.has_key(pkgname):
- item.isInstalled = self.cache[pkgname].isInstalled
- else:
- item.isInstalled = False
- item.toInstall = item.isInstalled
- item.mime = entry.DesktopEntry.getMimeType()
- # popcon data
- popcon_str = entry.DesktopEntry.get("X-AppInstall-Popcon")
- if popcon_str != "":
- popcon = int(popcon_str)
- item.popcon = popcon
- if popcon > self.popcon_max:
- self.popcon_max = popcon
-
- item.execCmd = entry.DesktopEntry.getExec()
- item.needsTerminal = entry.DesktopEntry.getTerminal()
- item.menupath = [_("Applications"),parent.name]
- #print item.menupath
- #store.set(store.append(),
- # COL_NAME, name,
- # COL_ITEM, item)
- self.pickle[parent].append(item)
-
- else:
- try:
- print "Got non-package menu entry %s" % entry
- except UnicodeEncodeError:
- pass
- elif isinstance(entry, xdg.Menu.Header):
- print "got header"
-
- def _dbg(self, level, msg):
- """Write debugging output to sys.stderr.
- """
- if level <= self.debug:
- print >> sys.stderr, msg
-
-
- if __name__ == "__main__":
- print "testing the menu"
-
- desktopdir = "/usr/share/app-install"
- from Util import MyCache
- cache = MyCache()
- treeview = gtk.TreeView()
- menu = ApplicationMenu(desktopdir, cache, treeview, treeview, apt.progress.OpProgress())
- #matches = menu.doMimeSearch("mp3",fuzzy=True)
- #print matches
- #matches = menu.doMimeSearch("audio/mp3")
- #print matches
-
-
-