Merge lp:~mvo/software-center/app-treeview-buy-plus-refactor into lp:software-center
- app-treeview-buy-plus-refactor
- Merge into trunk
Proposed by
Michael Vogt
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Lasker (community) | Approve | ||
Michael Vogt | Pending | ||
Review via email: mp+81710@code.launchpad.net |
Commit message
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.
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() |
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!