def test_app_view(self): from softwarecenter.ui.gtk3.views.appview import get_test_window enquirer = AppEnquire(self.cache, self.db) enquirer.set_query(xapian.Query(""), sortmode=SortMethods.BY_CATALOGED_TIME, limit=10, nonblocking_load=False) # get test window win = get_test_window() appview = win.get_data("appview") # set matches appview.clear_model() appview.display_matches(enquirer.matches) do_events() # verify that the order is actually the correct one model = appview.tree_view.get_model() docs_in_model = [item[0] for item in model] docs_from_enquirer = [m.document for m in enquirer.matches] self.assertEqual(len(docs_in_model), len(docs_from_enquirer)) for i in range(len(docs_in_model)): self.assertEqual(self.db.get_pkgname(docs_in_model[i]), self.db.get_pkgname(docs_from_enquirer[i])) win.destroy()
def test_app_enquire(self): db = StoreDatabase(cache=self.cache) db.open() # test the AppEnquire engine enquirer = AppEnquire(self.cache, db) enquirer.set_query(xapian.Query("a"), nonblocking_load=False) self.assertTrue(len(enquirer.get_docids()) > 0)
def __init__(self, cache, db, icons, apps_filter, apps_limit=0, root_category=None): CategoriesView.__init__(self, cache, db, icons, apps_filter, apps_limit) # state self._built = False # data self.root_category = root_category self.enquire = AppEnquire(self.cache, self.db) self.properties_helper = AppPropertiesHelper(self.db, self.cache, self.icons) # sections self.current_category = None self.departments = None self.top_rated = None self.recommended_for_you_in_cat = None self.appcount = None # widgetry self.vbox.set_margin_left(StockEms.MEDIUM - 2) self.vbox.set_margin_right(StockEms.MEDIUM - 2) self.vbox.set_margin_top(StockEms.MEDIUM) return
def test_app_store(self): # get a enquire object enquirer = AppEnquire(self.cache, self.db) enquirer.set_query(xapian.Query("")) # get a AppListStore and run functions on it model = AppListStore(self.db, self.cache, self.icons) # test if set from matches works self.assertEqual(len(model), 0) model.set_from_matches(enquirer.matches) self.assertTrue(len(model) > 0) # ensure the first row has a xapian doc type self.assertEqual(type(model[0][0]), xapian.Document) # lazy loading of the docs self.assertEqual(model[100][0], None) # test the load range stuff model.load_range(indices=[100], step=15) self.assertEqual(type(model[100][0]), xapian.Document) # ensure buffer_icons works and loads stuff into the cache model.buffer_icons() self.assertEqual(len(model.icon_cache), 0) while Gtk.events_pending(): Gtk.main_iteration() self.assertTrue(len(model.icon_cache) > 0) # ensure clear works model.clear() self.assertEqual(model.current_matches, None)
def setUp(self): # create a fake database to simualte a run of software-center-agent # create a StoreDatabase and add our other db self.db = get_test_db() self.db.add_database(xapian.Database(TEST_DB)) self.db.open(use_axi=True) self.enquire = AppEnquire(self.db._aptcache, self.db)
def get_test_window(): from softwarecenter.testutils import ( get_test_db, get_test_pkg_info, get_test_gtk3_icon_cache) from softwarecenter.db.enquire import AppEnquire from softwarecenter.ui.gtk3.models.appstore2 import AppListStore import xapian db = get_test_db() cache = get_test_pkg_info() icons = get_test_gtk3_icon_cache() # create the view appview = AppView(db, cache, icons, show_ratings=True) liststore = AppListStore(db, cache, icons) appview.set_model(liststore) # do a simple query and display that enquirer = AppEnquire(cache, db) enquirer.set_query(xapian.Query(""), sortmode=SortMethods.BY_CATALOGED_TIME, limit=20, nonblocking_load=False) appview.display_matches(enquirer.matches) # and put it in the window win = Gtk.Window() win.add(appview) win.set_data("appview", appview) win.connect("destroy", lambda x: Gtk.main_quit()) win.set_size_request(600, 400) win.show_all() return win
def get_test_enquirer_matches(db, query=None, limit=20, sortmode=0): if query is None: query = xapian.Query("") enquirer = AppEnquire(db._aptcache, db) enquirer.set_query(query, sortmode=sortmode, limit=limit, nonblocking_load=False) return enquirer.matches
def _update_appcount(self): enq = AppEnquire(self.cache, self.db) distro = get_distro() if get_global_filter().supported_only: query = distro.get_supported_query() else: query = xapian.Query('') length = enq.get_estimated_matches_count(query) text = gettext.ngettext("%(amount)s item", "%(amount)s items", length ) % {'amount': length} self.appcount.set_text(text)
def __init__(self, cache, db, distro, icons, datadir, show_ratings=True): Gtk.VBox.__init__(self) BasePane.__init__(self) # other classes we need self.enquirer = AppEnquire(cache, db) self._query_complete_handler = self.enquirer.connect( "query-complete", self.on_query_complete) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.show_ratings = show_ratings self.backend = get_install_backend() self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE # refreshes can happen out-of-bound so we need to be sure # that we only set the new model (when its available) if # the refresh_seq_nr of the ready model matches that of the # request (e.g. people click on ubuntu channel, get impatient, click # on partner channel) self.refresh_seq_nr = 0 # this should be initialized self.apps_search_term = "" # Create the basic frame for the common view self.state = DisplayState() vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.back_forward = vm.get_global_backforward() # a notebook below self.notebook = Gtk.Notebook() if not SOFTWARE_CENTER_DEBUG_TABS: self.notebook.set_show_tabs(False) self.notebook.set_show_border(False) # make a spinner view to display while the applist is loading self.spinner_notebook = SpinnerNotebook(self.notebook) self.pack_start(self.spinner_notebook, True, True, 0) # add a bar at the bottom (hidden by default) for contextual actions self.action_bar = ActionBar() self.pack_start(self.action_bar, False, True, 0) # cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH) # views to be created in init_view self.app_view = None self.app_details_view = None
class DBSearchTestCase(unittest.TestCase): APP_INFO_JSON = """ [ { "application_name": "The apt", "package_name": "apt", "description": "meep" } ] """ @classmethod def setUpClass(cls): cache = get_pkg_info() cache.open() db = xapian.WritableDatabase(TEST_DB, xapian.DB_CREATE_OR_OVERWRITE) update_from_json_string(db, cache, cls.APP_INFO_JSON, origin="local") db.close() def setUp(self): # create a fake database to simualte a run of software-center-agent # create a StoreDatabase and add our other db self.db = get_test_db() self.db.add_database(xapian.Database(TEST_DB)) self.db.open(use_axi=True) self.enquire = AppEnquire(self.db._aptcache, self.db) def test_search_app_pkgname_duplication_lp891613(self): # simulate a pkg "apt" that is both in the agent and in the x-a-i db search_term = "apt" search_query = self.db.get_query_list_from_search_entry(search_term) self.enquire.set_query(search_query, nonblocking_load=False) self.assertTrue(len(self.enquire._matches) > 2) for m in self.enquire._matches: doc = m.document # ensure that all hits are "apps" and do not come from a-x-i self.assertNotEqual(doc.get_value(XapianValues.PKGNAME), "") def test_search_custom_pkgs_list_lp1043159(self): # simulate a pkg "apt" that is both in the agent and in the x-a-i db pkgs = ["apt", "gedit"] search_query = self.db.get_query_for_pkgnames(pkgs) self.enquire.set_query( search_query, # custom package lists are always in this mode nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False, ) self.assertEqual(len(self.enquire._matches), 2)
class DBSearchTestCase(unittest.TestCase): APP_INFO_JSON = """ [ { "application_name": "The apt", "package_name": "apt", "description": "meep" } ] """ @classmethod def setUpClass(cls): cache = get_pkg_info() cache.open() db = xapian.WritableDatabase(TEST_DB, xapian.DB_CREATE_OR_OVERWRITE) update_from_json_string(db, cache, cls.APP_INFO_JSON, origin="local") db.close() def setUp(self): # create a fake database to simualte a run of software-center-agent # create a StoreDatabase and add our other db self.db = get_test_db() self.db.add_database(xapian.Database(TEST_DB)) self.db.open(use_axi=True) self.enquire = AppEnquire(self.db._aptcache, self.db) def test_search_app_pkgname_duplication_lp891613(self): # simulate a pkg "apt" that is both in the agent and in the x-a-i db search_term = "apt" search_query = self.db.get_query_list_from_search_entry(search_term) self.enquire.set_query(search_query, nonblocking_load=False) self.assertTrue(len(self.enquire._matches) > 2) for m in self.enquire._matches: doc = m.document # ensure that all hits are "apps" and do not come from a-x-i self.assertNotEqual(doc.get_value(XapianValues.PKGNAME), "") def test_search_custom_pkgs_list_lp1043159(self): # simulate a pkg "apt" that is both in the agent and in the x-a-i db pkgs = ["apt", "gedit"] search_query = self.db.get_query_for_pkgnames(pkgs) self.enquire.set_query( search_query, # custom package lists are always in this mode nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) self.assertEqual(len(self.enquire._matches), 2)
def get_documents(self, db): """ return the database docids for the given category """ enq = AppEnquire(db._aptcache, db) app_filter = AppFilter(db, db._aptcache) if "available-only" in self.flags: app_filter.set_available_only(True) if "not-installed-only" in self.flags: app_filter.set_not_installed_only(True) enq.set_query(self.query, limit=self.item_limit, filter=app_filter, sortmode=self.sortmode, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) return enq.get_documents()
def get_test_window(): import softwarecenter.log softwarecenter.log.root.setLevel(level=logging.DEBUG) softwarecenter.log.add_filters_from_string("performance") fmt = logging.Formatter("%(name)s - %(message)s", None) softwarecenter.log.handler.setFormatter(fmt) from softwarecenter.paths import XAPIAN_BASE_PATH xapian_base_path = XAPIAN_BASE_PATH pathname = os.path.join(xapian_base_path, "xapian") # the store from softwarecenter.db.pkginfo import get_pkg_info cache = get_pkg_info() cache.open() # the db from softwarecenter.db.database import StoreDatabase db = StoreDatabase(pathname, cache) db.open() # additional icons come from app-install-data icons = Gtk.IconTheme.get_default() icons.prepend_search_path("/usr/share/app-install/icons/") icons.prepend_search_path("/usr/share/software-center/icons/") # create a filter from softwarecenter.db.appfilter import AppFilter filter = AppFilter(db, cache) filter.set_supported_only(False) filter.set_installed_only(True) # appview from softwarecenter.ui.gtk3.models.appstore2 import AppListStore from softwarecenter.db.enquire import AppEnquire enquirer = AppEnquire(cache, db) store = AppListStore(db, cache, icons) from softwarecenter.ui.gtk3.views.appview import AppView view = AppView(db, cache, icons, show_ratings=True) view.set_model(store) entry = Gtk.Entry() entry.stamp = 0 entry.connect("changed", on_entry_changed, (view, enquirer)) entry.set_text("gtk3") scroll = Gtk.ScrolledWindow() box = Gtk.VBox() box.pack_start(entry, False, True, 0) box.pack_start(scroll, True, True, 0) win = Gtk.Window() win.connect("destroy", lambda x: Gtk.main_quit()) scroll.add(view) win.add(box) win.set_size_request(600, 400) win.show_all() return win
def _ceibal_get_docs(self): """ return the database docids for the given category """ enq = AppEnquire(self.db._aptcache, self.db) app_filter = AppFilter(self.db, self.db._aptcache) app_filter.set_available_only(True) #app_filter.set_not_installed_only(True) p = "http://apt.ceibal.edu.uy/recommendations/list.json" data = json.load(urllib2.urlopen(p)) query = get_query_for_pkgnames(data['packages']) enq.set_query(query, limit=20, filter=app_filter, sortmode=0, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) return enq.get_documents()
def _update_appcount(self): enq = AppEnquire(self.cache, self.db) distro = get_distro() if get_global_filter().supported_only: query = distro.get_supported_query() else: query = xapian.Query('') enq.set_query(query, limit=0, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=True) length = len(enq.matches) text = gettext.ngettext("%(amount)s item", "%(amount)s items", length ) % { 'amount' : length, } self.appcount.set_text(text)
def _update_appcount(self): enq = AppEnquire(self.cache, self.db) distro = get_distro() if get_global_filter().supported_only: query = distro.get_supported_query() else: query = xapian.Query('') enq.set_query(query, limit=0, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=True) length = len(enq.matches) text = gettext.ngettext("%(amount)s item", "%(amount)s items", length) % { 'amount': length, } self.appcount.set_text(text)
def _get_toprated_category_content(self): toprated_cat = get_category_by_name(self.categories, u"Top Rated") # unstranslated name if toprated_cat is None: LOG.warn("No 'toprated' category found!!") return None, [] enq = AppEnquire(self.cache, self.db) app_filter = AppFilter(self.db, self.cache) enq.set_query(toprated_cat.query, limit=TOP_RATED_CAROUSEL_LIMIT, sortmode=toprated_cat.sortmode, filter=app_filter, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) if not hasattr(self, "helper"): self.helper = AppPropertiesHelper(self.db, self.cache, self.icons) return toprated_cat, enq.get_documents()
def test_app_enquire(self): db = get_test_db() cache = get_test_pkg_info() xfilter = AppFilter(cache, db) enquirer = AppEnquire(cache, db) terms = [ "app", "this", "the", "that", "foo", "tool", "game", "graphic", "ubuntu", "debian", "gtk", "this", "bar", "baz"] # run a bunch of the querries in parallel for nonblocking in [False, True]: for i in range(10): for term in terms: enquirer.set_query( search_query=xapian.Query(term), limit=0, filter=xfilter, nonblocking_load=nonblocking) # give the threads a bit of time time.sleep(5)
def test_app_enquire(self): db = get_test_db() cache = get_test_pkg_info() xfilter = AppFilter(cache, db) enquirer = AppEnquire(cache, db) terms = [ "app", "this", "the", "that", "foo", "tool", "game", "graphic", "ubuntu", "debian", "gtk", "this", "bar", "baz" ] # run a bunch of the querries in parallel for nonblocking in [False, True]: for i in range(10): for term in terms: enquirer.set_query(search_query=xapian.Query(term), limit=0, filter=xfilter, nonblocking_load=nonblocking) # give the threads a bit of time time.sleep(5)
def _get_new_category_content(self): whatsnew_cat = get_category_by_name( self.categories, u"What\u2019s New") # unstranslated name if whatsnew_cat is None: LOG.warn("No 'new' category found!!") return None, [] enq = AppEnquire(self.cache, self.db) app_filter = AppFilter(self.db, self.cache) app_filter.set_available_only(True) app_filter.set_not_installed_only(True) enq.set_query(whatsnew_cat.query, limit=8, filter=app_filter, sortmode=SortMethods.BY_CATALOGED_TIME, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) if not hasattr(self, "helper"): self.helper = AppPropertiesHelper(self.db, self.cache, self.icons) return whatsnew_cat, enq.get_documents()
def __init__(self, cache, db, distro, icons, datadir, show_ratings=True): Gtk.VBox.__init__(self) BasePane.__init__(self) # other classes we need self.enquirer = AppEnquire(cache, db) self._query_complete_handler = self.enquirer.connect( "query-complete", self.on_query_complete) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.show_ratings = show_ratings self.backend = get_install_backend() self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE # refreshes can happen out-of-bound so we need to be sure # that we only set the new model (when its available) if # the refresh_seq_nr of the ready model matches that of the # request (e.g. people click on ubuntu channel, get impatient, click # on partner channel) self.refresh_seq_nr = 0 # this should be initialized self.apps_search_term = "" # Create the basic frame for the common view self.state = DisplayState() vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.back_forward = vm.get_global_backforward() # a notebook below self.notebook = Gtk.Notebook() if not "SOFTWARE_CENTER_DEBUG_TABS" in os.environ: self.notebook.set_show_tabs(False) self.notebook.set_show_border(False) # make a spinner view to display while the applist is loading self.spinner_notebook = SpinnerNotebook(self.notebook) self.pack_start(self.spinner_notebook, True, True, 0) # add a bar at the bottom (hidden by default) for contextual actions self.action_bar = ActionBar() self.pack_start(self.action_bar, False, True, 0) # cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH) # views to be created in init_view self.app_view = None self.app_details_view = None
def test_app_view(self): enquirer = AppEnquire(self.cache, self.db) enquirer.set_query(xapian.Query(""), sortmode=SortMethods.BY_CATALOGED_TIME, limit=10, nonblocking_load=False) # get test window win = get_test_window_appview() self.addCleanup(win.destroy) appview = win.get_data("appview") # set matches appview.clear_model() appview.display_matches(enquirer.matches) do_events() # verify that the order is actually the correct one model = appview.tree_view.get_model() docs_in_model = [item[0] for item in model] docs_from_enquirer = [m.document for m in enquirer.matches] self.assertEqual(len(docs_in_model), len(docs_from_enquirer)) for i in range(len(docs_in_model)): self.assertEqual(self.db.get_pkgname(docs_in_model[i]), self.db.get_pkgname(docs_from_enquirer[i]))
def _get_new_category_content(self): whatsnew_cat = get_category_by_name(self.categories, u"What\u2019s New") # unstranslated name if whatsnew_cat is None: LOG.warn("No 'new' category found!!") return None, [] enq = AppEnquire(self.cache, self.db) app_filter = AppFilter(self.db, self.cache) app_filter.set_available_only(True) app_filter.set_not_installed_only(True) enq.set_query(whatsnew_cat.query, limit=8, filter=app_filter, sortmode=SortMethods.BY_CATALOGED_TIME, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) if not hasattr(self, "helper"): self.helper = AppPropertiesHelper(self.db, self.cache, self.icons) return whatsnew_cat, enq.get_documents()
def get_test_window(): import softwarecenter.log softwarecenter.log.root.setLevel(level=logging.DEBUG) softwarecenter.log.add_filters_from_string("performance") fmt = logging.Formatter("%(name)s - %(message)s", None) softwarecenter.log.handler.setFormatter(fmt) from softwarecenter.testutils import (get_test_db, get_test_pkg_info, get_test_gtk3_icon_cache) from softwarecenter.ui.gtk3.models.appstore2 import AppListStore db = get_test_db() cache = get_test_pkg_info() icons = get_test_gtk3_icon_cache() # create a filter from softwarecenter.db.appfilter import AppFilter filter = AppFilter(db, cache) filter.set_supported_only(False) filter.set_installed_only(True) # appview from softwarecenter.db.enquire import AppEnquire enquirer = AppEnquire(cache, db) store = AppListStore(db, cache, icons) from softwarecenter.ui.gtk3.views.appview import AppView view = AppView(db, cache, icons, show_ratings=True) view.set_model(store) entry = Gtk.Entry() entry.stamp = 0 entry.connect("changed", on_entry_changed, (view, enquirer)) box = Gtk.VBox() box.pack_start(entry, False, True, 0) box.pack_start(view, True, True, 0) win = Gtk.Window() win.set_data("appview", view) win.set_data("entry", entry) win.connect("destroy", lambda x: Gtk.main_quit()) win.add(box) win.set_size_request(600, 400) win.show_all() return win
def __init__(self, datadir, desktopdir, cache, db, icons, apps_filter, apps_limit=0, root_category=None): CategoriesViewGtk.__init__(self, datadir, desktopdir, cache, db, icons, apps_filter, apps_limit) # state self._built = False # data self.root_category = root_category self.enquire = AppEnquire(self.cache, self.db) self.properties_helper = AppPropertiesHelper( self.db, self.cache, self.icons) # sections self.current_category = None self.departments = None self.top_rated = None self.recommended_for_you_in_cat = None self.appcount = None # widgetry self.vbox.set_margin_left(StockEms.MEDIUM - 2) self.vbox.set_margin_right(StockEms.MEDIUM - 2) self.vbox.set_margin_top(StockEms.MEDIUM) return
class SubCategoryView(CategoriesView): def __init__(self, cache, db, icons, apps_filter, apps_limit=0, root_category=None): CategoriesView.__init__(self, cache, db, icons, apps_filter, apps_limit) # state self._built = False # data self.root_category = root_category self.enquire = AppEnquire(self.cache, self.db) self.properties_helper = AppPropertiesHelper(self.db, self.cache, self.icons) # sections self.current_category = None self.departments = None self.top_rated = None self.recommended_for_you_in_cat = None self.appcount = None # widgetry self.vbox.set_margin_left(StockEms.MEDIUM - 2) self.vbox.set_margin_right(StockEms.MEDIUM - 2) self.vbox.set_margin_top(StockEms.MEDIUM) return def _get_sub_top_rated_content(self, category): app_filter = AppFilter(self.db, self.cache) self.enquire.set_query(category.query, limit=TOP_RATED_CAROUSEL_LIMIT, sortmode=SortMethods.BY_TOP_RATED, filter=app_filter, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) return self.enquire.get_documents() @wait_for_apt_cache_ready # be consistent with new apps def _update_sub_top_rated_content(self, category): self.top_rated.remove_all() # FIXME: should this be m = "%s %s" % (_(gettext text), header text) ?? # TRANSLATORS: %s is a category name, like Internet or Development # Tools m = _('Top Rated %(category)s') % { 'category': GLib.markup_escape_text(self.header) } self.top_rated_frame.set_header_label(m) docs = self._get_sub_top_rated_content(category) self.top_rated.add_tiles(self.properties_helper, docs, TOP_RATED_CAROUSEL_LIMIT) return def _append_sub_top_rated(self): self.top_rated = TileGrid() self.top_rated.connect("application-activated", self.on_application_activated) self.top_rated.set_row_spacing(6) self.top_rated.set_column_spacing(6) self.top_rated_frame = FramedHeaderBox() self.top_rated_frame.pack_start(self.top_rated, True, True, 0) self.vbox.pack_start(self.top_rated_frame, False, True, 0) return def _update_recommended_for_you_in_cat_content(self, category): if (self.recommended_for_you_in_cat and self.recommended_for_you_in_cat.get_parent()): self.recommended_for_you_in_cat.disconnect_by_func( self.on_application_activated) self.vbox.remove(self.recommended_for_you_in_cat) self.recommended_for_you_in_cat = RecommendationsPanelCategory( self.db, self.properties_helper, category) self.recommended_for_you_in_cat.connect("application-activated", self.on_application_activated) self.recommended_for_you_in_cat.connect('more-button-clicked', self.on_category_clicked) # only show the panel in the categories view when the user # is opted in to the recommender service # FIXME: this is needed vs. a simple hide() on the widget because # we do a show_all on the view if self.recommended_for_you_in_cat.recommender_agent.is_opted_in(): self.vbox.pack_start(self.recommended_for_you_in_cat, False, False, 0) def _update_subcat_departments(self, category, num_items): self.departments.remove_all() # set the subcat header m = "<b><big>%s</big></b>" self.subcat_label.set_markup(m % GLib.markup_escape_text(self.header)) # sort Category.name's alphabetically sorted_cats = categories_sorted_by_name(self.categories) enquire = xapian.Enquire(self.db.xapiandb) app_filter = AppFilter(self.db, self.cache) distro = get_distro() supported_only = get_global_filter().supported_only for cat in sorted_cats: # add the subcategory if and only if it is non-empty if supported_only: enquire.set_query( xapian.Query(xapian.Query.OP_AND, cat.query, distro.get_supported_query())) else: enquire.set_query(cat.query) if len(enquire.get_mset(0, 1)): tile = CategoryTile(cat.name, cat.iconname) tile.connect('clicked', self.on_category_clicked, cat) self.departments.add_child(tile) # partially work around a (quite rare) corner case if num_items == 0: enquire.set_query( xapian.Query(xapian.Query.OP_AND, category.query, xapian.Query("ATapplication"))) # assuming that we only want apps is not always correct ^^^ tmp_matches = enquire.get_mset(0, len(self.db), None, app_filter) num_items = tmp_matches.get_matches_estimated() # append an additional button to show all of the items in the category all_cat = Category("All", _("All"), "category-show-all", category.query) name = GLib.markup_escape_text('%s %s' % (_("All"), num_items)) tile = CategoryTile(name, "category-show-all") tile.connect('clicked', self.on_category_clicked, all_cat) self.departments.add_child(tile) self.departments.queue_draw() return num_items def _append_subcat_departments(self): self.subcat_label = Gtk.Label() self.subcat_label.set_alignment(0, 0.5) self.departments = TileGrid(paint_grid_pattern=False) self.departments.set_row_spacing(StockEms.SMALL) self.departments.set_column_spacing(StockEms.SMALL) self.departments_frame = FramedBox(spacing=StockEms.MEDIUM, padding=StockEms.MEDIUM) # set x/y-alignment and x/y-expand self.departments_frame.set(0.5, 0.0, 1.0, 1.0) self.departments_frame.pack_start(self.subcat_label, False, False, 0) self.departments_frame.pack_start(self.departments, True, True, 0) # append the departments section to the page self.vbox.pack_start(self.departments_frame, False, True, 0) return def _update_appcount(self, appcount): text = gettext.ngettext("%(amount)s item available", "%(amount)s items available", appcount) % { 'amount': appcount } self.appcount.set_text(text) return def _append_appcount(self): self.appcount = Gtk.Label() self.appcount.set_alignment(0.5, 0.5) self.appcount.set_margin_top(1) self.appcount.set_margin_bottom(4) self.vbox.pack_end(self.appcount, False, False, 0) return def _build_subcat_view(self): # these methods add sections to the page # changing order of methods changes order that they appear in the page self._append_subcat_departments() self._append_sub_top_rated() # NOTE that the recommended for you in category view is built and added # in the _update_recommended_for_you_in_cat method (and so is not # needed here) self._append_appcount() self._built = True return def _update_subcat_view(self, category, num_items=0): num_items = self._update_subcat_departments(category, num_items) self._update_sub_top_rated_content(category) self._update_recommended_for_you_in_cat_content(category) self._update_appcount(num_items) self.show_all() return def set_subcategory(self, root_category, num_items=0): # nothing to do if (root_category is None or self.categories == root_category.subcategories): return self._set_subcategory(root_category, num_items) def _set_subcategory(self, root_category, num_items): self.current_category = root_category self.header = root_category.name self.categories = root_category.subcategories if not self._built: self._build_subcat_view() self._update_subcat_view(root_category, num_items) GLib.idle_add(self.queue_draw) return def refresh_apps(self): supported_only = get_global_filter().supported_only if (self.current_category is None or self._supported_only == supported_only): return self._supported_only = supported_only if not self._built: self._build_subcat_view() self._update_subcat_view(self.current_category) GLib.idle_add(self.queue_draw) return
class SoftwarePane(Gtk.VBox, BasePane): """ Common base class for AvailablePane and InstalledPane""" class Pages: NAMES = ('appview', 'details', 'spinner') APPVIEW = 0 DETAILS = 1 SPINNER = 2 __gsignals__ = { "app-list-changed": ( GObject.SignalFlags.RUN_LAST, None, (int, ), ), } PADDING = 6 def __init__(self, cache, db, distro, icons, datadir, show_ratings=True): Gtk.VBox.__init__(self) BasePane.__init__(self) # other classes we need self.enquirer = AppEnquire(cache, db) self._query_complete_handler = self.enquirer.connect( "query-complete", self.on_query_complete) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.show_ratings = show_ratings self.backend = get_install_backend() self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE # refreshes can happen out-of-bound so we need to be sure # that we only set the new model (when its available) if # the refresh_seq_nr of the ready model matches that of the # request (e.g. people click on ubuntu channel, get impatient, click # on partner channel) self.refresh_seq_nr = 0 # this should be initialized self.apps_search_term = "" # Create the basic frame for the common view self.state = DisplayState() vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.back_forward = vm.get_global_backforward() # a notebook below self.notebook = Gtk.Notebook() if not SOFTWARE_CENTER_DEBUG_TABS: self.notebook.set_show_tabs(False) self.notebook.set_show_border(False) # make a spinner view to display while the applist is loading self.spinner_notebook = SpinnerNotebook(self.notebook) self.pack_start(self.spinner_notebook, True, True, 0) # add a bar at the bottom (hidden by default) for contextual actions self.action_bar = ActionBar() self.pack_start(self.action_bar, False, True, 0) # cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH) # views to be created in init_view self.app_view = None self.app_details_view = None def init_view(self): """ Initialize those UI components that are common to all subclasses of SoftwarePane. Note that this method is intended to be called by the subclass itself at the start of its own init_view() implementation. """ # common UI elements (applist and appdetails) # its the job of the Child class to put it into a good location # list self.box_app_list = Gtk.VBox() # search aid self.search_aid = SearchAid(self) self.box_app_list.pack_start(self.search_aid, False, False, 0) self.app_view = AppView(self.db, self.cache, self.icons, self.show_ratings) self.app_view.connect("sort-method-changed", self.on_app_view_sort_method_changed) self.init_atk_name(self.app_view, "app_view") self.box_app_list.pack_start(self.app_view, True, True, 0) self.app_view.connect("application-selected", self.on_application_selected) self.app_view.connect("application-activated", self.on_application_activated) # details self.scroll_details = Gtk.ScrolledWindow() self.scroll_details.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.app_details_view = AppDetailsView(self.db, self.distro, self.icons, self.cache, self.datadir) self.app_details_view.connect("different-application-selected", self.on_application_activated) self.scroll_details.add(self.app_details_view) # when the cache changes, refresh the app list self.cache.connect("cache-ready", self.on_cache_ready) # connect signals self.connect("app-list-changed", self.on_app_list_changed) # db reopen if self.db: self.db.connect("reopen", self.on_db_reopen) def init_atk_name(self, widget, name): """ init the atk name for a given gtk widget based on parent-pane and variable name (used for the mago tests) """ name = self.__class__.__name__ + "." + name Atk.Object.set_name(widget.get_accessible(), name) def on_cache_ready(self, cache): " refresh the application list when the cache is re-opened " LOG.debug("on_cache_ready") @wait_for_apt_cache_ready def on_application_activated(self, appview, app): """callback when an app is clicked""" LOG.debug("on_application_activated: '%s'" % app) self.state.application = app vm = get_viewmanager() vm.display_page(self, SoftwarePane.Pages.DETAILS, self.state, self.display_details_page) def show_app(self, app): self.on_application_activated(None, app) def on_nav_back_clicked(self, widget): vm = get_viewmanager() vm.nav_back() def on_nav_forward_clicked(self, widget): vm = get_viewmanager() vm.nav_forward() def on_query_complete(self, enquirer): self.emit("app-list-changed", len(enquirer.matches)) self.app_view.display_matches(enquirer.matches, self._is_in_search_mode()) self.hide_appview_spinner() def on_app_view_sort_method_changed(self, app_view, combo): if app_view.get_sort_mode() == self.enquirer.sortmode: return self.show_appview_spinner() app_view.clear_model() query = self.get_query() self._refresh_apps_with_apt_cache(query) def _is_in_search_mode(self): return (self.state.search_term and len(self.state.search_term) >= 2) def show_appview_spinner(self): """ display the spinner in the appview panel """ LOG.debug("show_appview_spinner") # FIXME: totally the wrong place! if not self.state.search_term: self.action_bar.clear() self.spinner_notebook.show_spinner() def hide_appview_spinner(self): """ hide the spinner and display the appview in the panel """ LOG.debug("hide_appview_spinner") self.spinner_notebook.hide_spinner() def set_section(self, section): self.section = section self.app_details_view.set_section(section) def section_sync(self): self.app_details_view.set_section(self.section) def on_app_list_changed(self, pane, length): """internal helper that keeps the the action bar up-to-date by keeping track of the app-list-changed signals """ self.show_nonapps_if_required(len(self.enquirer.matches)) self.search_aid.update_search_help(self.state) def hide_nonapps(self): """ hide non-applications control in the action bar """ self.action_bar.unset_label() def show_nonapps_if_required(self, length): """ update the state of the show/hide non-applications control in the action_bar """ enquirer = self.enquirer n_apps = enquirer.nr_apps n_pkgs = enquirer.nr_pkgs # calculate the number of apps/pkgs if enquirer.limit > 0 and enquirer.limit < n_pkgs: n_apps = min(enquirer.limit, n_apps) n_pkgs = min(enquirer.limit - n_apps, n_pkgs) if not (n_apps and n_pkgs): self.hide_nonapps() return LOG.debug("nonapps_visible value=%s (always visible: %s)" % (self.nonapps_visible, self.nonapps_visible == NonAppVisibility.ALWAYS_VISIBLE)) self.action_bar.unset_label() if self.nonapps_visible == NonAppVisibility.ALWAYS_VISIBLE: LOG.debug('non-apps-ALWAYS-visible') # TRANSLATORS: the text inbetween the underscores acts as a link # In most/all languages you will want the whole string as a link label = gettext.ngettext("_Hide %(amount)i technical item_", "_Hide %(amount)i technical items_", n_pkgs) % { 'amount': n_pkgs } self.action_bar.set_label(label, link_result=self._hide_nonapp_pkgs) else: label = gettext.ngettext("_Show %(amount)i technical item_", "_Show %(amount)i technical items_", n_pkgs) % { 'amount': n_pkgs } self.action_bar.set_label(label, link_result=self._show_nonapp_pkgs) def _show_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.ALWAYS_VISIBLE self.refresh_apps() return True def _hide_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE self.refresh_apps() return True def get_query(self): channel_query = None #name = self.pane_name if self.channel: channel_query = self.channel.query #name = self.channel.display_name # search terms if self.apps_search_term: query = self.db.get_query_list_from_search_entry( self.apps_search_term, channel_query) return query # overview list # if we are in a channel, limit to that if channel_query: return channel_query # ... otherwise show all return xapian.Query("") def refresh_apps(self, query=None): """refresh the applist and update the navigation bar """ LOG.debug("refresh_apps") # FIXME: make this available for all panes if query is None: query = self.get_query() # this can happen e.g. when opening a deb file, see bug #951238 if not self.app_view: return self.app_view.clear_model() self.search_aid.reset() self.show_appview_spinner() self._refresh_apps_with_apt_cache(query) def quick_query_len(self, query): """ do a blocking query that only returns the amount of matches from this query """ with ExecutionTime("enquirer.set_query() quick query"): self.enquirer.set_query(query, limit=self.get_app_items_limit(), nonapps_visible=self.nonapps_visible, nonblocking_load=False, filter=self.state.filter) return len(self.enquirer.matches) @wait_for_apt_cache_ready def _refresh_apps_with_apt_cache(self, query): LOG.debug("softwarepane query: %s" % query) self.app_view.configure_sort_method(self._is_in_search_mode()) # a nonblocking query calls on_query_complete once finished with ExecutionTime("enquirer.set_query()"): self.enquirer.set_query(query, limit=self.get_app_items_limit(), sortmode=self.get_sort_mode(), exact=self.is_custom_list(), nonapps_visible=self.nonapps_visible, filter=self.state.filter) def display_details_page(self, page, view_state): self.app_details_view.show_app(view_state.application) self.action_bar.unset_label() return True def is_custom_list(self): return self.apps_search_term and ',' in self.apps_search_term def get_current_page(self): return self.notebook.get_current_page() def get_app_items_limit(self): if self.state.search_term: return DEFAULT_SEARCH_LIMIT elif self.state.subcategory and self.state.subcategory.item_limit > 0: return self.state.subcategory.item_limit elif self.state.category and self.state.category.item_limit > 0: return self.state.category.item_limit return 0 def get_sort_mode(self): # if the category sets a custom sort order, that wins, this # is required for top-rated and whats-new if (self.state.category and self.state.category.sortmode != SortMethods.BY_ALPHABET): return self.state.category.sortmode # ask the app_view for the sort-mode return self.app_view.get_sort_mode() def on_search_entry_key_press_event(self, event): """callback when a key is pressed in the search entry widget""" if not self.is_applist_view_showing(): return if ((event.keyval == Gdk.keyval_from_name("Down") or event.keyval == Gdk.keyval_from_name("KP_Down")) and self.is_applist_view_showing() and len(self.app_view.tree_view.get_model()) > 0): # select the first item in the applist search result self.app_view.tree_view.grab_focus() self.app_view.tree_view.set_cursor(Gtk.TreePath(), None, False) def on_search_terms_changed(self, terms): """Stub implementation.""" pass def on_db_reopen(self, db): """Stub implementation.""" LOG.debug("%r: on_db_reopen (db is %r).", self.__class__.__name__, db) def is_category_view_showing(self): """Stub implementation.""" pass def is_applist_view_showing(self): """Stub implementation.""" pass def is_app_details_view_showing(self): """Stub implementation.""" pass def get_current_app(self): """Stub implementation.""" pass def on_application_selected(self, widget, app): """Stub implementation.""" pass def get_current_category(self): """Stub implementation.""" pass def unset_current_category(self): """Stub implementation.""" pass
class SoftwarePane(Gtk.VBox, BasePane): """ Common base class for AvailablePane and InstalledPane""" class Pages: NAMES = ('appview', 'details', 'spinner') APPVIEW = 0 DETAILS = 1 SPINNER = 2 __gsignals__ = { "app-list-changed" : (GObject.SignalFlags.RUN_LAST, None, (int,), ), } PADDING = 6 def __init__(self, cache, db, distro, icons, datadir, show_ratings=True): Gtk.VBox.__init__(self) BasePane.__init__(self) # other classes we need self.enquirer = AppEnquire(cache, db) self._query_complete_handler = self.enquirer.connect( "query-complete", self.on_query_complete) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.show_ratings = show_ratings self.backend = get_install_backend() self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE # refreshes can happen out-of-bound so we need to be sure # that we only set the new model (when its available) if # the refresh_seq_nr of the ready model matches that of the # request (e.g. people click on ubuntu channel, get impatient, click # on partner channel) self.refresh_seq_nr = 0 # keep track of applications that are candidates to be added # to the Unity launcher self.unity_launcher_items = {} # this should be initialized self.apps_search_term = "" # Create the basic frame for the common view self.state = DisplayState() vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.back_forward = vm.get_global_backforward() # a notebook below self.notebook = Gtk.Notebook() if not "SOFTWARE_CENTER_DEBUG_TABS" in os.environ: self.notebook.set_show_tabs(False) self.notebook.set_show_border(False) # an empty notebook, where the details view will eventually go self.details_notebook = Gtk.Notebook() self.details_notebook.set_show_border(False) # make a spinner view to display while the applist is loading self.spinner_view = SpinnerView() self.spinner_notebook = Gtk.Notebook() self.spinner_notebook.set_show_tabs(False) self.spinner_notebook.set_show_border(False) self.spinner_notebook.append_page(self.notebook, None) self.spinner_notebook.append_page(self.details_notebook, None) self.spinner_notebook.append_page(self.spinner_view, None) self.pack_start(self.spinner_notebook, True, True, 0) # add a bar at the bottom (hidden by default) for contextual actions self.action_bar = ActionBar() self.pack_start(self.action_bar, False, True, 0) # cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH) # views to be created in init_view self.app_view = None self.app_details_view = None def init_view(self): """ Initialize those UI components that are common to all subclasses of SoftwarePane. Note that this method is intended to be called by the subclass itself at the start of its own init_view() implementation. """ # common UI elements (applist and appdetails) # its the job of the Child class to put it into a good location # list self.box_app_list = Gtk.VBox() # search aid self.search_aid = SearchAid(self) self.box_app_list.pack_start(self.search_aid, False, False, 0) self.app_view = AppView(self.db, self.cache, self.icons, self.show_ratings) self.app_view.sort_methods_combobox.connect( "changed", self.on_app_view_sort_method_changed) self.init_atk_name(self.app_view, "app_view") self.box_app_list.pack_start(self.app_view, True, True, 0) self.app_view.connect("application-selected", self.on_application_selected) self.app_view.connect("application-activated", self.on_application_activated) # details self.scroll_details = Gtk.ScrolledWindow() self.scroll_details.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.app_details_view = AppDetailsView(self.db, self.distro, self.icons, self.cache, self.datadir, self) self.scroll_details.add(self.app_details_view) # when the cache changes, refresh the app list self.cache.connect("cache-ready", self.on_cache_ready) # aptdaemon self.backend.connect("transaction-started", self.on_transaction_started) self.backend.connect("transaction-finished", self.on_transaction_finished) self.backend.connect("transaction-stopped", self.on_transaction_stopped) # connect signals self.connect("app-list-changed", self.on_app_list_changed) # db reopen if self.db: self.db.connect("reopen", self.on_db_reopen) def init_atk_name(self, widget, name): """ init the atk name for a given gtk widget based on parent-pane and variable name (used for the mago tests) """ name = self.__class__.__name__ + "." + name Atk.Object.set_name(widget.get_accessible(), name) def on_cache_ready(self, cache): " refresh the application list when the cache is re-opened " LOG.debug("on_cache_ready") # it only makes sense to refresh if there is something to # refresh, otherwise we create a bunch of (not yet needed) # AppStore objects on startup when the cache sends its # initial "cache-ready" signal model = self.app_view.tree_view.get_model() if model is None: return # FIXME: preserve selection too self.refresh_apps() @wait_for_apt_cache_ready def on_application_activated(self, appview, app): """callback when an app is clicked""" LOG.debug("on_application_activated: '%s'" % app) self.state.application = app vm = get_viewmanager() vm.display_page(self, SoftwarePane.Pages.DETAILS, self.state, self.display_details_page) def show_app(self, app): self.on_application_activated(None, app) def on_nav_back_clicked(self, widget): vm = get_viewmanager() vm.nav_back() def on_nav_forward_clicked(self, widget): vm = get_viewmanager() vm.nav_forward() def on_transaction_started(self, backend, pkgname, appname, trans_id, trans_type): self._register_unity_launcher_transaction_started( backend, pkgname, appname, trans_id, trans_type) def _get_onscreen_icon_details_for_launcher_service(self, app): if self.is_app_details_view_showing(): return self.app_details_view.get_app_icon_details() else: # TODO: implement the app list view case once it has been specified return (0, 0, 0) def _register_unity_launcher_transaction_started(self, backend, pkgname, appname, trans_id, trans_type): # mvo: use use softwarecenter.utils explictely so that we can monkey # patch it in the test if not softwarecenter.utils.is_unity_running(): return # add to launcher only applies in the details view currently if not self.is_app_details_view_showing(): return # we only care about getting the launcher information on an install if not trans_type == TransactionTypes.INSTALL: if pkgname in self.unity_launcher_items: self.unity_launcher_items.pop(pkgname) self.action_bar.clear() return # gather details for this transaction and create the launcher_info object app = Application(pkgname=pkgname, appname=appname) appdetails = app.get_details(self.db) (icon_size, icon_x, icon_y) = self._get_onscreen_icon_details_for_launcher_service(app) launcher_info = UnityLauncherInfo(app.name, appdetails.icon, "", # we set the icon_file_path value *after* install icon_x, icon_y, icon_size, appdetails.desktop_file, "", # we set the installed_desktop_file_path *after* install trans_id) self.unity_launcher_items[app.pkgname] = launcher_info self.show_add_to_launcher_panel(backend, pkgname, appname, app, appdetails, trans_id, trans_type) def show_add_to_launcher_panel(self, backend, pkgname, appname, app, appdetails, trans_id, trans_type): """ if Unity is currently running, display a panel to allow the user the choose whether to add a newly-installed application to the launcher """ # TODO: handle local deb install case # TODO: implement the list view case (once it is specified) # only show the panel if unity is running and this is a package install # # we only show the prompt for apps with a desktop file if not appdetails.desktop_file: return # do not add apps without a exec line (like wine, see #848437) if (os.path.exists(appdetails.desktop_file) and is_no_display_desktop_file(appdetails.desktop_file)): return self.action_bar.add_button(ActionButtons.CANCEL_ADD_TO_LAUNCHER, _("Not Now"), self.on_cancel_add_to_launcher, pkgname) self.action_bar.add_button(ActionButtons.ADD_TO_LAUNCHER, _("Add to Launcher"), self.on_add_to_launcher, pkgname, app, appdetails, trans_id) self.action_bar.set_label(utf8(_("Add %s to the launcher?")) % utf8(app.name)) def on_query_complete(self, enquirer): self.emit("app-list-changed", len(enquirer.matches)) sort_by_relevance = (self._is_in_search_mode() and not self.app_view.user_defined_sort_method) self.app_view.display_matches(enquirer.matches, sort_by_relevance) self.hide_appview_spinner() return def on_app_view_sort_method_changed(self, combo): if self.app_view.get_sort_mode() == self.enquirer.sortmode: return self.show_appview_spinner() self.app_view.clear_model() query = self.get_query() self._refresh_apps_with_apt_cache(query) return def on_add_to_launcher(self, pkgname, app, appdetails, trans_id): """ callback indicating the user has chosen to add the indicated application to the launcher """ if pkgname in self.unity_launcher_items: launcher_info = self.unity_launcher_items[pkgname] if launcher_info.installed_desktop_file_path: # package install is complete, we can add to the launcher immediately self.unity_launcher_items.pop(pkgname) self.action_bar.clear() self._send_dbus_signal_to_unity_launcher(launcher_info) else: # package is not yet installed, it will be added to the launcher # once the installation is complete LOG.debug("the application '%s' will be added to the Unity launcher when installation is complete" % app.name) launcher_info.add_to_launcher_requested = True self.action_bar.set_label(_("%s will be added to the launcher when installation completes.") % app.name) self.action_bar.remove_button(ActionButtons.CANCEL_ADD_TO_LAUNCHER) self.action_bar.remove_button(ActionButtons.ADD_TO_LAUNCHER) def on_cancel_add_to_launcher(self, pkgname): if pkgname in self.unity_launcher_items: self.unity_launcher_items.pop(pkgname) self.action_bar.clear() def on_transaction_finished(self, backend, result): self._check_unity_launcher_transaction_finished(result) def _is_in_search_mode(self): return (self.state.search_term and len(self.state.search_term) >= 2) def _check_unity_launcher_transaction_finished(self, result): # add the completed transaction details to the corresponding # launcher_item if result.pkgname in self.unity_launcher_items: launcher_info = self.unity_launcher_items[result.pkgname] launcher_info.icon_file_path = get_file_path_from_iconname( self.icons, launcher_info.icon_name) installed_path = convert_desktop_file_to_installed_location( launcher_info.app_install_desktop_file_path, result.pkgname) launcher_info.installed_desktop_file_path = installed_path # if the request to add to launcher has already been made, do it now if launcher_info.add_to_launcher_requested: if result.success: self._send_dbus_signal_to_unity_launcher(launcher_info) self.unity_launcher_items.pop(result.pkgname) self.action_bar.clear() def _send_dbus_signal_to_unity_launcher(self, launcher_info): LOG.debug("sending dbus signal to Unity launcher for application: ", launcher_info.name) LOG.debug(" launcher_info.icon_file_path: ", launcher_info.icon_file_path) LOG.debug(" launcher_info.installed_desktop_file_path: ", launcher_info.installed_desktop_file_path) LOG.debug(" launcher_info.trans_id: ", launcher_info.trans_id) try: bus = dbus.SessionBus() launcher_obj = bus.get_object('com.canonical.Unity.Launcher', '/com/canonical/Unity/Launcher') launcher_iface = dbus.Interface(launcher_obj, 'com.canonical.Unity.Launcher') launcher_iface.AddLauncherItemFromPosition(launcher_info.name, launcher_info.icon_file_path, launcher_info.icon_x, launcher_info.icon_y, launcher_info.icon_size, launcher_info.installed_desktop_file_path, launcher_info.trans_id) except Exception as e: LOG.warn("could not send dbus signal to the Unity launcher: (%s)", e) def on_transaction_stopped(self, backend, result): if result.pkgname in self.unity_launcher_items: self.unity_launcher_items.pop(result.pkgname) self.action_bar.clear() def show_appview_spinner(self): """ display the spinner in the appview panel """ if not self.state.search_term: self.action_bar.clear() self.spinner_view.stop() self.spinner_notebook.set_current_page(SoftwarePane.Pages.SPINNER) # "mask" the spinner view momentarily to prevent it from flashing into # view in the case of short delays where it isn't actually needed GObject.timeout_add(100, self._unmask_appview_spinner) def _unmask_appview_spinner(self): self.spinner_view.start() return False def hide_appview_spinner(self): """ hide the spinner and display the appview in the panel """ self.spinner_notebook.set_current_page( SoftwarePane.Pages.APPVIEW) self.spinner_view.stop() def set_section(self, section): self.section = section self.app_details_view.set_section(section) return def section_sync(self): self.app_details_view.set_section(self.section) return def on_app_list_changed(self, pane, length): """internal helper that keeps the the action bar up-to-date by keeping track of the app-list-changed signals """ self.show_nonapps_if_required(len(self.enquirer.matches)) self.search_aid.update_search_help(self.state) return def hide_nonapps(self): """ hide non-applications control in the action bar """ self.action_bar.unset_label() return def show_nonapps_if_required(self, length): """ update the state of the show/hide non-applications control in the action_bar """ enquirer = self.enquirer n_apps = enquirer.nr_apps n_pkgs = enquirer.nr_pkgs # calculate the number of apps/pkgs if enquirer.limit > 0 and enquirer.limit < n_pkgs: n_apps = min(enquirer.limit, n_apps) n_pkgs = min(enquirer.limit - n_apps, n_pkgs) if not (n_apps and n_pkgs): self.hide_nonapps() return LOG.debug("nonapps_visible value=%s (always visible: %s)" % ( self.nonapps_visible, self.nonapps_visible == NonAppVisibility.ALWAYS_VISIBLE)) self.action_bar.unset_label() if self.nonapps_visible == NonAppVisibility.ALWAYS_VISIBLE: LOG.debug('non-apps-ALWAYS-visible') # TRANSLATORS: the text inbetween the underscores acts as a link # In most/all languages you will want the whole string as a link label = gettext.ngettext("_Hide %(amount)i technical item_", "_Hide %(amount)i technical items_", n_pkgs) % { 'amount': n_pkgs, } self.action_bar.set_label( label, link_result=self._hide_nonapp_pkgs) else: label = gettext.ngettext("_Show %(amount)i technical item_", "_Show %(amount)i technical items_", n_pkgs) % { 'amount': n_pkgs, } self.action_bar.set_label( label, link_result=self._show_nonapp_pkgs) def _on_label_app_list_header_activate_link(self, link, uri): #print "actiavte: ", link, uri if uri.startswith("search:"): self.searchentry.set_text(uri[len("search:"):]) elif uri.startswith("search-all:"): self.unset_current_category() self.refresh_apps() elif uri.startswith("search-parent:"): self.apps_subcategory = None; self.refresh_apps() elif uri.startswith("search-unsupported:"): self.apps_filter.set_supported_only(False) self.refresh_apps() # FIXME: add ability to remove categories restriction here # True stops event propergation return True def _show_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.ALWAYS_VISIBLE self.refresh_apps() def _hide_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE self.refresh_apps() def get_query(self): channel_query = None #name = self.pane_name if self.channel: channel_query = self.channel.query #name = self.channel.display_name # search terms if self.apps_search_term: query = self.db.get_query_list_from_search_entry( self.apps_search_term, channel_query) return query # overview list # if we are in a channel, limit to that if channel_query: return channel_query # ... otherwise show all return xapian.Query("") def refresh_apps(self, query=None): """refresh the applist and update the navigation bar """ LOG.debug("refresh_apps") # FIXME: make this available for all panes if query is None: query = self.get_query() self.app_view.clear_model() self.search_aid.reset() self.show_appview_spinner() self._refresh_apps_with_apt_cache(query) def quick_query(self, query): # a blocking query and does not emit "query-complete" with ExecutionTime("enquirer.set_query() quick query"): self.enquirer.set_query( query, limit=self.get_app_items_limit(), nonapps_visible=self.nonapps_visible, nonblocking_load=False, filter=self.state.filter) return len(self.enquirer.matches) @wait_for_apt_cache_ready def _refresh_apps_with_apt_cache(self, query): LOG.debug("softwarepane query: %s" % query) # a nonblocking query calls on_query_complete once finished with ExecutionTime("enquirer.set_query()"): self.enquirer.set_query( query, limit=self.get_app_items_limit(), sortmode=self.get_sort_mode(), exact=self.is_custom_list(), nonapps_visible=self.nonapps_visible, filter=self.state.filter) return def display_details_page(self, page, view_state): self.app_details_view.show_app(view_state.application) self.action_bar.unset_label() return True def is_custom_list(self): return self.apps_search_term and ',' in self.apps_search_term def get_current_page(self): return self.notebook.get_current_page() def get_app_items_limit(self): if self.state.search_term: return DEFAULT_SEARCH_LIMIT elif self.state.subcategory and self.state.subcategory.item_limit > 0: return self.state.subcategory.item_limit elif self.state.category and self.state.category.item_limit > 0: return self.state.category.item_limit return 0 def get_sort_mode(self): # if the category sets a custom sort order, that wins, this # is required for top-rated and whats-new if (self.state.category and self.state.category.sortmode != SortMethods.BY_ALPHABET): return self.state.category.sortmode # searches are always by ranking unless the user decided differently if (self._is_in_search_mode() and not self.app_view.user_defined_sort_method): return SortMethods.BY_SEARCH_RANKING # use the appview combo return self.app_view.get_sort_mode() def on_search_terms_changed(self, terms): " stub implementation " pass def on_db_reopen(self): " stub implementation " pass def is_category_view_showing(self): " stub implementation " pass def is_applist_view_showing(self): " stub implementation " pass def is_app_details_view_showing(self): " stub implementation " pass def get_current_app(self): " stub implementation " pass def on_application_selected(self, widget, app): " stub implementation " pass def get_current_category(self): " stub implementation " pass def unset_current_category(self): " stub implementation " pass
class SoftwarePane(Gtk.VBox, BasePane): """ Common base class for AvailablePane and InstalledPane""" class Pages: NAMES = ('appview', 'details', 'spinner') APPVIEW = 0 DETAILS = 1 SPINNER = 2 __gsignals__ = { "app-list-changed": (GObject.SignalFlags.RUN_LAST, None, (int,), ), } PADDING = 6 def __init__(self, cache, db, distro, icons, datadir, show_ratings=True): Gtk.VBox.__init__(self) BasePane.__init__(self) # other classes we need self.enquirer = AppEnquire(cache, db) self._query_complete_handler = self.enquirer.connect( "query-complete", self.on_query_complete) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.show_ratings = show_ratings self.backend = get_install_backend() self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE # refreshes can happen out-of-bound so we need to be sure # that we only set the new model (when its available) if # the refresh_seq_nr of the ready model matches that of the # request (e.g. people click on ubuntu channel, get impatient, click # on partner channel) self.refresh_seq_nr = 0 # this should be initialized self.apps_search_term = "" # Create the basic frame for the common view self.state = DisplayState() vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.back_forward = vm.get_global_backforward() # a notebook below self.notebook = Gtk.Notebook() if not "SOFTWARE_CENTER_DEBUG_TABS" in os.environ: self.notebook.set_show_tabs(False) self.notebook.set_show_border(False) # make a spinner view to display while the applist is loading self.spinner_notebook = SpinnerNotebook(self.notebook) self.pack_start(self.spinner_notebook, True, True, 0) # add a bar at the bottom (hidden by default) for contextual actions self.action_bar = ActionBar() self.pack_start(self.action_bar, False, True, 0) # cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH) # views to be created in init_view self.app_view = None self.app_details_view = None def init_view(self): """ Initialize those UI components that are common to all subclasses of SoftwarePane. Note that this method is intended to be called by the subclass itself at the start of its own init_view() implementation. """ # common UI elements (applist and appdetails) # its the job of the Child class to put it into a good location # list self.box_app_list = Gtk.VBox() # search aid self.search_aid = SearchAid(self) self.box_app_list.pack_start(self.search_aid, False, False, 0) self.app_view = AppView(self.db, self.cache, self.icons, self.show_ratings) self.app_view.connect("sort-method-changed", self.on_app_view_sort_method_changed) self.init_atk_name(self.app_view, "app_view") self.box_app_list.pack_start(self.app_view, True, True, 0) self.app_view.connect("application-selected", self.on_application_selected) self.app_view.connect("application-activated", self.on_application_activated) # details self.scroll_details = Gtk.ScrolledWindow() self.scroll_details.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.app_details_view = AppDetailsView(self.db, self.distro, self.icons, self.cache, self.datadir) self.app_details_view.connect( "different-application-selected", self.on_application_activated) self.scroll_details.add(self.app_details_view) # when the cache changes, refresh the app list self.cache.connect("cache-ready", self.on_cache_ready) # connect signals self.connect("app-list-changed", self.on_app_list_changed) # db reopen if self.db: self.db.connect("reopen", self.on_db_reopen) def init_atk_name(self, widget, name): """ init the atk name for a given gtk widget based on parent-pane and variable name (used for the mago tests) """ name = self.__class__.__name__ + "." + name Atk.Object.set_name(widget.get_accessible(), name) def on_cache_ready(self, cache): " refresh the application list when the cache is re-opened " LOG.debug("on_cache_ready") # it only makes sense to refresh if there is something to # refresh, otherwise we create a bunch of (not yet needed) # AppStore objects on startup when the cache sends its # initial "cache-ready" signal model = self.app_view.tree_view.get_model() if model is None: return # FIXME: preserve selection too self.refresh_apps() @wait_for_apt_cache_ready def on_application_activated(self, appview, app): """callback when an app is clicked""" LOG.debug("on_application_activated: '%s'" % app) self.state.application = app vm = get_viewmanager() vm.display_page(self, SoftwarePane.Pages.DETAILS, self.state, self.display_details_page) def show_app(self, app): self.on_application_activated(None, app) def on_nav_back_clicked(self, widget): vm = get_viewmanager() vm.nav_back() def on_nav_forward_clicked(self, widget): vm = get_viewmanager() vm.nav_forward() def on_query_complete(self, enquirer): self.emit("app-list-changed", len(enquirer.matches)) self.app_view.display_matches(enquirer.matches, self._is_in_search_mode()) self.hide_appview_spinner() def on_app_view_sort_method_changed(self, app_view, combo): if app_view.get_sort_mode() == self.enquirer.sortmode: return self.show_appview_spinner() app_view.clear_model() query = self.get_query() self._refresh_apps_with_apt_cache(query) def _is_in_search_mode(self): return (self.state.search_term and len(self.state.search_term) >= 2) def show_appview_spinner(self): """ display the spinner in the appview panel """ LOG.debug("show_appview_spinner") # FIXME: totally the wrong place! if not self.state.search_term: self.action_bar.clear() self.spinner_notebook.show_spinner() def hide_appview_spinner(self): """ hide the spinner and display the appview in the panel """ LOG.debug("hide_appview_spinner") self.spinner_notebook.hide_spinner() def set_section(self, section): self.section = section self.app_details_view.set_section(section) def section_sync(self): self.app_details_view.set_section(self.section) def on_app_list_changed(self, pane, length): """internal helper that keeps the the action bar up-to-date by keeping track of the app-list-changed signals """ self.show_nonapps_if_required(len(self.enquirer.matches)) self.search_aid.update_search_help(self.state) def hide_nonapps(self): """ hide non-applications control in the action bar """ self.action_bar.unset_label() def show_nonapps_if_required(self, length): """ update the state of the show/hide non-applications control in the action_bar """ enquirer = self.enquirer n_apps = enquirer.nr_apps n_pkgs = enquirer.nr_pkgs # calculate the number of apps/pkgs if enquirer.limit > 0 and enquirer.limit < n_pkgs: n_apps = min(enquirer.limit, n_apps) n_pkgs = min(enquirer.limit - n_apps, n_pkgs) if not (n_apps and n_pkgs): self.hide_nonapps() return LOG.debug("nonapps_visible value=%s (always visible: %s)" % ( self.nonapps_visible, self.nonapps_visible == NonAppVisibility.ALWAYS_VISIBLE)) self.action_bar.unset_label() if self.nonapps_visible == NonAppVisibility.ALWAYS_VISIBLE: LOG.debug('non-apps-ALWAYS-visible') # TRANSLATORS: the text inbetween the underscores acts as a link # In most/all languages you will want the whole string as a link label = gettext.ngettext("_Hide %(amount)i technical item_", "_Hide %(amount)i technical items_", n_pkgs) % {'amount': n_pkgs} self.action_bar.set_label( label, link_result=self._hide_nonapp_pkgs) else: label = gettext.ngettext("_Show %(amount)i technical item_", "_Show %(amount)i technical items_", n_pkgs) % {'amount': n_pkgs} self.action_bar.set_label( label, link_result=self._show_nonapp_pkgs) def _on_label_app_list_header_activate_link(self, link, uri): #print "actiavte: ", link, uri if uri.startswith("search:"): self.searchentry.set_text(uri[len("search:"):]) elif uri.startswith("search-all:"): self.unset_current_category() self.refresh_apps() elif uri.startswith("search-parent:"): self.apps_subcategory = None self.refresh_apps() elif uri.startswith("search-unsupported:"): self.apps_filter.set_supported_only(False) self.refresh_apps() # FIXME: add ability to remove categories restriction here # True stops event propergation return True def _show_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.ALWAYS_VISIBLE self.refresh_apps() def _hide_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE self.refresh_apps() def get_query(self): channel_query = None #name = self.pane_name if self.channel: channel_query = self.channel.query #name = self.channel.display_name # search terms if self.apps_search_term: query = self.db.get_query_list_from_search_entry( self.apps_search_term, channel_query) return query # overview list # if we are in a channel, limit to that if channel_query: return channel_query # ... otherwise show all return xapian.Query("") def refresh_apps(self, query=None): """refresh the applist and update the navigation bar """ LOG.debug("refresh_apps") # FIXME: make this available for all panes if query is None: query = self.get_query() self.app_view.clear_model() self.search_aid.reset() self.show_appview_spinner() self._refresh_apps_with_apt_cache(query) def quick_query(self, query): # a blocking query and does not emit "query-complete" with ExecutionTime("enquirer.set_query() quick query"): self.enquirer.set_query( query, limit=self.get_app_items_limit(), nonapps_visible=self.nonapps_visible, nonblocking_load=False, filter=self.state.filter) return len(self.enquirer.matches) @wait_for_apt_cache_ready def _refresh_apps_with_apt_cache(self, query): LOG.debug("softwarepane query: %s" % query) # a nonblocking query calls on_query_complete once finished with ExecutionTime("enquirer.set_query()"): self.enquirer.set_query( query, limit=self.get_app_items_limit(), sortmode=self.get_sort_mode(), exact=self.is_custom_list(), nonapps_visible=self.nonapps_visible, filter=self.state.filter) def display_details_page(self, page, view_state): self.app_details_view.show_app(view_state.application) self.action_bar.unset_label() return True def is_custom_list(self): return self.apps_search_term and ',' in self.apps_search_term def get_current_page(self): return self.notebook.get_current_page() def get_app_items_limit(self): if self.state.search_term: return DEFAULT_SEARCH_LIMIT elif self.state.subcategory and self.state.subcategory.item_limit > 0: return self.state.subcategory.item_limit elif self.state.category and self.state.category.item_limit > 0: return self.state.category.item_limit return 0 def get_sort_mode(self): # if the category sets a custom sort order, that wins, this # is required for top-rated and whats-new if (self.state.category and self.state.category.sortmode != SortMethods.BY_ALPHABET): return self.state.category.sortmode # searches are always by ranking unless the user decided differently if (self._is_in_search_mode() and not self.app_view.user_defined_sort_method): return SortMethods.BY_SEARCH_RANKING # use the appview combo return self.app_view.get_sort_mode() def on_search_entry_key_press_event(self, event): """callback when a key is pressed in the search entry widget""" if not self.is_applist_view_showing(): return if ((event.keyval == Gdk.keyval_from_name("Down") or event.keyval == Gdk.keyval_from_name("KP_Down")) and self.is_applist_view_showing() and len(self.app_view.tree_view.get_model()) > 0): # select the first item in the applist search result self.app_view.tree_view.grab_focus() self.app_view.tree_view.set_cursor(Gtk.TreePath(), None, False) def on_search_terms_changed(self, terms): " stub implementation " pass def on_db_reopen(self): " stub implementation " pass def is_category_view_showing(self): " stub implementation " pass def is_applist_view_showing(self): " stub implementation " pass def is_app_details_view_showing(self): " stub implementation " pass def get_current_app(self): " stub implementation " pass def on_application_selected(self, widget, app): " stub implementation " pass def get_current_category(self): " stub implementation " pass def unset_current_category(self): " stub implementation " pass
class SoftwarePane(Gtk.VBox, BasePane): """ Common base class for AvailablePane and InstalledPane""" class Pages: NAMES = ('appview', 'details', 'spinner') APPVIEW = 0 DETAILS = 1 SPINNER = 2 __gsignals__ = { "app-list-changed" : (GObject.SignalFlags.RUN_LAST, None, (int,), ), } PADDING = 6 def __init__(self, cache, db, distro, icons, datadir, show_ratings=True): Gtk.VBox.__init__(self) BasePane.__init__(self) # other classes we need self.enquirer = AppEnquire(cache, db) self._query_complete_handler = self.enquirer.connect( "query-complete", self.on_query_complete) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.show_ratings = show_ratings self.backend = get_install_backend() self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE # refreshes can happen out-of-bound so we need to be sure # that we only set the new model (when its available) if # the refresh_seq_nr of the ready model matches that of the # request (e.g. people click on ubuntu channel, get impatient, click # on partner channel) self.refresh_seq_nr = 0 # keep track of applications that are candidates to be added # to the Unity launcher self.unity_launcher_items = {} # this should be initialized self.apps_search_term = "" # Create the basic frame for the common view self.state = DisplayState() vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.back_forward = vm.get_global_backforward() # a notebook below self.notebook = Gtk.Notebook() if not "SOFTWARE_CENTER_DEBUG_TABS" in os.environ: self.notebook.set_show_tabs(False) self.notebook.set_show_border(False) # an empty notebook, where the details view will eventually go self.details_notebook = Gtk.Notebook() self.details_notebook.set_show_border(False) # make a spinner view to display while the applist is loading self.spinner_view = SpinnerView() self.spinner_notebook = Gtk.Notebook() self.spinner_notebook.set_show_tabs(False) self.spinner_notebook.set_show_border(False) self.spinner_notebook.append_page(self.notebook, None) self.spinner_notebook.append_page(self.details_notebook, None) self.spinner_notebook.append_page(self.spinner_view, None) self.pack_start(self.spinner_notebook, True, True, 0) # add a bar at the bottom (hidden by default) for contextual actions self.action_bar = ActionBar() self.pack_start(self.action_bar, False, True, 0) # cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH) # views to be created in init_view self.app_view = None self.app_details_view = None def init_view(self): """ Initialize those UI components that are common to all subclasses of SoftwarePane. Note that this method is intended to be called by the subclass itself at the start of its own init_view() implementation. """ # common UI elements (applist and appdetails) # its the job of the Child class to put it into a good location # list self.box_app_list = Gtk.VBox() # search aid self.search_aid = SearchAid(self) self.box_app_list.pack_start(self.search_aid, False, False, 0) self.app_view = AppView(self.db, self.cache, self.icons, self.show_ratings) self.app_view.connect("sort-method-changed", self.on_app_view_sort_method_changed) self.init_atk_name(self.app_view, "app_view") self.box_app_list.pack_start(self.app_view, True, True, 0) self.app_view.connect("application-selected", self.on_application_selected) self.app_view.connect("application-activated", self.on_application_activated) # details self.scroll_details = Gtk.ScrolledWindow() self.scroll_details.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.app_details_view = AppDetailsView(self.db, self.distro, self.icons, self.cache, self.datadir, self) self.scroll_details.add(self.app_details_view) # when the cache changes, refresh the app list self.cache.connect("cache-ready", self.on_cache_ready) # aptdaemon self.backend.connect("transaction-started", self.on_transaction_started) self.backend.connect("transaction-finished", self.on_transaction_finished) self.backend.connect("transaction-stopped", self.on_transaction_stopped) # connect signals self.connect("app-list-changed", self.on_app_list_changed) # db reopen if self.db: self.db.connect("reopen", self.on_db_reopen) def init_atk_name(self, widget, name): """ init the atk name for a given gtk widget based on parent-pane and variable name (used for the mago tests) """ name = self.__class__.__name__ + "." + name Atk.Object.set_name(widget.get_accessible(), name) def on_cache_ready(self, cache): " refresh the application list when the cache is re-opened " LOG.debug("on_cache_ready") # it only makes sense to refresh if there is something to # refresh, otherwise we create a bunch of (not yet needed) # AppStore objects on startup when the cache sends its # initial "cache-ready" signal model = self.app_view.tree_view.get_model() if model is None: return # FIXME: preserve selection too self.refresh_apps() @wait_for_apt_cache_ready def on_application_activated(self, appview, app): """callback when an app is clicked""" LOG.debug("on_application_activated: '%s'" % app) self.state.application = app vm = get_viewmanager() vm.display_page(self, SoftwarePane.Pages.DETAILS, self.state, self.display_details_page) def show_app(self, app): self.on_application_activated(None, app) def on_nav_back_clicked(self, widget): vm = get_viewmanager() vm.nav_back() def on_nav_forward_clicked(self, widget): vm = get_viewmanager() vm.nav_forward() def on_transaction_started(self, backend, pkgname, appname, trans_id, trans_type): self._register_unity_launcher_transaction_started( backend, pkgname, appname, trans_id, trans_type) def _get_onscreen_icon_details_for_launcher_service(self, app): if self.is_app_details_view_showing(): return self.app_details_view.get_app_icon_details() else: # TODO: implement the app list view case once it has been specified return (0, 0, 0) def _register_unity_launcher_transaction_started(self, backend, pkgname, appname, trans_id, trans_type): # mvo: use use softwarecenter.utils explictely so that we can monkey # patch it in the test if not softwarecenter.utils.is_unity_running(): return # add to launcher only applies in the details view currently if not self.is_app_details_view_showing(): return # we only care about getting the launcher information on an install if not trans_type == TransactionTypes.INSTALL: if pkgname in self.unity_launcher_items: self.unity_launcher_items.pop(pkgname) self.action_bar.clear() return # gather details for this transaction and create the launcher_info object app = Application(pkgname=pkgname, appname=appname) appdetails = app.get_details(self.db) (icon_size, icon_x, icon_y) = self._get_onscreen_icon_details_for_launcher_service(app) launcher_info = UnityLauncherInfo(app.name, appdetails.icon, "", # we set the icon_file_path value *after* install icon_x, icon_y, icon_size, appdetails.desktop_file, "", # we set the installed_desktop_file_path *after* install trans_id) self.unity_launcher_items[app.pkgname] = launcher_info self.show_add_to_launcher_panel(backend, pkgname, appname, app, appdetails, trans_id, trans_type) def show_add_to_launcher_panel(self, backend, pkgname, appname, app, appdetails, trans_id, trans_type): """ if Unity is currently running, display a panel to allow the user the choose whether to add a newly-installed application to the launcher """ # TODO: handle local deb install case # TODO: implement the list view case (once it is specified) # only show the panel if unity is running and this is a package install # # we only show the prompt for apps with a desktop file if not appdetails.desktop_file: return # do not add apps without a exec line (like wine, see #848437) if (os.path.exists(appdetails.desktop_file) and is_no_display_desktop_file(appdetails.desktop_file)): return self.action_bar.add_button(ActionButtons.CANCEL_ADD_TO_LAUNCHER, _("Not Now"), self.on_cancel_add_to_launcher, pkgname) self.action_bar.add_button(ActionButtons.ADD_TO_LAUNCHER, _("Add to Launcher"), self.on_add_to_launcher, pkgname, app, appdetails, trans_id) self.action_bar.set_label(utf8(_("Add %s to the launcher?")) % utf8(app.name)) def on_query_complete(self, enquirer): self.emit("app-list-changed", len(enquirer.matches)) self.app_view.display_matches(enquirer.matches, self._is_in_search_mode()) self.hide_appview_spinner() return def on_app_view_sort_method_changed(self, app_view, combo): if app_view.get_sort_mode() == self.enquirer.sortmode: return self.show_appview_spinner() app_view.clear_model() query = self.get_query() self._refresh_apps_with_apt_cache(query) return def on_add_to_launcher(self, pkgname, app, appdetails, trans_id): """ callback indicating the user has chosen to add the indicated application to the launcher """ if pkgname in self.unity_launcher_items: launcher_info = self.unity_launcher_items[pkgname] if launcher_info.installed_desktop_file_path: # package install is complete, we can add to the launcher immediately self.unity_launcher_items.pop(pkgname) self.action_bar.clear() self._send_dbus_signal_to_unity_launcher(launcher_info) else: # package is not yet installed, it will be added to the launcher # once the installation is complete LOG.debug("the application '%s' will be added to the Unity launcher when installation is complete" % app.name) launcher_info.add_to_launcher_requested = True self.action_bar.set_label(_("%s will be added to the launcher when installation completes.") % app.name) self.action_bar.remove_button(ActionButtons.CANCEL_ADD_TO_LAUNCHER) self.action_bar.remove_button(ActionButtons.ADD_TO_LAUNCHER) def on_cancel_add_to_launcher(self, pkgname): if pkgname in self.unity_launcher_items: self.unity_launcher_items.pop(pkgname) self.action_bar.clear() def on_transaction_finished(self, backend, result): self._check_unity_launcher_transaction_finished(result) def _is_in_search_mode(self): return (self.state.search_term and len(self.state.search_term) >= 2) def _check_unity_launcher_transaction_finished(self, result): # add the completed transaction details to the corresponding # launcher_item if result.pkgname in self.unity_launcher_items: launcher_info = self.unity_launcher_items[result.pkgname] launcher_info.icon_file_path = get_file_path_from_iconname( self.icons, launcher_info.icon_name) installed_path = convert_desktop_file_to_installed_location( launcher_info.app_install_desktop_file_path, result.pkgname) launcher_info.installed_desktop_file_path = installed_path # if the request to add to launcher has already been made, do it now if launcher_info.add_to_launcher_requested: if result.success: self._send_dbus_signal_to_unity_launcher(launcher_info) self.unity_launcher_items.pop(result.pkgname) self.action_bar.clear() def _send_dbus_signal_to_unity_launcher(self, launcher_info): LOG.debug("sending dbus signal to Unity launcher for application: ", launcher_info.name) LOG.debug(" launcher_info.icon_file_path: ", launcher_info.icon_file_path) LOG.debug(" launcher_info.installed_desktop_file_path: ", launcher_info.installed_desktop_file_path) LOG.debug(" launcher_info.trans_id: ", launcher_info.trans_id) try: bus = dbus.SessionBus() launcher_obj = bus.get_object('com.canonical.Unity.Launcher', '/com/canonical/Unity/Launcher') launcher_iface = dbus.Interface(launcher_obj, 'com.canonical.Unity.Launcher') launcher_iface.AddLauncherItemFromPosition(launcher_info.name, launcher_info.icon_file_path, launcher_info.icon_x, launcher_info.icon_y, launcher_info.icon_size, launcher_info.installed_desktop_file_path, launcher_info.trans_id) except Exception as e: LOG.warn("could not send dbus signal to the Unity launcher: (%s)", e) def on_transaction_stopped(self, backend, result): if result.pkgname in self.unity_launcher_items: self.unity_launcher_items.pop(result.pkgname) self.action_bar.clear() def show_appview_spinner(self): """ display the spinner in the appview panel """ if not self.state.search_term: self.action_bar.clear() self.spinner_view.stop() self.spinner_notebook.set_current_page(SoftwarePane.Pages.SPINNER) # "mask" the spinner view momentarily to prevent it from flashing into # view in the case of short delays where it isn't actually needed GObject.timeout_add(100, self._unmask_appview_spinner) def _unmask_appview_spinner(self): self.spinner_view.start() return False def hide_appview_spinner(self): """ hide the spinner and display the appview in the panel """ self.spinner_notebook.set_current_page( SoftwarePane.Pages.APPVIEW) self.spinner_view.stop() def set_section(self, section): self.section = section self.app_details_view.set_section(section) return def section_sync(self): self.app_details_view.set_section(self.section) return def on_app_list_changed(self, pane, length): """internal helper that keeps the the action bar up-to-date by keeping track of the app-list-changed signals """ self.show_nonapps_if_required(len(self.enquirer.matches)) self.search_aid.update_search_help(self.state) return def hide_nonapps(self): """ hide non-applications control in the action bar """ self.action_bar.unset_label() return def show_nonapps_if_required(self, length): """ update the state of the show/hide non-applications control in the action_bar """ enquirer = self.enquirer n_apps = enquirer.nr_apps n_pkgs = enquirer.nr_pkgs # calculate the number of apps/pkgs if enquirer.limit > 0 and enquirer.limit < n_pkgs: n_apps = min(enquirer.limit, n_apps) n_pkgs = min(enquirer.limit - n_apps, n_pkgs) if not (n_apps and n_pkgs): self.hide_nonapps() return LOG.debug("nonapps_visible value=%s (always visible: %s)" % ( self.nonapps_visible, self.nonapps_visible == NonAppVisibility.ALWAYS_VISIBLE)) self.action_bar.unset_label() if self.nonapps_visible == NonAppVisibility.ALWAYS_VISIBLE: LOG.debug('non-apps-ALWAYS-visible') # TRANSLATORS: the text inbetween the underscores acts as a link # In most/all languages you will want the whole string as a link label = gettext.ngettext("_Hide %(amount)i technical item_", "_Hide %(amount)i technical items_", n_pkgs) % { 'amount': n_pkgs, } self.action_bar.set_label( label, link_result=self._hide_nonapp_pkgs) else: label = gettext.ngettext("_Show %(amount)i technical item_", "_Show %(amount)i technical items_", n_pkgs) % { 'amount': n_pkgs, } self.action_bar.set_label( label, link_result=self._show_nonapp_pkgs) def _on_label_app_list_header_activate_link(self, link, uri): #print "actiavte: ", link, uri if uri.startswith("search:"): self.searchentry.set_text(uri[len("search:"):]) elif uri.startswith("search-all:"): self.unset_current_category() self.refresh_apps() elif uri.startswith("search-parent:"): self.apps_subcategory = None; self.refresh_apps() elif uri.startswith("search-unsupported:"): self.apps_filter.set_supported_only(False) self.refresh_apps() # FIXME: add ability to remove categories restriction here # True stops event propergation return True def _show_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.ALWAYS_VISIBLE self.refresh_apps() def _hide_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.MAYBE_VISIBLE self.refresh_apps() def get_query(self): channel_query = None #name = self.pane_name if self.channel: channel_query = self.channel.query #name = self.channel.display_name # search terms if self.apps_search_term: query = self.db.get_query_list_from_search_entry( self.apps_search_term, channel_query) return query # overview list # if we are in a channel, limit to that if channel_query: return channel_query # ... otherwise show all return xapian.Query("") def refresh_apps(self, query=None): """refresh the applist and update the navigation bar """ LOG.debug("refresh_apps") # FIXME: make this available for all panes if query is None: query = self.get_query() self.app_view.clear_model() self.search_aid.reset() self.show_appview_spinner() self._refresh_apps_with_apt_cache(query) def quick_query(self, query): # a blocking query and does not emit "query-complete" with ExecutionTime("enquirer.set_query() quick query"): self.enquirer.set_query( query, limit=self.get_app_items_limit(), nonapps_visible=self.nonapps_visible, nonblocking_load=False, filter=self.state.filter) return len(self.enquirer.matches) @wait_for_apt_cache_ready def _refresh_apps_with_apt_cache(self, query): LOG.debug("softwarepane query: %s" % query) # a nonblocking query calls on_query_complete once finished with ExecutionTime("enquirer.set_query()"): self.enquirer.set_query( query, limit=self.get_app_items_limit(), sortmode=self.get_sort_mode(), exact=self.is_custom_list(), nonapps_visible=self.nonapps_visible, filter=self.state.filter) return def display_details_page(self, page, view_state): self.app_details_view.show_app(view_state.application) self.action_bar.unset_label() return True def is_custom_list(self): return self.apps_search_term and ',' in self.apps_search_term def get_current_page(self): return self.notebook.get_current_page() def get_app_items_limit(self): if self.state.search_term: return DEFAULT_SEARCH_LIMIT elif self.state.subcategory and self.state.subcategory.item_limit > 0: return self.state.subcategory.item_limit elif self.state.category and self.state.category.item_limit > 0: return self.state.category.item_limit return 0 def get_sort_mode(self): # if the category sets a custom sort order, that wins, this # is required for top-rated and whats-new if (self.state.category and self.state.category.sortmode != SortMethods.BY_ALPHABET): return self.state.category.sortmode # searches are always by ranking unless the user decided differently if (self._is_in_search_mode() and not self.app_view.user_defined_sort_method): return SortMethods.BY_SEARCH_RANKING # use the appview combo return self.app_view.get_sort_mode() def on_search_terms_changed(self, terms): " stub implementation " pass def on_db_reopen(self): " stub implementation " pass def is_category_view_showing(self): " stub implementation " pass def is_applist_view_showing(self): " stub implementation " pass def is_app_details_view_showing(self): " stub implementation " pass def get_current_app(self): " stub implementation " pass def on_application_selected(self, widget, app): " stub implementation " pass def get_current_category(self): " stub implementation " pass def unset_current_category(self): " stub implementation " pass
class SubCategoryViewGtk(CategoriesViewGtk): def __init__(self, datadir, desktopdir, cache, db, icons, apps_filter, apps_limit=0, root_category=None): CategoriesViewGtk.__init__(self, datadir, desktopdir, cache, db, icons, apps_filter, apps_limit) # state self._built = False # data self.root_category = root_category self.enquire = AppEnquire(self.cache, self.db) self.properties_helper = AppPropertiesHelper( self.db, self.cache, self.icons) # sections self.current_category = None self.departments = None self.top_rated = None self.recommended_for_you_in_cat = None self.appcount = None # widgetry self.vbox.set_margin_left(StockEms.MEDIUM - 2) self.vbox.set_margin_right(StockEms.MEDIUM - 2) self.vbox.set_margin_top(StockEms.MEDIUM) return def _get_sub_top_rated_content(self, category): app_filter = AppFilter(self.db, self.cache) self.enquire.set_query(category.query, limit=TOP_RATED_CAROUSEL_LIMIT, sortmode=SortMethods.BY_TOP_RATED, filter=app_filter, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) return self.enquire.get_documents() @wait_for_apt_cache_ready # be consistent with new apps def _update_sub_top_rated_content(self, category): self.top_rated.remove_all() # FIXME: should this be m = "%s %s" % (_(gettext text), header text) ?? # TRANSLATORS: %s is a category name, like Internet or Development # Tools m = _('Top Rated %(category)s') % { 'category': GObject.markup_escape_text(self.header)} self.top_rated_frame.set_header_label(m) docs = self._get_sub_top_rated_content(category) self._add_tiles_to_flowgrid(docs, self.top_rated, TOP_RATED_CAROUSEL_LIMIT) return def _append_sub_top_rated(self): self.top_rated = FlowableGrid() self.top_rated.set_row_spacing(6) self.top_rated.set_column_spacing(6) self.top_rated_frame = FramedHeaderBox() self.top_rated_frame.pack_start(self.top_rated, True, True, 0) self.vbox.pack_start(self.top_rated_frame, False, True, 0) return def _update_recommended_for_you_in_cat_content(self, category): if (self.recommended_for_you_in_cat and self.recommended_for_you_in_cat.get_parent()): self.vbox.remove(self.recommended_for_you_in_cat) self.recommended_for_you_in_cat = RecommendationsPanelCategory( self, category) # only show the panel in the categories view when the user # is opted in to the recommender service # FIXME: this is needed vs. a simple hide() on the widget because # we do a show_all on the view if self.recommended_for_you_in_cat.recommender_agent.is_opted_in(): self.vbox.pack_start(self.recommended_for_you_in_cat, False, False, 0) def _update_subcat_departments(self, category, num_items): self.departments.remove_all() # set the subcat header m = "<b><big>%s</big></b>" self.subcat_label.set_markup(m % GObject.markup_escape_text( self.header)) # sort Category.name's alphabetically sorted_cats = categories_sorted_by_name(self.categories) enquire = xapian.Enquire(self.db.xapiandb) app_filter = AppFilter(self.db, self.cache) for cat in sorted_cats: # add the subcategory if and only if it is non-empty enquire.set_query(cat.query) if len(enquire.get_mset(0, 1)): tile = CategoryTile(cat.name, cat.iconname) tile.connect('clicked', self.on_category_clicked, cat) self.departments.add_child(tile) # partialy work around a (quite rare) corner case if num_items == 0: enquire.set_query(xapian.Query(xapian.Query.OP_AND, category.query, xapian.Query("ATapplication"))) # assuming that we only want apps is not always correct ^^^ tmp_matches = enquire.get_mset(0, len(self.db), None, app_filter) num_items = tmp_matches.get_matches_estimated() # append an additional button to show all of the items in the category all_cat = Category("All", _("All"), "category-show-all", category.query) name = GObject.markup_escape_text('%s %s' % (_("All"), num_items)) tile = CategoryTile(name, "category-show-all") tile.connect('clicked', self.on_category_clicked, all_cat) self.departments.add_child(tile) self.departments.queue_draw() return num_items def _append_subcat_departments(self): self.subcat_label = Gtk.Label() self.subcat_label.set_alignment(0, 0.5) self.departments = FlowableGrid(paint_grid_pattern=False) self.departments.set_row_spacing(StockEms.SMALL) self.departments.set_column_spacing(StockEms.SMALL) self.departments_frame = FramedBox(spacing=StockEms.MEDIUM, padding=StockEms.MEDIUM) # set x/y-alignment and x/y-expand self.departments_frame.set(0.5, 0.0, 1.0, 1.0) self.departments_frame.pack_start(self.subcat_label, False, False, 0) self.departments_frame.pack_start(self.departments, True, True, 0) # append the departments section to the page self.vbox.pack_start(self.departments_frame, False, True, 0) return def _update_appcount(self, appcount): text = gettext.ngettext("%(amount)s item available", "%(amount)s items available", appcount) % {'amount': appcount} self.appcount.set_text(text) return def _append_appcount(self): self.appcount = Gtk.Label() self.appcount.set_alignment(0.5, 0.5) self.appcount.set_margin_top(1) self.appcount.set_margin_bottom(4) self.vbox.pack_end(self.appcount, False, False, 0) return def _build_subcat_view(self): # these methods add sections to the page # changing order of methods changes order that they appear in the page self._append_subcat_departments() self._append_sub_top_rated() # NOTE that the recommended for you in category view is built and added # in the _update_recommended_for_you_in_cat method (and so is not # needed here) self._append_appcount() self._built = True return def _update_subcat_view(self, category, num_items=0): num_items = self._update_subcat_departments(category, num_items) self._update_sub_top_rated_content(category) self._update_recommended_for_you_in_cat_content(category) self._update_appcount(num_items) self.show_all() return def set_subcategory(self, root_category, num_items=0, block=False): # nothing to do if (root_category is None or self.categories == root_category.subcategories): return self.current_category = root_category self.header = root_category.name self.categories = root_category.subcategories if not self._built: self._build_subcat_view() self._update_subcat_view(root_category, num_items) GObject.idle_add(self.queue_draw) return def refresh_apps(self): supported_only = get_global_filter().supported_only if (self.current_category is None or self._supported_only == supported_only): return self._supported_only = supported_only if not self._built: self._build_subcat_view() self._update_subcat_view(self.current_category) GObject.idle_add(self.queue_draw) return
class SubCategoryViewGtk(CategoriesViewGtk): def __init__(self, datadir, desktopdir, cache, db, icons, apps_filter, apps_limit=0, root_category=None): CategoriesViewGtk.__init__(self, datadir, desktopdir, cache, db, icons, apps_filter, apps_limit) # state self._built = False # data self.root_category = root_category self.enquire = AppEnquire(self.cache, self.db) self.helper = AppPropertiesHelper(self.db, self.cache, self.icons) # sections self.current_category = None self.departments = None self.toprated = None self.appcount = None # widgetry self.vbox.set_margin_left(StockEms.MEDIUM - 2) self.vbox.set_margin_right(StockEms.MEDIUM - 2) self.vbox.set_margin_top(StockEms.MEDIUM) return def _get_sub_toprated_content(self, category): app_filter = AppFilter(self.db, self.cache) self.enquire.set_query(category.query, limit=TOP_RATED_CAROUSEL_LIMIT, sortmode=SortMethods.BY_TOP_RATED, filter=app_filter, nonapps_visible=NonAppVisibility.ALWAYS_VISIBLE, nonblocking_load=False) return self.enquire.get_documents() @wait_for_apt_cache_ready # be consistent with new apps def _update_sub_toprated_content(self, category): self.toprated.remove_all() # FIXME: should this be m = "%s %s" % (_(gettext text), header text) ?? m = _('Top Rated %s') % GObject.markup_escape_text(self.header) self.toprated_frame.set_header_label(m) docs = self._get_sub_toprated_content(category) self._add_tiles_to_flowgrid(docs, self.toprated, TOP_RATED_CAROUSEL_LIMIT) return def _append_sub_toprated(self): self.toprated = FlowableGrid() self.toprated.set_row_spacing(6) self.toprated.set_column_spacing(6) self.toprated_frame = FramedHeaderBox() self.toprated_frame.pack_start(self.toprated, True, True, 0) self.vbox.pack_start(self.toprated_frame, False, True, 0) return def _update_subcat_departments(self, category, num_items): self.departments.remove_all() # set the subcat header m = "<b><big>%s</big></b>" self.subcat_label.set_markup(m % GObject.markup_escape_text(self.header)) # sort Category.name's alphabetically sorted_cats = categories_sorted_by_name(self.categories) enquire = xapian.Enquire(self.db.xapiandb) app_filter = AppFilter(self.db, self.cache) for cat in sorted_cats: # add the subcategory if and only if it is non-empty enquire.set_query(cat.query) if len(enquire.get_mset(0, 1)): tile = CategoryTile(cat.name, cat.iconname) tile.connect('clicked', self.on_category_clicked, cat) self.departments.add_child(tile) # partialy work around a (quite rare) corner case if num_items == 0: enquire.set_query( xapian.Query(xapian.Query.OP_AND, category.query, xapian.Query("ATapplication"))) # assuming that we only want apps is not always correct ^^^ tmp_matches = enquire.get_mset(0, len(self.db), None, app_filter) num_items = tmp_matches.get_matches_estimated() # append an additional button to show all of the items in the category all_cat = Category("All", _("All"), "category-show-all", category.query) name = GObject.markup_escape_text('%s %s' % (_("All"), num_items)) tile = CategoryTile(name, "category-show-all") tile.connect('clicked', self.on_category_clicked, all_cat) self.departments.add_child(tile) self.departments.queue_draw() return num_items def _append_subcat_departments(self): self.subcat_label = Gtk.Label() self.subcat_label.set_alignment(0, 0.5) self.departments = FlowableGrid(paint_grid_pattern=False) self.departments.set_row_spacing(StockEms.SMALL) self.departments.set_column_spacing(StockEms.SMALL) frame = FramedBox(spacing=StockEms.MEDIUM, padding=StockEms.MEDIUM) # set x/y-alignment and x/y-expand frame.set(0.5, 0.0, 1.0, 1.0) frame.pack_start(self.subcat_label, False, False, 0) frame.pack_start(self.departments, True, True, 0) # append the departments section to the page self.vbox.pack_start(frame, False, True, 0) return def _update_appcount(self, appcount): text = gettext.ngettext("%(amount)s item available", "%(amount)s items available", appcount) % { 'amount': appcount, } self.appcount.set_text(text) return def _append_appcount(self): self.appcount = Gtk.Label() self.appcount.set_alignment(0.5, 0.5) self.appcount.set_margin_top(1) self.appcount.set_margin_bottom(4) self.vbox.pack_end(self.appcount, False, False, 0) return def _build_subcat_view(self): # these methods add sections to the page # changing order of methods changes order that they appear in the page self._append_subcat_departments() self._append_sub_toprated() self._append_appcount() self._built = True return def _update_subcat_view(self, category, num_items=0): num_items = self._update_subcat_departments(category, num_items) self._update_sub_toprated_content(category) self._update_appcount(num_items) self.show_all() return def set_subcategory(self, root_category, num_items=0, block=False): # nothing to do if (root_category is None or self.categories == root_category.subcategories): return self.current_category = root_category self.header = root_category.name self.categories = root_category.subcategories if not self._built: self._build_subcat_view() self._update_subcat_view(root_category, num_items) GObject.idle_add(self.queue_draw) return def refresh_apps(self): supported_only = get_global_filter().supported_only if (self.current_category is None or self._supported_only == supported_only): return self._supported_only = supported_only if not self._built: self._build_subcat_view() self._update_subcat_view(self.current_category) GObject.idle_add(self.queue_draw) return