class InstalledPane(SoftwarePane, CategoriesParser): """Widget that represents the installed panel in software-center It contains a search entry and navigation buttons """ class Pages(): # page names, useful for debugging NAMES = ('list', 'details') # the actual page id's (LIST, DETAILS) = range(2) # the default page HOME = LIST # pages for the installed view spinner notebook (PAGE_SPINNER, PAGE_INSTALLED) = range(2) __gsignals__ = {'installed-pane-created':(GObject.SignalFlags.RUN_FIRST, None, ())} def __init__(self, cache, db, distro, icons, datadir): # parent SoftwarePane.__init__(self, cache, db, distro, icons, datadir, show_ratings=False) CategoriesParser.__init__(self, db) self.current_appview_selection = None self.icons = icons self.loaded = False self.pane_name = _("Installed Software") self.installed_apps = 0 # None is local self.current_hostid = None self.current_hostname = None self.oneconf_additional_pkg = set() self.oneconf_missing_pkg = set() # switches to terminate build in progress self._build_in_progress = False self._halt_build = False self.nonapps_visible = NonAppVisibility.NEVER_VISIBLE self.visible_docids = None self.visible_cats = {} def init_view(self): if self.view_initialized: return SoftwarePane.init_view(self) # show a busy cursor and display the main spinner while we build the view window = self.get_window() if window: window.set_cursor(self.busy_cursor) self.show_appview_spinner() self.oneconf_viewpickler = OneConfViews(self.icons) self.oneconf_viewpickler.register_computer(None, _("This computer (%s)") % platform.node()) self.oneconf_viewpickler.select_first() self.oneconf_viewpickler.connect('computer-changed', self._selected_computer_changed) self.oneconf_viewpickler.connect('current-inventory-refreshed', self._current_inventory_need_refresh) # Start OneConf self.oneconf_handler = get_oneconf_handler(self.oneconf_viewpickler) if self.oneconf_handler: self.oneconf_handler.connect('show-oneconf-changed', self._show_oneconf_changed) self.oneconf_handler.connect('last-time-sync-changed', self._last_time_sync_oneconf_changed) # OneConf pane self.computerpane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL) self.oneconfcontrol = Gtk.Box() self.oneconfcontrol.set_orientation(Gtk.Orientation.VERTICAL) self.computerpane.pack1(self.oneconfcontrol, False, False) # size negotiation takes everything for the first one self.oneconfcontrol.set_property('width-request', 200) self.box_app_list.pack_start(self.computerpane, True, True, 0) scroll = Gtk.ScrolledWindow() scroll.set_shadow_type(Gtk.ShadowType.IN) scroll.add(self.oneconf_viewpickler) self.oneconfcontrol.pack_start(scroll, True, True, 0) oneconftoolbar = Gtk.Box() oneconftoolbar.set_orientation(Gtk.Orientation.HORIZONTAL) oneconfpropertymenu = Gtk.Menu() self.oneconfproperty = MenuButton(oneconfpropertymenu, Gtk.Image.new_from_stock(Gtk.STOCK_PROPERTIES, Gtk.IconSize.BUTTON)) stop_oneconf_share_menuitem = Gtk.MenuItem(label=_("Stop Syncing “%s”") % platform.node()) stop_oneconf_share_menuitem.connect("activate", self._on_stop_showing_oneconf_clicked) stop_oneconf_share_menuitem.show() oneconfpropertymenu.append(stop_oneconf_share_menuitem) self.oneconfcontrol.pack_start(oneconftoolbar, False, False, 1) self.oneconf_last_sync = Gtk.Label() self.oneconf_last_sync.set_line_wrap(True) oneconftoolbar.pack_start(self.oneconfproperty, False, False, 0) oneconftoolbar.pack_start(self.oneconf_last_sync, True, True, 1) self.notebook.append_page(self.box_app_list, Gtk.Label(label="list")) # details self.notebook.append_page(self.scroll_details, Gtk.Label(label="details")) # initial refresh self.state.search_term = "" # build models and filters self.base_model = AppTreeStore(self.db, self.cache, self.icons) self.treefilter = self.base_model.filter_new(None) self.treefilter.set_visible_func(self._row_visibility_func, AppTreeStore.COL_ROW_DATA) self.app_view.set_model(self.treefilter) self.app_view.tree_view.connect("row-collapsed", self._on_row_collapsed) self._all_cats = self.parse_applications_menu('/usr/share/app-install') self._all_cats = categories_sorted_by_name(self._all_cats) # we do not support the search aid feature in the installedview self.box_app_list.remove(self.search_aid) # create a local spinner notebook for the installed view self.installed_spinner_view = SpinnerView() self.installed_spinner_notebook = Gtk.Notebook() self.installed_spinner_notebook.set_show_tabs(False) self.installed_spinner_notebook.set_show_border(False) self.installed_spinner_notebook.append_page(self.installed_spinner_view, None) self.box_app_list.remove(self.app_view) self.installed_spinner_notebook.append_page(self.app_view, None) self.computerpane.pack2(self.installed_spinner_notebook, True, True) self.show_installed_view_spinner() self.show_all() # initialize view to hide the oneconf computer selector self.oneconf_viewpickler.select_first() self.oneconfcontrol.hide() # hacky, hide the header self.app_view.header_hbox.hide() # now we are initialized self.emit("installed-pane-created") self.view_initialized = True return False def show_installed_view_spinner(self): """ display the local spinner for the installed view panel """ self.installed_spinner_view.stop() self.installed_spinner_notebook.set_current_page(self.PAGE_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_installed_view_spinner) def _unmask_installed_view_spinner(self): self.installed_spinner_view.start() return False def hide_installed_view_spinner(self): """ hide the local spinner for the installed view panel """ self.installed_spinner_notebook.set_current_page(self.PAGE_INSTALLED) self.installed_spinner_view.stop() def _selected_computer_changed(self, oneconf_pickler, hostid, hostname): if self.current_hostid == hostid: return LOG.debug("Selected computer changed to %s (%s)" % (hostid, hostname)) self.current_hostid = hostid self.current_hostname = hostname if self.current_hostid: (self.oneconf_additional_pkg, self.oneconf_missing_pkg) = self.oneconf_handler.oneconf.diff(self.current_hostid, '') # FIXME for P: oneconf views don't support search if self.state.search_term: self._search() else: self.searchentry.show() self.refresh_apps() def _last_time_sync_oneconf_changed(self, oneconf_handler, msg): LOG.debug("refresh latest sync date") self.oneconf_last_sync.set_label(msg) def _show_oneconf_changed(self, oneconf_handler, oneconf_inventory_shown): LOG.debug('Share inventory status changed') if oneconf_inventory_shown: self.oneconfcontrol.show() else: self.oneconf_viewpickler.select_first() self.oneconfcontrol.hide() def _on_stop_showing_oneconf_clicked(self, widget): LOG.debug("Stop sharing the current computer inventory") self.oneconf_handler.sync_between_computers(False) def _current_inventory_need_refresh(self, oneconfviews): if self.current_hostid: (self.oneconf_additional_pkg, self.oneconf_missing_pkg) = self.oneconf_handler.oneconf.diff(self.current_hostid, '') self.refresh_apps() def _on_row_collapsed(self, view, it, path): return def _row_visibility_func(self, model, it, col): row = model.get_value(it, col) if self.visible_docids is None: if isinstance(row, CategoryRowReference): row.vis_count = row.pkg_count return True elif isinstance(row, CategoryRowReference): return row.untranslated_name in self.visible_cats.keys() elif row is None: return False return row.get_docid() in self.visible_docids def _use_category(self, cat): # System cat is large and slow to search, filter it in default mode if ('carousel-only' in cat.flags or ((self.nonapps_visible == NonAppVisibility.NEVER_VISIBLE) and cat.untranslated_name == 'System')): return False return True # override its SoftwarePane._hide_nonapp_pkgs... def _hide_nonapp_pkgs(self): self.nonapps_visible = NonAppVisibility.NEVER_VISIBLE self.refresh_apps() #~ @interrupt_build_and_wait def _build_categorised_installedview(self): LOG.debug('Rebuilding categorised installedview...') # display the busy cursor and a local spinner while we build the view window = self.get_window() if window: window.set_cursor(self.busy_cursor) self.show_installed_view_spinner() model = self.base_model # base model not treefilter model.clear() def rebuild_categorised_view(): self.cat_docid_map = {} enq = self.enquirer i = 0 while Gtk.events_pending(): Gtk.main_iteration() xfilter = AppFilter(self.db, self.cache) xfilter.set_installed_only(True) for cat in self._all_cats: # for each category do category query and append as a new # node to tree_view if not self._use_category(cat): continue query = self.get_query_for_cat(cat) LOG.debug("xfilter.installed_only: %s" % xfilter.installed_only) enq.set_query(query, sortmode=SortMethods.BY_ALPHABET, nonapps_visible=self.nonapps_visible, filter=xfilter, nonblocking_load=False, persistent_duplicate_filter=(i>0)) L = len(enq.matches) if L: i += L docs = enq.get_documents() self.cat_docid_map[cat.untranslated_name] = \ set([doc.get_docid() for doc in docs]) model.set_category_documents(cat, docs) while Gtk.events_pending(): Gtk.main_iteration() # check for uncategorised pkgs if self.state.channel: self._run_channel_enquirer(persistent_duplicate_filter=(i>0)) L = len(enq.matches) if L: # some foo for channels # if no categorised results but in channel, then use # the channel name for the category channel_name = None if not i and self.state.channel: channel_name = self.state.channel.display_name docs = enq.get_documents() tag = channel_name or 'Uncategorized' self.cat_docid_map[tag] = set([doc.get_docid() for doc in docs]) model.set_nocategory_documents(docs, untranslated_name=tag, display_name=channel_name) i += L if i: self.app_view.tree_view.set_cursor(Gtk.TreePath(), None, False) if i <= 10: self.app_view.tree_view.expand_all() # cache the installed app count self.installed_count = i self.app_view._append_appcount(self.installed_count, mode=AppView.INSTALLED_MODE) # hide the local spinner self.hide_installed_view_spinner() # hide the main spinner (if it's showing) self.hide_appview_spinner() if window: window.set_cursor(None) # reapply search if needed if self.state.search_term: self._do_search(self.state.search_term) self.emit("app-list-changed", i) return GObject.idle_add(rebuild_categorised_view) return def _build_oneconfview(self): LOG.debug('Rebuilding oneconfview for %s...' % self.current_hostid) # display the busy cursor and the local spinner while we build the view window = self.get_window() if window: window.set_cursor(self.busy_cursor) self.show_installed_view_spinner() model = self.base_model # base model not treefilter model.clear() def rebuild_oneconfview(): # FIXME for P: hide the search entry self.searchentry.hide() self.cat_docid_map = {} enq = self.enquirer query = xapian.Query("") if self.state.channel and self.state.channel.query: query = xapian.Query(xapian.Query.OP_AND, query, self.state.channel.query) i = 0 # First search: missing apps only xfilter = AppFilter(self.db, self.cache) xfilter.set_restricted_list(self.oneconf_additional_pkg) xfilter.set_not_installed_only(True) enq.set_query(query, sortmode=SortMethods.BY_ALPHABET, nonapps_visible=self.nonapps_visible, filter=xfilter, nonblocking_load=True, # we don't block this one for better oneconf responsiveness persistent_duplicate_filter=(i>0)) L = len(enq.matches) if L: cat_title = ngettext('%(amount)s item on “%(machine)s” not on this computer', '%(amount)s items on “%(machine)s” not on this computer', L) % { 'amount' : L, 'machine': self.current_hostname} i += L docs = enq.get_documents() self.cat_docid_map["missingpkg"] = set([doc.get_docid() for doc in docs]) model.set_nocategory_documents(docs, untranslated_name="additionalpkg", display_name=cat_title) # Second search: additional apps xfilter.set_restricted_list(self.oneconf_missing_pkg) xfilter.set_not_installed_only(False) xfilter.set_installed_only(True) enq.set_query(query, sortmode=SortMethods.BY_ALPHABET, nonapps_visible=self.nonapps_visible, filter=xfilter, nonblocking_load=False, persistent_duplicate_filter=(i>0)) L = len(enq.matches) if L: cat_title = ngettext('%(amount)s item on this computer not on “%(machine)s”', '%(amount)s items on this computer not on “%(machine)s”', L) % { 'amount' : L, 'machine': self.current_hostname} i += L docs = enq.get_documents() self.cat_docid_map["additionalpkg"] = set([doc.get_docid() for doc in docs]) model.set_nocategory_documents(docs, untranslated_name="additionalpkg", display_name=cat_title) if i: self.app_view.tree_view.set_cursor(Gtk.TreePath(), None, False) if i <= 10: self.app_view.tree_view.expand_all() # cache the installed app count self.installed_count = i self.app_view._append_appcount(self.installed_count, mode=AppView.DIFF_MODE) # hide the local spinner self.hide_installed_view_spinner() if window: window.set_cursor(None) self.emit("app-list-changed", i) return GObject.idle_add(rebuild_oneconfview) return def _check_expand(self): it = self.treefilter.get_iter_first() while it: path = self.treefilter.get_path(it) if self.state.search_term:# or path in self._user_expanded_paths: self.app_view.tree_view.expand_row(path, False) else: self.app_view.tree_view.collapse_row(path) it = self.treefilter.iter_next(it) return def _do_search(self, terms): self.state.search_term = terms xfilter = AppFilter(self.db, self.cache) xfilter.set_installed_only(True) self.enquirer.set_query(self.get_query(), nonapps_visible=self.nonapps_visible, filter=xfilter, nonblocking_load=True) self.visible_docids = self.enquirer.get_docids() self.visible_cats = self._get_vis_cats(self.visible_docids) self.treefilter.refilter() self.app_view.tree_view.expand_all() def _run_channel_enquirer(self, persistent_duplicate_filter=True): xfilter = AppFilter(self.db, self.cache) xfilter.set_installed_only(True) if self.state.channel: self.enquirer.set_query( self.state.channel.query, sortmode=SortMethods.BY_ALPHABET, nonapps_visible=NonAppVisibility.MAYBE_VISIBLE, filter=xfilter, nonblocking_load=False, persistent_duplicate_filter=persistent_duplicate_filter) def _search(self, terms=None): if not terms: self.visible_docids = None self.state.search_term = "" self._clear_search() self.treefilter.refilter() self._check_expand() # run channel enquirer to ensure that the channel specific # info for show/hide nonapps is actually correct self._run_channel_enquirer() # trigger update of the show/hide self.emit("app-list-changed", 0) elif self.state.search_term != terms: self._do_search(terms) return def get_query(self): # search terms return self.db.get_query_list_from_search_entry( self.state.search_term) def get_query_for_cat(self, cat): LOG.debug("self.state.channel: %s" % self.state.channel) if self.state.channel and self.state.channel.query: query = xapian.Query(xapian.Query.OP_AND, cat.query, self.state.channel.query) return query return cat.query @wait_for_apt_cache_ready def refresh_apps(self, *args, **kwargs): """refresh the applist and update the navigation bar """ logging.debug("installedpane refresh_apps") if self.current_hostid: self._build_oneconfview() else: self._build_categorised_installedview() return def _clear_search(self): # remove the details and clear the search self.searchentry.clear_with_no_signal() def on_search_terms_changed(self, searchentry, terms): """callback when the search entry widget changes""" logging.debug("on_search_terms_changed: '%s'" % terms) self._search(terms.strip()) self.state.search_term = terms self.notebook.set_current_page(InstalledPane.Pages.LIST) self.hide_installed_view_spinner() return def _get_vis_cats(self, visids): vis_cats = {} appcount = 0 visids = set(visids) for cat_uname, docids in self.cat_docid_map.iteritems(): children = len(docids & visids) if children: appcount += children vis_cats[cat_uname] = children self.app_view._append_appcount(appcount, mode=AppView.DIFF_MODE) return vis_cats def on_db_reopen(self, db): self.refresh_apps(rebuild=True) self.app_details_view.refresh_app() def on_application_selected(self, appview, app): """callback when an app is selected""" logging.debug("on_application_selected: '%s'" % app) self.current_appview_selection = app def get_callback_for_page(self, page, state): if page == InstalledPane.Pages.LIST: return self.display_overview_page return self.display_details_page def display_search(self): model = self.app_view.get_model() if model: self.emit("app-list-changed", len(model)) self.searchentry.show() def display_overview_page(self, page, view_state): LOG.debug("view_state: %s" % view_state) if self.current_hostid: # FIXME for P: oneconf views don't support search # this one ensure that even when switching between pane, we # don't have the search item if self.state.search_term: self._search() self._build_oneconfview() else: self._build_categorised_installedview() if self.state.search_term: self._search(self.state.search_term) return True def get_current_app(self): """return the current active application object applicable to the context""" return self.current_appview_selection def is_category_view_showing(self): # there is no category view in the installed pane return False def is_applist_view_showing(self): """Return True if we are in the applist view """ return (self.notebook.get_current_page() == InstalledPane.Pages.LIST) def is_app_details_view_showing(self): """Return True if we are in the app_details view """ return self.notebook.get_current_page() == InstalledPane.Pages.DETAILS
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): if self.view_initialized: return SoftwarePane.init_view(self) # show a busy cursor and display the main spinner while we build the view window = self.get_window() if window: window.set_cursor(self.busy_cursor) self.show_appview_spinner() self.oneconf_viewpickler = OneConfViews(self.icons) self.oneconf_viewpickler.register_computer(None, _("This computer (%s)") % platform.node()) self.oneconf_viewpickler.select_first() self.oneconf_viewpickler.connect('computer-changed', self._selected_computer_changed) self.oneconf_viewpickler.connect('current-inventory-refreshed', self._current_inventory_need_refresh) # Start OneConf self.oneconf_handler = get_oneconf_handler(self.oneconf_viewpickler) if self.oneconf_handler: self.oneconf_handler.connect('show-oneconf-changed', self._show_oneconf_changed) self.oneconf_handler.connect('last-time-sync-changed', self._last_time_sync_oneconf_changed) # OneConf pane self.computerpane = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL) self.oneconfcontrol = Gtk.Box() self.oneconfcontrol.set_orientation(Gtk.Orientation.VERTICAL) self.computerpane.pack1(self.oneconfcontrol, False, False) # size negotiation takes everything for the first one self.oneconfcontrol.set_property('width-request', 200) self.box_app_list.pack_start(self.computerpane, True, True, 0) scroll = Gtk.ScrolledWindow() scroll.set_shadow_type(Gtk.ShadowType.IN) scroll.add(self.oneconf_viewpickler) self.oneconfcontrol.pack_start(scroll, True, True, 0) oneconftoolbar = Gtk.Box() oneconftoolbar.set_orientation(Gtk.Orientation.HORIZONTAL) oneconfpropertymenu = Gtk.Menu() self.oneconfproperty = MenuButton(oneconfpropertymenu, Gtk.Image.new_from_stock(Gtk.STOCK_PROPERTIES, Gtk.IconSize.BUTTON)) stop_oneconf_share_menuitem = Gtk.MenuItem(label=_("Stop Syncing “%s”") % platform.node()) stop_oneconf_share_menuitem.connect("activate", self._on_stop_showing_oneconf_clicked) stop_oneconf_share_menuitem.show() oneconfpropertymenu.append(stop_oneconf_share_menuitem) self.oneconfcontrol.pack_start(oneconftoolbar, False, False, 1) self.oneconf_last_sync = Gtk.Label() self.oneconf_last_sync.set_line_wrap(True) oneconftoolbar.pack_start(self.oneconfproperty, False, False, 0) oneconftoolbar.pack_start(self.oneconf_last_sync, True, True, 1) self.notebook.append_page(self.box_app_list, Gtk.Label(label="list")) # details self.notebook.append_page(self.scroll_details, Gtk.Label(label="details")) # initial refresh self.state.search_term = "" # build models and filters self.base_model = AppTreeStore(self.db, self.cache, self.icons) self.treefilter = self.base_model.filter_new(None) self.treefilter.set_visible_func(self._row_visibility_func, AppTreeStore.COL_ROW_DATA) self.app_view.set_model(self.treefilter) self.app_view.tree_view.connect("row-collapsed", self._on_row_collapsed) self._all_cats = self.parse_applications_menu('/usr/share/app-install') self._all_cats = categories_sorted_by_name(self._all_cats) # we do not support the search aid feature in the installedview self.box_app_list.remove(self.search_aid) # create a local spinner notebook for the installed view self.installed_spinner_view = SpinnerView() self.installed_spinner_notebook = Gtk.Notebook() self.installed_spinner_notebook.set_show_tabs(False) self.installed_spinner_notebook.set_show_border(False) self.installed_spinner_notebook.append_page(self.installed_spinner_view, None) self.box_app_list.remove(self.app_view) self.installed_spinner_notebook.append_page(self.app_view, None) self.computerpane.pack2(self.installed_spinner_notebook, True, True) self.show_installed_view_spinner() self.show_all() # initialize view to hide the oneconf computer selector self.oneconf_viewpickler.select_first() self.oneconfcontrol.hide() # hacky, hide the header self.app_view.header_hbox.hide() # now we are initialized self.emit("installed-pane-created") self.view_initialized = True return False
def __init__(self, cache, db, distro, icons, datadir): Gtk.VBox.__init__(self) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.apps_filter = None self.state = DisplayState() self.pane_name = _("History") # Icon cache, invalidated upon icon theme changes self._app_icon_cache = {} self._reset_icon_cache() self.icons.connect('changed', self._reset_icon_cache) self._emblems = {} self._get_emblems(self.icons) vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.toolbar = Gtk.Toolbar() self.toolbar.show() self.toolbar.set_style(Gtk.ToolbarStyle.TEXT) self.pack_start(self.toolbar, False, True, 0) all_action = Gtk.RadioAction('filter_all', _('All Changes'), None, None, self.ALL) all_action.connect('changed', self.change_filter) all_button = all_action.create_tool_item() self.toolbar.insert(all_button, 0) installs_action = Gtk.RadioAction('filter_installs', _('Installations'), None, None, self.INSTALLED) installs_action.join_group(all_action) installs_button = installs_action.create_tool_item() self.toolbar.insert(installs_button, 1) upgrades_action = Gtk.RadioAction( 'filter_upgrads', _('Updates'), None, None, self.UPGRADED) upgrades_action.join_group(all_action) upgrades_button = upgrades_action.create_tool_item() self.toolbar.insert(upgrades_button, 2) removals_action = Gtk.RadioAction( 'filter_removals', _('Removals'), None, None, self.REMOVED) removals_action.join_group(all_action) removals_button = removals_action.create_tool_item() self.toolbar.insert(removals_button, 3) self.toolbar.connect('draw', self.on_toolbar_draw) self._actions_list = all_action.get_group() self._set_actions_sensitive(False) self.view = Gtk.TreeView() self.view.set_headers_visible(False) self.view.show() self.history_view = Gtk.ScrolledWindow() self.history_view.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.history_view.show() self.history_view.add(self.view) # make a spinner to display while history is loading self.spinner_view = SpinnerView(_('Loading history')) 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.history_view, None) self.spinner_notebook.append_page(self.spinner_view, None) self.pack_start(self.spinner_notebook, True, True, 0) self.store = Gtk.TreeStore(*self.COL_TYPES) self.visible_changes = 0 self.store_filter = self.store.filter_new(None) self.store_filter.set_visible_func(self.filter_row, None) self.view.set_model(self.store_filter) all_action.set_active(True) self.last = None # to save (a lot of) time at startup we load history later, only when # it is selected to be viewed self.history = None self.column = Gtk.TreeViewColumn(_('Date')) self.view.append_column(self.column) self.cell_icon = Gtk.CellRendererPixbuf() self.column.pack_start(self.cell_icon, False) self.column.set_cell_data_func(self.cell_icon, self.render_cell_icon) self.cell_text = Gtk.CellRendererText() self.column.pack_start(self.cell_text, True) self.column.set_cell_data_func(self.cell_text, self.render_cell_text) # busy cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH)
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 HistoryPane(Gtk.VBox, BasePane): __gsignals__ = { "app-list-changed" : (GObject.SignalFlags.RUN_LAST, None, (int, ), ), "history-pane-created" : (GObject.SignalFlags.RUN_FIRST, None, ()), } (COL_WHEN, COL_ACTION, COL_PKG) = range(3) COL_TYPES = (object, int, str) (ALL, INSTALLED, REMOVED, UPGRADED) = range(4) ICON_SIZE = 32 PADDING = 6 # pages for the spinner notebook (PAGE_HISTORY_VIEW, PAGE_SPINNER) = range(2) def __init__(self, cache, db, distro, icons, datadir): Gtk.VBox.__init__(self) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.apps_filter = None self.state = DisplayState() self.pane_name = _("History") # Icon cache, invalidated upon icon theme changes self._app_icon_cache = {} self._reset_icon_cache() self.icons.connect('changed', self._reset_icon_cache) self._emblems = {} self._get_emblems(self.icons) vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.toolbar = Gtk.Toolbar() self.toolbar.show() self.toolbar.set_style(Gtk.ToolbarStyle.TEXT) self.pack_start(self.toolbar, False, True, 0) all_action = Gtk.RadioAction('filter_all', _('All Changes'), None, None, self.ALL) all_action.connect('changed', self.change_filter) all_button = all_action.create_tool_item() self.toolbar.insert(all_button, 0) installs_action = Gtk.RadioAction('filter_installs', _('Installations'), None, None, self.INSTALLED) installs_action.join_group(all_action) installs_button = installs_action.create_tool_item() self.toolbar.insert(installs_button, 1) upgrades_action = Gtk.RadioAction( 'filter_upgrads', _('Updates'), None, None, self.UPGRADED) upgrades_action.join_group(all_action) upgrades_button = upgrades_action.create_tool_item() self.toolbar.insert(upgrades_button, 2) removals_action = Gtk.RadioAction( 'filter_removals', _('Removals'), None, None, self.REMOVED) removals_action.join_group(all_action) removals_button = removals_action.create_tool_item() self.toolbar.insert(removals_button, 3) self.toolbar.connect('draw', self.on_toolbar_draw) self._actions_list = all_action.get_group() self._set_actions_sensitive(False) self.view = Gtk.TreeView() self.view.set_headers_visible(False) self.view.show() self.history_view = Gtk.ScrolledWindow() self.history_view.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.history_view.show() self.history_view.add(self.view) # make a spinner to display while history is loading self.spinner_view = SpinnerView(_('Loading history')) 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.history_view, None) self.spinner_notebook.append_page(self.spinner_view, None) self.pack_start(self.spinner_notebook, True, True, 0) self.store = Gtk.TreeStore(*self.COL_TYPES) self.visible_changes = 0 self.store_filter = self.store.filter_new(None) self.store_filter.set_visible_func(self.filter_row, None) self.view.set_model(self.store_filter) all_action.set_active(True) self.last = None # to save (a lot of) time at startup we load history later, only when # it is selected to be viewed self.history = None self.column = Gtk.TreeViewColumn(_('Date')) self.view.append_column(self.column) self.cell_icon = Gtk.CellRendererPixbuf() self.column.pack_start(self.cell_icon, False) self.column.set_cell_data_func(self.cell_icon, self.render_cell_icon) self.cell_text = Gtk.CellRendererText() self.column.pack_start(self.cell_text, True) self.column.set_cell_data_func(self.cell_text, self.render_cell_text) # busy cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH) def init_view(self): if self.history == None: # if the history is not yet initialized we have to load and parse it # show a spinner while we do that self.realize() window = self.get_window() window.set_cursor(self.busy_cursor) self.spinner_view.start() self.spinner_notebook.set_current_page(self.PAGE_SPINNER) self.load_and_parse_history() self.spinner_notebook.set_current_page(self.PAGE_HISTORY_VIEW) self.spinner_view.stop() self._set_actions_sensitive(True) window.set_cursor(None) self.emit("history-pane-created") def on_toolbar_draw(self, widget, cr): a = widget.get_allocation() context = widget.get_style_context() color = context.get_border_color(widget.get_state_flags()) cr.set_source_rgba(color.red, color.green, color.blue, 0.5) cr.set_line_width(1) cr.move_to(0.5, a.height - 0.5) cr.rel_line_to(a.width-1, 0) cr.stroke() return def _get_emblems(self, icons): emblem_names = ("package-install", "package-remove", "package-upgrade") for i, emblem in enumerate(emblem_names): pb = icons.load_icon(emblem, self.ICON_SIZE, 0) self._emblems[i + 1] = pb return def _set_actions_sensitive(self, sensitive): for action in self._actions_list: action.set_sensitive(sensitive) def _reset_icon_cache(self, theme=None): self._app_icon_cache.clear() try: missing = self.icons.load_icon(Icons.MISSING_APP, self.ICON_SIZE, 0) except GObject.GError: missing = None self._app_icon_cache[Icons.MISSING_APP] = missing def load_and_parse_history(self): from softwarecenter.db.history import get_pkg_history self.history = get_pkg_history() # FIXME: a signal from AptHistory is nicer while not self.history.history_ready: while Gtk.events_pending(): Gtk.main_iteration() self.parse_history() self.history.set_on_update(self.parse_history) def parse_history(self): date = None when = None last_row = None day = self.store.get_iter_first() if day is not None: date = self.store.get_value(day, self.COL_WHEN) if len(self.history.transactions) == 0: logging.debug("AptHistory is currently empty") return new_last = self.history.transactions[0].start_date for trans in self.history.transactions: while Gtk.events_pending(): Gtk.main_iteration() when = trans.start_date if self.last is not None and when <= self.last: break if when.date() != date: date = when.date() day = self.store.append(None, (date, self.ALL, None)) last_row = None actions = {self.INSTALLED: trans.install, self.REMOVED: trans.remove, self.UPGRADED: trans.upgrade, } for action, pkgs in actions.items(): for pkgname in pkgs: row = (when, action, pkgname) last_row = self.store.insert_after(day, last_row, row) self.last = new_last self.update_view() def get_current_page(self): # single page views can return None here return None def get_callback_for_page(self, page, state): # single page views can return None here return None def on_search_terms_changed(self, entry, terms): self.update_view() 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 change_filter(self, action, current): self.filter = action.get_current_value() self.update_view() def update_view(self): self.store_filter.refilter() self.view.collapse_all() # Expand all the matching rows if self.searchentry.get_text(): self.view.expand_all() # Compute the number of visible changes # don't do this atm - the spec doesn't mention that the history pane # should have a status text and it gives us a noticable performance gain # if we don't calculate this # self.visible_changes = 0 # day = self.store_filter.get_iter_first() # while day is not None: # self.visible_changes += self.store_filter.iter_n_children(day) # day = self.store_filter.iter_next(day) # Expand the most recent day day = self.store.get_iter_first() if day is not None: path = self.store.get_path(day) self.view.expand_row(path, False) self.view.scroll_to_cell(path) # self.emit('app-list-changed', self.visible_changes) def _row_matches(self, store, iter): # Whether a child row matches the current filter and the search entry pkg = store.get_value(iter, self.COL_PKG) or '' filter_values = (self.ALL, store.get_value(iter, self.COL_ACTION)) filter_matches = self.filter in filter_values search_matches = self.searchentry.get_text().lower() in pkg.lower() return filter_matches and search_matches def filter_row(self, store, iter, user_data): pkg = store.get_value(iter, self.COL_PKG) if pkg is not None: return self._row_matches(store, iter) else: i = store.iter_children(iter) while i is not None: if self._row_matches(store, i): return True i = store.iter_next(i) return False def render_cell_icon(self, column, cell, store, iter, user_data): pkg = store.get_value(iter, self.COL_PKG) if pkg is None: cell.set_visible(False) return cell.set_visible(True) when = store.get_value(iter, self.COL_WHEN) if isinstance(when, datetime.datetime): action = store.get_value(iter, self.COL_ACTION) cell.set_property('pixbuf', self._emblems[action]) #~ icon_name = Icons.MISSING_APP #~ for m in self.db.xapiandb.postlist("AP" + pkg): #~ doc = self.db.xapiandb.get_document(m.docid) #~ icon_value = doc.get_value(XapianValues.ICON) #~ if icon_value: #~ icon_name = os.path.splitext(icon_value)[0] #~ break #~ if icon_name in self._app_icon_cache: #~ icon = self._app_icon_cache[icon_name] #~ else: #~ try: #~ icon = self.icons.load_icon(icon_name, self.ICON_SIZE, 0) #~ except GObject.GError: #~ icon = self._app_icon_cache[Icons.MISSING_APP] #~ self._app_icon_cache[icon_name] = icon def render_cell_text(self, column, cell, store, iter, user_data): when = store.get_value(iter, self.COL_WHEN) if isinstance(when, datetime.datetime): action = store.get_value(iter, self.COL_ACTION) pkg = store.get_value(iter, self.COL_PKG) subs = {'pkgname': pkg, 'color': '#8A8A8A', # Translators : time displayed in history, display hours (0-12), minutes and AM/PM. %H should be used instead of %I to display hours 0-24 'time': when.time().strftime(_('%I:%M %p')), } if action == self.INSTALLED: text = _('%(pkgname)s <span color="%(color)s">installed %(time)s</span>') % subs elif action == self.REMOVED: text = _('%(pkgname)s <span color="%(color)s">removed %(time)s</span>') % subs elif action == self.UPGRADED: text = _('%(pkgname)s <span color="%(color)s">updated %(time)s</span>') % subs elif isinstance(when, datetime.date): today = datetime.date.today() monday = today - datetime.timedelta(days=today.weekday()) if when == today: text = _("Today") elif when >= monday: # Current week, display the name of the day text = when.strftime(_('%A')) else: if when.year == today.year: # Current year, display the day and month text = when.strftime(_('%d %B')) else: # Display the full date: day, month, year text = when.strftime(_('%d %B %Y')) cell.set_property('markup', text)
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
def __init__(self, cache, db, distro, icons, datadir): Gtk.VBox.__init__(self) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.apps_filter = None self.state = DisplayState() self.pane_name = _("History") # Icon cache, invalidated upon icon theme changes self._app_icon_cache = {} self._reset_icon_cache() self.icons.connect('changed', self._reset_icon_cache) self._emblems = {} self._get_emblems(self.icons) vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.toolbar = Gtk.Toolbar() self.toolbar.show() self.toolbar.set_style(Gtk.ToolbarStyle.TEXT) self.pack_start(self.toolbar, False, True, 0) all_action = Gtk.RadioAction('filter_all', _('All Changes'), None, None, self.ALL) all_action.connect('changed', self.change_filter) all_button = all_action.create_tool_item() self.toolbar.insert(all_button, 0) installs_action = Gtk.RadioAction('filter_installs', _('Installations'), None, None, self.INSTALLED) installs_action.join_group(all_action) installs_button = installs_action.create_tool_item() self.toolbar.insert(installs_button, 1) upgrades_action = Gtk.RadioAction('filter_upgrads', _('Updates'), None, None, self.UPGRADED) upgrades_action.join_group(all_action) upgrades_button = upgrades_action.create_tool_item() self.toolbar.insert(upgrades_button, 2) removals_action = Gtk.RadioAction('filter_removals', _('Removals'), None, None, self.REMOVED) removals_action.join_group(all_action) removals_button = removals_action.create_tool_item() self.toolbar.insert(removals_button, 3) self.toolbar.connect('draw', self.on_toolbar_draw) self._actions_list = all_action.get_group() self._set_actions_sensitive(False) self.view = Gtk.TreeView() self.view.set_headers_visible(False) self.view.show() self.history_view = Gtk.ScrolledWindow() self.history_view.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.history_view.show() self.history_view.add(self.view) # make a spinner to display while history is loading self.spinner_view = SpinnerView(_('Loading history')) 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.history_view, None) self.spinner_notebook.append_page(self.spinner_view, None) self.pack_start(self.spinner_notebook, True, True, 0) self.store = Gtk.TreeStore(*self.COL_TYPES) self.visible_changes = 0 self.store_filter = self.store.filter_new(None) self.store_filter.set_visible_func(self.filter_row, None) self.view.set_model(self.store_filter) all_action.set_active(True) self.last = None # to save (a lot of) time at startup we load history later, only when # it is selected to be viewed self.history = None self.column = Gtk.TreeViewColumn(_('Date')) self.view.append_column(self.column) self.cell_icon = Gtk.CellRendererPixbuf() self.column.pack_start(self.cell_icon, False) self.column.set_cell_data_func(self.cell_icon, self.render_cell_icon) self.cell_text = Gtk.CellRendererText() self.column.pack_start(self.cell_text, True) self.column.set_cell_data_func(self.cell_text, self.render_cell_text) # busy cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH)
class HistoryPane(Gtk.VBox, BasePane): __gsignals__ = { "app-list-changed": ( GObject.SignalFlags.RUN_LAST, None, (int, ), ), "history-pane-created": (GObject.SignalFlags.RUN_FIRST, None, ()), } (COL_WHEN, COL_ACTION, COL_PKG) = range(3) COL_TYPES = (object, int, str) (ALL, INSTALLED, REMOVED, UPGRADED) = range(4) ICON_SIZE = 32 PADDING = 6 # pages for the spinner notebook (PAGE_HISTORY_VIEW, PAGE_SPINNER) = range(2) def __init__(self, cache, db, distro, icons, datadir): Gtk.VBox.__init__(self) self.cache = cache self.db = db self.distro = distro self.icons = icons self.datadir = datadir self.apps_filter = None self.state = DisplayState() self.pane_name = _("History") # Icon cache, invalidated upon icon theme changes self._app_icon_cache = {} self._reset_icon_cache() self.icons.connect('changed', self._reset_icon_cache) self._emblems = {} self._get_emblems(self.icons) vm = get_viewmanager() self.searchentry = vm.get_global_searchentry() self.toolbar = Gtk.Toolbar() self.toolbar.show() self.toolbar.set_style(Gtk.ToolbarStyle.TEXT) self.pack_start(self.toolbar, False, True, 0) all_action = Gtk.RadioAction('filter_all', _('All Changes'), None, None, self.ALL) all_action.connect('changed', self.change_filter) all_button = all_action.create_tool_item() self.toolbar.insert(all_button, 0) installs_action = Gtk.RadioAction('filter_installs', _('Installations'), None, None, self.INSTALLED) installs_action.join_group(all_action) installs_button = installs_action.create_tool_item() self.toolbar.insert(installs_button, 1) upgrades_action = Gtk.RadioAction('filter_upgrads', _('Updates'), None, None, self.UPGRADED) upgrades_action.join_group(all_action) upgrades_button = upgrades_action.create_tool_item() self.toolbar.insert(upgrades_button, 2) removals_action = Gtk.RadioAction('filter_removals', _('Removals'), None, None, self.REMOVED) removals_action.join_group(all_action) removals_button = removals_action.create_tool_item() self.toolbar.insert(removals_button, 3) self.toolbar.connect('draw', self.on_toolbar_draw) self._actions_list = all_action.get_group() self._set_actions_sensitive(False) self.view = Gtk.TreeView() self.view.set_headers_visible(False) self.view.show() self.history_view = Gtk.ScrolledWindow() self.history_view.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.history_view.show() self.history_view.add(self.view) # make a spinner to display while history is loading self.spinner_view = SpinnerView(_('Loading history')) 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.history_view, None) self.spinner_notebook.append_page(self.spinner_view, None) self.pack_start(self.spinner_notebook, True, True, 0) self.store = Gtk.TreeStore(*self.COL_TYPES) self.visible_changes = 0 self.store_filter = self.store.filter_new(None) self.store_filter.set_visible_func(self.filter_row, None) self.view.set_model(self.store_filter) all_action.set_active(True) self.last = None # to save (a lot of) time at startup we load history later, only when # it is selected to be viewed self.history = None self.column = Gtk.TreeViewColumn(_('Date')) self.view.append_column(self.column) self.cell_icon = Gtk.CellRendererPixbuf() self.column.pack_start(self.cell_icon, False) self.column.set_cell_data_func(self.cell_icon, self.render_cell_icon) self.cell_text = Gtk.CellRendererText() self.column.pack_start(self.cell_text, True) self.column.set_cell_data_func(self.cell_text, self.render_cell_text) # busy cursor self.busy_cursor = Gdk.Cursor.new(Gdk.CursorType.WATCH) def init_view(self): if self.history == None: # if the history is not yet initialized we have to load and parse it # show a spinner while we do that self.realize() window = self.get_window() window.set_cursor(self.busy_cursor) self.spinner_view.start() self.spinner_notebook.set_current_page(self.PAGE_SPINNER) self.load_and_parse_history() self.spinner_notebook.set_current_page(self.PAGE_HISTORY_VIEW) self.spinner_view.stop() self._set_actions_sensitive(True) window.set_cursor(None) self.emit("history-pane-created") def on_toolbar_draw(self, widget, cr): a = widget.get_allocation() context = widget.get_style_context() color = context.get_border_color(widget.get_state_flags()) cr.set_source_rgba(color.red, color.green, color.blue, 0.5) cr.set_line_width(1) cr.move_to(0.5, a.height - 0.5) cr.rel_line_to(a.width - 1, 0) cr.stroke() return def _get_emblems(self, icons): emblem_names = ("package-install", "package-remove", "package-upgrade") for i, emblem in enumerate(emblem_names): pb = icons.load_icon(emblem, self.ICON_SIZE, 0) self._emblems[i + 1] = pb return def _set_actions_sensitive(self, sensitive): for action in self._actions_list: action.set_sensitive(sensitive) def _reset_icon_cache(self, theme=None): self._app_icon_cache.clear() try: missing = self.icons.load_icon(Icons.MISSING_APP, self.ICON_SIZE, 0) except GObject.GError: missing = None self._app_icon_cache[Icons.MISSING_APP] = missing def load_and_parse_history(self): from softwarecenter.db.history import get_pkg_history self.history = get_pkg_history() # FIXME: a signal from AptHistory is nicer while not self.history.history_ready: while Gtk.events_pending(): Gtk.main_iteration() self.parse_history() self.history.set_on_update(self.parse_history) def parse_history(self): date = None when = None last_row = None day = self.store.get_iter_first() if day is not None: date = self.store.get_value(day, self.COL_WHEN) if len(self.history.transactions) == 0: logging.debug("AptHistory is currently empty") return new_last = self.history.transactions[0].start_date for trans in self.history.transactions: while Gtk.events_pending(): Gtk.main_iteration() when = trans.start_date if self.last is not None and when <= self.last: break if when.date() != date: date = when.date() day = self.store.append(None, (date, self.ALL, None)) last_row = None actions = { self.INSTALLED: trans.install, self.REMOVED: trans.remove, self.UPGRADED: trans.upgrade, } for action, pkgs in actions.items(): for pkgname in pkgs: row = (when, action, pkgname) last_row = self.store.insert_after(day, last_row, row) self.last = new_last self.update_view() def get_current_page(self): # single page views can return None here return None def get_callback_for_page(self, page, state): # single page views can return None here return None def on_search_terms_changed(self, entry, terms): self.update_view() 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 change_filter(self, action, current): self.filter = action.get_current_value() self.update_view() def update_view(self): self.store_filter.refilter() self.view.collapse_all() # Expand all the matching rows if self.searchentry.get_text(): self.view.expand_all() # Compute the number of visible changes # don't do this atm - the spec doesn't mention that the history pane # should have a status text and it gives us a noticable performance gain # if we don't calculate this # self.visible_changes = 0 # day = self.store_filter.get_iter_first() # while day is not None: # self.visible_changes += self.store_filter.iter_n_children(day) # day = self.store_filter.iter_next(day) # Expand the most recent day day = self.store.get_iter_first() if day is not None: path = self.store.get_path(day) self.view.expand_row(path, False) self.view.scroll_to_cell(path) # self.emit('app-list-changed', self.visible_changes) def _row_matches(self, store, iter): # Whether a child row matches the current filter and the search entry pkg = store.get_value(iter, self.COL_PKG) or '' filter_values = (self.ALL, store.get_value(iter, self.COL_ACTION)) filter_matches = self.filter in filter_values search_matches = self.searchentry.get_text().lower() in pkg.lower() return filter_matches and search_matches def filter_row(self, store, iter, user_data): pkg = store.get_value(iter, self.COL_PKG) if pkg is not None: return self._row_matches(store, iter) else: i = store.iter_children(iter) while i is not None: if self._row_matches(store, i): return True i = store.iter_next(i) return False def render_cell_icon(self, column, cell, store, iter, user_data): pkg = store.get_value(iter, self.COL_PKG) if pkg is None: cell.set_visible(False) return cell.set_visible(True) when = store.get_value(iter, self.COL_WHEN) if isinstance(when, datetime.datetime): action = store.get_value(iter, self.COL_ACTION) cell.set_property('pixbuf', self._emblems[action]) #~ icon_name = Icons.MISSING_APP #~ for m in self.db.xapiandb.postlist("AP" + pkg): #~ doc = self.db.xapiandb.get_document(m.docid) #~ icon_value = doc.get_value(XapianValues.ICON) #~ if icon_value: #~ icon_name = os.path.splitext(icon_value)[0] #~ break #~ if icon_name in self._app_icon_cache: #~ icon = self._app_icon_cache[icon_name] #~ else: #~ try: #~ icon = self.icons.load_icon(icon_name, self.ICON_SIZE, 0) #~ except GObject.GError: #~ icon = self._app_icon_cache[Icons.MISSING_APP] #~ self._app_icon_cache[icon_name] = icon def render_cell_text(self, column, cell, store, iter, user_data): when = store.get_value(iter, self.COL_WHEN) if isinstance(when, datetime.datetime): action = store.get_value(iter, self.COL_ACTION) pkg = store.get_value(iter, self.COL_PKG) subs = { 'pkgname': pkg, 'color': '#8A8A8A', # Translators : time displayed in history, display hours (0-12), minutes and AM/PM. %H should be used instead of %I to display hours 0-24 'time': when.time().strftime(_('%I:%M %p')), } if action == self.INSTALLED: text = _( '%(pkgname)s <span color="%(color)s">installed %(time)s</span>' ) % subs elif action == self.REMOVED: text = _( '%(pkgname)s <span color="%(color)s">removed %(time)s</span>' ) % subs elif action == self.UPGRADED: text = _( '%(pkgname)s <span color="%(color)s">updated %(time)s</span>' ) % subs elif isinstance(when, datetime.date): today = datetime.date.today() monday = today - datetime.timedelta(days=today.weekday()) if when == today: text = _("Today") elif when >= monday: # Current week, display the name of the day text = when.strftime(_('%A')) else: if when.year == today.year: # Current year, display the day and month text = when.strftime(_('%d %B')) else: # Display the full date: day, month, year text = when.strftime(_('%d %B %Y')) cell.set_property('markup', text)