1
# Copyright (C) 2009 Canonical
6
# This program is free software; you can redistribute it and/or modify it under
7
# the terms of the GNU General Public License as published by the Free Software
8
# Foundation; version 3.
10
# This program is distributed in the hope that it will be useful, but WITHOUT
11
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15
# You should have received a copy of the GNU General Public License along with
16
# this program; if not, write to the Free Software Foundation, Inc.,
17
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
34
# purely to initialize the netstatus
35
import softwarecenter.netstatus
36
# make pyflakes shut up
37
softwarecenter.netstatus.NETWORK_STATE
40
from ui.gtk.SimpleGtkbuilderApp import SimpleGtkbuilderApp
41
from softwarecenter.db.application import Application
42
from softwarecenter.db.debfile import DebFileApplication
44
from softwarecenter.enums import (Icons,
50
MOUSE_EVENT_FORWARD_BUTTON,
51
MOUSE_EVENT_BACK_BUTTON,
52
SOFTWARE_CENTER_NAME_KEYRING,
53
SOFTWARE_CENTER_SSO_DESCRIPTION,
56
from softwarecenter.paths import SOFTWARE_CENTER_PLUGIN_DIR, ICON_PATH
57
from softwarecenter.utils import (clear_token_from_ubuntu_sso,
58
wait_for_apt_cache_ready)
59
from softwarecenter.version import VERSION
60
from softwarecenter.db.database import StoreDatabase
61
import softwarecenter.ui.gtk.dependency_dialogs as dependency_dialogs
62
import softwarecenter.ui.gtk.deauthorize_dialog as deauthorize_dialog
63
from softwarecenter.backend.aptd import TransactionFinishedResult
65
from ui.gtk.viewswitcher import ViewSwitcher
66
from ui.gtk.pendingview import PendingView
67
from ui.gtk.installedpane import InstalledPane
68
from ui.gtk.channelpane import ChannelPane
69
from ui.gtk.availablepane import AvailablePane
70
from ui.gtk.softwarepane import SoftwareSection
71
from ui.gtk.historypane import HistoryPane
72
from ui.gtk.viewmanager import ViewManager
74
from config import get_config
75
from backend import get_install_backend
76
from paths import SOFTWARE_CENTER_ICON_CACHE_DIR
78
from plugin import PluginManager
79
from backend.reviews import get_review_loader, UsefulnessCache
80
from distro import get_distro
81
from db.pkginfo import get_pkg_info
82
from gettext import gettext as _
84
LOG = logging.getLogger(__name__)
87
class SoftwarecenterDbusController(dbus.service.Object):
89
This is a helper to provide the SoftwarecenterIFace
91
It provides only a bringToFront method that takes
92
additional arguments about what packages to show
94
def __init__(self, parent, bus_name,
95
object_path='/com/ubuntu/Softwarecenter'):
96
dbus.service.Object.__init__(self, bus_name, object_path)
99
@dbus.service.method('com.ubuntu.SoftwarecenterIFace')
100
def bringToFront(self, args):
101
if args != 'nothing-to-show':
102
self.parent.show_available_packages(args)
103
self.parent.window_main.present()
106
@dbus.service.method('com.ubuntu.SoftwarecenterIFace')
107
def triggerDatabaseReopen(self):
108
self.parent.db.emit("reopen")
110
@dbus.service.method('com.ubuntu.SoftwarecenterIFace')
111
def triggerCacheReload(self):
112
self.parent.cache.emit("cache-ready")
114
class SoftwareCenterApp(SimpleGtkbuilderApp):
116
WEBLINK_URL = "http://apt.ubuntu.com/p/%s"
118
# the size of the icon for dialogs
119
APP_ICON_SIZE = 48 # gtk.ICON_SIZE_DIALOG ?
121
def __init__(self, datadir, xapian_base_path, options, args=None):
123
self.datadir = datadir
124
SimpleGtkbuilderApp.__init__(self,
125
datadir+"/ui/SoftwareCenter.ui",
127
gettext.bindtextdomain("software-center", "/usr/share/locale")
128
gettext.textdomain("software-center")
131
locale.setlocale(locale.LC_ALL, "")
133
LOG.exception("setlocale failed, resetting to C")
134
locale.setlocale(locale.LC_ALL, "C")
136
# setup dbus and exit if there is another instance already
138
self.setup_dbus_or_bring_other_instance_to_front(args)
139
self.setup_database_rebuilding_listener()
141
# distro specific stuff
142
self.distro = get_distro()
144
# Disable software-properties if it does not exist
145
if not os.path.exists("/usr/bin/software-properties-gtk"):
146
sources = self.builder.get_object("menuitem_software_sources")
147
sources.set_sensitive(False)
149
# a main iteration friendly apt cache
150
self.cache = get_pkg_info()
151
self.cache.connect("cache-broken", self._on_apt_cache_broken)
154
self.backend = get_install_backend()
155
self.backend.connect("transaction-finished", self._on_transaction_finished)
156
self.backend.connect("channels-changed", self.on_channels_changed)
158
pathname = os.path.join(xapian_base_path, "xapian")
159
self._use_axi = not options.disable_apt_xapian_index
162
self.db = StoreDatabase(pathname, self.cache)
163
self.db.open(use_axi = self._use_axi)
164
if self.db.schema_version() != DB_SCHEMA_VERSION:
165
LOG.warn("database format '%s' expected, but got '%s'" % (
166
DB_SCHEMA_VERSION, self.db.schema_version()))
167
if os.access(pathname, os.W_OK):
168
self._rebuild_and_reopen_local_db(pathname)
169
except xapian.DatabaseOpeningError:
170
# Couldn't use that folder as a database
171
# This may be because we are in a bzr checkout and that
172
# folder is empty. If the folder is empty, and we can find the
173
# script that does population, populate a database in it.
174
if os.path.isdir(pathname) and not os.listdir(pathname):
175
self._rebuild_and_reopen_local_db(pathname)
176
except xapian.DatabaseCorruptError, e:
177
LOG.exception("xapian open failed")
178
ui.gtk.dialogs.error(None,
179
_("Sorry, can not open the software database"),
180
_("Please re-install the 'software-center' "
182
# FIXME: force rebuild by providing a dbus service for this
186
self.review_loader = get_review_loader(self.cache, self.db)
187
# FIXME: add some kind of throttle, I-M-S here
188
self.review_loader.refresh_review_stats(self.on_review_stats_loaded)
189
#load usefulness votes from server when app starts
190
self.useful_cache = UsefulnessCache(True)
192
# additional icons come from app-install-data
193
self.icons = gtk.icon_theme_get_default()
194
self.icons.append_search_path(ICON_PATH)
195
self.icons.append_search_path(os.path.join(self.datadir,"icons"))
196
self.icons.append_search_path(os.path.join(self.datadir,"emblems"))
197
# HACK: make it more friendly for local installs (for mpt)
198
self.icons.append_search_path(self.datadir+"/icons/32x32/status")
199
# add the humanity icon theme to the iconpath, as not all icon
200
# themes contain all the icons we need
201
# this *shouldn't* lead to any performance regressions
202
path = '/usr/share/icons/Humanity'
203
if os.path.exists(path):
204
for subpath in os.listdir(path):
205
subpath = os.path.join(path, subpath)
206
if os.path.isdir(subpath):
207
for subsubpath in os.listdir(subpath):
208
subsubpath = os.path.join(subpath, subsubpath)
209
if os.path.isdir(subsubpath):
210
self.icons.append_search_path(subsubpath)
211
gtk.window_set_default_icon_name("softwarecenter")
214
self._block_menuitem_view = False
216
# for use when viewing previous purchases
220
# hackery, paint viewport borders around notebook
221
self.notebook_view.set_border_width(1)
222
self.notebook_view.connect('expose-event', self._on_notebook_expose)
224
# register view manager and create view panes/widgets
225
self.view_manager = ViewManager(self.notebook_view)
228
self.available_pane = AvailablePane(self.cache,
233
self.navhistory_back_action,
234
self.navhistory_forward_action)
235
self.available_pane.connect("available-pane-created", self.on_available_pane_created)
236
self.view_manager.register(self.available_pane, ViewPages.AVAILABLE)
238
# channel pane (view not fully initialized at this point)
239
self.channel_pane = ChannelPane(self.cache,
244
self.channel_pane.connect("channel-pane-created", self.on_channel_pane_created)
245
self.view_manager.register(self.channel_pane, ViewPages.CHANNEL)
247
# installed pane (view not fully initialized at this point)
248
self.installed_pane = InstalledPane(self.cache,
253
self.installed_pane.connect("installed-pane-created", self.on_installed_pane_created)
254
self.view_manager.register(self.installed_pane, ViewPages.INSTALLED)
256
# history pane (not fully loaded at this point)
257
self.history_pane = HistoryPane(self.cache,
262
self.history_pane.connect("history-pane-created", self.on_history_pane_created)
263
self.view_manager.register(self.history_pane, ViewPages.HISTORY)
266
self.pending_view = PendingView(self.icons)
267
self.view_manager.register(self.pending_view, ViewPages.PENDING)
269
# keep track of the current active pane
270
self.active_pane = self.available_pane
273
self.view_switcher = ViewSwitcher(self.view_manager, self.datadir, self.db, self.cache, self.icons)
274
self.scrolledwindow_viewswitcher.add(self.view_switcher)
275
self.view_switcher.show()
276
self.view_switcher.connect("view-changed",
277
self.on_view_switcher_changed)
278
self.view_switcher.width = self.scrolledwindow_viewswitcher.get_property('width-request')
279
self.view_switcher.connect('size-allocate', self.on_viewswitcher_resized)
281
# expand the Get Software node in the viewswitcher by default so that its important subitems
282
# (e.g., For Purchase and Independent) are always clearly visible and available
283
self.view_switcher.expand_available_node()
285
# launchpad integration help, its ok if that fails
287
import LaunchpadIntegration
288
LaunchpadIntegration.set_sourcepackagename("software-center")
289
LaunchpadIntegration.add_items(self.menu_help, 1, True, False)
291
LOG.debug("launchpad integration error: '%s'" % e)
293
# set up accelerator keys for navigation history actions
294
accel_group = gtk.AccelGroup()
295
self.window_main.add_accel_group(accel_group)
296
self.menuitem_go_back.add_accelerator("activate",
299
gtk.gdk.CONTROL_MASK,
301
self.menuitem_go_forward.add_accelerator("activate",
304
gtk.gdk.CONTROL_MASK,
306
self.menuitem_go_back.add_accelerator("activate",
308
gtk.gdk.keyval_from_name("Left"),
311
self.menuitem_go_forward.add_accelerator("activate",
313
gtk.gdk.keyval_from_name("Right"),
316
self.menuitem_go_back.add_accelerator("activate",
318
gtk.gdk.keyval_from_name("KP_Left"),
321
self.menuitem_go_forward.add_accelerator("activate",
323
gtk.gdk.keyval_from_name("KP_Right"),
327
# specify the smallest allowable window size
328
self.window_main.set_size_request(700, 400)
330
# setup window name and about information (needs branding)
331
name = self.distro.get_app_name()
332
self.window_main.set_title(name)
333
self.aboutdialog.set_name(name)
334
about_description = self.distro.get_app_description()
335
self.aboutdialog.set_comments(about_description)
338
self.aboutdialog.connect("response",
339
lambda dialog, rid: dialog.hide())
340
self.aboutdialog.connect("delete_event", self.aboutdialog.hide_on_delete)
343
self.config = get_config()
346
# create label_status for in our eventbox
347
self.label_status = gtk.Label()
348
self.status_box.a11y = self.status_box.get_accessible()
349
self.status_box.a11y.set_role(atk.ROLE_STATUSBAR)
350
self.status_box.add(self.label_status)
352
# open plugin manager and load plugins
353
self.plugin_manager = PluginManager(self, SOFTWARE_CENTER_PLUGIN_DIR)
354
self.plugin_manager.load_plugins()
356
# make the local cache directory if it doesn't already exist
357
icon_cache_dir = SOFTWARE_CENTER_ICON_CACHE_DIR
358
if not os.path.exists(icon_cache_dir):
359
os.makedirs(icon_cache_dir)
360
self.icons.append_search_path(icon_cache_dir)
362
# run s-c-agent update
363
if options.disable_buy:
364
file_menu = self.builder.get_object("menu1")
365
file_menu.remove(self.builder.get_object("menuitem_reinstall_purchases"))
367
sc_agent_update = os.path.join(
368
self.datadir, "update-software-center-agent")
369
(pid, stdin, stdout, stderr) = glib.spawn_async(
370
[sc_agent_update], flags=glib.SPAWN_DO_NOT_REAP_CHILD)
371
glib.child_watch_add(
372
pid, self._on_update_software_center_agent_finished)
375
# FIXME: REMOVE THIS once launchpad integration is enabled
377
if not options.enable_lp:
378
file_menu = self.builder.get_object("menu1")
379
file_menu.remove(self.builder.get_object("menuitem_launchpad_private_ppas"))
381
if options.disable_buy and not options.enable_lp:
382
file_menu.remove(self.builder.get_object("separator_login"))
384
# TODO: Remove the following two lines once we have remove repository
385
# support in aptdaemon (see LP: #723911)
386
file_menu = self.builder.get_object("menu1")
387
file_menu.remove(self.builder.get_object("menuitem_deauthorize_computer"))
390
def _rebuild_and_reopen_local_db(self, pathname):
391
""" helper that rebuilds a db and reopens it """
392
from softwarecenter.db.update import rebuild_database
393
LOG.info("building local database")
394
rebuild_database(pathname)
395
self.db = StoreDatabase(pathname, self.cache)
396
self.db.open(use_axi=self._use_axi)
399
def on_available_pane_created(self, widget):
400
available_section = SoftwareSection()
401
available_section.set_view_id(ViewPages.AVAILABLE)
402
self.available_pane.set_section(available_section)
405
self.available_pane.connect("app-list-changed",
406
self.on_app_list_changed,
408
self.available_pane.app_details_view.connect("selected",
409
self.on_app_details_changed,
411
self.available_pane.app_details_view.connect("application-request-action",
412
self.on_application_request_action)
413
self.available_pane.app_view.connect("application-request-action",
414
self.on_application_request_action)
415
self.available_pane.app_view.connect("mouse-nav-requested",
416
self.on_window_main_button_press_event)
417
self.available_pane.searchentry.grab_focus()
419
def on_channel_pane_created(self, widget):
420
channel_section = SoftwareSection()
421
# note that the view_id for each channel's section is set later
422
# depending on whether the channel view will display available or
424
self.channel_pane.set_section(channel_section)
427
self.channel_pane.connect("app-list-changed",
428
self.on_app_list_changed,
430
self.channel_pane.app_details_view.connect("selected",
431
self.on_app_details_changed,
433
self.channel_pane.app_details_view.connect("application-request-action",
434
self.on_application_request_action)
435
self.channel_pane.app_view.connect("application-request-action",
436
self.on_application_request_action)
438
def on_installed_pane_created(self, widget):
439
installed_section = SoftwareSection()
440
installed_section.set_view_id(ViewPages.INSTALLED)
441
self.installed_pane.set_section(installed_section)
444
self.installed_pane.connect("app-list-changed",
445
self.on_app_list_changed,
447
self.installed_pane.app_details_view.connect("selected",
448
self.on_app_details_changed,
450
self.installed_pane.app_details_view.connect("application-request-action",
451
self.on_application_request_action)
452
self.installed_pane.app_view.connect("application-request-action",
453
self.on_application_request_action)
455
def on_history_pane_created(self, widget):
457
self.history_pane.connect("app-list-changed",
458
self.on_app_list_changed,
461
def _on_update_software_center_agent_finished(self, pid, condition):
462
LOG.info("software-center-agent finished with status %i" % os.WEXITSTATUS(condition))
463
if os.WEXITSTATUS(condition) == 0:
466
def on_review_stats_loaded(self, reviews):
467
LOG.debug("on_review_stats_loaded: '%s'" % len(reviews))
469
def on_app_details_changed(self, widget, app, page):
470
self.update_status_bar()
472
def on_app_list_changed(self, pane, new_len, page):
473
if self.view_manager.get_active_view() == page:
474
self.update_status_bar()
476
def on_window_main_delete_event(self, widget, event):
477
if hasattr(self, "glaunchpad"):
478
self.glaunchpad.shutdown()
482
def on_window_main_key_press_event(self, widget, event):
484
Implement the backspace key as a hotkey to back up one level in
485
the navigation heirarchy. This works everywhere except when
486
purchasing software in the purchase_view where backspace works
487
as expected in the webkit text fields.
489
if (event.keyval == gtk.gdk.keyval_from_name("BackSpace") and
491
hasattr(self.active_pane, 'navigation_bar') and
492
not self.active_pane.searchentry.is_focus() and
493
not self.active_pane.navigation_bar.has_id(NavButtons.PURCHASE)):
494
self.active_pane.navigation_bar.navigate_up()
496
def on_window_main_button_press_event(self, widget, event):
498
Implement back/forward navigation via mouse navigation keys using
499
the same button codes as used in Nautilus.
501
if (event.button == MOUSE_EVENT_BACK_BUTTON and
503
hasattr(self.active_pane, 'navigation_bar') and
504
not self.active_pane.navigation_bar.has_id(NavButtons.PURCHASE)):
505
self.on_navhistory_back_action_activate()
506
elif (event.button == MOUSE_EVENT_FORWARD_BUTTON and
508
hasattr(self.active_pane, 'navigation_bar') and
509
not self.active_pane.navigation_bar.has_id(NavButtons.PURCHASE)):
510
self.on_navhistory_forward_action_activate()
512
def on_view_switcher_changed(self, view_switcher, view_id, channel):
513
LOG.debug("view_switcher_activated: %s %s" % (view_switcher, view_id))
516
self.active_pane = self.view_manager.get_view_widget(view_id)
519
self.menuitem_view_supported_only.set_sensitive(self.active_pane != None)
520
self.menuitem_view_all.set_sensitive(self.active_pane != None)
523
self._block_menuitem_view = True
524
if not self.active_pane.apps_filter:
525
self.menuitem_view_all.set_sensitive(False)
526
self.menuitem_view_supported_only.set_sensitive(False)
527
elif self.active_pane.apps_filter.get_supported_only():
528
self.menuitem_view_supported_only.activate()
530
self.menuitem_view_all.activate()
531
self._block_menuitem_view = False
532
if view_id == ViewPages.AVAILABLE:
533
back_action = self.available_pane.nav_history.navhistory_back_action
534
forward_action = self.available_pane.nav_history.navhistory_forward_action
535
self.menuitem_go_back.set_sensitive(back_action.get_sensitive())
536
self.menuitem_go_forward.set_sensitive(forward_action.get_sensitive())
538
self.menuitem_go_back.set_sensitive(False)
539
self.menuitem_go_forward.set_sensitive(False)
541
self.view_manager.set_active_view(view_id)
542
if (view_id == ViewPages.INSTALLED and
543
not self.installed_pane.loaded and
544
not self.installed_pane.get_current_app()):
545
self.installed_pane.refresh_apps()
546
self.update_app_list_view(channel)
547
self.update_status_bar()
549
def on_viewswitcher_resized(self, widget, allocation):
550
self.view_switcher.width = allocation.width
552
def _on_lp_login(self, lp, token):
553
self._lp_login_successful = True
554
private_archives = self.glaunchpad.get_subscribed_archives()
555
self.view_switcher.get_model().channel_manager.feed_in_private_sources_list_entries(
558
def _on_sso_login(self, sso, oauth_result):
559
self._sso_login_successful = True
560
# consumer key is the openid identifier
561
self.scagent.query_available_for_me(oauth_result["token"],
562
oauth_result["consumer_key"])
564
def _available_for_me_result(self, scagent, result_list):
565
#print "available_for_me_result", result_list
566
from db.update import add_from_purchased_but_needs_reinstall_data
567
available_for_me_query = add_from_purchased_but_needs_reinstall_data(
568
result_list, self.db, self.cache)
569
self.available_pane.on_previous_purchases_activated(available_for_me_query)
571
def on_application_request_action(self, widget, app, addons_install, addons_remove, action):
572
"""callback when an app action is requested from the appview,
573
if action is "remove", must check if other dependencies have to be
574
removed as well and show a dialog in that case
576
LOG.debug("on_application_action_requested: '%s' %s" % (app, action))
577
appdetails = app.get_details(self.db)
578
if action == "remove":
579
if not dependency_dialogs.confirm_remove(None, self.datadir, app,
580
self.db, self.icons):
581
# craft an instance of TransactionFinishedResult to send with the
582
# transaction-stopped signal
583
result = TransactionFinishedResult(None, None)
584
result.pkgname = app.pkgname
585
self.backend.emit("transaction-stopped", result)
587
elif action == "install":
588
# If we are installing a package, check for dependencies that will
589
# also be removed and show a dialog for confirmation
590
# generic removal text (fixing LP bug #554319)
591
if not dependency_dialogs.confirm_install(None, self.datadir, app,
592
self.db, self.icons):
593
# craft an instance of TransactionFinishedResult to send with the
594
# transaction-stopped signal
595
result = TransactionFinishedResult(None, None)
596
result.pkgname = app.pkgname
597
self.backend.emit("transaction-stopped", result)
600
# this allows us to 'upgrade' deb files
601
if action == 'upgrade' and app.request and type(app) == DebFileApplication:
604
# action_func is one of: "install", "remove", "upgrade", "apply_changes"
605
action_func = getattr(self.backend, action)
606
if action == 'install':
607
# the package.deb path name is in the request
608
if app.request and type(app) == DebFileApplication:
609
debfile_name = app.request
612
action_func(app.pkgname, app.appname, appdetails.icon, debfile_name, addons_install, addons_remove)
613
elif callable(action_func):
614
action_func(app.pkgname, app.appname, appdetails.icon, addons_install=addons_install, addons_remove=addons_remove)
616
LOG.error("Not a valid action in AptdaemonBackend: '%s'" % action)
618
def get_icon_filename(self, iconname, iconsize):
619
iconinfo = self.icons.lookup_icon(iconname, iconsize, 0)
621
iconinfo = self.icons.lookup_icon(Icons.MISSING_APP, iconsize, 0)
622
return iconinfo.get_filename()
625
def on_menu_file_activate(self, menuitem):
626
"""Enable/disable install/remove"""
627
LOG.debug("on_menu_file_activate")
628
# check if we have a pkg for this page
631
app = self.active_pane.get_current_app()
633
self.menuitem_install.set_sensitive(False)
634
self.menuitem_remove.set_sensitive(False)
636
# wait for the cache to become ready (if needed)
637
if not self.cache.ready:
638
glib.timeout_add(100, lambda: self.on_menu_file_activate(menuitem))
643
# FIXME: Use a gtk.Action for the Install/Remove/Buy/Add Source/Update Now action
644
# so that all UI controls (menu item, applist view button and appdetails
645
# view button) are managed centrally: button text, button sensitivity,
646
# and callback method
647
# FIXME: Add buy support here by implementing the above
648
appdetails = app.get_details(self.db)
650
pkg_state = appdetails.pkg_state
651
error = appdetails.error
652
if self.active_pane.app_view.is_action_in_progress_for_selected_app():
653
self.menuitem_install.set_sensitive(False)
654
self.menuitem_remove.set_sensitive(False)
655
elif pkg_state == PkgStates.UPGRADABLE or pkg_state == PkgStates.REINSTALLABLE and not error:
656
self.menuitem_install.set_sensitive(True)
657
self.menuitem_remove.set_sensitive(True)
658
elif pkg_state == PkgStates.INSTALLED:
659
self.menuitem_install.set_sensitive(False)
660
self.menuitem_remove.set_sensitive(True)
661
elif pkg_state == PkgStates.UNINSTALLED and not error:
662
self.menuitem_install.set_sensitive(True)
663
self.menuitem_remove.set_sensitive(False)
664
elif (not pkg_state and
665
not self.active_pane.is_category_view_showing() and
666
app.pkgname in self.cache and
667
not self.active_pane.app_view.is_action_in_progress_for_selected_app() and
669
pkg = self.cache[app.pkgname]
670
installed = bool(pkg.installed)
671
self.menuitem_install.set_sensitive(not installed)
672
self.menuitem_remove.set_sensitive(installed)
674
self.menuitem_install.set_sensitive(False)
675
self.menuitem_remove.set_sensitive(False)
676
# return False to ensure that a possible glib.timeout_add ends
679
def on_menuitem_launchpad_private_ppas_activate(self, menuitem):
680
from backend.launchpad import GLaunchpad
681
self.glaunchpad = GLaunchpad()
682
self.glaunchpad.connect("login-successful", self._on_lp_login)
683
from view.logindialog import LoginDialog
684
d = LoginDialog(self.glaunchpad, self.datadir, parent=self.window_main)
687
def _create_buildin_sso_if_needed(self):
689
from backend.restfulclient import UbuntuSSOlogin
690
self.sso = UbuntuSSOlogin()
691
self.sso.connect("login-successful", self._on_sso_login)
692
def _login_via_buildin_sso(self):
693
self._create_buildin_sso_if_needed()
694
if "SOFTWARE_CENTER_TEST_REINSTALL_PURCHASED" in os.environ:
695
self.scagent.query_available_for_me("dummy", "mvo")
697
from view.logindialog import LoginDialog
698
d = LoginDialog(self.sso, self.datadir, parent=self.window_main)
701
def _create_dbus_sso_if_needed(self):
703
from backend.login_sso import get_sso_backend
704
appname = SOFTWARE_CENTER_NAME_KEYRING
705
login_text = SOFTWARE_CENTER_SSO_DESCRIPTION
706
self.sso = get_sso_backend(self.window_main.window.xid,
707
appname, _(login_text))
708
self.sso.connect("login-successful", self._on_sso_login)
710
def _login_via_dbus_sso(self):
711
self._create_dbus_sso_if_needed()
714
def _create_scagent_if_needed(self):
716
from backend.scagent import SoftwareCenterAgent
717
self.scagent = SoftwareCenterAgent(xid=self.window_main.window.xid)
718
self.scagent.connect("available-for-me", self._available_for_me_result)
720
def on_menuitem_reinstall_purchases_activate(self, menuitem):
721
self.view_switcher.select_available_node()
722
self._create_scagent_if_needed()
723
# support both buildin or ubuntu-sso-login
724
if "SOFTWARE_CENTER_USE_BUILTIN_LOGIN" in os.environ:
725
self._login_via_buildin_sso()
727
self._login_via_dbus_sso()
729
def on_menuitem_deauthorize_computer_activate(self, menuitem):
731
# FIXME: need Ubuntu SSO username here
732
# account_name = get_person_from_config()
735
# get a list of installed purchased packages
736
installed_purchased_packages = self.db.get_installed_purchased_packages()
738
# display the deauthorize computer dialog
739
deauthorize = deauthorize_dialog.deauthorize_computer(None,
744
installed_purchased_packages)
746
# clear the ubuntu SSO token for this account
747
# FIXME: this needs to be consolidated - one token is
748
# aquired for purchase in utils/submit_review.py
749
# the other one in softwarecenter/app.py
750
clear_token_from_ubuntu_sso(_("Ubuntu Software Center"))
751
clear_token_from_ubuntu_sso(_("Ubuntu Software Center Store"))
753
# uninstall the list of purchased packages
754
# TODO: do we need to check for dependencies and show a removal
755
# dialog for that case? seems not since these are purchased apps
756
for pkgname in installed_purchased_packages:
757
app = Application(pkgname=pkgname)
758
appdetails = app.get_details(self.db)
759
self.backend.remove(app.pkgname, app.appname, appdetails.icon)
761
# TODO: remove the corresponding private PPA sources
762
# FIXME: this should really be done using aptdaemon, update this if/when
763
# remove repository support is added to aptdaemon
764
# (private-ppa.launchpad.net_commercial-ppa-uploaders*)
765
purchased_sources = glob.glob("/etc/apt/sources.list.d/private-ppa.launchpad.net_commercial-ppa-uploaders*")
766
for source in purchased_sources:
767
print "source: ", source
769
def on_menuitem_install_activate(self, menuitem):
770
app = self.active_pane.get_current_app()
771
self.on_application_request_action(self, app, [], [], AppActions.INSTALL)
773
def on_menuitem_remove_activate(self, menuitem):
774
app = self.active_pane.get_current_app()
775
self.on_application_request_action(self, app, [], [], AppActions.REMOVE)
777
def on_menuitem_close_activate(self, widget):
780
def on_menu_edit_activate(self, menuitem):
782
Check whether the search field is focused and if so, focus some items
784
edit_menu_items = [self.menuitem_undo,
788
self.menuitem_copy_web_link,
790
self.menuitem_delete,
791
self.menuitem_select_all,
792
self.menuitem_search]
793
for item in edit_menu_items:
794
item.set_sensitive(False)
795
if (self.active_pane and
796
self.active_pane.searchentry and
797
self.active_pane.searchentry.flags() & gtk.VISIBLE):
798
# undo, redo, cut, copy, paste, delete, select_all sensitive
799
# if searchentry is focused (and other more specific conditions)
800
if self.active_pane.searchentry.is_focus():
801
if len(self.active_pane.searchentry._undo_stack) > 1:
802
self.menuitem_undo.set_sensitive(True)
803
if len(self.active_pane.searchentry._redo_stack) > 0:
804
self.menuitem_redo.set_sensitive(True)
805
bounds = self.active_pane.searchentry.get_selection_bounds()
807
self.menuitem_cut.set_sensitive(True)
808
self.menuitem_copy.set_sensitive(True)
809
self.menuitem_paste.set_sensitive(True)
810
if self.active_pane.searchentry.get_text():
811
self.menuitem_delete.set_sensitive(True)
812
self.menuitem_select_all.set_sensitive(True)
813
# search sensitive if searchentry is not focused
815
self.menuitem_search.set_sensitive(True)
819
app = self.active_pane.get_current_app()
820
if app and app.pkgname in self.cache:
821
self.menuitem_copy_web_link.set_sensitive(True)
824
if (self.active_pane and
825
self.active_pane.is_app_details_view_showing()):
827
self.menuitem_select_all.set_sensitive(True)
828
sel_text = self.active_pane.app_details_view.desc.get_selected_text()
831
self.menuitem_copy.set_sensitive(True)
833
def on_menuitem_undo_activate(self, menuitem):
834
self.active_pane.searchentry.undo()
836
def on_menuitem_redo_activate(self, menuitem):
837
self.active_pane.searchentry.redo()
839
def on_menuitem_cut_activate(self, menuitem):
840
self.active_pane.searchentry.cut_clipboard()
842
def on_menuitem_copy_activate(self, menuitem):
843
if (self.active_pane and
844
self.active_pane.is_app_details_view_showing()):
846
self.active_pane.app_details_view.desc.copy_clipboard()
848
elif self.active_pane:
849
self.active_pane.searchentry.copy_clipboard()
851
def on_menuitem_paste_activate(self, menuitem):
852
self.active_pane.searchentry.paste_clipboard()
854
def on_menuitem_delete_activate(self, menuitem):
855
self.active_pane.searchentry.set_text("")
857
def on_menuitem_select_all_activate(self, menuitem):
858
if (self.active_pane and
859
self.active_pane.is_app_details_view_showing()):
861
self.active_pane.app_details_view.desc.select_all()
862
self.active_pane.app_details_view.desc.grab_focus()
864
elif self.active_pane:
865
self.active_pane.searchentry.select_region(0, -1)
867
def on_menuitem_copy_web_link_activate(self, menuitem):
868
app = self.active_pane.get_current_app()
870
clipboard = gtk.Clipboard()
871
clipboard.set_text(self.WEBLINK_URL % app.pkgname)
873
def on_menuitem_search_activate(self, widget):
875
self.active_pane.searchentry.grab_focus()
876
self.active_pane.searchentry.select_region(0, -1)
878
def on_menuitem_software_sources_activate(self, widget):
879
#print "on_menu_item_software_sources_activate"
880
self.window_main.set_sensitive(False)
881
# run software-properties-gtk
882
p = subprocess.Popen(
884
"--desktop", "/usr/share/applications/software-properties-gtk.desktop",
886
"/usr/bin/software-properties-gtk",
888
"-t", str(self.window_main.window.xid)])
889
# Monitor the subprocess regularly
890
glib.timeout_add(100, self._poll_software_sources_subprocess, p)
892
def _poll_software_sources_subprocess(self, popen):
897
# A return code of 1 means that the sources have changed
899
self.run_update_cache()
900
self.window_main.set_sensitive(True)
904
def on_menuitem_about_activate(self, widget):
905
self.aboutdialog.set_version(VERSION)
906
self.aboutdialog.set_transient_for(self.window_main)
907
self.aboutdialog.show()
909
def on_menuitem_help_activate(self, menuitem):
911
p = subprocess.Popen(["yelp","ghelp:software-center"])
912
# collect the exit status (otherwise we leave zombies)
913
glib.timeout_add_seconds(1, lambda p: p.poll() == None, p)
915
def on_menuitem_view_all_activate(self, widget):
916
if (not self._block_menuitem_view and
917
self.active_pane.apps_filter and
918
self.active_pane.apps_filter.get_supported_only()):
919
self.active_pane.apps_filter.set_supported_only(False)
920
self.active_pane.refresh_apps()
922
# update recommended widget counter
923
if self.available_pane and self.available_pane.cat_view:
924
self.available_pane.cat_view._append_recommendations()
926
# update subcategory view
927
if (self.available_pane and
928
self.available_pane == self.active_pane and
929
self.available_pane.subcategories_view and
930
self.available_pane.subcategories_view.current_category):
931
self.available_pane.subcategories_view._append_subcat_departments(
932
self.available_pane.subcategories_view.current_category,
933
len(self.available_pane.app_view.get_model()))
935
def on_menuitem_view_supported_only_activate(self, widget):
936
if (not self._block_menuitem_view and
937
self.active_pane.apps_filter and
938
not self.active_pane.apps_filter.get_supported_only()):
939
self.active_pane.apps_filter.set_supported_only(True)
940
self.active_pane.refresh_apps()
942
# navigate up if the details page is no longer available
943
ap = self.active_pane
944
if (ap and ap.is_app_details_view_showing and ap.app_details_view.app and
945
not self.distro.is_supported(self.cache, None, ap.app_details_view.app.pkgname)):
946
if len(ap.app_view.get_model()) == 0:
947
ap.navigation_bar.navigate_up_twice()
949
ap.navigation_bar.navigate_up()
950
ap.on_application_selected(None, None)
952
# navigate up if the list page is empty
953
elif (ap and ap.is_applist_view_showing() and
954
len(ap.app_view.get_model()) == 0):
955
ap.navigation_bar.navigate_up()
956
ap.on_application_selected(None, None)
958
# update recommended widget counter
959
if self.available_pane and self.available_pane.cat_view:
960
self.available_pane.cat_view._append_recommendations()
962
# update subcategory view
963
if (self.available_pane and
964
self.available_pane == self.active_pane and
965
self.available_pane.subcategories_view and
966
self.available_pane.subcategories_view.current_category):
967
self.available_pane.subcategories_view._append_subcat_departments(
968
self.available_pane.subcategories_view.current_category,
969
len(self.available_pane.app_view.get_model()))
971
def on_navhistory_back_action_activate(self, navhistory_back_action=None):
972
self.available_pane.nav_history.nav_back()
973
self.available_pane._status_text = ""
974
self.update_status_bar()
976
def on_navhistory_forward_action_activate(self, navhistory_forward_action=None):
977
self.available_pane.nav_history.nav_forward()
978
self.available_pane._status_text = ""
979
self.update_status_bar()
981
def _ask_and_repair_broken_cache(self):
982
# wait until the window window is available
983
if self.window_main.props.visible == False:
984
glib.timeout_add_seconds(1, self._ask_and_repair_broken_cache)
986
if ui.gtk.dialogs.confirm_repair_broken_cache(self.window_main,
988
self.backend.fix_broken_depends()
990
def _on_notebook_expose(self, widget, event):
991
# use availabel pane as the Style source so viewport colours are the same
993
self.available_pane.style.paint_shadow(widget.window,
1000
widget.allocation.y,
1001
widget.allocation.width,
1002
widget.allocation.height)
1005
def _on_apt_cache_broken(self, aptcache):
1006
self._ask_and_repair_broken_cache()
1008
def _on_transaction_finished(self, backend, result):
1009
""" callback when an application install/remove transaction
1010
(or a cache reload) has finished
1014
def on_channels_changed(self, backend, res):
1015
""" callback when the set of software channels has changed """
1016
LOG.debug("on_channels_changed %s" % res)
1018
# reopen the database, this will ensure that the right signals
1019
# are send and triggers "refresh_apps"
1020
# and refresh the displayed app in the details as well
1022
self.update_status_bar()
1026
def run_update_cache(self):
1027
"""update the apt cache (e.g. after new sources where added """
1028
self.backend.reload()
1030
def update_status_bar(self):
1031
"Helper that updates the status bar"
1032
if self.active_pane:
1033
s = self.active_pane.get_status_text()
1035
# FIXME: deal with the pending view status
1037
self.label_status.set_text(s)
1041
self.status_box.a11y.set_name(s)
1042
self.status_box.set_property('can-focus', True)
1044
self.status_box.set_property('can-focus', False)
1046
def update_app_list_view(self, channel=None):
1047
"""Helper that updates the app view list """
1048
if self.active_pane is None:
1050
if channel is None and self.active_pane.is_category_view_showing():
1053
self.channel_pane.set_channel(channel)
1054
self.active_pane.refresh_apps()
1056
def _on_database_rebuilding_handler(self, is_rebuilding):
1057
LOG.debug("_on_database_rebuilding_handler %s" % is_rebuilding)
1058
self._database_is_rebuilding = is_rebuilding
1063
# we need to reopen when the database finished updating
1066
def setup_database_rebuilding_listener(self):
1068
Setup system bus listener for database rebuilding
1070
self._database_is_rebuilding = False
1073
bus = dbus.SystemBus()
1075
LOG.exception("could not get system bus")
1077
# check if its currently rebuilding (most likely not, so we
1078
# just ignore errors from dbus because the interface
1080
proxy_obj = bus.get_object("com.ubuntu.Softwarecenter",
1081
"/com/ubuntu/Softwarecenter")
1082
iface = dbus.Interface(proxy_obj, "com.ubuntu.Softwarecenter")
1083
res = iface.IsRebuilding()
1084
self._on_database_rebuilding_handler(res)
1085
except Exception ,e:
1086
LOG.debug("query for the update-database exception '%s' (probably ok)" % e)
1088
# add signal handler
1089
bus.add_signal_receiver(self._on_database_rebuilding_handler,
1090
"DatabaseRebuilding",
1091
"com.ubuntu.Softwarecenter")
1093
def setup_dbus_or_bring_other_instance_to_front(self, args):
1095
This sets up a dbus listener
1098
bus = dbus.SessionBus()
1100
LOG.exception("could not initiate dbus")
1102
# if there is another Softwarecenter running bring it to front
1103
# and exit, otherwise install the dbus controller
1105
proxy_obj = bus.get_object('com.ubuntu.Softwarecenter',
1106
'/com/ubuntu/Softwarecenter')
1107
iface = dbus.Interface(proxy_obj, 'com.ubuntu.SoftwarecenterIFace')
1109
iface.bringToFront(args)
1111
# None can not be transported over dbus
1112
iface.bringToFront('nothing-to-show')
1114
except dbus.DBusException:
1115
bus_name = dbus.service.BusName('com.ubuntu.Softwarecenter',bus)
1116
self.dbusControler = SoftwarecenterDbusController(self, bus_name)
1118
def show_available_packages(self, packages):
1119
""" Show packages given as arguments in the available_pane
1120
If the list of packages is only one element long show that,
1121
otherwise turn it into a comma seperated search
1123
# strip away the apt: prefix
1124
if packages and packages[0].startswith("apt:///"):
1125
# this is for 'apt:pkgname' in alt+F2 in gnome
1126
packages[0] = packages[0].partition("apt:///")[2]
1127
elif packages and packages[0].startswith("apt://"):
1128
packages[0] = packages[0].partition("apt://")[2]
1129
elif packages and packages[0].startswith("apt:"):
1130
packages[0] = packages[0].partition("apt:")[2]
1132
# allow s-c to be called with a search term
1133
if packages and packages[0].startswith("search:"):
1134
packages[0] = packages[0].partition("search:")[2]
1135
self.available_pane.navigation_bar.remove_all(animate=False) # animate *must* be false here
1136
self.view_switcher.set_view(ViewPages.AVAILABLE)
1137
self.available_pane.notebook.set_current_page(
1138
self.available_pane.PAGE_APPLIST)
1139
self.available_pane.searchentry.set_text(" ".join(packages))
1142
if len(packages) == 1:
1143
request = packages[0]
1145
# are we dealing with a path?
1146
if os.path.exists(request) and not os.path.isdir(request):
1147
if not request.startswith('/'):
1148
# we may have been given a relative path
1149
request = os.path.join(os.getcwd(), request)
1150
app = DebFileApplication(request)
1152
# package from archive
1153
# if there is a "/" in the string consider it as tuple
1154
# of (pkgname, appname) for exact matching (used by
1156
(pkgname, sep, appname) = packages[0].partition("/")
1157
app = Application(appname, pkgname)
1159
@wait_for_apt_cache_ready
1160
def show_app(self, app):
1161
# if the pkg is installed, show it in the installed pane
1162
if (app.pkgname in self.cache and
1163
self.cache[app.pkgname].installed):
1164
self.installed_pane.loaded = True
1165
self.view_switcher.set_view(ViewPages.INSTALLED)
1166
self.installed_pane.loaded = False
1167
self.available_pane.bypassed = True
1168
self.installed_pane.show_app(app)
1170
self.view_switcher.set_view(ViewPages.AVAILABLE)
1171
self.available_pane.show_app(app)
1175
if len(packages) > 1:
1176
# turn multiple packages into a search with ","
1177
self.available_pane.searchentry.set_text(",".join(packages))
1178
self.available_pane.notebook.set_current_page(
1179
self.available_pane.PAGE_APPLIST)
1181
def restore_state(self):
1182
if self.config.has_option("general", "size"):
1183
(x, y) = self.config.get("general", "size").split(",")
1184
self.window_main.set_default_size(int(x), int(y))
1186
# on first launch, specify the default window size to take advantage
1187
# of the available screen real estate (but set a reasonable limit
1188
# in case of a crazy-huge monitor)
1189
screen_height = gtk.gdk.screen_height()
1190
screen_width = gtk.gdk.screen_width()
1191
self.window_main.set_default_size(min(int(.85 * screen_width), 1200),
1192
min(int(.85 * screen_height), 800))
1193
if (self.config.has_option("general", "maximized") and
1194
self.config.getboolean("general", "maximized")):
1195
self.window_main.maximize()
1196
if (self.config.has_option("general", "installed-node-expanded") and
1197
self.config.getboolean("general", "installed-node-expanded")):
1198
self.view_switcher.expand_installed_node()
1199
if (self.config.has_option("general", "sidebar-width")):
1200
width = int(self.config.get("general", "sidebar-width"))
1201
self.scrolledwindow_viewswitcher.set_property('width_request', width)
1203
def save_state(self):
1204
LOG.debug("save_state")
1205
# this happens on a delete event, we explicitely save_state() there
1206
if self.window_main.window is None:
1208
if not self.config.has_section("general"):
1209
self.config.add_section("general")
1210
maximized = self.window_main.window.get_state() & gtk.gdk.WINDOW_STATE_MAXIMIZED
1212
self.config.set("general", "maximized", "True")
1214
self.config.set("general", "maximized", "False")
1215
# size only matters when non-maximized
1216
size = self.window_main.get_size()
1217
self.config.set("general","size", "%s, %s" % (size[0], size[1]))
1218
installed_node_expanded = self.view_switcher.is_installed_node_expanded()
1219
if installed_node_expanded:
1220
self.config.set("general", "installed-node-expanded", "True")
1222
self.config.set("general", "installed-node-expanded", "False")
1223
width = self.view_switcher.width
1226
self.config.set("general", "sidebar-width", str(width))
1229
def run(self, args):
1230
self.window_main.show_all()
1231
# support both "pkg1 pkg" and "pkg1,pkg2" (and pkg1,pkg2 pkg3)
1233
for (i, arg) in enumerate(args[:]):
1235
args.extend(arg.split(","))
1237
self.show_available_packages(args)
1238
atexit.register(self.save_state)
1239
SimpleGtkbuilderApp.run(self)