Merge lp:~mvo/software-center/app-treeview-buy-plus-refactor into lp:software-center

Proposed by Michael Vogt on 2011-11-09
Status: Merged
Merged at revision: 2552
Proposed branch: lp:~mvo/software-center/app-treeview-buy-plus-refactor
Merge into: lp:software-center
Diff against target: 1398 lines (+429/-360)
16 files modified
softwarecenter/drawing.py (+0/-86)
softwarecenter/enums.py (+1/-0)
softwarecenter/testutils.py (+11/-0)
softwarecenter/ui/gtk3/app.py (+12/-84)
softwarecenter/ui/gtk3/models/appstore2.py (+17/-7)
softwarecenter/ui/gtk3/panes/availablepane.py (+16/-10)
softwarecenter/ui/gtk3/session/appmanager.py (+180/-0)
softwarecenter/ui/gtk3/session/viewmanager.py (+1/-1)
softwarecenter/ui/gtk3/views/appdetailsview.py (+5/-72)
softwarecenter/ui/gtk3/views/appdetailsview_gtk.py (+16/-27)
softwarecenter/ui/gtk3/views/appview.py (+6/-13)
softwarecenter/ui/gtk3/views/purchaseview.py (+2/-0)
softwarecenter/ui/gtk3/widgets/apptreeview.py (+43/-30)
softwarecenter/ui/gtk3/widgets/buttons.py (+1/-1)
softwarecenter/ui/gtk3/widgets/cellrenderers.py (+39/-29)
test/gtk3/test_appmanager.py (+79/-0)
To merge this branch: bzr merge lp:~mvo/software-center/app-treeview-buy-plus-refactor
Reviewer Review Type Date Requested Status
Gary Lasker (community) Approve on 2011-11-09
Michael Vogt Pending
Review via email: mp+81710@code.launchpad.net

Description of the change

This is based on the great work of Matthew McGowan, I just added a bit of extra tests and tweaking. All tests pass for me.

To post a comment you must log in.
Gary Lasker (gary-lasker) wrote :

This all looks great. All tests pass for me as well, and I tried out the new capabilities myself in various views, etc., some corner cases like canceling purchases and such, and everything works as I expected. Very nice!

The only thing that I wonder is whether we might want to move the location of the price in the list view. It seems like we have consolidated the useful info about an app to the left-hand side, and to me it feels like the price should be there too. But, that's something we can think about (and check with mpt too) and it would be an easy thing to tweak later. So, I'll mark this one approved and I'll merge it now.

Thanks so much! Very nice work, Matt and mvo!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'softwarecenter/drawing.py'
2--- softwarecenter/drawing.py 2011-08-09 08:47:43 +0000
3+++ softwarecenter/drawing.py 1970-01-01 00:00:00 +0000
4@@ -1,86 +0,0 @@
5-import math
6-
7-from gtk.gdk import Color
8-
9-PI = math.pi
10-PI_OVER_180 = PI/180
11-
12-
13-def alpha_composite(fgcolor, bgcolor):
14- """ Creates a composite rgb of a foreground rgba and a background rgb.
15-
16- - fgcolor: an rgba of floats
17- - bgcolor: an rgb of floats
18- """
19-
20- src_r, src_g, src_b, src_a = fgcolor
21- bg_r, bg_g, bg_b = bgcolor
22-
23- # Source: http://en.wikipedia.org/wiki/Alpha_compositing
24- r = ((1 - src_a) * bg_r) + (src_a * src_r)
25- g = ((1 - src_a) * bg_g) + (src_a * src_g)
26- b = ((1 - src_a) * bg_b) + (src_a * src_b)
27- return r, g, b
28-
29-def color_floats(color):
30- if isinstance(color, Color):
31- c = color
32- elif isinstance(color, str):
33- c = Color(color)
34- elif isinstance(color, (list, tuple)) and len(color) >= 3:
35- # assume that a list or tuple of floats has been set as arg, do nothing
36- if isinstance(color[0], float):
37- return color
38- if isinstance(color, int):
39- r,g,b = color
40- c = Color(red=r, green=g, blue=b)
41- else:
42- raise TypeError('Expected gtk.gdk.Color, hash or list of integers. Received: %s' % color)
43- return c.red_float, c.green_float, c.blue_float
44-
45-def rounded_rect(cr, x, y, w, h, r):
46- cr.new_sub_path()
47- cr.arc(r+x, r+y, r, PI, 270*PI_OVER_180)
48- cr.arc(x+w-r, r+y, r, 270*PI_OVER_180, 0)
49- cr.arc(x+w-r, y+h-r, r, 0, 90*PI_OVER_180)
50- cr.arc(r+x, y+h-r, r, 90*PI_OVER_180, PI)
51- cr.close_path()
52- return
53-
54-def rounded_rect2(cr, x, y, w, h, radii):
55- nw, ne, se, sw = radii
56-
57- cr.save()
58- cr.translate(x, y)
59- if nw:
60- cr.new_sub_path()
61- cr.arc(nw, nw, nw, PI, 270 * PI_OVER_180)
62- else:
63- cr.move_to(0, 0)
64- if ne:
65- cr.arc(w-ne, ne, ne, 270 * PI_OVER_180, 0)
66- else:
67- cr.rel_line_to(w-nw, 0)
68- if se:
69- cr.arc(w-se, h-se, se, 0, 90 * PI_OVER_180)
70- else:
71- cr.rel_line_to(0, h-ne)
72- if sw:
73- cr.arc(sw, h-sw, sw, 90 * PI_OVER_180, PI)
74- else:
75- cr.rel_line_to(-(w-se), 0)
76-
77- cr.close_path()
78- cr.restore()
79- return
80-
81-def circle(cr, x, y, w, h):
82- cr.new_path()
83-
84- r = min(w, h)*0.5
85- x += int((w-2*r)/2)
86- y += int((h-2*r)/2)
87-
88- cr.arc(r+x, r+y, r, 0, 360*PI_OVER_180)
89- cr.close_path()
90- return
91
92=== modified file 'softwarecenter/enums.py'
93--- softwarecenter/enums.py 2011-10-20 16:08:23 +0000
94+++ softwarecenter/enums.py 2011-11-09 10:56:38 +0000
95@@ -181,6 +181,7 @@
96 REMOVE = "remove"
97 UPGRADE = "upgrade"
98 APPLY = "apply_changes"
99+ PURCHASE = "purchase"
100
101 # transaction types
102 class TransactionTypes:
103
104=== modified file 'softwarecenter/testutils.py'
105--- softwarecenter/testutils.py 2011-10-07 10:44:12 +0000
106+++ softwarecenter/testutils.py 2011-11-09 10:56:38 +0000
107@@ -77,6 +77,11 @@
108 db.open()
109 return db
110
111+def get_test_install_backend():
112+ from softwarecenter.backend.installbackend import get_install_backend
113+ backend = get_install_backend()
114+ return backend
115+
116 def get_test_gtk3_icon_cache():
117 from softwarecenter.ui.gtk3.utils import get_sc_icon_theme
118 import softwarecenter.paths
119@@ -104,3 +109,9 @@
120 limit=limit,
121 nonblocking_load=False)
122 return enquirer.matches
123+
124+def do_events():
125+ from gi.repository import GObject
126+ main_loop = GObject.main_context_default()
127+ while main_loop.pending():
128+ main_loop.iteration()
129
130=== modified file 'softwarecenter/ui/gtk3/app.py'
131--- softwarecenter/ui/gtk3/app.py 2011-11-04 16:35:55 +0000
132+++ softwarecenter/ui/gtk3/app.py 2011-11-09 10:56:38 +0000
133@@ -66,7 +66,6 @@
134 init_sc_css_provider)
135 from softwarecenter.version import VERSION
136 from softwarecenter.db.database import StoreDatabase
137-from softwarecenter.backend.transactionswatcher import TransactionFinishedResult
138 try:
139 from aptd_gtk3 import InstallBackendUI
140 InstallBackendUI # pyflakes
141@@ -74,7 +73,6 @@
142 from softwarecenter.backend.installbackend import InstallBackendUI
143
144 # ui imports
145-import softwarecenter.ui.gtk3.dialogs.dependency_dialogs as dependency_dialogs
146 import softwarecenter.ui.gtk3.dialogs.deauthorize_dialog as deauthorize_dialog
147 import softwarecenter.ui.gtk3.dialogs as dialogs
148
149@@ -84,7 +82,9 @@
150 from softwarecenter.ui.gtk3.panes.historypane import HistoryPane
151 from softwarecenter.ui.gtk3.panes.globalpane import GlobalPane
152 from softwarecenter.ui.gtk3.panes.pendingpane import PendingPane
153-from softwarecenter.ui.gtk3.session.viewmanager import ViewManager, get_viewmanager
154+from softwarecenter.ui.gtk3.session.appmanager import ApplicationManager
155+from softwarecenter.ui.gtk3.session.viewmanager import (
156+ ViewManager, get_viewmanager)
157
158 from softwarecenter.config import get_config
159 from softwarecenter.backend import get_install_backend
160@@ -240,12 +240,18 @@
161 # FIXME: force rebuild by providing a dbus service for this
162 sys.exit(1)
163
164+ # additional icons come from app-install-data
165+ self.icons = get_sc_icon_theme(self.datadir)
166+
167 # backend
168 self.backend = get_install_backend()
169 self.backend.ui = InstallBackendUI()
170 self.backend.connect("transaction-finished", self._on_transaction_finished)
171 self.backend.connect("channels-changed", self.on_channels_changed)
172
173+ # high level app management
174+ self.app_manager = ApplicationManager(self.db, self.backend, self.icons)
175+
176 # misc state
177 self._block_menuitem_view = False
178
179@@ -254,8 +260,6 @@
180 self.sso = None
181 self.available_for_me_query = None
182
183- # additional icons come from app-install-data
184- self.icons = get_sc_icon_theme(self.datadir)
185 Gtk.Window.set_default_icon_name("softwarecenter")
186
187 # inhibit the error-bell, Bug #846138...
188@@ -294,7 +298,7 @@
189 self.distro,
190 self.icons,
191 self.datadir)
192- self.installed_pane.connect("installed-pane-created", self.on_installed_pane_created)
193+ #~ self.installed_pane.connect("installed-pane-created", self.on_installed_pane_created)
194 self.view_manager.register(self.installed_pane, ViewPages.INSTALLED)
195
196 # history pane (not fully loaded at this point)
197@@ -420,39 +424,10 @@
198 return
199
200 def on_available_pane_created(self, widget):
201- # connect signals
202- self.available_pane.app_details_view.connect("application-request-action",
203- self.on_application_request_action)
204- self.available_pane.app_view.connect("application-request-action",
205- self.on_application_request_action)
206- # FIXME
207- #~ self.available_pane.app_view.connect("mouse-nav-requested",
208- #~ self.on_window_main_button_press_event)
209 self.available_pane.searchentry.grab_focus()
210
211- def on_channel_pane_created(self, widget):
212- #~ channel_section = SoftwareSection()
213- # note that the view_id for each channel's section is set later
214- # depending on whether the channel view will display available or
215- # installed items
216- #~ self.channel_pane.set_section(channel_section)
217-
218- # connect signals
219- self.channel_pane.app_details_view.connect("application-request-action",
220- self.on_application_request_action)
221- self.channel_pane.app_view.connect("application-request-action",
222- self.on_application_request_action)
223-
224- def on_installed_pane_created(self, widget):
225- #~ installed_section = SoftwareSection()
226- #~ installed_section.set_view_id(ViewPages.INSTALLED)
227- #~ self.installed_pane.set_section(installed_section)
228-
229- # connect signals
230- self.installed_pane.app_details_view.connect("application-request-action",
231- self.on_application_request_action)
232- self.installed_pane.app_view.connect("application-request-action",
233- self.on_application_request_action)
234+ #~ def on_installed_pane_created(self, widget):
235+ #~ pass
236
237 def _on_update_software_center_agent_finished(self, pid, condition):
238 LOG.info("software-center-agent finished with status %i" % os.WEXITSTATUS(condition))
239@@ -592,54 +567,7 @@
240 self.available_for_me_query = add_from_purchased_but_needs_reinstall_data(
241 result_list, self.db, self.cache)
242 self.available_pane.on_previous_purchases_activated(self.available_for_me_query)
243-
244- def on_application_request_action(self, widget, app, addons_install, addons_remove, action):
245- """callback when an app action is requested from the appview,
246- if action is "remove", must check if other dependencies have to be
247- removed as well and show a dialog in that case
248- """
249- LOG.debug("on_application_action_requested: '%s' %s" % (app, action))
250- appdetails = app.get_details(self.db)
251- if action == "remove":
252- if not dependency_dialogs.confirm_remove(None, self.datadir, app,
253- self.db, self.icons):
254- # craft an instance of TransactionFinishedResult to send with the
255- # transaction-stopped signal
256- result = TransactionFinishedResult(None, False)
257- result.pkgname = app.pkgname
258- self.backend.emit("transaction-stopped", result)
259- return
260- elif action == "install":
261- # If we are installing a package, check for dependencies that will
262- # also be removed and show a dialog for confirmation
263- # generic removal text (fixing LP bug #554319)
264- if not dependency_dialogs.confirm_install(None, self.datadir, app,
265- self.db, self.icons):
266- # craft an instance of TransactionFinishedResult to send with the
267- # transaction-stopped signal
268- result = TransactionFinishedResult(None, False)
269- result.pkgname = app.pkgname
270- self.backend.emit("transaction-stopped", result)
271- return
272
273- # this allows us to 'upgrade' deb files
274- if action == 'upgrade' and app.request and type(app) == DebFileApplication:
275- action = 'install'
276-
277- # action_func is one of: "install", "remove", "upgrade", "apply_changes"
278- action_func = getattr(self.backend, action)
279- if action == 'install':
280- # the package.deb path name is in the request
281- if app.request and type(app) == DebFileApplication:
282- debfile_name = app.request
283- else:
284- debfile_name = None
285- action_func(app.pkgname, app.appname, appdetails.icon, debfile_name, addons_install, addons_remove)
286- elif callable(action_func):
287- action_func(app.pkgname, app.appname, appdetails.icon, addons_install=addons_install, addons_remove=addons_remove)
288- else:
289- LOG.error("Not a valid action in AptdaemonBackend: '%s'" % action)
290-
291 def get_icon_filename(self, iconname, iconsize):
292 iconinfo = self.icons.lookup_icon(iconname, iconsize, 0)
293 if not iconinfo:
294
295=== modified file 'softwarecenter/ui/gtk3/models/appstore2.py'
296--- softwarecenter/ui/gtk3/models/appstore2.py 2011-11-01 22:44:33 +0000
297+++ softwarecenter/ui/gtk3/models/appstore2.py 2011-11-09 10:56:38 +0000
298@@ -44,6 +44,7 @@
299
300
301 LOG = logging.getLogger(__name__)
302+_FREE_AS_IN_BEER = ("0.00", "")
303
304 class CategoryRowReference:
305 """ A simple container for Category properties to be
306@@ -100,23 +101,29 @@
307 def update_availability(self, doc):
308 doc.available = None
309 doc.installed = None
310+ doc.purchasable = None
311 self.is_installed(doc)
312 return
313
314 def is_available(self, doc):
315 if doc.available is None:
316 pkgname = self.get_pkgname(doc)
317- doc.available = pkgname in self.cache
318+ doc.available = (pkgname in self.cache or
319+ self.is_purchasable(doc))
320 return doc.available
321
322 def is_installed(self, doc):
323 if doc.installed is None:
324 pkgname = self.get_pkgname(doc)
325- if doc.available is None:
326- doc.available = pkgname in self.cache
327- doc.installed = doc.available and self.cache[pkgname].is_installed
328+ doc.installed = (self.is_available(doc) and
329+ self.cache[pkgname].is_installed)
330 return doc.installed
331
332+ def is_purchasable(self, doc):
333+ if doc.purchasable is None:
334+ doc.purchasable = doc.get_value(XapianValues.PRICE) not in _FREE_AS_IN_BEER
335+ return doc.purchasable
336+
337 def get_pkgname(self, doc):
338 return self.db.get_pkgname(doc)
339
340@@ -151,6 +158,9 @@
341 GObject.markup_escape_text(appname),
342 GObject.markup_escape_text(summary))
343
344+ def get_price(self, doc):
345+ return doc.get_value(XapianValues.PRICE)
346+
347 def get_icon(self, doc):
348 try:
349 icon_file_name = split_icon_ext(self.db.get_iconname(doc))
350@@ -411,7 +421,7 @@
351
352 with ExecutionTime("store.append_initial"):
353 for doc in [m.document for m in matches][:extent]:
354- doc.available = doc.installed = None
355+ doc.available = doc.installed = doc.purchasable = None
356 self.append((doc,))
357
358 if n_matches == extent:
359@@ -445,7 +455,7 @@
360
361 if row_content: continue
362 doc = db.get_document(matches[i].docid)
363- doc.available = doc.installed = None
364+ doc.available = doc.installed = doc.purchasable = None
365 self[(i,)][0] = doc
366 return
367
368@@ -471,7 +481,7 @@
369
370 def set_documents(self, parent, documents):
371 for doc in documents:
372- doc.available = None; doc.installed = None
373+ doc.available = None; doc.installed = doc.purchasable = None
374 self.append(parent, (doc,))
375
376 self.transaction_path_map = {}
377
378=== modified file 'softwarecenter/ui/gtk3/panes/availablepane.py'
379--- softwarecenter/ui/gtk3/panes/availablepane.py 2011-10-16 16:21:39 +0000
380+++ softwarecenter/ui/gtk3/panes/availablepane.py 2011-11-09 10:56:38 +0000
381@@ -41,6 +41,7 @@
382 SubCategoryViewGtk)
383 from softwarepane import SoftwarePane
384 from softwarecenter.ui.gtk3.session.viewmanager import get_viewmanager
385+from softwarecenter.ui.gtk3.session.appmanager import get_appmanager
386
387 LOG = logging.getLogger(__name__)
388
389@@ -114,11 +115,10 @@
390 #~ self.app_view._append_appcount(appcount)
391 #~ liststore.connect('appcount-changed', on_appcount_changed)
392 self.app_view.set_model(liststore)
393- # setup purchase stuff
394- self.app_details_view.connect("purchase-requested",
395- self.on_purchase_requested)
396 # purchase view
397 self.purchase_view = PurchaseView()
398+ app_manager = get_appmanager()
399+ app_manager.connect("purchase-requested", self.on_purchase_requested)
400 self.purchase_view.connect("purchase-succeeded", self.on_purchase_succeeded)
401 self.purchase_view.connect("purchase-failed", self.on_purchase_failed)
402 self.purchase_view.connect("purchase-cancelled-by-user", self.on_purchase_cancelled_by_user)
403@@ -198,14 +198,14 @@
404 if window is not None:
405 window.set_cursor(None)
406
407- def on_purchase_requested(self, widget, app, url):
408-
409- self.appdetails = app.get_details(self.db)
410- iconname = self.appdetails.icon
411+ def on_purchase_requested(self, appmanager, app, iconname, url):
412 self.purchase_view.initiate_purchase(app, iconname, url)
413 vm = get_viewmanager()
414- vm.display_page(self, AvailablePane.Pages.PURCHASE, self.state, self.display_purchase)
415-
416+ vm.display_page(
417+ self, AvailablePane.Pages.PURCHASE, self.state,
418+ self.display_purchase)
419+ return
420+
421 def on_purchase_succeeded(self, widget):
422 # switch to the details page to display the transaction is in progress
423 self._return_to_appdetails_view()
424@@ -604,7 +604,7 @@
425
426 def on_show_category_applist(self, widget):
427 self._show_hide_subcategories(show_category_applist=True)
428-
429+
430 def on_previous_purchases_activated(self, query):
431 """ called to activate the previous purchases view """
432 #print cat_view, name, query
433@@ -667,6 +667,7 @@
434 def get_test_window():
435 from softwarecenter.testutils import (get_test_db,
436 get_test_datadir,
437+ get_test_install_backend,
438 get_test_gtk3_viewmanager,
439 get_test_pkg_info,
440 get_test_gtk3_icon_cache,
441@@ -678,6 +679,11 @@
442 cache = get_test_pkg_info()
443 datadir = get_test_datadir()
444 icons = get_test_gtk3_icon_cache()
445+ backend = get_test_install_backend()
446+
447+ # create global AppManager instance
448+ from softwarecenter.ui.gtk3.session.appmanager import ApplicationManager
449+ ApplicationManager(db, backend, icons)
450
451 navhistory_back_action = Gtk.Action("navhistory_back_action", "Back", "Back", None)
452 navhistory_forward_action = Gtk.Action("navhistory_forward_action", "Forward", "Forward", None)
453
454=== added file 'softwarecenter/ui/gtk3/session/appmanager.py'
455--- softwarecenter/ui/gtk3/session/appmanager.py 1970-01-01 00:00:00 +0000
456+++ softwarecenter/ui/gtk3/session/appmanager.py 2011-11-09 10:56:38 +0000
457@@ -0,0 +1,180 @@
458+# Copyright (C) 2011 Canonical
459+#
460+# Authors:
461+# Matthew McGowan
462+#
463+# This program is free software; you can redistribute it and/or modify it under
464+# the terms of the GNU General Public License as published by the Free Software
465+# Foundation; version 3.
466+#
467+# This program is distributed in the hope that it will be useful, but WITHOUT
468+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
469+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
470+# details.
471+#
472+# You should have received a copy of the GNU General Public License along with
473+# this program; if not, write to the Free Software Foundation, Inc.,
474+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
475+
476+try:
477+ from urllib.parse import urlencode
478+ urlencode # pyflakes
479+except ImportError:
480+ from urllib import urlencode
481+
482+from gi.repository import GObject
483+
484+from softwarecenter.enums import AppActions
485+from softwarecenter.db import DebFileApplication
486+from softwarecenter.distro import get_current_arch, get_distro
487+from softwarecenter.i18n import get_language
488+from softwarecenter.ui.gtk3.dialogs import dependency_dialogs
489+from softwarecenter.backend.transactionswatcher import TransactionFinishedResult
490+import softwarecenter.paths
491+
492+_appmanager = None # the global AppManager instance
493+def get_appmanager():
494+ """ get a existing appmanager instance (or None if none is created yet) """
495+ return _appmanager
496+
497+
498+class ApplicationManager(GObject.GObject):
499+
500+ __gsignals__ = {
501+ "purchase-requested" : (GObject.SignalFlags.RUN_LAST,
502+ None,
503+ (GObject.TYPE_PYOBJECT, str, str,)
504+ ),
505+ }
506+
507+ def __init__(self, db, backend, icons):
508+ GObject.GObject.__init__(self)
509+ self._globalise_instance()
510+ self.db = db
511+ self.backend = backend
512+ self.distro = get_distro()
513+ self.datadir = softwarecenter.paths.datadir
514+ self.icons = icons
515+
516+ def _globalise_instance(self):
517+ global _appmanager
518+ if _appmanager is not None:
519+ msg = "Only one instance of ApplicationManager is allowed!"
520+ raise ValueError(msg)
521+ else:
522+ _appmanager = self
523+
524+ def request_action(self, app, addons_install, addons_remove, action):
525+ """callback when an app action is requested from the appview,
526+ if action is "remove", must check if other dependencies have to be
527+ removed as well and show a dialog in that case
528+ """
529+ #~ LOG.debug("on_application_action_requested: '%s' %s" % (app, action))
530+ appdetails = app.get_details(self.db)
531+ if action == AppActions.REMOVE:
532+ if not dependency_dialogs.confirm_remove(
533+ None, self.datadir, app, self.db, self.icons):
534+ # craft an instance of TransactionFinishedResult to send with the
535+ # transaction-stopped signal
536+ result = TransactionFinishedResult(None, False)
537+ result.pkgname = app.pkgname
538+ self.backend.emit("transaction-stopped", result)
539+ return
540+ elif action == AppActions.INSTALL:
541+ # If we are installing a package, check for dependencies that will
542+ # also be removed and show a dialog for confirmation
543+ # generic removal text (fixing LP bug #554319)
544+ if not dependency_dialogs.confirm_install(
545+ None, self.datadir, app, self.db, self.icons):
546+ # craft an instance of TransactionFinishedResult to send with the
547+ # transaction-stopped signal
548+ result = TransactionFinishedResult(None, False)
549+ result.pkgname = app.pkgname
550+ self.backend.emit("transaction-stopped", result)
551+ return
552+
553+ # this allows us to 'upgrade' deb files
554+ if (action == AppActions.UPGRADE and app.request and
555+ isinstance(app, DebFileApplication)):
556+ action = AppActions.INSTALL
557+
558+ # action_func is one of: "install", "remove", "upgrade", "apply_changes"
559+ action_func = getattr(self.backend, action)
560+ if action == AppActions.INSTALL:
561+ # the package.deb path name is in the request
562+ if app.request and isinstance(app, DebFileApplication):
563+ debfile_name = app.request
564+ else:
565+ debfile_name = None
566+
567+ action_func(app.pkgname, app.appname, appdetails.icon,
568+ debfile_name, addons_install, addons_remove)
569+ elif callable(action_func):
570+ action_func(app.pkgname, app.appname, appdetails.icon,
571+ addons_install=addons_install,
572+ addons_remove=addons_remove)
573+ #~ else:
574+ #~ LOG.error("Not a valid action in AptdaemonBackend: '%s'" % action)
575+
576+ # public interface
577+ def reload(self):
578+ """ reload the package cache, this goes straight to the backend """
579+ self.backend.reload()
580+
581+ def install(self, app, addons_to_install, addons_to_remove):
582+ """ install the current application, fire an action request """
583+ self.request_action(
584+ app, addons_to_install, addons_to_remove, AppActions.INSTALL)
585+
586+ def remove(self, app, addons_to_install, addons_to_remove):
587+ """ remove the current application, , fire an action request """
588+ self.request_action(
589+ app, addons_to_install, addons_to_remove, AppActions.REMOVE)
590+
591+ def upgrade(self, app, addons_to_install, addons_to_remove):
592+ """ upgrade the current application, fire an action request """
593+ self.request_action(
594+ app, addons_to_install, addons_to_remove, AppActions.UPGRADE)
595+
596+ def apply_changes(self, app, addons_to_install, addons_to_remove):
597+ """ apply changes concerning add-ons """
598+ self.request_action(
599+ app, addons_to_install, addons_to_remove, AppActions.APPLY)
600+
601+ def buy_app(self, app):
602+ """ initiate the purchase transaction """
603+ lang = get_language()
604+ appdetails = app.get_details(self.db)
605+ url = self.distro.PURCHASE_APP_URL % (
606+ lang, self.distro.get_codename(), urlencode(
607+ {'archive_id' : appdetails.ppaname,
608+ 'arch' : get_current_arch(),}
609+ )
610+ )
611+ self.emit("purchase-requested", app, appdetails.icon, url)
612+
613+ def reinstall_purchased(self, app):
614+ """ reinstall a purchased app """
615+ #~ LOG.debug("reinstall_purchased %s" % self.app)
616+ appdetails = app.get_details(self.db)
617+ iconname = appdetails.icon
618+ deb_line = appdetails.deb_line
619+ license_key = appdetails.license_key
620+ license_key_path = appdetails.license_key_path
621+ signing_key_id = appdetails.signing_key_id
622+ self.backend.add_repo_add_key_and_install_app(deb_line,
623+ signing_key_id,
624+ app,
625+ iconname,
626+ license_key,
627+ license_key_path)
628+
629+ def enable_software_source(self, app):
630+ """ enable the software source for the given app """
631+ appdetails = app.get_details(self.db)
632+ if appdetails.channelfile and appdetails._unavailable_channel():
633+ self.backend.enable_channel(appdetails.channelfile)
634+ elif appdetails.component:
635+ components = appdetails.component.split('&')
636+ for component in components:
637+ self.backend.enable_component(component)
638
639=== modified file 'softwarecenter/ui/gtk3/session/viewmanager.py'
640--- softwarecenter/ui/gtk3/session/viewmanager.py 2011-09-27 04:06:46 +0000
641+++ softwarecenter/ui/gtk3/session/viewmanager.py 2011-11-09 10:56:38 +0000
642@@ -58,7 +58,7 @@
643 global _viewmanager
644 if _viewmanager is not None:
645 msg = "Only one instance of ViewManager is allowed!"
646- raise SystemExit(msg)
647+ raise ValueError(msg)
648 else:
649 _viewmanager = self
650
651
652=== modified file 'softwarecenter/ui/gtk3/views/appdetailsview.py'
653--- softwarecenter/ui/gtk3/views/appdetailsview.py 2011-10-13 13:29:14 +0000
654+++ softwarecenter/ui/gtk3/views/appdetailsview.py 2011-11-09 10:56:38 +0000
655@@ -16,48 +16,26 @@
656 # this program; if not, write to the Free Software Foundation, Inc.,
657 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
658
659-from gi.repository import GObject
660-
661 import logging
662 import softwarecenter.ui.gtk3.dialogs as dialogs
663
664-try:
665- from urllib.parse import urlencode
666- urlencode # pyflakes
667-except ImportError:
668- from urllib import urlencode
669-
670 from gettext import gettext as _
671
672 from softwarecenter.db.application import AppDetails
673 from softwarecenter.backend.reviews import get_review_loader
674 from softwarecenter.backend import get_install_backend
675-from softwarecenter.enums import AppActions
676-from softwarecenter.distro import get_current_arch
677-from softwarecenter.i18n import get_language
678+
679
680 LOG=logging.getLogger(__name__)
681
682 class AppDetailsViewBase(object):
683
684- __gsignals__ = {
685- "application-request-action" : (GObject.SignalFlags.RUN_LAST,
686- None,
687- (GObject.TYPE_PYOBJECT,
688- GObject.TYPE_PYOBJECT,
689- GObject.TYPE_PYOBJECT,
690- str,)),
691- "purchase-requested" : (GObject.SignalFlags.RUN_LAST,
692- None,
693- (GObject.TYPE_PYOBJECT,
694- str,)),
695- }
696-
697 def __init__(self, db, distro, icons, cache, datadir):
698 self.db = db
699 self.distro = distro
700 self.icons = icons
701 self.cache = cache
702+ self.backend = get_install_backend()
703 self.cache.connect("cache-ready", self._on_cache_ready)
704 self.datadir = datadir
705 self.app = None
706@@ -67,13 +45,13 @@
707 # reviews
708 self.review_loader = get_review_loader(self.cache, self.db)
709 # aptdaemon
710- self.backend = get_install_backend()
711-
712+
713 def _draw(self):
714 """ draw the current app into the window, maybe the function
715 you need to overwrite
716 """
717 pass
718+
719 # public API
720 def show_app(self, app):
721 """ show the given application """
722@@ -86,6 +64,7 @@
723 self._draw()
724 self._check_for_reviews()
725 self.emit("selected", self.app)
726+
727 def refresh_app(self):
728 self.show_app(self.app)
729
730@@ -150,52 +129,6 @@
731 self.review_loader.spawn_delete_review_ui(
732 review_id, parent_xid, self.datadir, self._reviews_ready_callback)
733
734- # public interface
735- def reload(self):
736- """ reload the package cache, this goes straight to the backend """
737- self.backend.reload()
738- def install(self):
739- """ install the current application, fire an action request """
740- self.emit("application-request-action", self.app, self.addons_to_install, self.addons_to_remove, AppActions.INSTALL)
741- def remove(self):
742- """ remove the current application, , fire an action request """
743- self.emit("application-request-action", self.app, self.addons_to_install, self.addons_to_remove, AppActions.REMOVE)
744- def upgrade(self):
745- """ upgrade the current application, fire an action request """
746- self.emit("application-request-action", self.app, self.addons_to_install, self.addons_to_remove, AppActions.UPGRADE)
747- def apply_changes(self):
748- """ apply changes concerning add-ons """
749- self.emit("application-request-action", self.app, self.addons_to_install, self.addons_to_remove, AppActions.APPLY)
750-
751- def buy_app(self):
752- """ initiate the purchase transaction """
753- lang = get_language()
754- distro = self.distro.get_codename()
755- url = self.distro.PURCHASE_APP_URL % (lang, distro, urlencode({
756- 'archive_id' : self.appdetails.ppaname,
757- 'arch' : get_current_arch() ,
758- }))
759-
760- self.emit("purchase-requested", self.app, url)
761-
762- def reinstall_purchased(self):
763- """ reinstall a purchased app """
764- LOG.debug("reinstall_purchased %s" % self.app)
765- appdetails = self.app.get_details(self.db)
766- iconname = appdetails.icon
767- deb_line = appdetails.deb_line
768- license_key = appdetails.license_key
769- license_key_path = appdetails.license_key_path
770- signing_key_id = appdetails.signing_key_id
771- backend = get_install_backend()
772- backend.add_repo_add_key_and_install_app(deb_line,
773- signing_key_id,
774- self.app,
775- iconname,
776- license_key,
777- license_key_path)
778-
779-
780 # internal callbacks
781 def _on_cache_ready(self, cache):
782 # re-show the application if the cache changes, it may affect the
783
784=== modified file 'softwarecenter/ui/gtk3/views/appdetailsview_gtk.py'
785--- softwarecenter/ui/gtk3/views/appdetailsview_gtk.py 2011-11-04 14:19:54 +0000
786+++ softwarecenter/ui/gtk3/views/appdetailsview_gtk.py 2011-11-09 10:56:38 +0000
787@@ -53,6 +53,7 @@
788
789 from softwarecenter.ui.gtk3.em import StockEms, em
790 from softwarecenter.ui.gtk3.drawing import color_to_hex
791+from softwarecenter.ui.gtk3.session.appmanager import get_appmanager
792 from softwarecenter.ui.gtk3.widgets.separators import HBar
793 from softwarecenter.ui.gtk3.widgets.viewport import Viewport
794 from softwarecenter.ui.gtk3.widgets.reviews import UIReviewsList
795@@ -150,23 +151,28 @@
796 def _on_button_clicked(self, button):
797 button.set_sensitive(False)
798 state = self.pkg_state
799- self.view.addons_to_install = self.view.addons_manager.addons_to_install
800- self.view.addons_to_remove = self.view.addons_manager.addons_to_remove
801+ app = self.view.app
802+ app_manager = get_appmanager()
803+ addons_to_install = self.view.addons_manager.addons_to_install
804+ addons_to_remove = self.view.addons_manager.addons_to_remove
805 if state == PkgStates.INSTALLED:
806- AppDetailsViewBase.remove(self.view)
807+ app_manager.remove(
808+ app, addons_to_install, addons_to_remove)
809 elif state == PkgStates.PURCHASED_BUT_REPO_MUST_BE_ENABLED:
810- AppDetailsViewBase.reinstall_purchased(self.view)
811+ app_manager.reinstall_purchased(app)
812 elif state == PkgStates.NEEDS_PURCHASE:
813- AppDetailsViewBase.buy_app(self.view)
814+ app_manager.buy_app(app)
815 elif state == PkgStates.UNINSTALLED:
816- AppDetailsViewBase.install(self.view)
817+ app_manager.install(
818+ app, addons_to_install, addons_to_remove)
819 elif state == PkgStates.REINSTALLABLE:
820- AppDetailsViewBase.install(self.view)
821+ app_manager.install(
822+ app, addons_to_install, addons_to_remove)
823 elif state == PkgStates.UPGRADABLE:
824- AppDetailsViewBase.upgrade(self.view)
825+ app_manager.upgrade(
826+ app, addons_to_install, addons_to_remove)
827 elif state == PkgStates.NEEDS_SOURCE:
828- # FIXME: This should be in AppDetailsViewBase
829- self.view.use_this_source()
830+ app_manager.enable_software_source(app)
831 return
832
833 def set_label(self, label):
834@@ -639,14 +645,6 @@
835 "application-selected" : (GObject.SignalFlags.RUN_LAST,
836 None,
837 (GObject.TYPE_PYOBJECT, )),
838- 'application-request-action' : (GObject.SignalFlags.RUN_LAST,
839- None,
840- (GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT, str),
841- ),
842- 'purchase-requested' : (GObject.SignalFlags.RUN_LAST,
843- None,
844- (GObject.TYPE_PYOBJECT,
845- str,)),
846 }
847
848
849@@ -1431,15 +1429,6 @@
850 self.emit("selected", self.app)
851 return
852
853- # public interface
854- def use_this_source(self):
855- if self.app_details.channelfile and self.app_details._unavailable_channel():
856- self.backend.enable_channel(self.app_details.channelfile)
857- elif self.app_details.component:
858- components = self.app_details.component.split('&')
859- for component in components:
860- self.backend.enable_component(component)
861-
862 # internal callback
863 def _update_interface_on_trans_ended(self, result):
864 state = self.pkg_statusbar.pkg_state
865
866=== modified file 'softwarecenter/ui/gtk3/views/appview.py'
867--- softwarecenter/ui/gtk3/views/appview.py 2011-10-11 12:39:31 +0000
868+++ softwarecenter/ui/gtk3/views/appview.py 2011-11-09 10:56:38 +0000
869@@ -36,9 +36,9 @@
870
871 __gsignals__ = {
872 "sort-method-changed": (GObject.SignalFlags.RUN_LAST,
873- None,
874- (GObject.TYPE_PYOBJECT, ),
875- ),
876+ None,
877+ (GObject.TYPE_PYOBJECT, ),
878+ ),
879 "application-activated" : (GObject.SignalFlags.RUN_LAST,
880 None,
881 (GObject.TYPE_PYOBJECT, ),
882@@ -47,14 +47,7 @@
883 None,
884 (GObject.TYPE_PYOBJECT, ),
885 ),
886- "application-request-action" : (GObject.SignalFlags.RUN_LAST,
887- None,
888- (GObject.TYPE_PYOBJECT,
889- GObject.TYPE_PYOBJECT,
890- GObject.TYPE_PYOBJECT,
891- str),
892- ),
893- }
894+ }
895
896 (INSTALLED_MODE, AVAILABLE_MODE, DIFF_MODE) = range(3)
897
898@@ -71,7 +64,7 @@
899
900 def __init__(self, db, cache, icons, show_ratings):
901 Gtk.VBox.__init__(self)
902- self.set_name("app-view")
903+ #~ self.set_name("app-view")
904 # app properties helper
905 self.helper = AppPropertiesHelper(db, cache, icons)
906 # misc internal containers
907@@ -94,7 +87,7 @@
908 self.header_hbox.pack_end(combo_alignment, False, False, 0)
909
910 # content views
911- self.tree_view = AppTreeView(self, icons,
912+ self.tree_view = AppTreeView(self, db, icons,
913 show_ratings, store=None)
914 self.tree_view_scroll.add(self.tree_view)
915
916
917=== modified file 'softwarecenter/ui/gtk3/views/purchaseview.py'
918--- softwarecenter/ui/gtk3/views/purchaseview.py 2011-10-13 13:29:14 +0000
919+++ softwarecenter/ui/gtk3/views/purchaseview.py 2011-11-09 10:56:38 +0000
920@@ -52,6 +52,7 @@
921 # print name, value
922 #headers.foreach(_show_header, None)
923
924+
925 class ScrolledWebkitWindow(Gtk.ScrolledWindow):
926
927 def __init__(self):
928@@ -64,6 +65,7 @@
929 self.add(self.webkit)
930 self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
931
932+
933 class PurchaseView(Gtk.VBox):
934 """
935 View that displays the webkit-based UI for purchasing an item.
936
937=== modified file 'softwarecenter/ui/gtk3/widgets/apptreeview.py'
938--- softwarecenter/ui/gtk3/widgets/apptreeview.py 2011-10-07 12:27:48 +0000
939+++ softwarecenter/ui/gtk3/widgets/apptreeview.py 2011-11-09 10:56:38 +0000
940@@ -5,6 +5,9 @@
941
942 from gettext import gettext as _
943
944+from softwarecenter.ui.gtk3.session.appmanager import get_appmanager
945+
946+
947 from cellrenderers import (CellRendererAppView,
948 CellButtonRenderer,
949 CellButtonIDs)
950@@ -26,14 +29,16 @@
951 VARIANT_INFO = 0
952 VARIANT_REMOVE = 1
953 VARIANT_INSTALL = 2
954-
955- ACTION_BTNS = (VARIANT_REMOVE, VARIANT_INSTALL)
956-
957- def __init__(self, app_view, icons, show_ratings, store=None):
958+ VARIANT_PURCHASE = 3
959+
960+ ACTION_BTNS = (VARIANT_REMOVE, VARIANT_INSTALL, VARIANT_PURCHASE)
961+
962+ def __init__(self, app_view, db, icons, show_ratings, store=None):
963 Gtk.TreeView.__init__(self)
964 self._logger = logging.getLogger("softwarecenter.view.appview")
965
966 self.app_view = app_view
967+ self.db = db
968
969 self.pressed = False
970 self.focal_btn = None
971@@ -57,6 +62,7 @@
972 # it needs to be the first one, because that is what the tools look
973 # at by default
974 tr = CellRendererAppView(icons,
975+ self.create_pango_layout(''),
976 show_ratings,
977 Icons.INSTALLED_OVERLAY)
978 tr.set_pixbuf_width(32)
979@@ -70,9 +76,11 @@
980
981 action = CellButtonRenderer(self,
982 name=CellButtonIDs.ACTION)
983+
984 action.set_markup_variants(
985 {self.VARIANT_INSTALL: _('Install'),
986- self.VARIANT_REMOVE: _('Remove')})
987+ self.VARIANT_REMOVE: _('Remove'),
988+ self.VARIANT_PURCHASE: _(u'Buy\u2026')})
989
990 tr.button_pack_start(info)
991 tr.button_pack_end(action)
992@@ -141,17 +149,6 @@
993 model.row_changed(path, model.get_iter(path))
994 return
995
996-# def is_action_in_progress_for_selected_app(self):
997-# """
998-# return True if an install or remove of the current package
999-# is in progress
1000-# """
1001-# (path, column) = self.get_cursor()
1002-# if path:
1003-# model = self.get_model()
1004-# return (model[path][AppGenericStore.COL_ROW_DATA].transaction_progress != -1)
1005-# return False
1006-
1007 def get_scrolled_window_vadjustment(self):
1008 ancestor = self.get_ancestor(Gtk.ScrolledWindow)
1009 if ancestor:
1010@@ -289,9 +286,14 @@
1011 action_btn.set_sensitive(True)
1012 action_btn.show()
1013 elif self.appmodel.is_available(app):
1014- action_btn.set_variant(self.VARIANT_INSTALL)
1015+ if self.appmodel.is_purchasable(app):
1016+ action_btn.set_variant(self.VARIANT_PURCHASE)
1017+ else:
1018+ action_btn.set_variant(self.VARIANT_INSTALL)
1019+
1020 action_btn.set_sensitive(True)
1021 action_btn.show()
1022+
1023 if not network_state_is_connected():
1024 action_btn.set_sensitive(False)
1025 self.app_view.emit("application-selected",
1026@@ -318,16 +320,19 @@
1027 def _on_row_activated(self, view, path, column, tr):
1028 rowref = self.get_rowref(view.get_model(), path)
1029
1030- if not rowref: return
1031-
1032- if self.rowref_is_category(rowref): return
1033+ if not rowref:
1034+ return
1035+ elif self.rowref_is_category(rowref):
1036+ return
1037
1038 x, y = self.get_pointer()
1039 for btn in tr.get_buttons():
1040 if btn.point_in(x, y):
1041 return
1042
1043- self.app_view.emit("application-activated", self.appmodel.get_application(rowref))
1044+ app = self.appmodel.get_application(rowref)
1045+ if app:
1046+ self.app_view.emit("application-activated", app)
1047 return
1048
1049 def _on_button_event_get_path(self, view, event):
1050@@ -468,25 +473,33 @@
1051 pkgname = self.appmodel.get_pkgname(app)
1052
1053 if btn_id == CellButtonIDs.INFO:
1054- self.app_view.emit("application-activated", self.appmodel.get_application(app))
1055+ self.app_view.emit("application-activated",
1056+ self.appmodel.get_application(app))
1057 elif btn_id == CellButtonIDs.ACTION:
1058 btn.set_sensitive(False)
1059 store.row_changed(path, store.get_iter(path))
1060- # be sure we dont request an action for a pkg with pre-existing actions
1061+ app_manager = get_appmanager()
1062+ # be sure we dont request an action for a pkg with
1063+ # pre-existing actions
1064 if pkgname in self._action_block_list:
1065- logging.debug("Action already in progress for package: '%s'" % pkgname)
1066+ logging.debug("Action already in progress for package:"
1067+ " '%s'" % pkgname)
1068 return False
1069 self._action_block_list.append(pkgname)
1070 if self.appmodel.is_installed(app):
1071- perform_action = AppActions.REMOVE
1072+ action = AppActions.REMOVE
1073+ elif self.appmodel.is_purchasable(app):
1074+ app_manager.buy_app(self.appmodel.get_application(app))
1075+ store.notify_action_request(app, path)
1076+ return
1077 else:
1078- perform_action = AppActions.INSTALL
1079+ action = AppActions.INSTALL
1080
1081 store.notify_action_request(app, path)
1082-
1083- self.app_view.emit("application-request-action",
1084- self.appmodel.get_application(app),
1085- [], [], perform_action)
1086+
1087+ app_manager.request_action(
1088+ self.appmodel.get_application(app), [], [],
1089+ action)
1090 return False
1091
1092 def _set_cursor(self, btn, cursor):
1093
1094=== modified file 'softwarecenter/ui/gtk3/widgets/buttons.py'
1095--- softwarecenter/ui/gtk3/widgets/buttons.py 2011-11-01 23:00:08 +0000
1096+++ softwarecenter/ui/gtk3/widgets/buttons.py 2011-11-09 10:56:38 +0000
1097@@ -182,7 +182,7 @@
1098 label = helper.get_appname(doc)
1099 icon = helper.get_icon_at_size(doc, icon_size, icon_size)
1100 stats = helper.get_review_stats(doc)
1101- doc.installed = doc.available = None
1102+ helper.update_availability(doc)
1103 self.is_installed = helper.is_installed(doc)
1104 self._overlay = helper.icons.load_icon(Icons.INSTALLED_OVERLAY,
1105 self.INSTALLED_OVERLAY_SIZE,
1106
1107=== modified file 'softwarecenter/ui/gtk3/widgets/cellrenderers.py'
1108--- softwarecenter/ui/gtk3/widgets/cellrenderers.py 2011-09-21 06:54:58 +0000
1109+++ softwarecenter/ui/gtk3/widgets/cellrenderers.py 2011-11-09 10:56:38 +0000
1110@@ -19,6 +19,7 @@
1111
1112 from gi.repository import Gtk, Gdk, GObject, Pango
1113
1114+from softwarecenter.utils import utf8
1115 from softwarecenter.ui.gtk3.em import EM
1116 from softwarecenter.ui.gtk3.models.appstore2 import CategoryRowReference
1117
1118@@ -53,7 +54,7 @@
1119 }
1120
1121
1122- def __init__(self, icons, show_ratings, overlay_icon_name):
1123+ def __init__(self, icons, layout, show_ratings, overlay_icon_name):
1124 GObject.GObject.__init__(self)
1125
1126 # geometry-state values
1127@@ -71,7 +72,7 @@
1128 self._all_buttons = {}
1129
1130 # cache a layout
1131- self._layout = None
1132+ self._layout = layout
1133 # star painter, paints stars
1134 self._stars = StarRenderer()
1135 self._stars.size = StarSize.SMALL
1136@@ -106,12 +107,23 @@
1137 else:
1138 x = cell_area.x + cell_area.width - lw
1139 y = cell_area.y + (cell_area.height - lh)/2
1140- #w = cell_area.width
1141- #h = cell_area.height
1142
1143 Gtk.render_layout(context, cr, x, y, layout)
1144 return
1145
1146+ def _render_price(self, context, cr, app, layout, cell_area, xpad, ypad, is_rtl):
1147+ layout.set_markup("US$ %s" % self.model.get_price(app), -1)
1148+
1149+ if is_rtl:
1150+ x = cell_area.x + xpad
1151+ else:
1152+ x = (cell_area.x + cell_area.width - xpad -
1153+ self._layout_get_pixel_width(layout))
1154+
1155+ Gtk.render_layout(context, cr,
1156+ x, ypad + cell_area.y, layout)
1157+ return
1158+
1159 def _render_icon(self, cr, app, cell_area, xpad, ypad, is_rtl):
1160 # calc offsets so icon is nicely centered
1161 icon = self.model.get_icon(app)
1162@@ -185,13 +197,15 @@
1163
1164 stats = self.model.get_review_stats(app)
1165 if not stats: return
1166+
1167 sr = self._stars
1168
1169 if not is_rtl:
1170- x = cell_area.x+3*xpad+self.pixbuf_width+self.apptitle_width
1171+ x = (cell_area.x + 3 * xpad + self.pixbuf_width +
1172+ self.apptitle_width)
1173 else:
1174 x = (cell_area.x + cell_area.width
1175- - 3*xpad
1176+ - 3 * xpad
1177 - self.pixbuf_width
1178 - self.apptitle_width
1179 - star_width)
1180@@ -207,12 +221,10 @@
1181
1182 layout.set_markup("<small>%s</small>" % s, -1)
1183
1184- lw = self._layout_get_pixel_width(layout)
1185- w = star_width
1186 if not is_rtl:
1187- x += xpad+w
1188+ x += xpad+star_width
1189 else:
1190- x -= xpad+lw
1191+ x -= xpad+self._layout_get_pixel_width(layout)
1192
1193 context.save()
1194 context.add_class("cellrenderer-avgrating-label")
1195@@ -250,9 +262,8 @@
1196 context.restore ()
1197 return
1198
1199- def _render_buttons(self,
1200- context, cr, cell_area, layout, xpad, ypad,
1201- is_rtl, is_available):
1202+ def _render_buttons(
1203+ self, context, cr, cell_area, layout, xpad, ypad, is_rtl):
1204
1205 # layout buttons and paint
1206 y = cell_area.y+cell_area.height-ypad
1207@@ -261,13 +272,13 @@
1208 if not is_rtl:
1209 start = Gtk.PackType.START
1210 end = Gtk.PackType.END
1211- xs = cell_area.x + 2*xpad + self.pixbuf_width
1212+ xs = cell_area.x + 2 * xpad + self.pixbuf_width
1213 xb = cell_area.x + cell_area.width - xpad
1214 else:
1215 start = Gtk.PackType.END
1216 end = Gtk.PackType.START
1217 xs = cell_area.x + xpad
1218- xb = cell_area.x + cell_area.width - 2*xpad - self.pixbuf_width
1219+ xb = cell_area.x + cell_area.width - 2 * xpad - self.pixbuf_width
1220
1221 for btn in self._buttons[start]:
1222 btn.set_position(xs, y-btn.height)
1223@@ -277,8 +288,6 @@
1224 for btn in self._buttons[end]:
1225 xb -= btn.width
1226 btn.set_position(xb, y-btn.height)
1227- #~ if not is_available:
1228- #~ btn.set_sensitive(False)
1229 btn.render(context, cr, layout)
1230
1231 xb -= spacing
1232@@ -334,15 +343,12 @@
1233 if not app: return
1234
1235 self.model = widget.appmodel
1236+
1237 context = widget.get_style_context()
1238 xpad = self.get_property('xpad')
1239 ypad = self.get_property('ypad')
1240 star_width, star_height = self._stars.get_visible_size(context)
1241 is_rtl = widget.get_direction() == Gtk.TextDirection.RTL
1242-
1243- if not self._layout:
1244- self._layout = widget.create_pango_layout('')
1245-
1246 layout = self._layout
1247
1248 # important! ensures correct text rendering, esp. when using hicolor theme
1249@@ -389,30 +395,31 @@
1250 is_rtl)
1251
1252 progress = self.model.get_transaction_progress(app)
1253- #~ print progress
1254 if progress > 0:
1255 self._render_progress(context, cr, progress,
1256 cell_area,
1257 ypad,
1258 is_rtl)
1259
1260+ elif self.model.is_purchasable(app):
1261+ self._render_price(context, cr, app, layout,
1262+ cell_area, xpad, ypad, is_rtl)
1263+
1264 # below is the stuff that is only done for the active cell
1265 if not self.props.isactive:
1266 return
1267
1268- is_available = self.model.is_available(app)
1269 self._render_buttons(context, cr,
1270 cell_area,
1271 layout,
1272 xpad, ypad,
1273- is_rtl,
1274- is_available)
1275+ is_rtl)
1276
1277 context.restore()
1278 return
1279
1280
1281-class CellButtonRenderer:
1282+class CellButtonRenderer(object):
1283
1284 def __init__(self, widget, name, use_max_variant_width=True):
1285 # use_max_variant_width is currently ignored. assumed to be True
1286@@ -453,7 +460,8 @@
1287 max_size = (0,0)
1288
1289 for k, variant in self.markup_variants.items():
1290- layout.set_markup(GObject.markup_escape_text(variant), -1)
1291+ safe_markup = GObject.markup_escape_text(utf8(variant))
1292+ layout.set_markup(safe_markup, -1)
1293 size = layout.get_size()
1294 max_size = max(max_size, size)
1295
1296@@ -548,14 +556,16 @@
1297 context.restore()
1298
1299 if self.has_focus:
1300- Gtk.render_focus(context, cr, x+3, y+3, width-6, height-6)
1301+ Gtk.render_focus(context, cr,
1302+ x + 3, y + 3,
1303+ width - 6, height - 6)
1304
1305 # position and render layout markup
1306 context.save()
1307 context.add_class(Gtk.STYLE_CLASS_BUTTON)
1308 layout.set_markup(self.markup_variants[self.current_variant], -1)
1309 layout_width = layout.get_pixel_extents()[1].width
1310- x = x + (width - layout_width)/2
1311+ x = x + (width - layout_width) / 2
1312 y += self.ypad
1313 Gtk.render_layout(context, cr, x, y, layout)
1314 context.restore()
1315
1316=== added file 'test/gtk3/test_appmanager.py'
1317--- test/gtk3/test_appmanager.py 1970-01-01 00:00:00 +0000
1318+++ test/gtk3/test_appmanager.py 2011-11-09 10:56:38 +0000
1319@@ -0,0 +1,79 @@
1320+#!/usr/bin/python
1321+
1322+from mock import Mock
1323+import unittest
1324+
1325+import sys
1326+sys.path.insert(0,"../")
1327+
1328+import softwarecenter.paths
1329+from softwarecenter.db.application import Application
1330+from softwarecenter.distro import get_distro
1331+from softwarecenter.testutils import (
1332+ get_test_db, get_test_gtk3_icon_cache, do_events)
1333+from softwarecenter.ui.gtk3.session.appmanager import (
1334+ ApplicationManager, get_appmanager)
1335+
1336+class TestAppManager(unittest.TestCase):
1337+ """ tests the appmanager """
1338+
1339+ def setUp(self):
1340+ # get required test stuff
1341+ self.db = get_test_db()
1342+ self.backend = Mock()
1343+ self.distro = get_distro()
1344+ self.datadir = softwarecenter.paths.datadir
1345+ self.icons = get_test_gtk3_icon_cache()
1346+ # create it once, it becomes global instance
1347+ if get_appmanager() is None:
1348+ ApplicationManager(
1349+ self.db, self.backend, self.icons)
1350+
1351+ def test_get_appmanager(self):
1352+ app_manager = get_appmanager()
1353+ self.assertNotEqual(app_manager, None)
1354+ # test singleton
1355+ app_manager2 = get_appmanager()
1356+ self.assertEqual(app_manager, app_manager2)
1357+ # test creating it twice raises a error
1358+ self.assertRaises(
1359+ ValueError, ApplicationManager, self.db, self.backend, self.icons)
1360+
1361+ def test_appmanager(self):
1362+ app_manager = get_appmanager()
1363+ self.assertNotEqual(app_manager, None)
1364+ # test interface
1365+ app_manager.reload()
1366+ app = Application("", "2vcard")
1367+ # call and ensure the stuff is passed to the backend
1368+ app_manager.install(app, [], [])
1369+ self.assertTrue(self.backend.install.called)
1370+
1371+ app_manager.remove(app, [], [])
1372+ self.assertTrue(self.backend.remove.called)
1373+
1374+ app_manager.upgrade(app, [], [])
1375+ self.assertTrue(self.backend.upgrade.called)
1376+
1377+ app_manager.apply_changes(app, [], [])
1378+ self.assertTrue(self.backend.apply_changes.called)
1379+
1380+ app_manager.enable_software_source(app)
1381+ self.assertTrue(self.backend.enable_component.called)
1382+
1383+ app_manager.reinstall_purchased(app)
1384+ self.assertTrue(self.backend.add_repo_add_key_and_install_app.called)
1385+
1386+ # buy is special as it needs help from the purchase view
1387+ app_manager.connect("purchase-requested", self._on_purchase_requested)
1388+ app_manager.buy_app(app)
1389+ self.assertTrue(self._purchase_requested_signal)
1390+ do_events()
1391+
1392+ def _on_purchase_requested(self, *args):
1393+ self._purchase_requested_signal = True
1394+
1395+if __name__ == "__main__":
1396+ import logging
1397+ logging.basicConfig(level=logging.DEBUG)
1398+ unittest.main()

Subscribers

People subscribed via source and target branches