Remove warnings about non-existing LaunchpadIntegration module
[appstream:software-center.git] / softwarecenter / ui / gtk3 / app.py
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 # order is import here, otherwise test/gtk3/test_purchase.py is unhappy
20 from gi.repository import GObject
21 from gi.repository import Gtk
22 import gi
23
24 import atexit
25 import collections
26 import dbus
27 import dbus.service
28 from dbus.mainloop.glib import DBusGMainLoop
29 DBusGMainLoop(set_as_default=True)
30
31 import gettext
32 import logging
33 import os
34 import subprocess
35 import sys
36 import xapian
37 import glob
38
39 import webbrowser
40
41 from gettext import gettext as _
42
43 # purely to initialize the netstatus
44 import softwarecenter.netstatus
45 # make pyflakes shut up
46 softwarecenter.netstatus.NETWORK_STATE
47
48 # db imports
49 from softwarecenter.db.application import Application
50 from softwarecenter.db import DebFileApplication
51 from softwarecenter.i18n import init_locale
52
53 # misc imports
54 from softwarecenter.plugin import PluginManager
55 from softwarecenter.paths import SOFTWARE_CENTER_PLUGIN_DIRS
56 from softwarecenter.enums import (Icons,
57                                   PkgStates,
58                                   ViewPages,
59                                   AppActions,
60                                   DB_SCHEMA_VERSION,
61                                   MOUSE_EVENT_FORWARD_BUTTON,
62                                   MOUSE_EVENT_BACK_BUTTON)
63 from softwarecenter.utils import (clear_token_from_ubuntu_sso,
64                                   get_http_proxy_string_from_gsettings,
65                                   wait_for_apt_cache_ready,
66                                   ExecutionTime,
67                                   is_unity_running)
68 from softwarecenter.ui.gtk3.utils import (get_sc_icon_theme,
69                                           init_sc_css_provider)
70 from softwarecenter.version import VERSION
71 from softwarecenter.db.database import StoreDatabase
72 try:
73     from aptd_gtk3 import InstallBackendUI
74     InstallBackendUI # pyflakes
75 except:
76     from softwarecenter.backend.installbackend import InstallBackendUI
77
78 # ui imports
79 import softwarecenter.ui.gtk3.dialogs.deauthorize_dialog as deauthorize_dialog
80 import softwarecenter.ui.gtk3.dialogs as dialogs
81
82 from softwarecenter.ui.gtk3.SimpleGtkbuilderApp import SimpleGtkbuilderApp
83 from softwarecenter.ui.gtk3.panes.installedpane import InstalledPane
84 from softwarecenter.ui.gtk3.panes.availablepane import AvailablePane
85 from softwarecenter.ui.gtk3.panes.historypane import HistoryPane
86 from softwarecenter.ui.gtk3.panes.globalpane import GlobalPane
87 from softwarecenter.ui.gtk3.panes.pendingpane import PendingPane
88 from softwarecenter.ui.gtk3.session.appmanager import (ApplicationManager,
89                                                        get_appmanager)
90 from softwarecenter.ui.gtk3.session.viewmanager import (
91     ViewManager, get_viewmanager)
92
93 from softwarecenter.config import get_config
94 from softwarecenter.backend import get_install_backend
95 from softwarecenter.backend.login_sso import get_sso_backend
96
97 from softwarecenter.backend.channel import AllInstalledChannel
98 from softwarecenter.backend.reviews import get_review_loader, UsefulnessCache
99 from softwarecenter.backend.oneconfhandler import get_oneconf_handler, is_oneconf_available
100 from softwarecenter.distro import get_distro
101 from softwarecenter.db.pkginfo import get_pkg_info
102
103
104 from gi.repository import Gdk
105
106 LOG = logging.getLogger(__name__)
107
108 # py3 compat
109 def callable(func):
110     return isinstance(func, collections.Callable)
111
112 class SoftwarecenterDbusController(dbus.service.Object):
113     """ 
114     This is a helper to provide the SoftwarecenterIFace
115     
116     It provides only a bringToFront method that takes 
117     additional arguments about what packages to show
118     """
119     def __init__(self, parent, bus_name,
120                  object_path='/com/ubuntu/Softwarecenter'):
121         dbus.service.Object.__init__(self, bus_name, object_path)
122         self.parent = parent
123
124     @dbus.service.method('com.ubuntu.SoftwarecenterIFace')
125     def bringToFront(self, args):
126         if args != 'nothing-to-show':
127             self.parent.show_available_packages(args)
128         self.parent.window_main.present()
129         return True
130
131     @dbus.service.method('com.ubuntu.SoftwarecenterIFace')
132     def triggerDatabaseReopen(self):
133         self.parent.db.emit("reopen")
134
135     @dbus.service.method('com.ubuntu.SoftwarecenterIFace')
136     def triggerCacheReload(self):
137         self.parent.cache.emit("cache-ready")
138
139 # XXX Haven't really thought this through....
140 #~ class SoftwareCenterInitOndemand(object):
141 #~ 
142     #~ """ Init objects/data that are low priority, i.e, use case is
143         #~ niche and/or load times are low and will not impact user
144         #~ experience.  All data and objects are loaded on request.
145     #~ """
146 #~ 
147     #~ def init(self):
148         #~ pass
149
150
151 #~ class SoftwareCenterInitDelayed(object):
152 #~ 
153     #~ """ Init objects/data that are medium priority, not needed instantly
154         #~ but rather _potentially_ required within the first few seconds
155         #~ of USC usage.
156     #~ """
157 #~ 
158     #~ def init(self):
159         #~ # reviews
160         #~ self.review_loader = get_review_loader(self.cache, self.db)
161         #~ # FIXME: add some kind of throttle, I-M-S here
162         #~ self.review_loader.refresh_review_stats(self.on_review_stats_loaded)
163         #~ #load usefulness votes from server when app starts
164         #~ self.useful_cache = UsefulnessCache(True)
165         #~ self.setup_database_rebuilding_listener()
166         #~ # open plugin manager and load plugins
167         #~ self.plugin_manager = PluginManager(self, SOFTWARE_CENTER_PLUGIN_DIRS)
168         #~ self.plugin_manager.load_plugins()
169
170
171
172 class SoftwareCenterAppGtk3(SimpleGtkbuilderApp):
173
174     WEBLINK_URL = "http://apt.ubuntu.com/p/%s"
175
176     # the size of the icon for dialogs
177     APP_ICON_SIZE = Gtk.IconSize.DIALOG
178
179     def __init__(self, datadir, xapian_base_path, options, args=None):
180         # setup dbus and exit if there is another instance already
181         # running
182         self.setup_dbus_or_bring_other_instance_to_front(args)
183
184         self.datadir = datadir
185         SimpleGtkbuilderApp.__init__(self, 
186                                      datadir+"/ui/gtk3/SoftwareCenter.ui", 
187                                      "software-center")
188         gettext.bindtextdomain("software-center", "/usr/share/locale")
189         gettext.textdomain("software-center")
190
191         init_locale()
192
193         if "SOFTWARE_CENTER_DEBUG_TABS" in os.environ:
194             self.notebook_view.set_show_tabs(True)
195
196         # distro specific stuff
197         self.distro = get_distro()
198
199         # setup proxy
200         self._setup_proxy_initially()
201
202         # Disable software-properties if it does not exist
203         if not os.path.exists("/usr/bin/software-properties-gtk"):
204             sources = self.builder.get_object("menuitem_software_sources")
205             sources.set_sensitive(False)
206
207         with ExecutionTime("opening the pkginfo"):
208             # a main iteration friendly apt cache
209             self.cache = get_pkg_info()
210             # cache is opened later in run()
211             self.cache.connect("cache-broken", self._on_apt_cache_broken)
212
213         with ExecutionTime("opening the xapiandb"):
214             pathname = os.path.join(xapian_base_path, "xapian")
215             self._use_axi = self.distro.USE_AXI and not options.disable_apt_xapian_index
216             try:
217                 self.db = StoreDatabase(pathname, self.cache)
218                 self.db.open(use_axi = self._use_axi)
219                 if self.db.schema_version() != DB_SCHEMA_VERSION:
220                     LOG.warn("database format '%s' expected, but got '%s'" % (
221                             DB_SCHEMA_VERSION, self.db.schema_version()))
222                     if os.access(pathname, os.W_OK):
223                         self._rebuild_and_reopen_local_db(pathname)
224             except xapian.DatabaseOpeningError:
225                 # Couldn't use that folder as a database
226                 # This may be because we are in a bzr checkout and that
227                 #   folder is empty. If the folder is empty, and we can find the
228                 # script that does population, populate a database in it.
229                 if os.path.isdir(pathname) and not os.listdir(pathname):
230                     self._rebuild_and_reopen_local_db(pathname)
231             except xapian.DatabaseCorruptError:
232                 LOG.exception("xapian open failed")
233                 dialogs.error(None, 
234                               _("Sorry, can not open the software database"),
235                               _("Please re-install the 'software-center' "
236                                 "package."))
237                 # FIXME: force rebuild by providing a dbus service for this
238                 sys.exit(1)
239
240         self.prefill_pkgnames = []
241         for doc in self.db:
242             self.prefill_pkgnames.append(self.db.get_pkgname(doc))
243
244         # additional icons come from app-install-data
245         with ExecutionTime("building the icon cache"):
246             self.icons = get_sc_icon_theme(self.datadir)
247
248         # backend
249         with ExecutionTime("creating the backend"):
250             self.backend = get_install_backend()
251             self.backend.ui = InstallBackendUI()
252             self.backend.connect("transaction-finished", self._on_transaction_finished)
253             self.backend.connect("channels-changed", self.on_channels_changed)
254
255         # high level app management
256         with ExecutionTime("get the app-manager"):
257             self.app_manager = ApplicationManager(self.db, self.backend, self.icons)
258
259         # misc state
260         self._block_menuitem_view = False
261         
262         # for use when viewing previous purchases
263         self.scagent = None
264         self.sso = None
265         self.available_for_me_query = None
266         self.recommender_uuid = ""
267
268         Gtk.Window.set_default_icon_name("softwarecenter")
269
270         # inhibit the error-bell, Bug #846138...
271         settings = Gtk.Settings.get_default()
272         settings.set_property("gtk-error-bell", False)
273
274         # wire up the css provider to reconfigure on theme-changes
275         self.window_main.connect("style-updated",
276                                  self._on_style_updated,
277                                  init_sc_css_provider,
278                                  settings,
279                                  Gdk.Screen.get_default(),
280                                  datadir)
281
282         # register view manager and create view panes/widgets
283         with ExecutionTime("ViewManager"):
284             self.view_manager = ViewManager(self.notebook_view, options)
285
286         with ExecutionTime("building panes"):
287             self.global_pane = GlobalPane(self.view_manager, self.datadir, self.db, self.cache, self.icons)
288             self.vbox1.pack_start(self.global_pane, False, False, 0)
289             self.vbox1.reorder_child(self.global_pane, 1)
290
291             # available pane
292             self.available_pane = AvailablePane(self.cache,
293                                                 self.db,
294                                                 self.distro,
295                                                 self.icons,
296                                                 self.datadir,
297                                                 self.navhistory_back_action,
298                                                 self.navhistory_forward_action)
299             self.available_pane.connect("available-pane-created", self.on_available_pane_created)
300             self.view_manager.register(self.available_pane, ViewPages.AVAILABLE)
301
302             # installed pane (view not fully initialized at this point)
303             self.installed_pane = InstalledPane(self.cache,
304                                                 self.db, 
305                                                 self.distro,
306                                                 self.icons,
307                                                 self.datadir)
308             #~ self.installed_pane.connect("installed-pane-created", self.on_installed_pane_created)
309             self.view_manager.register(self.installed_pane, ViewPages.INSTALLED)
310
311             # history pane (not fully loaded at this point)
312             self.history_pane = HistoryPane(self.cache,
313                                             self.db,
314                                             self.distro,
315                                             self.icons,
316                                             self.datadir)
317             self.view_manager.register(self.history_pane, ViewPages.HISTORY)
318
319             # pending pane
320             self.pending_pane = PendingPane(self.icons)
321             self.view_manager.register(self.pending_pane, ViewPages.PENDING)
322
323         # TRANSLATORS: this is the help menuitem label, e.g. Ubuntu Software Center _Help
324         self.menuitem_help.set_label(_("%s _Help")%self.distro.get_app_name())
325
326         # specify the smallest allowable window size
327         self.window_main.set_size_request(730, 470)
328
329         # reviews
330         with ExecutionTime("create review loader"):
331             self.review_loader = get_review_loader(self.cache, self.db)
332             # FIXME: add some kind of throttle, I-M-S here
333             self.review_loader.refresh_review_stats(self.on_review_stats_loaded)
334             #load usefulness votes from server when app starts
335             self.useful_cache = UsefulnessCache(True)
336             self.setup_database_rebuilding_listener()
337
338         with ExecutionTime("create plugin manager"):
339             # open plugin manager and load plugins
340             self.plugin_manager = PluginManager(self, SOFTWARE_CENTER_PLUGIN_DIRS)
341             self.plugin_manager.load_plugins()
342
343         # setup window name and about information (needs branding)
344         name = self.distro.get_app_name()
345         self.window_main.set_title(name)
346         self.aboutdialog.set_program_name(name)
347         about_description = self.distro.get_app_description()
348         self.aboutdialog.set_comments(about_description)
349
350         # about dialog
351         self.aboutdialog.connect("response", lambda dialog, rid: dialog.hide())
352         self.aboutdialog.connect("delete_event", lambda w,e: self.aboutdialog.hide_on_delete())
353
354         # restore state
355         self.config = get_config()
356         self.restore_state()
357
358         # Adapt menu entries
359         supported_menuitem = self.builder.get_object("menuitem_view_supported_only")
360         supported_menuitem.set_label(self.distro.get_supported_filter_name())
361         file_menu = self.builder.get_object("menu1")
362         
363         if not self.distro.DEVELOPER_URL:
364             help_menu = self.builder.get_object("menu_help")
365             developer_separator = self.builder.get_object("separator_developer")
366             help_menu.remove(developer_separator)
367             developer_menuitem = self.builder.get_object("menuitem_developer")
368             help_menu.remove(developer_menuitem)
369
370         # Check if oneconf is available
371         och = is_oneconf_available()
372         if not och:
373             file_menu.remove(self.builder.get_object("menuitem_sync_between_computers"))
374             
375         # restore the state of the add to launcher menu item, or remove the menu
376         # item if Unity is not currently running
377         add_to_launcher_menuitem = self.builder.get_object(
378                                                     "menuitem_add_to_launcher")
379         if is_unity_running():
380             add_to_launcher_menuitem.set_active(
381                                 self.available_pane.add_to_launcher_enabled)
382         else:
383             view_menu = self.builder.get_object("menu_view")
384             add_to_launcher_separator = self.builder.get_object(
385                                                     "add_to_launcher_separator")
386             view_menu.remove(add_to_launcher_separator)
387             view_menu.remove(add_to_launcher_menuitem)
388
389         # run s-c-agent update
390         if options.disable_buy or not self.distro.PURCHASE_APP_URL:
391             file_menu.remove(self.builder.get_object("menuitem_reinstall_purchases"))
392             if not (options.enable_lp or och):
393                 file_menu.remove(self.builder.get_object("separator_login"))
394         else:
395             # running the agent will trigger a db reload so we do it later
396             GObject.timeout_add_seconds(30, self._run_software_center_agent)
397
398
399         # keep the cache clean
400         GObject.timeout_add_seconds(15, self._run_expunge_cache_helper)
401
402         # TODO: Remove the following two lines once we have remove repository
403         #       support in aptdaemon (see LP: #723911)
404         file_menu = self.builder.get_object("menu1")
405         file_menu.remove(self.builder.get_object("menuitem_deauthorize_computer"))
406
407         # keep track of the current active pane
408         self.active_pane = self.available_pane
409         self.window_main.connect("realize", self.on_realize)
410
411         # launchpad integration help, its ok if that fails
412         if gi.Repository.get_default().enumerate_versions('LaunchpadIntegration'):
413             try:
414                 from gi.repository import LaunchpadIntegration
415                 LaunchpadIntegration.set_sourcepackagename("software-center")
416                 LaunchpadIntegration.add_items(self.menu_help, 3, True, False)
417             except Exception, e:
418                 LOG.debug("launchpad integration error: '%s'" % e)
419
420
421     # helper
422     def _run_software_center_agent(self):
423         """ helper that triggers the update-software-center-agent helper """
424         sc_agent_update = os.path.join(
425             self.datadir, "update-software-center-agent")
426         (pid, stdin, stdout, stderr) = GObject.spawn_async(
427             [sc_agent_update, "--datadir", self.datadir],
428             flags=GObject.SPAWN_DO_NOT_REAP_CHILD)
429         GObject.child_watch_add(
430             pid, self._on_update_software_center_agent_finished)
431
432     def _run_expunge_cache_helper(self):
433         """ helper that expires the piston-mini-client cache """
434         sc_expunge_cache = os.path.join(
435             self.datadir, "expunge-cache.py")
436         (pid, stdin, stdout, stderr) = GObject.spawn_async(
437             [sc_expunge_cache, 
438              "--by-unsuccessful-http-states",
439              softwarecenter.paths.SOFTWARE_CENTER_CACHE_DIR,
440              ])
441
442     def _rebuild_and_reopen_local_db(self, pathname):
443         """ helper that rebuilds a db and reopens it """
444         from softwarecenter.db.update import rebuild_database
445         LOG.info("building local database")
446         # debian_sources is a bit misnamed, it will look for annotated
447         # desktop files in /usr/share/app-install - enabling this on
448         # non-debian systems will do no harm
449         debian_sources = True
450         # the appstream sources, enabling this on non-appstream systems
451         # will do no harm
452         appstream_sources = True
453         rebuild_database(pathname, debian_sources, appstream_sources)
454         self.db = StoreDatabase(pathname, self.cache)
455         self.db.open(use_axi=self._use_axi)
456
457     def _setup_proxy_initially(self):
458         from gi.repository import Gio
459         self._setup_proxy()
460         self._gsettings = Gio.Settings.new("org.gnome.system.proxy.http")
461         self._gsettings.connect("changed", self._setup_proxy)
462
463     def _setup_proxy(self, setting=None, key=None):
464         proxy = get_http_proxy_string_from_gsettings()
465         if proxy:
466             os.environ["http_proxy"] = proxy
467         elif "http_proxy" in os.environ:
468             del os.environ["http_proxy"]
469
470
471     # callbacks
472     def on_realize(self, widget):
473         return
474
475     def on_available_pane_created(self, widget):
476         self.available_pane.searchentry.grab_focus()
477         # connect a signal to monitor the recommendations opt-in state and
478         # persist the recommendations uuid on an opt-in
479         self.available_pane.cat_view.recommended_for_you_panel.connect(
480                         "recommendations-opt-in",
481                         self._on_recommendations_opt_in)
482         self.available_pane.cat_view.recommended_for_you_panel.connect(
483                         "recommendations-opt-out",
484                         self._on_recommendations_opt_out)
485     
486     #~ def on_installed_pane_created(self, widget):
487         #~ pass
488         
489     def _on_recommendations_opt_in(self, agent, recommender_uuid):
490         self.recommender_uuid = recommender_uuid
491     
492     def _on_recommendations_opt_out(self):
493         # if the user opts back out of the recommender service, we
494         # reset the UUID to indicate it
495         self.recommender_uuid = ""
496     
497     def _on_update_software_center_agent_finished(self, pid, condition):
498         LOG.info("software-center-agent finished with status %i" % os.WEXITSTATUS(condition))
499         if os.WEXITSTATUS(condition) == 0:
500             self.db.reopen()
501
502     def on_review_stats_loaded(self, reviews):
503         LOG.debug("on_review_stats_loaded: '%s'" % len(reviews))
504
505     def on_window_main_delete_event(self, widget, event):
506         if hasattr(self, "glaunchpad"):
507             self.glaunchpad.shutdown()
508         self.save_state()
509         try:
510             Gtk.main_quit()
511         except Exception as e:
512             LOG.warning(e)
513         
514     def on_window_main_key_press_event(self, widget, event):
515         """ Define all the accelerator keys here - slightly messy, but the ones
516             defined in the menu don't seem to work.. """
517
518         # close
519         if ((event.keyval == Gdk.keyval_from_name("w") or
520              event.keyval == Gdk.keyval_from_name("q")) and
521             event.state == Gdk.ModifierType.CONTROL_MASK):
522             self.menuitem_close.activate()
523
524         # undo/redo
525         if (event.keyval == Gdk.keyval_from_name("z") and
526             event.state == Gdk.ModifierType.CONTROL_MASK):
527             self.menuitem_edit.activate()
528             if self.menuitem_undo.get_sensitive():
529                 self.menuitem_undo.activate()
530
531         if (event.keyval == Gdk.keyval_from_name("Z") and
532             event.state == (Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK)):
533             self.menuitem_edit.activate()
534             if self.menuitem_redo.get_sensitive():
535                 self.menuitem_redo.activate()
536
537         # cut/copy/paste
538         if (event.keyval == Gdk.keyval_from_name("x") and
539             event.state == Gdk.ModifierType.CONTROL_MASK):
540             self.menuitem_edit.activate()
541             if self.menuitem_cut.get_sensitive():
542                 self.menuitem_cut.activate()
543
544         if (event.keyval == Gdk.keyval_from_name("c") and
545             event.state == Gdk.ModifierType.CONTROL_MASK):
546             self.menuitem_edit.activate()
547             if self.menuitem_copy.get_sensitive():
548                 self.menuitem_copy.activate()
549
550         if (event.keyval == Gdk.keyval_from_name("v") and
551             event.state == Gdk.ModifierType.CONTROL_MASK):
552             self.menuitem_edit.activate()
553             if self.menuitem_paste.get_sensitive():
554                 self.menuitem_paste.activate()
555
556         # copy web link
557         if (event.keyval == Gdk.keyval_from_name("C") and
558             event.state == (Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.CONTROL_MASK)):
559             self.menuitem_edit.activate()
560             if self.menuitem_copy_web_link.get_sensitive():
561                 self.menuitem_copy_web_link.activate()
562
563         # select all
564         if (event.keyval == Gdk.keyval_from_name("a") and
565             event.state == Gdk.ModifierType.CONTROL_MASK):
566             self.menuitem_edit.activate()
567             if self.menuitem_select_all.get_sensitive():
568                 self.menuitem_select_all.activate()
569
570         # search
571         if (event.keyval == Gdk.keyval_from_name("f") and
572             event.state == Gdk.ModifierType.CONTROL_MASK):
573             self.menuitem_edit.activate()
574             if self.menuitem_search.get_sensitive():
575                 self.menuitem_search.activate()
576
577         # back
578         if ((event.keyval == Gdk.keyval_from_name("bracketleft") and
579              event.state == Gdk.ModifierType.CONTROL_MASK) or
580             ((event.keyval == Gdk.keyval_from_name("Left") or
581               event.keyval == Gdk.keyval_from_name("KP_Left")) and
582              event.state == Gdk.ModifierType.MOD1_MASK)):
583             # using the backspace key to navigate back has been disabled as it
584             # has started to show dodgy side effects which I can't figure how
585             # to deal with
586             self.menuitem_view.activate()
587             if self.menuitem_go_back.get_sensitive():
588                 self.menuitem_go_back.activate()
589
590         # forward
591         if ((event.keyval == Gdk.keyval_from_name("bracketright") and
592              event.state == Gdk.ModifierType.CONTROL_MASK) or
593             ((event.keyval == Gdk.keyval_from_name("Right") or
594               event.keyval == Gdk.keyval_from_name("KP_Right")) and
595              event.state == Gdk.ModifierType.MOD1_MASK)):
596             self.menuitem_view.activate()
597             if self.menuitem_go_forward.get_sensitive():
598                 self.menuitem_go_forward.activate()
599             
600     def on_window_main_button_press_event(self, widget, event):
601         """
602         Implement back/forward navigation via mouse navigation keys using
603         the same button codes as used in Nautilus.
604         """
605         if event.button == MOUSE_EVENT_BACK_BUTTON:
606             self.menuitem_view.activate()
607             if self.menuitem_go_back.get_sensitive():
608                 self.menuitem_go_back.activate()
609         elif event.button == MOUSE_EVENT_FORWARD_BUTTON:
610             self.menuitem_view.activate()
611             if self.menuitem_go_forward.get_sensitive():
612                 self.menuitem_go_forward.activate()
613         
614     def _on_lp_login(self, lp, token):
615         self._lp_login_successful = True
616         private_archives = self.glaunchpad.get_subscribed_archives()
617         self.view_switcher.get_model().channel_manager.feed_in_private_sources_list_entries(
618             private_archives)
619
620     def _on_sso_login(self, sso, oauth_result):
621         self._sso_login_successful = True
622         # appmanager needs to know about the oauth token for the reinstall
623         # previous purchases add_license_key call
624         self.app_manager.oauth_token = oauth_result
625         self.scagent.query_available_for_me()
626
627     def _on_style_updated(self, widget, init_css_callback, *args):
628         init_css_callback(widget, *args)
629         return
630
631     def _available_for_me_result(self, scagent, result_list):
632         #print "available_for_me_result", result_list
633         from softwarecenter.db.update import (
634             add_from_purchased_but_needs_reinstall_data)
635         self.available_for_me_query = add_from_purchased_but_needs_reinstall_data(
636             result_list, self.db, self.cache)
637         self.available_pane.on_previous_purchases_activated(self.available_for_me_query) 
638
639     def get_icon_filename(self, iconname, iconsize):
640         iconinfo = self.icons.lookup_icon(iconname, iconsize, 0)
641         if not iconinfo:
642             iconinfo = self.icons.lookup_icon(Icons.MISSING_APP_ICON, iconsize, 0)
643         return iconinfo.get_filename()
644
645     # File Menu
646     def on_menu_file_activate(self, menuitem):
647         """Enable/disable install/remove"""
648         LOG.debug("on_menu_file_activate")
649
650         # reset it all
651         self.menuitem_install.set_sensitive(False)
652         self.menuitem_remove.set_sensitive(False)
653
654         # get our active pane
655         vm = get_viewmanager()
656         if vm is None:
657             return False
658         self.active_pane = vm.get_view_widget(vm.get_active_view())
659         if self.active_pane is None:
660             return False
661
662         # determine the current app
663         app = self.active_pane.get_current_app()
664         if not app:
665             return False
666
667         # wait for the cache to become ready (if needed)
668         if not self.cache.ready:
669             GObject.timeout_add(
670                 100, lambda: self.on_menu_file_activate(menuitem))
671             return False
672
673         # update menu items
674         pkg_state = None
675         error = None
676         # FIXME:  Use a Gtk.Action for the Install/Remove/Buy/Add Source/Update Now action
677         #         so that all UI controls (menu item, applist view button and appdetails
678         #         view button) are managed centrally:  button text, button sensitivity,
679         #         and callback method
680         # FIXME:  Add buy support here by implementing the above
681         appdetails = app.get_details(self.db)
682         if appdetails:
683             pkg_state = appdetails.pkg_state
684             error = appdetails.error
685         if app.pkgname in self.active_pane.app_view.tree_view._action_block_list:
686             return False
687         elif pkg_state == PkgStates.UPGRADABLE or pkg_state == PkgStates.REINSTALLABLE and not error:
688             self.menuitem_install.set_sensitive(True)
689             self.menuitem_remove.set_sensitive(True)
690         elif pkg_state == PkgStates.INSTALLED:
691             self.menuitem_remove.set_sensitive(True)
692         elif pkg_state == PkgStates.UNINSTALLED and not error:
693             self.menuitem_install.set_sensitive(True)
694         elif (not pkg_state and 
695               not self.active_pane.is_category_view_showing() and 
696               app.pkgname in self.cache and 
697               not app.pkgname in self.active_pane.app_view.tree_view._action_block_list and
698               not error):
699             # when does this happen?
700             pkg = self.cache[app.pkgname]
701             installed = bool(pkg.installed)
702             self.menuitem_install.set_sensitive(not installed)
703             self.menuitem_remove.set_sensitive(installed)
704         # return False to ensure that a possible GObject.timeout_add ends
705         return False
706
707     def on_menuitem_launchpad_private_ppas_activate(self, menuitem):
708         from backend.launchpad import GLaunchpad
709         self.glaunchpad = GLaunchpad()
710         self.glaunchpad.connect("login-successful", self._on_lp_login)
711         from view.logindialog import LoginDialog
712         d = LoginDialog(self.glaunchpad, self.datadir, parent=self.window_main)
713         d.login()
714
715     def _create_dbus_sso(self):
716         # see bug #773214 for the rationale
717         #appname = _("Ubuntu Software Center")
718         appname = "Ubuntu Software Center"
719         help_text = _("To reinstall previous purchases, sign in to the "
720                       "Ubuntu Single Sign-On account you used to pay for them.")
721         #window = self.window_main.get_window()
722         #xid = self.get_window().xid
723         xid = 0
724         self.sso = get_sso_backend(xid,
725                                    appname,
726                                    help_text)
727         self.sso.connect("login-successful", self._on_sso_login)
728
729     def _login_via_dbus_sso(self):
730         self._create_dbus_sso()
731         self.sso.login()
732
733     def _create_scagent_if_needed(self):
734         if not self.scagent:
735             from softwarecenter.backend.scagent import SoftwareCenterAgent
736             self.scagent = SoftwareCenterAgent()
737             self.scagent.connect("available-for-me", self._available_for_me_result)
738             
739     def on_menuitem_reinstall_purchases_activate(self, menuitem):
740         self.view_manager.set_active_view(ViewPages.AVAILABLE)
741         self.available_pane.show_appview_spinner()
742         if self.available_for_me_query:
743             # we already have the list of available items, so just show it
744             self.available_pane.on_previous_purchases_activated(self.available_for_me_query)
745         else:
746             # fetch the list of available items and show it
747             self._create_scagent_if_needed()
748             self._login_via_dbus_sso()
749             
750     def on_menuitem_deauthorize_computer_activate(self, menuitem):
751     
752         # FIXME: need Ubuntu SSO username here
753         # account_name = get_person_from_config()
754         account_name = None
755         
756         # get a list of installed purchased packages
757         installed_purchased_packages = self.db.get_installed_purchased_packages()
758
759         # display the deauthorize computer dialog
760         deauthorize = deauthorize_dialog.deauthorize_computer(None,
761                                                               self.datadir,
762                                                               self.db,
763                                                               self.icons,
764                                                               account_name,
765                                                               installed_purchased_packages)
766         if deauthorize:
767             # clear the ubuntu SSO token for this account
768             clear_token_from_ubuntu_sso(_("Ubuntu Software Center"))
769             
770             # uninstall the list of purchased packages
771             # TODO: do we need to check for dependencies and show a removal
772             # dialog for that case?  seems not since these are purchased apps
773             for pkgname in installed_purchased_packages:
774                 app = Application(pkgname=pkgname)
775                 appdetails = app.get_details(self.db)
776                 self.backend.remove(app, appdetails.icon)
777             
778             # TODO: remove the corresponding private PPA sources
779             # FIXME: this should really be done using aptdaemon, update this if/when
780             #        remove repository support is added to aptdaemon
781             # (private-ppa.launchpad.net_commercial-ppa-uploaders*)
782             purchased_sources = glob.glob("/etc/apt/sources.list.d/private-ppa.launchpad.net_commercial-ppa-uploaders*")
783             for source in purchased_sources:
784                 print("source: %s" % source)
785
786     def on_menuitem_sync_between_computers_activate(self, menuitem):
787         if self.view_manager.get_active_view() != ViewPages.INSTALLED:
788             pane = self.view_manager.set_active_view(ViewPages.INSTALLED)
789             state = pane.state.copy()
790             state.channel = AllInstalledChannel()
791             page = None
792             self.view_manager.display_page(pane, page, state)
793             self.installed_pane.refresh_apps()
794         get_oneconf_handler().sync_between_computers(True)
795         
796     def on_menuitem_install_activate(self, menuitem):
797         app = self.active_pane.get_current_app()
798         get_appmanager().request_action(app, [], [], AppActions.INSTALL)
799
800     def on_menuitem_remove_activate(self, menuitem):
801         app = self.active_pane.get_current_app()
802         get_appmanager().request_action(app, [], [], AppActions.REMOVE)
803         
804     def on_menuitem_close_activate(self, widget):
805         Gtk.main_quit()
806
807 # Edit Menu
808     def on_menu_edit_activate(self, menuitem):
809         """
810         Check whether the search field is focused and if so, focus some items
811         """
812         edit_menu_items = [self.menuitem_undo,
813                            self.menuitem_redo,
814                            self.menuitem_cut, 
815                            self.menuitem_copy,
816                            self.menuitem_copy_web_link,
817                            self.menuitem_paste,
818                            self.menuitem_delete,
819                            self.menuitem_select_all,
820                            self.menuitem_search]
821         for item in edit_menu_items:
822             item.set_sensitive(False)
823
824         # get our active pane
825         vm = get_viewmanager()
826         if vm is None:
827             return False
828         self.active_pane = vm.get_view_widget(vm.get_active_view())
829
830         if (self.active_pane and 
831             self.active_pane.searchentry and
832             self.active_pane.searchentry.get_visible()):
833             # undo, redo, cut, copy, paste, delete, select_all sensitive 
834             # if searchentry is focused (and other more specific conditions)
835             if self.active_pane.searchentry.is_focus():
836                 if len(self.active_pane.searchentry._undo_stack) > 1:
837                     self.menuitem_undo.set_sensitive(True)
838                 if len(self.active_pane.searchentry._redo_stack) > 0:
839                     self.menuitem_redo.set_sensitive(True)
840                 bounds = self.active_pane.searchentry.get_selection_bounds()
841                 if bounds:
842                     self.menuitem_cut.set_sensitive(True)
843                     self.menuitem_copy.set_sensitive(True)
844                 self.menuitem_paste.set_sensitive(True)
845                 if self.active_pane.searchentry.get_text():
846                     self.menuitem_delete.set_sensitive(True)
847                     self.menuitem_select_all.set_sensitive(True)
848             # search sensitive if searchentry is not focused
849             else:
850                 self.menuitem_search.set_sensitive(True)
851
852         # weblink
853         if self.active_pane:
854             app = self.active_pane.get_current_app()
855             if app and app.pkgname:
856                 self.menuitem_copy_web_link.set_sensitive(True)
857
858         # details view
859         if (self.active_pane and 
860             self.active_pane.is_app_details_view_showing()):
861
862             self.menuitem_select_all.set_sensitive(True)
863             sel_text = self.active_pane.app_details_view.desc.get_selected_text()
864
865             if sel_text:
866                 self.menuitem_copy.set_sensitive(True)
867
868     def on_menuitem_undo_activate(self, menuitem):
869         self.active_pane.searchentry.undo()
870         
871     def on_menuitem_redo_activate(self, menuitem):
872         self.active_pane.searchentry.redo()
873
874     def on_menuitem_cut_activate(self, menuitem):
875         self.active_pane.searchentry.cut_clipboard()
876
877     def on_menuitem_copy_activate(self, menuitem):
878         if (self.active_pane and
879             self.active_pane.is_app_details_view_showing()):
880
881             self.active_pane.app_details_view.desc.copy_clipboard()
882
883         elif self.active_pane:
884             self.active_pane.searchentry.copy_clipboard()
885
886     def on_menuitem_paste_activate(self, menuitem):
887         self.active_pane.searchentry.paste_clipboard()
888
889     def on_menuitem_delete_activate(self, menuitem):
890         self.active_pane.searchentry.set_text("")
891
892     def on_menuitem_select_all_activate(self, menuitem):
893         if (self.active_pane and
894             self.active_pane.is_app_details_view_showing()):
895
896             self.active_pane.app_details_view.desc.select_all()
897             self.active_pane.app_details_view.desc.grab_focus()
898
899         elif self.active_pane:
900             self.active_pane.searchentry.select_region(0, -1)
901
902     def on_menuitem_copy_web_link_activate(self, menuitem):
903         app = self.active_pane.get_current_app()
904         if app:
905             display = Gdk.Display.get_default()
906             selection = Gdk.Atom.intern ("CLIPBOARD", False)
907             clipboard = Gtk.Clipboard.get_for_display(display, selection)
908             clipboard.set_text(self.WEBLINK_URL % app.pkgname, -1)
909
910     def on_menuitem_search_activate(self, widget):
911         if self.active_pane:
912             self.active_pane.searchentry.grab_focus()
913             self.active_pane.searchentry.select_region(0, -1)
914
915     def on_menuitem_software_sources_activate(self, widget):
916         self.window_main.set_sensitive(False)
917         # run software-properties-gtk
918         window = self.window_main.get_window()
919         if hasattr(window, 'xid'):
920             xid = window.xid
921         else:
922             xid = 0
923
924         p = subprocess.Popen(
925             ["/usr/bin/software-properties-gtk", 
926              "-n", 
927              "-t", str(xid)])
928         # Monitor the subprocess regularly
929         GObject.timeout_add(100, self._poll_software_sources_subprocess, p)
930
931     def _poll_software_sources_subprocess(self, popen):
932         ret = popen.poll()
933         if ret is None:
934             # Keep monitoring
935             return True
936         # A return code of 1 means that the sources have changed
937         if ret == 1:
938             self.run_update_cache()
939         self.window_main.set_sensitive(True)
940         # Stop monitoring
941         return False
942
943 # View Menu
944     def on_menu_view_activate(self, menuitem):
945         vm = get_viewmanager()
946         if vm is None:
947             self.menuitem_view_all.set_sensitive(False)
948             self.menuitem_view_supported_only.set_sensitive(False)
949             self.menuitem_go_back.set_sensitive(False)
950             self.menuitem_go_forward.set_sensitive(False)
951             return False
952             
953         left_sensitive = vm.back_forward.left.get_sensitive()
954         self.menuitem_go_back.set_sensitive(left_sensitive)
955         right_sensitive = vm.back_forward.right.get_sensitive()
956         self.menuitem_go_forward.set_sensitive(right_sensitive)
957
958         self.menuitem_view.blocked = True
959
960         # get our active pane
961         self.active_pane = vm.get_view_widget(vm.get_active_view())
962         if (self.active_pane and
963             self.active_pane == self.available_pane or
964             self.active_pane == self.installed_pane):
965             self.menuitem_view_all.set_sensitive(True)
966             self.menuitem_view_supported_only.set_sensitive(True)
967
968             from softwarecenter.db.appfilter import get_global_filter
969             supported_only = get_global_filter().supported_only
970             self.menuitem_view_all.set_active(not supported_only)
971             self.menuitem_view_supported_only.set_active(supported_only)
972         else:
973             self.menuitem_view_all.set_sensitive(False)
974             self.menuitem_view_supported_only.set_sensitive(False)
975
976         self.menuitem_view.blocked = False
977
978     def on_menuitem_view_all_activate(self, widget):
979         if self.menuitem_view.blocked:
980             return
981         from softwarecenter.db.appfilter import get_global_filter
982         if get_global_filter().supported_only == True:
983             get_global_filter().supported_only = False
984
985             self.available_pane.refresh_apps()
986             try:
987                 self.installed_pane.refresh_apps()
988             except: # may not be initialised
989                 pass
990
991     def on_menuitem_view_supported_only_activate(self, widget):
992         if self.menuitem_view.blocked:
993             return
994         from softwarecenter.db.appfilter import get_global_filter
995         if get_global_filter().supported_only == False:
996             get_global_filter().supported_only = True
997
998             self.available_pane.refresh_apps()
999             try:
1000                 self.installed_pane.refresh_apps()
1001             except: # may not be initialised
1002                 pass
1003
1004             # navigate up if the details page is no longer available
1005             #~ ap = self.active_pane
1006             #~ if (ap and ap.is_app_details_view_showing and ap.app_details_view.app and
1007                 #~ not self.distro.is_supported(self.cache, None, ap.app_details_view.app.pkgname)):
1008                 #~ if len(ap.app_view.get_model()) == 0:
1009                     #~ ap.navigation_bar.navigate_up_twice()
1010                 #~ else:
1011                     #~ ap.navigation_bar.navigate_up()
1012                 #~ ap.on_application_selected(None, None)    
1013
1014             #~ # navigate up if the list page is empty
1015             #~ elif (ap and ap.is_applist_view_showing() and 
1016                 #~ len(ap.app_view.get_model()) == 0):
1017                 #~ ap.navigation_bar.navigate_up()
1018                 #~ ap.on_application_selected(None, None)    
1019
1020     def on_navhistory_back_action_activate(self, navhistory_back_action=None):
1021         vm = get_viewmanager()
1022         vm.nav_back()
1023
1024     def on_navhistory_forward_action_activate(self, navhistory_forward_action=None):
1025         vm = get_viewmanager()
1026         vm.nav_forward()
1027         
1028     def on_menuitem_add_to_launcher_toggled(self, menu_item):
1029         self.available_pane.add_to_launcher_enabled = menu_item.get_active()
1030
1031 # Help Menu
1032     def on_menuitem_about_activate(self, widget):
1033         self.aboutdialog.set_version(VERSION)
1034         self.aboutdialog.set_transient_for(self.window_main)
1035         self.aboutdialog.show()
1036
1037     def on_menuitem_help_activate(self, menuitem):
1038         # run yelp
1039         p = subprocess.Popen(["yelp","ghelp:software-center"])
1040         # collect the exit status (otherwise we leave zombies)
1041         GObject.timeout_add_seconds(1, lambda p: p.poll() == None, p)
1042
1043     def on_menuitem_developer_activate(self, menuitem):
1044         webbrowser.open(self.distro.DEVELOPER_URL)
1045
1046     def _ask_and_repair_broken_cache(self):
1047         # wait until the window window is available
1048         if self.window_main.props.visible == False:
1049             GObject.timeout_add_seconds(1, self._ask_and_repair_broken_cache)
1050             return
1051         if dialogs.confirm_repair_broken_cache(self.window_main,
1052                                                       self.datadir):
1053             self.backend.fix_broken_depends()
1054
1055     def _on_apt_cache_broken(self, aptcache):
1056         self._ask_and_repair_broken_cache()
1057
1058     def _on_transaction_finished(self, backend, result):
1059         """ callback when an application install/remove transaction 
1060             (or a cache reload) has finished 
1061         """
1062         self.cache.open()
1063
1064     def on_channels_changed(self, backend, res):
1065         """ callback when the set of software channels has changed """
1066         LOG.debug("on_channels_changed %s" % res)
1067         if res:
1068             # reopen the database, this will ensure that the right signals
1069             # are send and triggers "refresh_apps"
1070             # and refresh the displayed app in the details as well
1071             self.db.reopen()
1072
1073     # helper
1074
1075     def run_update_cache(self):
1076         """update the apt cache (e.g. after new sources where added """
1077         self.backend.reload()
1078
1079     def update_app_list_view(self, channel=None):
1080         """Helper that updates the app view list """
1081         if self.active_pane is None:
1082             return
1083         if channel is None and self.active_pane.is_category_view_showing():
1084             return
1085         if channel:
1086             self.channel_pane.set_channel(channel)
1087             self.active_pane.refresh_apps()
1088
1089     def _on_database_rebuilding_handler(self, is_rebuilding):
1090         LOG.debug("_on_database_rebuilding_handler %s" % is_rebuilding)
1091         self._database_is_rebuilding = is_rebuilding
1092
1093         if is_rebuilding:
1094             pass
1095         else:
1096             # we need to reopen when the database finished updating
1097             self.db.reopen()
1098
1099     def setup_database_rebuilding_listener(self):
1100         """
1101         Setup system bus listener for database rebuilding
1102         """
1103         self._database_is_rebuilding = False
1104         # get dbus
1105         try:
1106             bus = dbus.SystemBus()
1107         except:
1108             LOG.exception("could not get system bus")
1109             return
1110         # check if its currently rebuilding (most likely not, so we
1111         # just ignore errors from dbus because the interface
1112         try:
1113             proxy_obj = bus.get_object("com.ubuntu.Softwarecenter",
1114                                        "/com/ubuntu/Softwarecenter")
1115             iface = dbus.Interface(proxy_obj, "com.ubuntu.Softwarecenter")
1116             res = iface.IsRebuilding()
1117             self._on_database_rebuilding_handler(res)
1118         except Exception as e:
1119             LOG.debug("query for the update-database exception '%s' (probably ok)" % e)
1120
1121         # add signal handler
1122         bus.add_signal_receiver(self._on_database_rebuilding_handler,
1123                                 "DatabaseRebuilding",
1124                                 "com.ubuntu.Softwarecenter")
1125
1126     def setup_dbus_or_bring_other_instance_to_front(self, args):
1127         """ 
1128         This sets up a dbus listener
1129         """
1130         try:
1131             bus = dbus.SessionBus()
1132         except:
1133             LOG.exception("could not initiate dbus")
1134             return
1135         # if there is another Softwarecenter running bring it to front
1136         # and exit, otherwise install the dbus controller
1137         try:
1138             proxy_obj = bus.get_object('com.ubuntu.Softwarecenter', 
1139                                        '/com/ubuntu/Softwarecenter')
1140             iface = dbus.Interface(proxy_obj, 'com.ubuntu.SoftwarecenterIFace')
1141             if args:
1142                 iface.bringToFront(args)
1143             else:
1144                 # None can not be transported over dbus
1145                 iface.bringToFront('nothing-to-show')
1146             sys.exit()
1147         except dbus.DBusException:
1148             bus_name = dbus.service.BusName('com.ubuntu.Softwarecenter',bus)
1149             self.dbusControler = SoftwarecenterDbusController(self, bus_name)
1150
1151     def show_available_packages(self, packages):
1152         """ Show packages given as arguments in the available_pane
1153             If the list of packages is only one element long show that,
1154             otherwise turn it into a comma seperated search
1155         """
1156         # strip away the apt: prefix
1157         if packages and packages[0].startswith("apt:///"):
1158             # this is for 'apt:pkgname' in alt+F2 in gnome
1159             packages[0] = packages[0].partition("apt:///")[2]
1160         elif packages and packages[0].startswith("apt://"):
1161             packages[0] = packages[0].partition("apt://")[2]
1162         elif packages and packages[0].startswith("apt:"):
1163             packages[0] = packages[0].partition("apt:")[2]
1164
1165         # allow s-c to be called with a search term
1166         if packages and packages[0].startswith("search:"):
1167             packages[0] = packages[0].partition("search:")[2]
1168             self.available_pane.init_view()
1169             self.available_pane.searchentry.set_text(" ".join(packages))
1170             return
1171
1172         if len(packages) == 1:
1173             request = packages[0]
1174
1175             # are we dealing with a path?
1176             if os.path.exists(request) and not os.path.isdir(request):
1177                 if not request.startswith('/'):
1178                 # we may have been given a relative path
1179                     request = os.path.join(os.getcwd(), request)
1180                 app = DebFileApplication(request)
1181             else:
1182                 # package from archive
1183                 # if there is a "/" in the string consider it as tuple
1184                 # of (pkgname, appname) for exact matching (used by
1185                 # e.g. unity
1186                 (pkgname, sep, appname) = packages[0].partition("/")
1187                 app = Application(appname, pkgname)
1188
1189             @wait_for_apt_cache_ready
1190             def show_app(self, app):
1191                 # if the pkg is installed, show it in the installed pane
1192                 if (app.pkgname in self.cache and 
1193                     self.cache[app.pkgname].installed):
1194                     with ExecutionTime("installed_pane.init_view()"):
1195                         self.installed_pane.init_view()
1196                     with ExecutionTime("installed_pane.show_app()"):
1197                         self.installed_pane.show_app(app)
1198                 else:
1199                     self.available_pane.init_view()
1200                     self.available_pane.show_app(app)
1201             show_app(self, app)
1202             return    
1203         elif len(packages) > 1:
1204             # turn multiple packages into a search with ","
1205             self.available_pane.init_view()
1206             self.available_pane.searchentry.set_text(",".join(packages))
1207             return
1208         # normal startup, show the lobby (it will have a spinner when
1209         # its not ready yet) - it will also initialize the view
1210         self.view_manager.set_active_view(ViewPages.AVAILABLE)
1211
1212     def restore_state(self):
1213         if self.config.has_option("general", "size"):
1214             (x, y) = self.config.get("general", "size").split(",")
1215             self.window_main.set_default_size(int(x), int(y))
1216         else:
1217             # on first launch, specify the default window size to take advantage
1218             # of the available screen real estate (but set a reasonable limit
1219             # in case of a crazy-huge monitor)
1220             screen_height = Gdk.Screen.height()
1221             screen_width = Gdk.Screen.width()
1222             self.window_main.set_default_size(
1223                                         min(int(.85 * screen_width), 1200),
1224                                         min(int(.85 * screen_height), 800))
1225         if (self.config.has_option("general", "maximized") and
1226             self.config.getboolean("general", "maximized")):
1227             self.window_main.maximize()
1228         if self.config.has_option("general", "add_to_launcher"):
1229             self.available_pane.add_to_launcher_enabled = (
1230                     self.config.getboolean(
1231                     "general",
1232                     "add_to_launcher"))
1233         else:
1234             # initial default state is to add to launcher, per spec
1235             self.available_pane.add_to_launcher_enabled = True
1236         if self.config.has_option("general", "recommender_uuid"):
1237             self.recommender_uuid = self.config.get("general",
1238                                                     "recommender_uuid")
1239
1240     def save_state(self):
1241         LOG.debug("save_state")
1242         # this happens on a delete event, we explicitely save_state() there
1243         window = self.window_main.get_window()
1244         if window is None:
1245             return
1246         if not self.config.has_section("general"):
1247             self.config.add_section("general")
1248         maximized = window.get_state() & Gdk.WindowState.MAXIMIZED
1249         if maximized:
1250             self.config.set("general", "maximized", "True")
1251         else:
1252             self.config.set("general", "maximized", "False")
1253             # size only matters when non-maximized
1254             size = self.window_main.get_size() 
1255             self.config.set("general","size", "%s, %s" % (size[0], size[1]))
1256         if self.available_pane.add_to_launcher_enabled:
1257             self.config.set("general", "add_to_launcher", "True")
1258         else:
1259             self.config.set("general", "add_to_launcher", "False")
1260         self.config.set("general",
1261                         "recommender_uuid",
1262                         self.recommender_uuid)
1263         self.config.write()
1264
1265     def cache_magic(self):
1266         self.cache.open()
1267         if hasattr(self.cache, 'prefill_cache'):
1268             self.cache.prefill_cache(wanted_pkgs = self.prefill_pkgnames)
1269
1270     def run(self, args):
1271         # show window as early as possible
1272         self.window_main.show_all()
1273
1274         # delay cache open
1275         GObject.timeout_add(1, self.cache_magic)
1276
1277         # support both "pkg1 pkg" and "pkg1,pkg2" (and pkg1,pkg2 pkg3)
1278         if args:
1279             for (i, arg) in enumerate(args[:]):
1280                 if "," in arg:
1281                     args.extend(arg.split(","))
1282                     del args[i]
1283
1284         # FIXME: make this more predictable and less random
1285         # show args when the app is ready
1286         self.show_available_packages(args)
1287
1288         atexit.register(self.save_state)