~mvo/software-center/qml

« back to all changes in this revision

Viewing changes to softwarecenter/app.py

  • Committer: Michael Vogt
  • Date: 2011-10-05 13:08:09 UTC
  • mfrom: (1887.1.603 software-center)
  • Revision ID: michael.vogt@ubuntu.com-20111005130809-0tin9nr00f0uw65b
mergedĀ fromĀ trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2009 Canonical
2
 
#
3
 
# Authors:
4
 
#  Michael Vogt
5
 
#
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.
9
 
#
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
13
 
# details.
14
 
#
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
18
 
 
19
 
import atexit
20
 
import atk
21
 
import locale
22
 
import dbus
23
 
import dbus.service
24
 
import gettext
25
 
import logging
26
 
import glib
27
 
import gtk
28
 
import os
29
 
import subprocess
30
 
import sys
31
 
import xapian
32
 
import glob
33
 
 
34
 
# purely to initialize the netstatus
35
 
import softwarecenter.netstatus
36
 
# make pyflakes shut up
37
 
softwarecenter.netstatus.NETWORK_STATE
38
 
 
39
 
import ui.gtk
40
 
from ui.gtk.SimpleGtkbuilderApp import SimpleGtkbuilderApp
41
 
from softwarecenter.db.application import Application
42
 
from softwarecenter.db.debfile import DebFileApplication
43
 
 
44
 
from softwarecenter.enums import (Icons,
45
 
                                  PkgStates,
46
 
                                  ViewPages,
47
 
                                  NavButtons,
48
 
                                  AppActions,
49
 
                                  DB_SCHEMA_VERSION,
50
 
                                  MOUSE_EVENT_FORWARD_BUTTON,
51
 
                                  MOUSE_EVENT_BACK_BUTTON,
52
 
                                  SOFTWARE_CENTER_NAME_KEYRING,
53
 
                                  SOFTWARE_CENTER_SSO_DESCRIPTION,
54
 
                                 )
55
 
 
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
64
 
 
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
73
 
 
74
 
from config import get_config
75
 
from backend import get_install_backend
76
 
from paths import SOFTWARE_CENTER_ICON_CACHE_DIR
77
 
 
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 _
83
 
 
84
 
LOG = logging.getLogger(__name__)
85
 
 
86
 
 
87
 
class SoftwarecenterDbusController(dbus.service.Object):
88
 
    """ 
89
 
    This is a helper to provide the SoftwarecenterIFace
90
 
    
91
 
    It provides only a bringToFront method that takes 
92
 
    additional arguments about what packages to show
93
 
    """
94
 
    def __init__(self, parent, bus_name,
95
 
                 object_path='/com/ubuntu/Softwarecenter'):
96
 
        dbus.service.Object.__init__(self, bus_name, object_path)
97
 
        self.parent = parent
98
 
 
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()
104
 
        return True
105
 
 
106
 
    @dbus.service.method('com.ubuntu.SoftwarecenterIFace')
107
 
    def triggerDatabaseReopen(self):
108
 
        self.parent.db.emit("reopen")
109
 
 
110
 
    @dbus.service.method('com.ubuntu.SoftwarecenterIFace')
111
 
    def triggerCacheReload(self):
112
 
        self.parent.cache.emit("cache-ready")
113
 
 
114
 
class SoftwareCenterApp(SimpleGtkbuilderApp):
115
 
    
116
 
    WEBLINK_URL = "http://apt.ubuntu.com/p/%s"
117
 
    
118
 
    # the size of the icon for dialogs
119
 
    APP_ICON_SIZE = 48  # gtk.ICON_SIZE_DIALOG ?
120
 
 
121
 
    def __init__(self, datadir, xapian_base_path, options, args=None):
122
 
 
123
 
        self.datadir = datadir
124
 
        SimpleGtkbuilderApp.__init__(self, 
125
 
                                     datadir+"/ui/SoftwareCenter.ui", 
126
 
                                     "software-center")
127
 
        gettext.bindtextdomain("software-center", "/usr/share/locale")
128
 
        gettext.textdomain("software-center")
129
 
 
130
 
        try:
131
 
            locale.setlocale(locale.LC_ALL, "")
132
 
        except:
133
 
            LOG.exception("setlocale failed, resetting to C")
134
 
            locale.setlocale(locale.LC_ALL, "C")
135
 
 
136
 
        # setup dbus and exit if there is another instance already
137
 
        # running
138
 
        self.setup_dbus_or_bring_other_instance_to_front(args)
139
 
        self.setup_database_rebuilding_listener()
140
 
        
141
 
        # distro specific stuff
142
 
        self.distro = get_distro()
143
 
 
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)
148
 
 
149
 
        # a main iteration friendly apt cache
150
 
        self.cache = get_pkg_info()
151
 
        self.cache.connect("cache-broken", self._on_apt_cache_broken)
152
 
 
153
 
        # backend
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)
157
 
        # xapian
158
 
        pathname = os.path.join(xapian_base_path, "xapian")
159
 
        self._use_axi = not options.disable_apt_xapian_index
160
 
 
161
 
        try:
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' "
181
 
                            "package."))
182
 
            # FIXME: force rebuild by providing a dbus service for this
183
 
            sys.exit(1)
184
 
 
185
 
        # reviews
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)
191
 
    
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")
212
 
 
213
 
        # misc state
214
 
        self._block_menuitem_view = False
215
 
        
216
 
        # for use when viewing previous purchases
217
 
        self.scagent = None
218
 
        self.sso = None
219
 
 
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)
223
 
 
224
 
        # register view manager and create view panes/widgets
225
 
        self.view_manager = ViewManager(self.notebook_view)
226
 
        
227
 
        # available pane
228
 
        self.available_pane = AvailablePane(self.cache,
229
 
                                            self.db,
230
 
                                            self.distro,
231
 
                                            self.icons,
232
 
                                            self.datadir,
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)
237
 
 
238
 
        # channel pane (view not fully initialized at this point)
239
 
        self.channel_pane = ChannelPane(self.cache,
240
 
                                        self.db,
241
 
                                        self.distro,
242
 
                                        self.icons,
243
 
                                        self.datadir)
244
 
        self.channel_pane.connect("channel-pane-created", self.on_channel_pane_created)
245
 
        self.view_manager.register(self.channel_pane, ViewPages.CHANNEL)
246
 
        
247
 
        # installed pane (view not fully initialized at this point)
248
 
        self.installed_pane = InstalledPane(self.cache,
249
 
                                            self.db, 
250
 
                                            self.distro,
251
 
                                            self.icons,
252
 
                                            self.datadir)
253
 
        self.installed_pane.connect("installed-pane-created", self.on_installed_pane_created)
254
 
        self.view_manager.register(self.installed_pane, ViewPages.INSTALLED)
255
 
        
256
 
        # history pane (not fully loaded at this point)
257
 
        self.history_pane = HistoryPane(self.cache,
258
 
                                        self.db,
259
 
                                        self.distro,
260
 
                                        self.icons,
261
 
                                        self.datadir)
262
 
        self.history_pane.connect("history-pane-created", self.on_history_pane_created)
263
 
        self.view_manager.register(self.history_pane, ViewPages.HISTORY)
264
 
 
265
 
        # pending view
266
 
        self.pending_view = PendingView(self.icons)
267
 
        self.view_manager.register(self.pending_view, ViewPages.PENDING)
268
 
        
269
 
        # keep track of the current active pane
270
 
        self.active_pane = self.available_pane
271
 
 
272
 
        # view switcher
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)
280
 
        
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()
284
 
 
285
 
        # launchpad integration help, its ok if that fails
286
 
        try:
287
 
            import LaunchpadIntegration
288
 
            LaunchpadIntegration.set_sourcepackagename("software-center")
289
 
            LaunchpadIntegration.add_items(self.menu_help, 1, True, False)
290
 
        except Exception, e:
291
 
            LOG.debug("launchpad integration error: '%s'" % e)
292
 
            
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",
297
 
                                              accel_group,
298
 
                                              ord('['),
299
 
                                              gtk.gdk.CONTROL_MASK,
300
 
                                              gtk.ACCEL_VISIBLE)
301
 
        self.menuitem_go_forward.add_accelerator("activate",
302
 
                                                 accel_group,
303
 
                                                 ord(']'),
304
 
                                                 gtk.gdk.CONTROL_MASK,
305
 
                                                 gtk.ACCEL_VISIBLE)
306
 
        self.menuitem_go_back.add_accelerator("activate",
307
 
                                              accel_group,
308
 
                                              gtk.gdk.keyval_from_name("Left"),
309
 
                                              gtk.gdk.MOD1_MASK,
310
 
                                              gtk.ACCEL_VISIBLE)
311
 
        self.menuitem_go_forward.add_accelerator("activate",
312
 
                                                 accel_group,
313
 
                                                 gtk.gdk.keyval_from_name("Right"),
314
 
                                                 gtk.gdk.MOD1_MASK,
315
 
                                                 gtk.ACCEL_VISIBLE)
316
 
        self.menuitem_go_back.add_accelerator("activate",
317
 
                                              accel_group,
318
 
                                              gtk.gdk.keyval_from_name("KP_Left"),
319
 
                                              gtk.gdk.MOD1_MASK,
320
 
                                              gtk.ACCEL_VISIBLE)
321
 
        self.menuitem_go_forward.add_accelerator("activate",
322
 
                                                 accel_group,
323
 
                                                 gtk.gdk.keyval_from_name("KP_Right"),
324
 
                                                 gtk.gdk.MOD1_MASK,
325
 
                                                 gtk.ACCEL_VISIBLE)
326
 
 
327
 
        # specify the smallest allowable window size
328
 
        self.window_main.set_size_request(700, 400)
329
 
 
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)
336
 
 
337
 
        # about dialog
338
 
        self.aboutdialog.connect("response",
339
 
                                 lambda dialog, rid: dialog.hide())
340
 
        self.aboutdialog.connect("delete_event", self.aboutdialog.hide_on_delete)
341
 
 
342
 
        # restore state
343
 
        self.config = get_config()
344
 
        self.restore_state()
345
 
 
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)
351
 
 
352
 
        # open plugin manager and load plugins
353
 
        self.plugin_manager = PluginManager(self, SOFTWARE_CENTER_PLUGIN_DIR)
354
 
        self.plugin_manager.load_plugins()
355
 
        
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)
361
 
 
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"))
366
 
        else:
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)
373
 
 
374
 
 
375
 
        # FIXME:  REMOVE THIS once launchpad integration is enabled
376
 
        #         by default
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"))
380
 
 
381
 
        if options.disable_buy and not options.enable_lp:
382
 
            file_menu.remove(self.builder.get_object("separator_login"))
383
 
            
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"))
388
 
            
389
 
    # helper
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)
397
 
 
398
 
    # callbacks
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)
403
 
 
404
 
        # connect signals
405
 
        self.available_pane.connect("app-list-changed", 
406
 
                                    self.on_app_list_changed,
407
 
                                    ViewPages.AVAILABLE)
408
 
        self.available_pane.app_details_view.connect("selected", 
409
 
                                                     self.on_app_details_changed,
410
 
                                                     ViewPages.AVAILABLE)
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()
418
 
    
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
423
 
        # installed items
424
 
        self.channel_pane.set_section(channel_section)
425
 
 
426
 
        # connect signals
427
 
        self.channel_pane.connect("app-list-changed", 
428
 
                                    self.on_app_list_changed,
429
 
                                    ViewPages.CHANNEL)
430
 
        self.channel_pane.app_details_view.connect("selected", 
431
 
                                                   self.on_app_details_changed,
432
 
                                                   ViewPages.CHANNEL)
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)
437
 
                                           
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)
442
 
        
443
 
        # connect signals
444
 
        self.installed_pane.connect("app-list-changed", 
445
 
                                    self.on_app_list_changed,
446
 
                                    ViewPages.INSTALLED)
447
 
        self.installed_pane.app_details_view.connect("selected", 
448
 
                                                     self.on_app_details_changed,
449
 
                                                     ViewPages.INSTALLED)
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)
454
 
                                             
455
 
    def on_history_pane_created(self, widget):
456
 
        # connect signal
457
 
        self.history_pane.connect("app-list-changed", 
458
 
                                  self.on_app_list_changed,
459
 
                                  ViewPages.HISTORY)
460
 
    
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:
464
 
            self.db.reopen()
465
 
 
466
 
    def on_review_stats_loaded(self, reviews):
467
 
        LOG.debug("on_review_stats_loaded: '%s'" % len(reviews))
468
 
 
469
 
    def on_app_details_changed(self, widget, app, page):
470
 
        self.update_status_bar()
471
 
 
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()
475
 
 
476
 
    def on_window_main_delete_event(self, widget, event):
477
 
        if hasattr(self, "glaunchpad"):
478
 
            self.glaunchpad.shutdown()
479
 
        self.save_state()
480
 
        gtk.main_quit()
481
 
        
482
 
    def on_window_main_key_press_event(self, widget, event):
483
 
        """
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.
488
 
        """
489
 
        if (event.keyval == gtk.gdk.keyval_from_name("BackSpace") and 
490
 
            self.active_pane 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()
495
 
            
496
 
    def on_window_main_button_press_event(self, widget, event):
497
 
        """
498
 
        Implement back/forward navigation via mouse navigation keys using
499
 
        the same button codes as used in Nautilus.
500
 
        """
501
 
        if (event.button == MOUSE_EVENT_BACK_BUTTON and
502
 
            self.active_pane 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
507
 
            self.active_pane 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()
511
 
        
512
 
    def on_view_switcher_changed(self, view_switcher, view_id, channel):
513
 
        LOG.debug("view_switcher_activated: %s %s" % (view_switcher, view_id))
514
 
 
515
 
        # set active pane
516
 
        self.active_pane = self.view_manager.get_view_widget(view_id)
517
 
 
518
 
        # set menu sensitve
519
 
        self.menuitem_view_supported_only.set_sensitive(self.active_pane != None)
520
 
        self.menuitem_view_all.set_sensitive(self.active_pane != None)
521
 
        # set menu state
522
 
        if self.active_pane:
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()
529
 
            else:
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())
537
 
        else:
538
 
            self.menuitem_go_back.set_sensitive(False)
539
 
            self.menuitem_go_forward.set_sensitive(False)
540
 
         # switch to new page
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()
548
 
 
549
 
    def on_viewswitcher_resized(self, widget, allocation):
550
 
        self.view_switcher.width = allocation.width
551
 
 
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(
556
 
            private_archives)
557
 
 
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"])
563
 
 
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) 
570
 
        
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
575
 
        """
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)
586
 
                    return
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)
598
 
                    return
599
 
 
600
 
        # this allows us to 'upgrade' deb files
601
 
        if action == 'upgrade' and app.request and type(app) == DebFileApplication:
602
 
            action = 'install'
603
 
 
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
610
 
            else:
611
 
                debfile_name = None
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)
615
 
        else:
616
 
            LOG.error("Not a valid action in AptdaemonBackend: '%s'" % action)
617
 
            
618
 
    def get_icon_filename(self, iconname, iconsize):
619
 
        iconinfo = self.icons.lookup_icon(iconname, iconsize, 0)
620
 
        if not iconinfo:
621
 
            iconinfo = self.icons.lookup_icon(Icons.MISSING_APP, iconsize, 0)
622
 
        return iconinfo.get_filename()
623
 
 
624
 
    # Menu Items
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
629
 
        app = None
630
 
        if self.active_pane:
631
 
            app = self.active_pane.get_current_app()
632
 
        if app is None:
633
 
            self.menuitem_install.set_sensitive(False)
634
 
            self.menuitem_remove.set_sensitive(False)
635
 
            return 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))
639
 
            return False
640
 
        # update menu items
641
 
        pkg_state = None
642
 
        error = None
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)
649
 
        if appdetails:
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
668
 
              not error):
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)
673
 
        else:
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
677
 
        return False
678
 
 
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)
685
 
        d.login()
686
 
 
687
 
    def _create_buildin_sso_if_needed(self):
688
 
        if not self.sso:
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")
696
 
        else:
697
 
            from view.logindialog import LoginDialog
698
 
            d = LoginDialog(self.sso, self.datadir, parent=self.window_main)
699
 
            d.login()
700
 
 
701
 
    def _create_dbus_sso_if_needed(self):
702
 
        if not self.sso:
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)
709
 
 
710
 
    def _login_via_dbus_sso(self):
711
 
        self._create_dbus_sso_if_needed()
712
 
        self.sso.login()
713
 
 
714
 
    def _create_scagent_if_needed(self):
715
 
        if not self.scagent:
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)
719
 
            
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()
726
 
        else:
727
 
            self._login_via_dbus_sso()
728
 
            
729
 
    def on_menuitem_deauthorize_computer_activate(self, menuitem):
730
 
    
731
 
        # FIXME: need Ubuntu SSO username here
732
 
        # account_name = get_person_from_config()
733
 
        account_name = None
734
 
        
735
 
        # get a list of installed purchased packages
736
 
        installed_purchased_packages = self.db.get_installed_purchased_packages()
737
 
 
738
 
        # display the deauthorize computer dialog
739
 
        deauthorize = deauthorize_dialog.deauthorize_computer(None,
740
 
                                                              self.datadir,
741
 
                                                              self.db,
742
 
                                                              self.icons,
743
 
                                                              account_name,
744
 
                                                              installed_purchased_packages)
745
 
        if deauthorize:
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"))
752
 
            
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)
760
 
            
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
768
 
        
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)
772
 
 
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)
776
 
        
777
 
    def on_menuitem_close_activate(self, widget):
778
 
        gtk.main_quit()
779
 
 
780
 
    def on_menu_edit_activate(self, menuitem):
781
 
        """
782
 
        Check whether the search field is focused and if so, focus some items
783
 
        """
784
 
        edit_menu_items = [self.menuitem_undo,
785
 
                           self.menuitem_redo,
786
 
                           self.menuitem_cut, 
787
 
                           self.menuitem_copy,
788
 
                           self.menuitem_copy_web_link,
789
 
                           self.menuitem_paste,
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()
806
 
                if 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
814
 
            else:
815
 
                self.menuitem_search.set_sensitive(True)
816
 
 
817
 
        # weblink
818
 
        if self.active_pane:
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)
822
 
 
823
 
        # details view
824
 
        if (self.active_pane and 
825
 
            self.active_pane.is_app_details_view_showing()):
826
 
 
827
 
            self.menuitem_select_all.set_sensitive(True)
828
 
            sel_text = self.active_pane.app_details_view.desc.get_selected_text()
829
 
 
830
 
            if sel_text:
831
 
                self.menuitem_copy.set_sensitive(True)
832
 
 
833
 
    def on_menuitem_undo_activate(self, menuitem):
834
 
        self.active_pane.searchentry.undo()
835
 
        
836
 
    def on_menuitem_redo_activate(self, menuitem):
837
 
        self.active_pane.searchentry.redo()
838
 
 
839
 
    def on_menuitem_cut_activate(self, menuitem):
840
 
        self.active_pane.searchentry.cut_clipboard()
841
 
 
842
 
    def on_menuitem_copy_activate(self, menuitem):
843
 
        if (self.active_pane and
844
 
            self.active_pane.is_app_details_view_showing()):
845
 
 
846
 
            self.active_pane.app_details_view.desc.copy_clipboard()
847
 
 
848
 
        elif self.active_pane:
849
 
            self.active_pane.searchentry.copy_clipboard()
850
 
 
851
 
    def on_menuitem_paste_activate(self, menuitem):
852
 
        self.active_pane.searchentry.paste_clipboard()
853
 
 
854
 
    def on_menuitem_delete_activate(self, menuitem):
855
 
        self.active_pane.searchentry.set_text("")
856
 
 
857
 
    def on_menuitem_select_all_activate(self, menuitem):
858
 
        if (self.active_pane and
859
 
            self.active_pane.is_app_details_view_showing()):
860
 
 
861
 
            self.active_pane.app_details_view.desc.select_all()
862
 
            self.active_pane.app_details_view.desc.grab_focus()
863
 
 
864
 
        elif self.active_pane:
865
 
            self.active_pane.searchentry.select_region(0, -1)
866
 
 
867
 
    def on_menuitem_copy_web_link_activate(self, menuitem):
868
 
        app = self.active_pane.get_current_app()
869
 
        if app:
870
 
            clipboard = gtk.Clipboard()
871
 
            clipboard.set_text(self.WEBLINK_URL % app.pkgname)
872
 
 
873
 
    def on_menuitem_search_activate(self, widget):
874
 
        if self.active_pane:
875
 
            self.active_pane.searchentry.grab_focus()
876
 
            self.active_pane.searchentry.select_region(0, -1)
877
 
 
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(
883
 
            ["gksu",
884
 
             "--desktop", "/usr/share/applications/software-properties-gtk.desktop",
885
 
             "--",
886
 
             "/usr/bin/software-properties-gtk", 
887
 
             "-n", 
888
 
             "-t", str(self.window_main.window.xid)])
889
 
        # Monitor the subprocess regularly
890
 
        glib.timeout_add(100, self._poll_software_sources_subprocess, p)
891
 
 
892
 
    def _poll_software_sources_subprocess(self, popen):
893
 
        ret = popen.poll()
894
 
        if ret is None:
895
 
            # Keep monitoring
896
 
            return True
897
 
        # A return code of 1 means that the sources have changed
898
 
        if ret == 1:
899
 
            self.run_update_cache()
900
 
        self.window_main.set_sensitive(True)
901
 
        # Stop monitoring
902
 
        return False
903
 
 
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()
908
 
 
909
 
    def on_menuitem_help_activate(self, menuitem):
910
 
        # run yelp
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)
914
 
 
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()
921
 
 
922
 
            # update recommended widget counter
923
 
            if self.available_pane and self.available_pane.cat_view:
924
 
                self.available_pane.cat_view._append_recommendations()
925
 
 
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()))
934
 
 
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()
941
 
 
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()
948
 
                else:
949
 
                    ap.navigation_bar.navigate_up()
950
 
                ap.on_application_selected(None, None)    
951
 
 
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)    
957
 
 
958
 
            # update recommended widget counter
959
 
            if self.available_pane and self.available_pane.cat_view:
960
 
                self.available_pane.cat_view._append_recommendations()
961
 
 
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()))
970
 
 
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()
975
 
        
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()
980
 
            
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)
985
 
            return
986
 
        if ui.gtk.dialogs.confirm_repair_broken_cache(self.window_main,
987
 
                                                      self.datadir):
988
 
            self.backend.fix_broken_depends()
989
 
 
990
 
    def _on_notebook_expose(self, widget, event):
991
 
        # use availabel pane as the Style source so viewport colours are the same
992
 
        # as a real Viewport
993
 
        self.available_pane.style.paint_shadow(widget.window,
994
 
                                    gtk.STATE_NORMAL,
995
 
                                    gtk.SHADOW_IN,
996
 
                                    event.area,
997
 
                                    widget,
998
 
                                    'viewport',
999
 
                                    widget.allocation.x,
1000
 
                                    widget.allocation.y,
1001
 
                                    widget.allocation.width,
1002
 
                                    widget.allocation.height)
1003
 
        return
1004
 
 
1005
 
    def _on_apt_cache_broken(self, aptcache):
1006
 
        self._ask_and_repair_broken_cache()
1007
 
 
1008
 
    def _on_transaction_finished(self, backend, result):
1009
 
        """ callback when an application install/remove transaction 
1010
 
            (or a cache reload) has finished 
1011
 
        """
1012
 
        self.cache.open()
1013
 
 
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)
1017
 
        if 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
1021
 
            self.db.reopen()
1022
 
            self.update_status_bar()
1023
 
 
1024
 
    # helper
1025
 
 
1026
 
    def run_update_cache(self):
1027
 
        """update the apt cache (e.g. after new sources where added """
1028
 
        self.backend.reload()
1029
 
 
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()
1034
 
        else:
1035
 
            # FIXME: deal with the pending view status
1036
 
            s = ""
1037
 
        self.label_status.set_text(s)
1038
 
 
1039
 
        # update a11y
1040
 
        if s:
1041
 
            self.status_box.a11y.set_name(s)
1042
 
            self.status_box.set_property('can-focus', True)
1043
 
        else:
1044
 
            self.status_box.set_property('can-focus', False)
1045
 
        
1046
 
    def update_app_list_view(self, channel=None):
1047
 
        """Helper that updates the app view list """
1048
 
        if self.active_pane is None:
1049
 
            return
1050
 
        if channel is None and self.active_pane.is_category_view_showing():
1051
 
            return
1052
 
        if channel:
1053
 
            self.channel_pane.set_channel(channel)
1054
 
            self.active_pane.refresh_apps()
1055
 
 
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
1059
 
 
1060
 
        if is_rebuilding:
1061
 
            pass
1062
 
        else:
1063
 
            # we need to reopen when the database finished updating
1064
 
            self.db.reopen()
1065
 
 
1066
 
    def setup_database_rebuilding_listener(self):
1067
 
        """
1068
 
        Setup system bus listener for database rebuilding
1069
 
        """
1070
 
        self._database_is_rebuilding = False
1071
 
        # get dbus
1072
 
        try:
1073
 
            bus = dbus.SystemBus()
1074
 
        except:
1075
 
            LOG.exception("could not get system bus")
1076
 
            return
1077
 
        # check if its currently rebuilding (most likely not, so we
1078
 
        # just ignore errors from dbus because the interface
1079
 
        try:
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)
1087
 
 
1088
 
        # add signal handler
1089
 
        bus.add_signal_receiver(self._on_database_rebuilding_handler,
1090
 
                                "DatabaseRebuilding",
1091
 
                                "com.ubuntu.Softwarecenter")
1092
 
 
1093
 
    def setup_dbus_or_bring_other_instance_to_front(self, args):
1094
 
        """ 
1095
 
        This sets up a dbus listener
1096
 
        """
1097
 
        try:
1098
 
            bus = dbus.SessionBus()
1099
 
        except:
1100
 
            LOG.exception("could not initiate dbus")
1101
 
            return
1102
 
        # if there is another Softwarecenter running bring it to front
1103
 
        # and exit, otherwise install the dbus controller
1104
 
        try:
1105
 
            proxy_obj = bus.get_object('com.ubuntu.Softwarecenter', 
1106
 
                                       '/com/ubuntu/Softwarecenter')
1107
 
            iface = dbus.Interface(proxy_obj, 'com.ubuntu.SoftwarecenterIFace')
1108
 
            if args:
1109
 
                iface.bringToFront(args)
1110
 
            else:
1111
 
                # None can not be transported over dbus
1112
 
                iface.bringToFront('nothing-to-show')
1113
 
            sys.exit()
1114
 
        except dbus.DBusException:
1115
 
            bus_name = dbus.service.BusName('com.ubuntu.Softwarecenter',bus)
1116
 
            self.dbusControler = SoftwarecenterDbusController(self, bus_name)
1117
 
 
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
1122
 
        """
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]
1131
 
 
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))
1140
 
            return
1141
 
 
1142
 
        if len(packages) == 1:
1143
 
            request = packages[0]
1144
 
 
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)
1151
 
            else:
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
1155
 
                # e.g. unity
1156
 
                (pkgname, sep, appname) = packages[0].partition("/")
1157
 
                app = Application(appname, pkgname)
1158
 
 
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)
1169
 
                else:
1170
 
                    self.view_switcher.set_view(ViewPages.AVAILABLE)
1171
 
                    self.available_pane.show_app(app)
1172
 
 
1173
 
            show_app(self, app)
1174
 
 
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)
1180
 
 
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))
1185
 
        else:
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)
1202
 
 
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:
1207
 
            return
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
1211
 
        if maximized:
1212
 
            self.config.set("general", "maximized", "True")
1213
 
        else:
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")
1221
 
        else:
1222
 
            self.config.set("general", "installed-node-expanded", "False")
1223
 
        width = self.view_switcher.width
1224
 
        if width != 1:
1225
 
            width += 2
1226
 
        self.config.set("general", "sidebar-width", str(width))
1227
 
        self.config.write()
1228
 
 
1229
 
    def run(self, args):
1230
 
        self.window_main.show_all()
1231
 
        # support both "pkg1 pkg" and "pkg1,pkg2" (and pkg1,pkg2 pkg3)
1232
 
        if args:
1233
 
            for (i, arg) in enumerate(args[:]):
1234
 
                if "," in arg:
1235
 
                    args.extend(arg.split(","))
1236
 
                    del args[i]
1237
 
        self.show_available_packages(args)
1238
 
        atexit.register(self.save_state)
1239
 
        SimpleGtkbuilderApp.run(self)