# -*- coding: utf-8 -*- # Copyright (C) 2009 Canonical # # Authors: # Michael Vogt # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; version 3. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # order is import here, otherwise test/gtk3/test_purchase.py is unhappy from gi.repository import GObject from gi.repository import GLib from gi.repository import Gtk import atexit import collections import dbus import dbus.service import gettext import glob import logging import os import re import subprocess import sys import time import webbrowser import xapian from dbus.mainloop.glib import DBusGMainLoop DBusGMainLoop(set_as_default=True) from os.path import normpath from gettext import gettext as _ # purely to initialize the netstatus import softwarecenter.netstatus # make pyflakes shut up softwarecenter.netstatus.NETWORK_STATE from softwarecenter.backend.ubuntusso import UbuntuSSO # db imports from softwarecenter.db.application import Application from softwarecenter.db import ( DebFileApplication, DebFileOpenError, ) from softwarecenter.db.utils import run_software_center_agent from softwarecenter.i18n import init_locale # misc imports from softwarecenter.plugin import PluginManager from softwarecenter.paths import SOFTWARE_CENTER_PLUGIN_DIRS from softwarecenter.enums import ( AppActions, DB_SCHEMA_VERSION, Icons, MOUSE_EVENT_FORWARD_BUTTON, MOUSE_EVENT_BACK_BUTTON, PkgStates, SearchSeparators, SOFTWARE_CENTER_DEBUG_TABS, SOFTWARE_CENTER_NAME_KEYRING, SOFTWARE_CENTER_TOS_LINK, ViewPages, ) from softwarecenter.utils import ( clear_token_from_ubuntu_sso_sync, get_http_proxy_string_from_gsettings, wait_for_apt_cache_ready, ExecutionTime, is_unity_running, ) from softwarecenter.ui.gtk3.utils import ( get_sc_icon_theme, init_sc_css_provider, ) from softwarecenter.version import VERSION from softwarecenter.db.database import ( StoreDatabase, get_reinstall_previous_purchases_query, ) try: from aptd_gtk3 import InstallBackendUI InstallBackendUI # pyflakes except: from softwarecenter.backend.installbackend import InstallBackendUI # ui imports import softwarecenter.ui.gtk3.dialogs.deauthorize_dialog as deauthorize_dialog import softwarecenter.ui.gtk3.dialogs as dialogs from softwarecenter.ui.gtk3.SimpleGtkbuilderApp import SimpleGtkbuilderApp with ExecutionTime("import InstalledPane"): from softwarecenter.ui.gtk3.panes.installedpane import InstalledPane with ExecutionTime("import AvailablePane"): from softwarecenter.ui.gtk3.panes.availablepane import AvailablePane from softwarecenter.ui.gtk3.panes.historypane import HistoryPane from softwarecenter.ui.gtk3.panes.globalpane import GlobalPane from softwarecenter.ui.gtk3.panes.pendingpane import PendingPane from softwarecenter.ui.gtk3.session.appmanager import ( ApplicationManager, get_appmanager, ) from softwarecenter.ui.gtk3.session.viewmanager import ( ViewManager, get_viewmanager, ) from softwarecenter.ui.gtk3.widgets.recommendations import ( RecommendationsOptInDialog) from softwarecenter.config import get_config from softwarecenter.backend.installbackend import get_install_backend from softwarecenter.backend.login import get_login_backend from softwarecenter.backend.recagent import RecommenderAgent from softwarecenter.backend.channel import AllInstalledChannel from softwarecenter.backend.reviews import get_review_loader, UsefulnessCache from softwarecenter.backend.oneconfhandler import ( get_oneconf_handler, is_oneconf_available, ) from softwarecenter.distro import get_distro from softwarecenter.db.pkginfo import get_pkg_info from gi.repository import Gdk LOG = logging.getLogger(__name__) PACKAGE_PREFIX = 'apt:' FILE_PREFIX = 'file:' # "apt:///" is a valid prefix for 'apt:pkgname' in alt+F2 in gnome PACKAGE_PREFIX_REGEX = re.compile('^%s(?:/{2,3})*' % PACKAGE_PREFIX) SEARCH_PREFIX = 'search:' # py3 compat def callable(func): return isinstance(func, collections.Callable) def parse_packages_args(packages): search_text = '' app = None # avoid treating strings as sequences ('foo' should not be 'f', 'o', 'o') if isinstance(packages, basestring): packages = (packages,) if not isinstance(packages, collections.Iterable): LOG.warning('show_available_packages: argument is not an iterable %r', packages) return search_text, app items = [] # make a copy of the given sequence for arg in packages: # support both "pkg1 pkg" and "pkg1,pkg2" (and "pkg1,pkg2 pkg3") if "," in arg: items.extend(arg.split(SearchSeparators.PACKAGE)) else: items.append(arg) if len(items) > 0: # allow s-c to be called with a search term if items[0].startswith(SEARCH_PREFIX): # remove the initial search prefix items[0] = items[0].replace(SEARCH_PREFIX, '', 1) search_text = SearchSeparators.REGULAR.join(items) elif items[0].startswith(FILE_PREFIX): # strip away the initial file: prefix, if present items[0] = items[0].replace(FILE_PREFIX, '', 1) # normalize the path to strip duplicate path separators, etc items[0] = normpath(items[0]) else: # strip away the initial apt: prefix, if present items[0] = re.sub(PACKAGE_PREFIX_REGEX, '', items[0]) if len(items) > 1: # turn multiple packages into a search with "," as separator search_text = SearchSeparators.PACKAGE.join(items) if not search_text and len(items) == 1: request = items[0] # are we dealing with a path? if os.path.exists(request) and not os.path.isdir(request): if not request.startswith('/'): # we may have been given a relative path request = os.path.abspath(request) # will raise DebOpenFileError if request is invalid app = DebFileApplication(request) else: # package from archive # if there is a "/" in the string consider it as tuple # of (pkgname, appname) for exact matching (used by # e.g. unity (pkgname, sep, appname) = request.partition("/") if pkgname or appname: app = Application(appname, pkgname) else: LOG.warning('show_available_packages: received %r but ' 'can not build an Application from it.', request) return search_text, app class SoftwarecenterDbusController(dbus.service.Object): """ This is a helper to provide the SoftwarecenterIFace It provides only a bringToFront method that takes additional arguments about what packages to show """ def __init__(self, parent, bus_name, object_path='/com/ubuntu/Softwarecenter'): dbus.service.Object.__init__(self, bus_name, object_path) self.parent = parent def stop(self): """ stop the dbus controller and remove from the bus """ self.remove_from_connection() @dbus.service.method('com.ubuntu.SoftwarecenterIFace', utf8_strings=True) def bringToFront(self, args): if args != 'nothing-to-show': self.parent.show_available_packages(args) self.parent.window_main.present() return True @dbus.service.method('com.ubuntu.SoftwarecenterIFace') def triggerDatabaseReopen(self): self.parent.db.emit("reopen") @dbus.service.method('com.ubuntu.SoftwarecenterIFace') def triggerCacheReload(self): self.parent.cache.emit("cache-ready") @dbus.service.method('com.ubuntu.SoftwarecenterIFace') def writeMemoryDump(self): self.parent.write_memory_dump() class SoftwareCenterAppGtk3(SimpleGtkbuilderApp): WEBLINK_URL = "http://apt.ubuntu.com/p/%s" # the size of the icon for dialogs APP_ICON_SIZE = Gtk.IconSize.DIALOG START_DBUS = True def __init__(self, options, args=None): self.dbusController = None if self.START_DBUS: # setup dbus and exit if there is another instance already running self.setup_dbus_or_bring_other_instance_to_front(args) self.datadir = softwarecenter.paths.datadir super(SoftwareCenterAppGtk3, self).__init__( os.path.join(self.datadir, "ui", "gtk3", "SoftwareCenter.ui"), "software-center") gettext.bindtextdomain("software-center", "/usr/share/locale") gettext.textdomain("software-center") init_locale() if SOFTWARE_CENTER_DEBUG_TABS: self.notebook_view.set_show_tabs(True) # distro specific stuff self.distro = get_distro() # setup proxy self._setup_proxy_initially() # Disable software-properties if it does not exist if not os.path.exists("/usr/bin/software-properties-gtk"): self.menuitem_software_sources.set_sensitive(False) with ExecutionTime("opening the pkginfo"): # a main iteration friendly apt cache self.cache = get_pkg_info() # cache is opened later in run() self.cache.connect("cache-broken", self._on_apt_cache_broken) xapian_base_path = softwarecenter.paths.XAPIAN_BASE_PATH with ExecutionTime("opening the xapiandb"): pathname = os.path.join(xapian_base_path, "xapian") self._use_axi = not options.disable_apt_xapian_index try: self.db = StoreDatabase(pathname, self.cache) self.db.open(use_axi=self._use_axi) if self.db.schema_version() != DB_SCHEMA_VERSION: LOG.warn("database format '%s' expected, but got '%s'" % ( DB_SCHEMA_VERSION, self.db.schema_version())) if os.access(pathname, os.W_OK): self._rebuild_and_reopen_local_db(pathname) except xapian.DatabaseOpeningError: # Couldn't use that folder as a database # This may be because we are in a bzr checkout and that # folder is empty. If the folder is empty, and we can find # the script that does population, populate a database in it. if os.path.isdir(pathname) and not os.listdir(pathname): self._rebuild_and_reopen_local_db(pathname) except xapian.DatabaseCorruptError: LOG.exception("xapian open failed") dialogs.error(None, _("Sorry, can not open the software database"), _("Please re-install the 'software-center' " "package.")) # FIXME: force rebuild by providing a dbus service for this sys.exit(1) # additional icons come from app-install-data with ExecutionTime("building the icon cache"): self.icons = get_sc_icon_theme() # backend with ExecutionTime("creating the backend"): self.backend = get_install_backend() self.backend.ui = InstallBackendUI() self.backend.connect("transaction-finished", self._on_transaction_finished) self.backend.connect("channels-changed", self.on_channels_changed) # high level app management with ExecutionTime("get the app-manager"): self.app_manager = ApplicationManager(self.db, self.backend, self.icons) # misc state self._block_menuitem_view = False # for use when viewing previous purchases self.scagent = None self.sso = None self.available_for_me_query = get_reinstall_previous_purchases_query() Gtk.Window.set_default_icon_name("softwarecenter") # inhibit the error-bell, Bug #846138... settings = Gtk.Settings.get_default() settings.set_property("gtk-error-bell", False) # initial css loading init_sc_css_provider(self.window_main, settings, Gdk.Screen.get_default()) # wire up the css provider to reconfigure on theme-changes self.window_main.connect("style-updated", self._on_style_updated, init_sc_css_provider, settings, Gdk.Screen.get_default()) # workaround broken engines (LP: #1021308) self.window_main.emit("style-updated") # register view manager and create view panes/widgets self.view_manager = ViewManager(self.notebook_view, options) with ExecutionTime("building panes"): self.global_pane = GlobalPane(self.view_manager, self.db, self.cache, self.icons) self.vbox1.pack_start(self.global_pane, False, False, 0) self.vbox1.reorder_child(self.global_pane, 1) # start with the toolbar buttons insensitive and don't make them # sensitive until the panel elements are ready self.global_pane.view_switcher.set_sensitive(False) # available pane self.available_pane = AvailablePane(self.cache, self.db, self.distro, self.icons, self.navhistory_back_action, self.navhistory_forward_action) self.available_pane.connect("available-pane-created", self.on_available_pane_created) self.view_manager.register(self.available_pane, ViewPages.AVAILABLE) # installed pane (view not fully initialized at this point) self.installed_pane = InstalledPane(self.cache, self.db, self.distro, self.icons) self.installed_pane.connect("installed-pane-created", self.on_installed_pane_created) self.view_manager.register(self.installed_pane, ViewPages.INSTALLED) # history pane (not fully loaded at this point) self.history_pane = HistoryPane(self.cache, self.db, self.distro, self.icons) self.view_manager.register(self.history_pane, ViewPages.HISTORY) # pending pane self.pending_pane = PendingPane(self.icons) self.view_manager.register(self.pending_pane, ViewPages.PENDING) # TRANSLATORS: this is the help menuitem label, # e.g. Ubuntu Software Center _Help self.menuitem_help.set_label(_("%s _Help") % self.distro.get_app_name()) # specify the smallest allowable window size self.window_main.set_size_request(730, 470) # reviews with ExecutionTime("create review loader"): self.review_loader = get_review_loader(self.cache, self.db) self.review_loader.connect( "refresh-review-stats-finished", self.on_review_stats_loaded) self.review_loader.refresh_review_stats() #load usefulness votes from server when app starts self.useful_cache = UsefulnessCache(True) self.setup_database_rebuilding_listener() # open plugin manager and load plugins self.plugin_manager = PluginManager(self, SOFTWARE_CENTER_PLUGIN_DIRS) self.plugin_manager.load_plugins() # setup window name and about information (needs branding) name = self.distro.get_app_name() self.window_main.set_title(name) self.aboutdialog.set_program_name(name) about_description = self.distro.get_app_description() self.aboutdialog.set_comments(about_description) # about dialog self.aboutdialog.connect("response", lambda dialog, rid: dialog.hide()) self.aboutdialog.connect("delete_event", lambda w, e: self.aboutdialog.hide_on_delete()) # restore state self.config = get_config() self.restore_state() # Adapt menu entries self.menuitem_view_supported_only.set_label( self.distro.get_supported_filter_name()) # this will be set sensitive once a the availablepane is available self.menuitem_recommendations.set_sensitive(False) if not self.distro.DEVELOPER_URL: self.menu_help.remove(self.separator_developer) self.menu_help.remove(self.menuitem_developer) # Check if oneconf is available och = is_oneconf_available() if not och: self.menu_file.remove(self.menuitem_sync_between_computers) # restore the state of the add to launcher menu item, or remove the # menu item if Unity is not currently running if is_unity_running(): self.menuitem_add_to_launcher.set_active( self.config.add_to_unity_launcher) else: self.menu_view.remove(self.add_to_launcher_separator) self.menu_view.remove(self.menuitem_add_to_launcher) # run s-c-agent update if options.disable_buy or not self.distro.PURCHASE_APP_URL: self.menu_file.remove(self.menuitem_reinstall_purchases) else: # running the agent will trigger a db reload so we do it later GLib.timeout_add_seconds(3, self._run_software_center_agent) # keep the cache clean GLib.timeout_add_seconds(15, self._run_expunge_cache_helper) # check to see if a new recommendations profile upload is # needed and upload if necessary GLib.timeout_add_seconds(45, self._upload_recommendations_profile) # TODO: Remove the following two lines once we have remove repository # support in aptdaemon (see LP: #723911) self.menu_file.remove(self.menuitem_deauthorize_computer) # keep track of the current active pane self.active_pane = self.available_pane # launchpad integration help, its ok if that fails try: from gi.repository import LaunchpadIntegration LaunchpadIntegration.set_sourcepackagename("software-center") LaunchpadIntegration.add_items(self.menu_help, 3, True, False) except Exception, e: LOG.debug("launchpad integration error: '%s'" % e) # helper def _run_software_center_agent(self): """ helper that triggers the update-software-center-agent helper """ run_software_center_agent(self.db) def _run_expunge_cache_helper(self): """ helper that expires the piston-mini-client cache """ sc_expunge_cache = os.path.join( self.datadir, "expunge-cache.py") (pid, stdin, stdout, stderr) = GLib.spawn_async( [sc_expunge_cache, "--by-unsuccessful-http-states", softwarecenter.paths.SOFTWARE_CENTER_CACHE_DIR, ]) def _rebuild_and_reopen_local_db(self, pathname): """ helper that rebuilds a db and reopens it """ from softwarecenter.db.update import rebuild_database LOG.info("building local database") # debian_sources is a bit misnamed, it will look for annotated # desktop files in /usr/share/app-install - enabling this on # non-debian systems will do no harm debian_sources = True # the appstream sources, enabling this on non-appstream systems # will do no harm appstream_sources = True rebuild_database(pathname, debian_sources, appstream_sources) self.db = StoreDatabase(pathname, self.cache) self.db.open(use_axi=self._use_axi) def _setup_proxy_initially(self): from gi.repository import Gio self._setup_proxy() self._gsettings = Gio.Settings.new("org.gnome.system.proxy.http") self._gsettings.connect("changed", self._setup_proxy) def _setup_proxy(self, setting=None, key=None): try: proxy = get_http_proxy_string_from_gsettings() LOG.info("setting up proxy '%s'" % proxy) if proxy: os.environ["http_proxy"] = proxy else: os.environ.pop("http_proxy", None) except Exception as e: # if no gnome settings are installed, do not mess with # http_proxy LOG.warn("could not get proxy settings '%s'" % e) pass # callbacks def on_available_pane_created(self, widget): self.available_pane.searchentry.grab_focus() self._update_recommendations_menuitem( opted_in=self._get_recommender_agent().is_opted_in()) # connect a signal to monitor the recommendations opt-in state and # persist the recommendations uuid on an opt-in self.available_pane.cat_view.recommended_for_you_panel.connect( "recommendations-opt-in", self._on_recommendations_opt_in) self.available_pane.cat_view.recommended_for_you_panel.connect( "recommendations-opt-out", self._on_recommendations_opt_out) self.menuitem_recommendations.set_sensitive(True) # set the main toolbar buttons sensitive self.global_pane.view_switcher.set_sensitive(True) def on_installed_pane_created(self, widget): # set the main toolbar buttons sensitive self.global_pane.view_switcher.set_sensitive(True) def _on_recommendations_opt_in(self, rec_panel): self._update_recommendations_menuitem(opted_in=True) def _on_recommendations_opt_out(self, rec_panel): self._update_recommendations_menuitem(opted_in=False) def _update_recommendations_menuitem(self, opted_in): if opted_in: self.menuitem_recommendations.set_label( _(u"Turn Off Recommendations")) else: self.menuitem_recommendations.set_label( _(u"Turn On Recommendations…")) @wait_for_apt_cache_ready def _upload_recommendations_profile(self): recommender_agent = self._get_recommender_agent() if recommender_agent.is_opted_in(): recommender_agent.post_submit_profile(self.db) def _get_recommender_agent(self): if not hasattr(self, "_recommender_agent"): self._recommender_agent = RecommenderAgent() return self._recommender_agent def on_review_stats_loaded(self, loader, reviews): LOG.debug("on_review_stats_loaded: '%s'" % len(reviews)) def destroy(self): """Destroy this instance and every used resource.""" self.window_main.destroy() # remove global instances of Managers self.app_manager.destroy() self.view_manager.destroy() if self.dbusController is not None: # ensure that the dbus controller is really gone self.dbusController.stop() def close_app(self): """ perform tasks like save-state etc when the application is exited """ # this may happen during the early initialization # when "app.run()" was called but has not finished setting up the # stuff yet, in this case its ok to just exit if Gtk.main_level() == 0: LOG.debug("closing before the regular main loop was run") sys.exit(0) # hide the main window early even if saving the state etc takes # a wee bit self.window_main.hide() self.save_state() self.destroy() # this will not throw exceptions in pygi but "only" log via g_critical # to the terminal but it might in the future so we add a handler here try: Gtk.main_quit() except: LOG.exception("Gtk.main_quit failed") # exit here explicitly to ensure that no further gtk event loops or # threads run and cause havoc on exit (LP: #914393) sys.exit(0) def on_window_main_key_press_event(self, widget, event): """ Define all the accelerator keys here - slightly messy, but the ones defined in the menu don't seem to work.. """ # close if ((event.keyval == Gdk.keyval_from_name("w") or event.keyval == Gdk.keyval_from_name("q")) and event.state == Gdk.ModifierType.CONTROL_MASK): self.menuitem_close.activate() # undo/redo if (event.keyval == Gdk.keyval_from_name("z") and event.state == Gdk.ModifierType.CONTROL_MASK): self.menuitem_edit.activate() if self.menuitem_undo.get_sensitive(): self.menuitem_undo.activate() if (event.keyval == Gdk.keyval_from_name("Z") and event.state == (Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK)): self.menuitem_edit.activate() if self.menuitem_redo.get_sensitive(): self.menuitem_redo.activate() # cut/copy/paste if (event.keyval == Gdk.keyval_from_name("x") and event.state == Gdk.ModifierType.CONTROL_MASK): self.menuitem_edit.activate() if (event.keyval == Gdk.keyval_from_name("c") and event.state == Gdk.ModifierType.CONTROL_MASK): self.menuitem_edit.activate() if self.menuitem_copy.get_sensitive(): self.menuitem_copy.activate() # copy web link if (event.keyval == Gdk.keyval_from_name("C") and event.state == (Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK)): self.menuitem_edit.activate() if self.menuitem_copy_web_link.get_sensitive(): self.menuitem_copy_web_link.activate() # select all if (event.keyval == Gdk.keyval_from_name("a") and event.state == Gdk.ModifierType.CONTROL_MASK): self.menuitem_edit.activate() if self.menuitem_select_all.get_sensitive(): self.menuitem_select_all.activate() # search if (event.keyval == Gdk.keyval_from_name("f") and event.state == Gdk.ModifierType.CONTROL_MASK): self.menuitem_edit.activate() if self.menuitem_search.get_sensitive(): self.menuitem_search.activate() # back if ((event.keyval == Gdk.keyval_from_name("bracketleft") and event.state == Gdk.ModifierType.CONTROL_MASK) or ((event.keyval == Gdk.keyval_from_name("Left") or event.keyval == Gdk.keyval_from_name("KP_Left")) and event.state == Gdk.ModifierType.MOD1_MASK)): # using the backspace key to navigate back has been disabled as it # has started to show dodgy side effects which I can't figure how # to deal with self.menuitem_view.activate() if self.menuitem_go_back.get_sensitive(): self.menuitem_go_back.activate() # forward if ((event.keyval == Gdk.keyval_from_name("bracketright") and event.state == Gdk.ModifierType.CONTROL_MASK) or ((event.keyval == Gdk.keyval_from_name("Right") or event.keyval == Gdk.keyval_from_name("KP_Right")) and event.state == Gdk.ModifierType.MOD1_MASK)): self.menuitem_view.activate() if self.menuitem_go_forward.get_sensitive(): self.menuitem_go_forward.activate() def on_window_main_button_press_event(self, widget, event): """ Implement back/forward navigation via mouse navigation keys using the same button codes as used in Nautilus. """ if event.button == MOUSE_EVENT_BACK_BUTTON: self.menuitem_view.activate() if self.menuitem_go_back.get_sensitive(): self.menuitem_go_back.activate() elif event.button == MOUSE_EVENT_FORWARD_BUTTON: self.menuitem_view.activate() if self.menuitem_go_forward.get_sensitive(): self.menuitem_go_forward.activate() def _on_style_updated(self, widget, init_css_callback, *args): init_css_callback(widget, *args) def get_icon_filename(self, iconname, iconsize): iconinfo = self.icons.lookup_icon(iconname, iconsize, 0) if not iconinfo: iconinfo = self.icons.lookup_icon(Icons.MISSING_APP_ICON, iconsize, 0) return iconinfo.get_filename() # File Menu def on_menu_file_activate(self, menuitem): """Enable/disable install/remove""" LOG.debug("on_menu_file_activate") # reset it all self.menuitem_install.set_sensitive(False) self.menuitem_remove.set_sensitive(False) # get our active pane vm = get_viewmanager() if vm is None: return False self.active_pane = vm.get_view_widget(vm.get_active_view()) if self.active_pane is None: return False # determine the current app app = self.active_pane.get_current_app() if not app: return False # wait for the cache to become ready (if needed) if not self.cache.ready: GLib.timeout_add( 100, lambda: self.on_menu_file_activate(menuitem)) return False # update menu items pkg_state = None error = None # FIXME: Use a Gtk.Action for the Install/Remove/Buy/Add Source/Update # Now action so that all UI controls (menu item, applist view # button and appdetails view button) are managed centrally: # button text, button sensitivity, and callback method # FIXME: Add buy support here by implementing the above appdetails = app.get_details(self.db) if appdetails: pkg_state = appdetails.pkg_state error = appdetails.error if (app.pkgname in self.active_pane.app_view.tree_view._action_block_list): return False elif (pkg_state == PkgStates.UPGRADABLE or pkg_state == PkgStates.REINSTALLABLE and not error): self.menuitem_install.set_sensitive(True) self.menuitem_remove.set_sensitive(True) elif pkg_state == PkgStates.INSTALLED: self.menuitem_remove.set_sensitive(True) elif pkg_state == PkgStates.UNINSTALLED and not error: self.menuitem_install.set_sensitive(True) elif (not pkg_state and not self.active_pane.is_category_view_showing() and app.pkgname in self.cache and not app.pkgname in self.active_pane.app_view.tree_view._action_block_list and not error): # when does this happen? pkg = self.cache[app.pkgname] installed = bool(pkg.installed) self.menuitem_install.set_sensitive(not installed) self.menuitem_remove.set_sensitive(installed) # return False to ensure that a possible GLib.timeout_add ends return False def _on_reinstall_purchased_login(self, sso, oauth_result): self._sso_login_successful = True # appmanager needs to know about the oauth token for the reinstall # previous purchases add_license_key call self.app_manager.oauth_token = oauth_result # the software-center-agent will ensure the previous-purchases # get merged in self._run_software_center_agent() # show spinner as this may take some time, the spinner will # automatically go away when a DB refreshes self.available_pane.show_appview_spinner() # show previous purchased self.available_pane.on_previous_purchases_activated( self.available_for_me_query) def on_menuitem_reinstall_purchases_activate(self, menuitem): self.view_manager.set_active_view(ViewPages.AVAILABLE) self.view_manager.search_entry.clear_with_no_signal() # its ok to use the sync version here to get the token, this is # very quick helper = UbuntuSSO() token = helper.find_oauth_token_sync() if token: # trigger a software-center-agent run to ensure the merged in # purchases are fresh self._run_software_center_agent() # we already have the list of available items, so just show it # (no need for spinner here) self.available_pane.on_previous_purchases_activated( self.available_for_me_query) else: # see bug #773214 for the rationale, do not translate the appname #appname = _("Ubuntu Software Center") appname = SOFTWARE_CENTER_NAME_KEYRING help_text = _( "To reinstall previous purchases, sign in to the " "Ubuntu Single Sign-On account you used to pay for them.") #window = self.window_main.get_window() #xid = self.get_window().xid xid = 0 self.sso = get_login_backend(xid, appname, help_text) self.sso.connect( "login-successful", self._on_reinstall_purchased_login) self.sso.login() def on_menuitem_recommendations_activate(self, menu_item): rec_panel = self.available_pane.cat_view.recommended_for_you_panel if self._get_recommender_agent().is_opted_in(): rec_panel.opt_out_of_recommendations_service() else: # build and show the opt-in dialog opt_in_dialog = RecommendationsOptInDialog(self.icons) res = opt_in_dialog.run() opt_in_dialog.destroy() if res == Gtk.ResponseType.YES: rec_panel.opt_in_to_recommendations_service() def on_menuitem_deauthorize_computer_activate(self, menuitem): # FIXME: need Ubuntu SSO username here # account_name = get_person_from_config() account_name = None # get a list of installed purchased packages installed_purchases = self.db.get_installed_purchased_packages() # display the deauthorize computer dialog deauthorize = deauthorize_dialog.deauthorize_computer(None, self.datadir, self.db, self.icons, account_name, installed_purchases) if deauthorize: # clear the ubuntu SSO token for this account # FIXME: as this is a sync call it maybe slow so we should # probably provide a async() version of this as well clear_token_from_ubuntu_sso_sync(SOFTWARE_CENTER_NAME_KEYRING) # uninstall the list of purchased packages # TODO: do we need to check for dependencies and show a removal # dialog for that case? seems not since these are purchased apps for pkgname in installed_purchases: app = Application(pkgname=pkgname) appdetails = app.get_details(self.db) self.backend.remove(app, appdetails.icon) # TODO: remove the corresponding private PPA sources # FIXME: this should really be done using aptdaemon, update this # if/when remove repository support is added to aptdaemon # (private-ppa.launchpad.net_commercial-ppa-uploaders*) purchased_sources = glob.glob("/etc/apt/sources.list.d/" "private-ppa.launchpad.net_commercial-ppa-uploaders*") for source in purchased_sources: print("source: %s" % source) def on_menuitem_sync_between_computers_activate(self, menuitem): if self.view_manager.get_active_view() != ViewPages.INSTALLED: pane = self.view_manager.set_active_view(ViewPages.INSTALLED) state = pane.state.copy() state.channel = AllInstalledChannel() page = None self.view_manager.display_page(pane, page, state) self.installed_pane.refresh_apps() get_oneconf_handler().sync_between_computers(True) def on_menuitem_install_activate(self, menuitem): app = self.active_pane.get_current_app() get_appmanager().request_action(app, [], [], AppActions.INSTALL) def on_menuitem_remove_activate(self, menuitem): app = self.active_pane.get_current_app() get_appmanager().request_action(app, [], [], AppActions.REMOVE) def on_menuitem_close_activate(self, widget): self.close_app() def on_window_main_delete_event(self, widget, event): self.close_app() # Edit Menu def on_menu_edit_activate(self, menuitem): """ Check whether the search field is focused and if so, focus some items """ edit_menu_items = [self.menuitem_undo, self.menuitem_redo, self.menuitem_copy, self.menuitem_copy_web_link, self.menuitem_paste, self.menuitem_delete, self.menuitem_select_all, self.menuitem_search] for item in edit_menu_items: item.set_sensitive(False) # get our active pane vm = get_viewmanager() if vm is None: return False self.active_pane = vm.get_view_widget(vm.get_active_view()) if (self.active_pane and self.active_pane.searchentry and self.active_pane.searchentry.get_visible()): # undo, redo, cut, copy, paste, delete, select_all sensitive # if searchentry is focused (and other more specific conditions) if self.active_pane.searchentry.is_focus(): if len(self.active_pane.searchentry._undo_stack) > 1: self.menuitem_undo.set_sensitive(True) if len(self.active_pane.searchentry._redo_stack) > 0: self.menuitem_redo.set_sensitive(True) self.menuitem_paste.set_sensitive(True) if self.active_pane.searchentry.get_text(): self.menuitem_delete.set_sensitive(True) self.menuitem_select_all.set_sensitive(True) # search sensitive if searchentry is not focused else: self.menuitem_search.set_sensitive(True) # weblink if self.active_pane: app = self.active_pane.get_current_app() if app and app.pkgname: self.menuitem_copy_web_link.set_sensitive(True) # details view if (self.active_pane and self.active_pane.is_app_details_view_showing()): self.menuitem_select_all.set_sensitive(True) desc = self.active_pane.app_details_view.desc if desc.get_selected_text(): self.menuitem_copy.set_sensitive(True) def on_menuitem_undo_activate(self, menuitem): self.active_pane.searchentry.undo() def on_menuitem_redo_activate(self, menuitem): self.active_pane.searchentry.redo() def on_menuitem_copy_activate(self, menuitem): if (self.active_pane and self.active_pane.is_app_details_view_showing()): self.active_pane.app_details_view.desc.copy_clipboard() def on_menuitem_paste_activate(self, menuitem): # LP: #1308354 if self.active_pane.searchentry.get_property("visible") == False: return self.active_pane.searchentry.paste_clipboard() def on_menuitem_delete_activate(self, menuitem): self.active_pane.searchentry.set_text("") def on_menuitem_select_all_activate(self, menuitem): if (self.active_pane and self.active_pane.is_app_details_view_showing()): self.active_pane.app_details_view.desc.select_all() self.active_pane.app_details_view.desc.grab_focus() elif self.active_pane: self.active_pane.searchentry.select_region(0, -1) def on_menuitem_copy_web_link_activate(self, menuitem): app = self.active_pane.get_current_app() if app: display = Gdk.Display.get_default() selection = Gdk.Atom.intern("CLIPBOARD", False) clipboard = Gtk.Clipboard.get_for_display(display, selection) clipboard.set_text(self.WEBLINK_URL % app.pkgname, -1) def on_menuitem_search_activate(self, widget): if self.active_pane: self.active_pane.searchentry.grab_focus() self.active_pane.searchentry.select_region(0, -1) def on_menuitem_software_sources_activate(self, widget): # run software-properties-gtk window = self.window_main.get_window() if hasattr(window, 'xid'): xid = window.xid else: xid = 0 p = subprocess.Popen( ["/usr/bin/software-properties-gtk", "-n", "-t", str(xid)]) # Monitor the subprocess regularly GLib.timeout_add(100, self._poll_software_sources_subprocess, p) def _poll_software_sources_subprocess(self, popen): ret = popen.poll() if ret is None: # Keep monitoring return True # A return code of 1 means that the sources have changed if ret == 1: self.run_update_cache() # Stop monitoring return False # View Menu def on_menu_view_activate(self, menuitem): vm = get_viewmanager() if vm is None: self.menuitem_view_all.set_sensitive(False) self.menuitem_view_supported_only.set_sensitive(False) self.menuitem_go_back.set_sensitive(False) self.menuitem_go_forward.set_sensitive(False) return False left_sensitive = vm.back_forward.left.get_sensitive() self.menuitem_go_back.set_sensitive(left_sensitive) right_sensitive = vm.back_forward.right.get_sensitive() self.menuitem_go_forward.set_sensitive(right_sensitive) self.menuitem_view.blocked = True # get our active pane self.active_pane = vm.get_view_widget(vm.get_active_view()) if (self.active_pane and self.active_pane == self.available_pane or self.active_pane == self.installed_pane): self.menuitem_view_all.set_sensitive(True) self.menuitem_view_supported_only.set_sensitive(True) from softwarecenter.db.appfilter import get_global_filter supported_only = get_global_filter().supported_only self.menuitem_view_all.set_active(not supported_only) self.menuitem_view_supported_only.set_active(supported_only) else: self.menuitem_view_all.set_sensitive(False) self.menuitem_view_supported_only.set_sensitive(False) self.menuitem_view.blocked = False def on_menuitem_view_all_activate(self, widget): if self.menuitem_view.blocked: return from softwarecenter.db.appfilter import get_global_filter if get_global_filter().supported_only is True: get_global_filter().supported_only = False self.available_pane.refresh_apps() try: self.installed_pane.refresh_apps() except: # may not be initialised pass def on_menuitem_view_supported_only_activate(self, widget): if self.menuitem_view.blocked: return from softwarecenter.db.appfilter import get_global_filter if get_global_filter().supported_only is False: get_global_filter().supported_only = True self.available_pane.refresh_apps() try: self.installed_pane.refresh_apps() except: # may not be initialised pass # navigate up if the details page is no longer available #~ ap = self.active_pane #~ if (ap and ap.is_app_details_view_showing and #~ ap.app_details_view.app and #~ not self.distro.is_supported(self.cache, None, #~ ap.app_details_view.app.pkgname)): #~ if len(ap.app_view.get_model()) == 0: #~ ap.navigation_bar.navigate_up_twice() #~ else: #~ ap.navigation_bar.navigate_up() #~ ap.on_application_selected(None, None) #~ # navigate up if the list page is empty #~ elif (ap and ap.is_applist_view_showing() and #~ len(ap.app_view.get_model()) == 0): #~ ap.navigation_bar.navigate_up() #~ ap.on_application_selected(None, None) def on_navhistory_back_action_activate(self, navhistory_back_action=None): vm = get_viewmanager() vm.nav_back() def on_navhistory_forward_action_activate(self, navhistory_forward_action=None): vm = get_viewmanager() vm.nav_forward() def on_menuitem_add_to_launcher_toggled(self, menu_item): self.config.add_to_unity_launcher = menu_item.get_active() # Help Menu def on_menuitem_about_activate(self, widget): self.aboutdialog.set_version(VERSION) self.aboutdialog.set_transient_for(self.window_main) self.aboutdialog.show() def on_menuitem_help_activate(self, menuitem): # run browser (pid, stdin, stdout, stderr) = GLib.spawn_async( ["yelp", "help:software-center"], flags=GObject.SPAWN_SEARCH_PATH) def on_menuitem_tos_activate(self, menuitem): webbrowser.open_new_tab(SOFTWARE_CENTER_TOS_LINK) def on_menuitem_developer_activate(self, menuitem): webbrowser.open(self.distro.DEVELOPER_URL) def _ask_and_repair_broken_cache(self): # wait until the window window is available if self.window_main.props.visible is False: GLib.timeout_add_seconds(1, self._ask_and_repair_broken_cache) return if dialogs.confirm_repair_broken_cache(self.window_main, self.datadir): self.backend.fix_broken_depends() def _on_apt_cache_broken(self, aptcache): self._ask_and_repair_broken_cache() def _on_transaction_finished(self, backend, result): """ callback when an application install/remove transaction (or a cache reload) has finished """ self.cache.open() def on_channels_changed(self, backend, res): """ callback when the set of software channels has changed """ LOG.debug("on_channels_changed %s" % res) if res: # reopen the database, this will ensure that the right signals # are send and triggers "refresh_apps" # and refresh the displayed app in the details as well self.db.reopen() # helper def run_update_cache(self): """update the apt cache (e.g. after new sources where added """ self.backend.reload() def update_app_list_view(self, channel=None): """Helper that updates the app view list """ if self.active_pane is None: return if channel is None and self.active_pane.is_category_view_showing(): return if channel: self.channel_pane.set_channel(channel) self.active_pane.refresh_apps() def _on_database_rebuilding_handler(self, is_rebuilding): LOG.debug("_on_database_rebuilding_handler %s" % is_rebuilding) self._database_is_rebuilding = is_rebuilding if is_rebuilding: pass else: # we need to reopen when the database finished updating self.db.reopen() def setup_database_rebuilding_listener(self): """ Setup system bus listener for database rebuilding """ self._database_is_rebuilding = False # get dbus try: bus = dbus.SystemBus() except: LOG.exception("could not get system bus") return # check if its currently rebuilding (most likely not, so we # just ignore errors from dbus because the interface try: proxy_obj = bus.get_object("com.ubuntu.Softwarecenter", "/com/ubuntu/Softwarecenter") iface = dbus.Interface(proxy_obj, "com.ubuntu.Softwarecenter") res = iface.IsRebuilding() self._on_database_rebuilding_handler(res) except Exception as e: LOG.debug( "query for the update-database exception '%s' (probably ok)" % e) # add signal handler bus.add_signal_receiver(self._on_database_rebuilding_handler, "DatabaseRebuilding", "com.ubuntu.Softwarecenter") def setup_dbus_or_bring_other_instance_to_front(self, args): """ This sets up a dbus listener """ try: bus = dbus.SessionBus() except: LOG.exception("could not initiate dbus") return # if there is another Softwarecenter running bring it to front # and exit, otherwise install the dbus controller try: proxy_obj = bus.get_object('com.ubuntu.Softwarecenter', '/com/ubuntu/Softwarecenter') iface = dbus.Interface(proxy_obj, 'com.ubuntu.SoftwarecenterIFace') if args: res = iface.bringToFront(args) else: # None can not be transported over dbus res = iface.bringToFront('nothing-to-show') # ensure that the running s-c is working if res is not True: LOG.info("found a running software-center on dbus, " "reconnecting") sys.exit() except dbus.DBusException: bus_name = dbus.service.BusName('com.ubuntu.Softwarecenter', bus) self.dbusController = SoftwarecenterDbusController(self, bus_name) @wait_for_apt_cache_ready def show_app(self, app): """Show 'app' in the installed pane if is installed. If 'app' is not installed, show it in the available pane. """ if (app.pkgname in self.cache and self.cache[app.pkgname].installed): with ExecutionTime("installed_pane.init_view()"): self.installed_pane.init_view() with ExecutionTime("installed_pane.show_app()"): self.installed_pane.show_app(app) else: self.available_pane.init_view() self.available_pane.show_app(app) def show_available_packages(self, packages): """ Show packages given as arguments in the available_pane If the list of packages is only one element long show that, otherwise turn it into a comma separated search """ try: search_text, app = parse_packages_args(packages) except DebFileOpenError as e: LOG.exception("show_available_packages: can not open %r, error:", packages) dialogs.error(None, _("Error"), _("The file “%s” could not be opened.") % e.path) search_text = app = None LOG.debug('show_available_packages: search_text is %r, app is %r.', search_text, app) if search_text: self.available_pane.init_view() self.available_pane.searchentry.set_text(search_text) elif app is not None: self.show_app(app) else: # normal startup, show the lobby (it will have a spinner when # its not ready yet) - it will also initialize the view self.view_manager.set_active_view(ViewPages.AVAILABLE) def restore_state(self): (x, y) = self.config.app_window_size if x > 0 and y > 0: self.window_main.set_default_size(x, y) else: # on first launch, specify the default window size to take # advantage of the available screen real estate (but set a # reasonable limit in case of a crazy-huge monitor) screen_height = Gdk.Screen.height() screen_width = Gdk.Screen.width() self.window_main.set_default_size( min(int(.85 * screen_width), 1200), min(int(.85 * screen_height), 800)) if self.config.app_window_maximized: self.window_main.maximize() def save_state(self): LOG.debug("save_state") # this happens on a delete event, we explicitly save_state() there window = self.window_main.get_window() if window is None: return maximized = window.get_state() & Gdk.WindowState.MAXIMIZED self.config.app_window_maximized = maximized if not maximized: # size only matters when non-maximized size = self.window_main.get_size() self.config.app_window_size = [size[0], size[1]] self.config.write() def write_memory_dump(self, fname=None): # trigger this e.g. with: # dbus-send --print-reply --session --dest=com.ubuntu.Softwarecenter \ # /com/ubuntu/Softwarecenter \ # com.ubuntu.SoftwarecenterIFace.writeMemoryDump if fname is None: fname = "software-center_%s.meliae" % time.strftime( "%Y%m%d_%H%M%S") try: from meliae import scanner scanner.dump_all_objects(fname) except Exception: LOG.exception("write_memory_dump failed") def run(self, args): # show window as early as possible self.window_main.show_all() # delay cache open GLib.timeout_add(1, self.cache.open) # support both "pkg1 pkg" and "pkg1,pkg2" (and pkg1,pkg2 pkg3) if args: for (i, arg) in enumerate(args[:]): if "," in arg: args.extend(arg.split(",")) del args[i] # FIXME: make this more predictable and less random # show args when the app is ready self.show_available_packages(args) atexit.register(self.save_state)