Merge lp:~ilidrissi.amine/software-center/addons into lp:software-center

Proposed by Mohamed Amine Ilidrissi
Status: Merged
Merged at revision: 1049
Proposed branch: lp:~ilidrissi.amine/software-center/addons
Merge into: lp:software-center
Diff against target: 1444 lines (+858/-122)
11 files modified
debian/changelog (+16/-0)
softwarecenter/app.py (+13/-4)
softwarecenter/apt/aptcache.py (+227/-9)
softwarecenter/backend/aptd.py (+18/-8)
softwarecenter/db/application.py (+12/-5)
softwarecenter/enums.py (+1/-0)
softwarecenter/view/appdetailsview.py (+37/-6)
softwarecenter/view/appdetailsview_gtk.py (+502/-89)
test/test_appdetails_view.py (+21/-0)
test/test_aptd.py (+8/-0)
test/test_gui.py (+3/-1)
To merge this branch: bzr merge lp:~ilidrissi.amine/software-center/addons
Reviewer Review Type Date Requested Status
Matthew Paul Thomas design Approve
Mohamed Amine Ilidrissi (community) Needs Resubmitting
Review via email: mp+30946@code.launchpad.net

Description of the change

This branch introduces add-on handling as described in https://wiki.ubuntu.com/SoftwareCenter#add-ons.
How to test: pick any package that has potential add-ons (like gimp).
Limitations: Descriptions aren't sorted alphabetically.

Feel free to propose any changes - I know my Python skills are pathetic :P.

To post a comment you must log in.
934. By Mohamed Amine Ilidrissi

merge with trunk

935. By Mohamed Amine Ilidrissi

Modified changelog.

Revision history for this message
Matthew Paul Thomas (mpt) wrote :

Mohamed, thanks so much for working on this. I'm impressed.

From a quick test, I see a few interface glitches:

* By default, every software item screen now says "No changes to be done" by default. This text shouldn't ever be necessary.

* The add-ons section appears before the installed state bar. It should instead appear after the item description. <https://wiki.ubuntu.com/SoftwareCenter#software-item-screen>

* There should be a little more vertical space between add-ons. Currently the icon for the first add-on is closer to the icon for the second add-on than it is to its own title. Also, there should be more vertical space after "Choose add-ons:".

* Thunderbird lists 56 add-ons, of which about 50 of them are language/region packs, which should be installed from "System" > "Administration" > "Language Support" instead. Is there an easy way of recognizing language-pack add-ons to avoid showing them in USC?

review: Needs Fixing
Revision history for this message
Matthew Paul Thomas (mpt) wrote :

One of Banshee's add-ons is "Media Management and Playback application (debug symbols)". Similarly, one of Evolution's add-ons is "debugging symbols for Evolution". We also need to figure out how to hide those, at least by default.

936. By Mohamed Amine IL Idrissi <devildante@devildante-laptop>

Fixed a bunch of things at mpt's request.

Revision history for this message
Mohamed Amine Ilidrissi (ilidrissi.amine) wrote :

Okay, just fixed these issues. Feel free to request any other fixes.

review: Needs Resubmitting
Revision history for this message
Michael Vogt (mvo) wrote :

Sorry for the slow reply. I had a look over the code and its pretty good. There are some small issues I would like to see fixed.
- recommended_addons and suggested_addons are almost the same, would be nice to consolidate the bits that are equal into their own function to avoid duplications
- I moved some of the lowlevel stuff into PackageAddonsManager (in lp:~mvo/software-center/addons
- I would love to avoid the extra addons_install, addons_remove to each of the action methods in aptdaemon (as install()/remove() is really a apply() now for complex changes)
- tests are missing
- there are merge conflicts with trunk/ (because of the debfiles branch merge)

There are some design issues (probably more something for mpt):
- moving the install button to the end of the page is not ideal IMO, with the gimp and the default window size its not visible without scrolling
- just showing the summary without a package name or a way to navigate to the app is not that useful for the user to understand what he gets from the addon

937. By Mohamed Amine Ilidrissi

merge lp:~mvo/software-center/addons, many thanks.

938. By Mohamed Amine Ilidrissi

merge with trunk (argh conflicts).

939. By Mohamed Amine Ilidrissi

bunch of fixes.

940. By Mohamed Amine Ilidrissi

bunch of fixes... again.

941. By Mohamed Amine Ilidrissi

merge mvo's branch, many thanks.

942. By Mohamed Amine Ilidrissi

Thank you mvo for your efforts :)

943. By Mohamed Amine Ilidrissi

Renamed a function.

Revision history for this message
Michael Vogt (mvo) wrote :

Thanks, code is good now, I filed a feature-freee-exception for this now https://edge.launchpad.net/ubuntu/+source/software-center/+bug/617297

944. By Mohamed Amine Ilidrissi

merge with trunk.

945. By Mohamed Amine Ilidrissi

Removed useless code and fixed a "big icon" bug.

946. By Mohamed Amine Ilidrissi

Disable links to pkgnames until we fix it, and made update_totalsize into a gobject.idle_add() so selecting an add-on feels faster.

947. By Mohamed Amine Ilidrissi

software-center will not crash anymore when opening a pkg that does not exist, and it will now check that app_details.icon is not None.

948. By Mohamed Amine Ilidrissi

2 fixes for 2 small bugs.

949. By Mohamed Amine Ilidrissi

Fixed debian/changelog fail.

950. By Mohamed Amine Ilidrissi

merge kiwinote's branch, many thanks :)

951. By Mohamed Amine Ilidrissi

Fixed the bar padding, and the total size text.

952. By Mohamed Amine Ilidrissi

some fixes.

953. By Mohamed Amine Ilidrissi

Polishing.

954. By Mohamed Amine Ilidrissi

Fixed padding and added a :

955. By Mohamed Amine Ilidrissi

Reduced the add-on image size to 22x22 and made loading the appdetailsview snappier.

956. By Mohamed Amine Ilidrissi

Renamed TotalSizeBar to AddonsStateBar.

957. By Mohamed Amine Ilidrissi

Fixed padding issues.

958. By Mohamed Amine Ilidrissi

Fixed an implementation error.

Revision history for this message
Matthew Paul Thomas (mpt) wrote :

Once the vertical spacing above and below the add-ons section is sorted out, I think this is good to merge. Thank you Mohamed.

review: Approve (design)
959. By Mohamed Amine Ilidrissi

Fixed padding issues.

960. By Mohamed Amine Ilidrissi

fixed AddonsStateBar padding issue.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/changelog'
2--- debian/changelog 2010-08-13 14:55:27 +0000
3+++ debian/changelog 2010-08-16 20:56:42 +0000
4@@ -1,3 +1,19 @@
5+software-center (2.1.11) UNRELEASED; urgency=low
6+
7+ [ Mohamed Amine IL Idrissi ]
8+ * (all): Implemented add-on handling.
9+
10+ [ Kiwinote ]
11+ * softwarecenter/view/appdetailsview_gtk.py:
12+ - use package info lines rather than package info tables
13+ (for devildante to use in the addons branch)
14+
15+ [ Milo Casagrande ]
16+ * softwarecenter/db/application.py:
17+ - make 'source available' warning more suitable for translation.
18+
19+ -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 13 Aug 2010 16:46:13 +0200
20+
21 software-center (2.1.10) maverick; urgency=low
22
23 [ Kiwinote ]
24
25=== modified file 'softwarecenter/app.py'
26--- softwarecenter/app.py 2010-08-13 10:27:47 +0000
27+++ softwarecenter/app.py 2010-08-16 20:56:42 +0000
28@@ -206,6 +206,9 @@
29 self.on_app_selected)
30 self.available_pane.app_details.connect("application-request-action",
31 self.on_application_request_action)
32+ self.available_pane.app_details.connect("navigation-request",
33+ self.on_application_request_navigation,
34+ self.available_pane)
35 self.available_pane.app_view.connect("application-request-action",
36 self.on_application_request_action)
37 self.available_pane.connect("app-list-changed",
38@@ -248,6 +251,9 @@
39 self.on_app_selected)
40 self.installed_pane.app_details.connect("application-request-action",
41 self.on_application_request_action)
42+ self.installed_pane.app_details.connect("navigation-request",
43+ self.on_application_request_navigation,
44+ self.installed_pane)
45 self.installed_pane.app_view.connect("application-request-action",
46 self.on_application_request_action)
47 self.installed_pane.connect("app-list-changed",
48@@ -450,7 +456,10 @@
49 channel_display_name, icon=None, query=query)
50 self.view_switcher.select_channel_node(channel_display_name, False)
51
52- def on_application_request_action(self, widget, app, action):
53+ def on_application_request_navigation(self, widget, pkgname, pane):
54+ pane.show_app(Application("", pkgname))
55+
56+ def on_application_request_action(self, widget, app, addons_install, addons_remove, action):
57 """callback when an app action is requested from the appview,
58 if action is "remove", must check if other dependencies have to be
59 removed as well and show a dialog in that case
60@@ -481,7 +490,7 @@
61 self.backend.emit("transaction-stopped")
62 return
63
64- # action_func is one of: "install", "remove" or "upgrade"
65+ # action_func is one of: "install", "remove", "upgrade", or "apply_changes"
66 action_func = getattr(self.backend, action)
67 if action == 'install':
68 # the package.deb path name is in the request
69@@ -489,9 +498,9 @@
70 debfile_name = app.request
71 else:
72 debfile_name = None
73- action_func(app.pkgname, app.appname, appdetails.icon, debfile_name)
74+ action_func(app.pkgname, app.appname, appdetails.icon, debfile_name, addons_install, addons_remove)
75 elif callable(action_func):
76- action_func(app.pkgname, app.appname, appdetails.icon)
77+ action_func(app.pkgname, app.appname, appdetails.icon, addons_install=addons_install, addons_remove=addons_remove)
78 else:
79 logging.error("Not a valid action in AptdaemonBackend: '%s'" % action)
80
81
82=== modified file 'softwarecenter/apt/aptcache.py'
83--- softwarecenter/apt/aptcache.py 2010-07-09 03:16:33 +0000
84+++ softwarecenter/apt/aptcache.py 2010-08-16 20:56:42 +0000
85@@ -32,6 +32,7 @@
86 import time
87
88 from gettext import gettext as _
89+from softwarecenter.enums import *
90
91 class GtkMainIterationProgress(apt.progress.base.OpProgress):
92 """Progress that just runs the main loop"""
93@@ -48,6 +49,8 @@
94 DEPENDENCY_TYPES = ("PreDepends", "Depends")
95 RECOMMENDS_TYPES = ("Recommends",)
96 SUGGESTS_TYPES = ("Suggests",)
97+ ENHANCES_TYPES = ("Enhances",)
98+ PROVIDES_TYPES = ("Provides",)
99
100 # stamp file to monitor (provided by update-notifier via
101 # APT::Update::Post-Invoke-Success)
102@@ -104,16 +107,16 @@
103 return self._cache.__iter__()
104 def __contains__(self, k):
105 return self._cache.__contains__(k)
106- def _get_installed_rdepends_by_type(self, pkg, type):
107- installed_rdeps = set()
108+ def _get_rdepends_by_type(self, pkg, type, onlyInstalled):
109+ rdeps = set()
110 for rdep in pkg._pkg.rev_depends_list:
111 dep_type = rdep.dep_type_untranslated
112 if dep_type in type:
113 rdep_name = rdep.parent_pkg.name
114- if (rdep_name in self._cache and
115- self._cache[rdep_name].is_installed):
116- installed_rdeps.add(rdep.parent_pkg.name)
117- return installed_rdeps
118+ if rdep_name in self._cache and (not onlyInstalled or
119+ (onlyInstalled and self._cache[rdep_name].is_installed)):
120+ rdeps.add(rdep.parent_pkg.name)
121+ return rdeps
122 def _installed_dependencies(self, pkg_name, all_deps=None):
123 """ recursively return all installed dependencies of a given pkg """
124 #print "_installed_dependencies", pkg_name, all_deps
125@@ -168,11 +171,15 @@
126 origins.add(item.origin)
127 return origins
128 def get_installed_rdepends(self, pkg):
129- return self._get_installed_rdepends_by_type(pkg, self.DEPENDENCY_TYPES)
130+ return self._get_rdepends_by_type(pkg, self.DEPENDENCY_TYPES, True)
131 def get_installed_rrecommends(self, pkg):
132- return self._get_installed_rdepends_by_type(pkg, self.RECOMMENDS_TYPES)
133+ return self._get_rdepends_by_type(pkg, self.RECOMMENDS_TYPES, True)
134 def get_installed_rsuggests(self, pkg):
135- return self._get_installed_rdepends_by_type(pkg, self.SUGGESTS_TYPES)
136+ return self._get_rdepends_by_type(pkg, self.SUGGESTS_TYPES, True)
137+ def get_installed_renhances(self, pkg):
138+ return self._get_rdepends_by_type(pkg, self.ENHANCES_TYPES, True)
139+ def get_installed_rprovides(self, pkg):
140+ return self._get_rdepends_by_type(pkg, self.PROVIDES_TYPES, True)
141 def component_available(self, distro_codename, component):
142 """ check if the given component is enabled """
143 # FIXME: test for more properties here?
144@@ -183,6 +190,202 @@
145 it.archive == distro_codename):
146 return True
147 return False
148+
149+ def _get_depends_by_type(self, pkg, types):
150+ version = pkg.installed
151+ if version == None:
152+ version = max(pkg.versions)
153+ return version.get_dependencies(*types)
154+ def _get_depends_by_type_str(self, pkg, *types):
155+ def not_in_list(list, item):
156+ for i in list:
157+ if i == item:
158+ return False
159+ return True
160+ deps = self._get_depends_by_type(pkg, *types)
161+ deps_str = []
162+ for dep in deps:
163+ for dep_ in dep.or_dependencies:
164+ if not_in_list(deps_str, dep_.name):
165+ deps_str.append(dep_.name)
166+ return deps_str
167+ def get_depends(self, pkg):
168+ return self._get_depends_by_type_str(pkg, self.DEPENDENCY_TYPES)
169+ def get_recommends(self, pkg):
170+ return self._get_depends_by_type_str(pkg, self.RECOMMENDS_TYPES)
171+ def get_suggests(self, pkg):
172+ return self._get_depends_by_type_str(pkg, self.SUGGESTS_TYPES)
173+ def get_enhances(self, pkg):
174+ return self._get_depends_by_type_str(pkg, self.ENHANCES_TYPES)
175+ def get_provides(self, pkg):
176+ return self._get_depends_by_type_str(pkg, self.PROVIDES_TYPES)
177+
178+ def get_rdepends(self, pkg):
179+ return self._get_rdepends_by_type(pkg, self.DEPENDENCY_TYPES, False)
180+ def get_rrecommends(self, pkg):
181+ return self._get_rdepends_by_type(pkg, self.RECOMMENDS_TYPES, False)
182+ def get_rsuggests(self, pkg):
183+ return self._get_rdepends_by_type(pkg, self.SUGGESTS_TYPES, False)
184+ def get_renhances(self, pkg):
185+ return self._get_rdepends_by_type(pkg, self.ENHANCES_TYPES, False)
186+ def get_rprovides(self, pkg):
187+ return self._get_rdepends_by_type(pkg, self.PROVIDES_TYPES, False)
188+
189+ def _get_changes_without_applying(self, pkg):
190+ if pkg.installed == None:
191+ pkg.mark_install()
192+ else:
193+ pkg.mark_delete()
194+ changes_tmp = self._cache.get_changes()
195+ changes = {}
196+ for change in changes_tmp:
197+ if change.marked_install or change.marked_reinstall:
198+ changes[change.name] = PKG_STATE_INSTALLING
199+ elif change.marked_delete:
200+ changes[change.name] = PKG_STATE_REMOVING
201+ elif change.marked_upgrade:
202+ changes[change.name] = PKG_STATE_UPGRADING
203+ else:
204+ changes[change.name] = PKG_STATE_UNKNOWN
205+ self._cache.clear()
206+ return changes
207+ def get_all_deps_installing(self, pkg):
208+ """ Return all dependencies of pkg that will be marked for install """
209+ changes = self._get_changes_without_applying(pkg)
210+ installing_deps = []
211+ for change in changes.keys():
212+ if change != pkg.name and changes[change] == PKG_STATE_INSTALLING:
213+ installing_deps.append(change)
214+ return installing_deps
215+ def get_all_deps_removing(self, pkg):
216+ changes = self._get_changes_without_applying(pkg)
217+ removing_deps = []
218+ for change in changes.keys():
219+ if change != pkg.name and changes[change] == PKG_STATE_REMOVING:
220+ removing_deps.append(change)
221+ return removing_deps
222+ def get_all_deps_upgrading(self, pkg):
223+ changes = self._get_changes_without_applying(pkg)
224+ upgrading_deps = []
225+ for change in changes.keys():
226+ if change != pkg.name and changes[change] == PKG_STATE_UPGRADING:
227+ upgrading_deps.append(change)
228+ return upgrading_deps
229+
230+class PackageAddonsManager(object):
231+ """ class that abstracts the addons handling """
232+
233+ LANGPACK_PKGDEPENDS = "/usr/share/language-selector/data/pkg_depends"
234+ (RECOMMENDED, SUGGESTED) = range(2)
235+
236+ def __init__(self, cache):
237+ self.cache = cache
238+ self._language_packages = self._read_language_pkgs()
239+
240+ def _remove_important_or_langpack(self, addon_list, app_pkg):
241+ """ remove packages that are essential or important
242+ or langpacks
243+ """
244+ for addon in addon_list:
245+ try:
246+ pkg = self.cache[addon]
247+ if pkg.essential or pkg._pkg.important or addon == app_pkg.name:
248+ addon_list.remove(addon)
249+ continue
250+
251+ deps = self.cache.get_depends(app_pkg)
252+ if addon in deps:
253+ addon_list.remove(addon)
254+ continue
255+
256+ rdeps = self.cache.get_installed_rdepends(pkg)
257+ if (len(rdeps) > 0 or
258+ self._is_language_pkg(addon)):
259+ addon_list.remove(addon)
260+ continue
261+ except KeyError:
262+ addon_list.remove(addon)
263+
264+ def _is_language_pkg(self, addon):
265+ # a simple "addon in self._language_packages" is not enough
266+ for template in self._language_packages:
267+ if addon.startswith(template):
268+ return True
269+ return False
270+
271+ def _read_language_pkgs(self):
272+ language_packages = set()
273+ for line in open(self.LANGPACK_PKGDEPENDS):
274+ line = line.strip()
275+ if line.startswith('#'):
276+ continue
277+ try:
278+ (cat, code, dep_pkg, language_pkg) = line.split(':')
279+ except ValueError:
280+ continue
281+ language_packages.add(language_pkg)
282+ return language_packages
283+
284+ def _addons_for_pkg(self, pkgname, type):
285+ try:
286+ pkg = self.cache[pkgname]
287+ except KeyError:
288+ return []
289+ deps = self.cache.get_depends(pkg)
290+ addons = []
291+ if type == self.RECOMMENDED:
292+ recommends = self.cache.get_recommends(pkg)
293+ if len(recommends) == 1:
294+ addons += recommends
295+ elif type == self.SUGGESTED:
296+ suggests = self.cache.get_suggests(pkg)
297+ if len(suggests) == 1:
298+ addons += suggests
299+ addons += self.cache.get_renhances(pkg)
300+
301+ for dep in deps:
302+ try:
303+ pkgdep = self.cache[dep]
304+ if len(self.cache.get_rdepends(pkgdep)) == 1:
305+ # pkg is the only known package that depends on pkgdep
306+ if type == self.RECOMMENDED:
307+ addons += self.cache.get_recommends(pkgdep)
308+ elif type == self.SUGGESTED:
309+ addons += self.cache.get_suggests(pkgdep)
310+ addons += self.cache.get_renhances(pkgdep)
311+ except KeyError:
312+ pass # FIXME: should we handle that differently?
313+ self._remove_important_or_langpack(addons, pkg)
314+ for addon in addons:
315+ try:
316+ pkg_ = self.cache[addon]
317+ except KeyError:
318+ addons.remove(addon)
319+ else:
320+ can_remove = False
321+ for addon_ in addons:
322+ try:
323+ if addon in self.cache.get_provides(self.cache[addon_]) \
324+ or addon in self.cache.get_depends(self.cache[addon_]) \
325+ or addon in self.cache.get_recommends(self.cache[addon_]):
326+ can_remove = True
327+ break
328+ except KeyError:
329+ addons.remove(addon_)
330+ break
331+ if can_remove or not pkg_.candidate or addons.count(addon) > 1 \
332+ or addon == pkg.name or self._is_language_pkg(addon):
333+ addons.remove(addon)
334+ # FIXME: figure out why I have to call this function two times to get rid of important packages
335+ self._remove_important_or_langpack(addons, pkg)
336+ return addons
337+
338+ def recommended_addons(self, pkgname):
339+ return self._addons_for_pkg(pkgname, self.RECOMMENDED)
340+
341+ def suggested_addons(self, pkgname):
342+ return self._addons_for_pkg(pkgname, self.SUGGESTED)
343+
344
345 if __name__ == "__main__":
346 c = AptCache()
347@@ -200,3 +403,18 @@
348 print c.get_installed_rdepends(pkg)
349 print c.get_installed_rrecommends(pkg)
350 print c.get_installed_rsuggests(pkg)
351+
352+ print "deps of gimp"
353+ pkg = c["gimp"]
354+ print c.get_depends(pkg)
355+ print c.get_recommends(pkg)
356+ print c.get_suggests(pkg)
357+ print c.get_enhances(pkg)
358+ print c.get_provides(pkg)
359+
360+ print "rdeps of gimp"
361+ print c.get_rdepends(pkg)
362+ print c.get_rrecommends(pkg)
363+ print c.get_rsuggests(pkg)
364+ print c.get_renhances(pkg)
365+ print c.get_rprovides(pkg)
366
367=== modified file 'softwarecenter/backend/aptd.py'
368--- softwarecenter/backend/aptd.py 2010-08-11 14:00:11 +0000
369+++ softwarecenter/backend/aptd.py 2010-08-16 20:56:42 +0000
370@@ -126,8 +126,9 @@
371 except Exception, error:
372 self._on_trans_error(error)
373
374+ # FIXME: upgrade add-ons here
375 @inline_callbacks
376- def upgrade(self, pkgname, appname, iconname, metadata=None):
377+ def upgrade(self, pkgname, appname, iconname, addons_install=None, addons_remove=None, metadata=None):
378 """ upgrade a single package """
379 self.emit("transaction-started")
380 try:
381@@ -138,7 +139,7 @@
382 self._on_trans_error(error, pkgname)
383
384 @inline_callbacks
385- def remove(self, pkgname, appname, iconname, metadata=None):
386+ def remove(self, pkgname, appname, iconname, addons_install=None, addons_remove=None, metadata=None):
387 """ remove a single package """
388 self.emit("transaction-started")
389 try:
390@@ -149,7 +150,7 @@
391 self._on_trans_error(error, pkgname)
392
393 @inline_callbacks
394- def remove_multiple(self, pkgnames, appnames, iconnames, metadatas=None):
395+ def remove_multiple(self, pkgnames, appnames, iconnames, addons_install=None, addons_remove=None, metadatas=None):
396 """ queue a list of packages for removal """
397 if metadatas == None:
398 metadatas = []
399@@ -159,7 +160,7 @@
400 yield self.remove(pkgname, appname, iconname, metadata)
401
402 @inline_callbacks
403- def install(self, pkgname, appname, iconname, filename=None, metadata=None):
404+ def install(self, pkgname, appname, iconname, filename=None, addons_install=None, addons_remove=None, metadata=None):
405 """Install a single package from the archive
406 If filename is given a local deb package is installed instead.
407 """
408@@ -169,21 +170,30 @@
409 trans = yield self.aptd_client.install_file(filename,
410 defer=True)
411 else:
412- trans = yield self.aptd_client.install_packages([pkgname],
413- defer=True)
414+ trans = yield self.aptd_client.commit_packages([pkgname] + addons_install, [], addons_remove, [], [])
415 yield self._run_transaction(trans, pkgname, appname, iconname, metadata)
416 except Exception, error:
417 self._on_trans_error(error, pkgname)
418
419 @inline_callbacks
420- def install_multiple(self, pkgnames, appnames, iconnames, metadatas=None):
421+ def install_multiple(self, pkgnames, appnames, iconnames, addons_install=None, addons_remove=None, metadatas=None):
422 """ queue a list of packages for install """
423 if metadatas == None:
424 metadatas = []
425 for item in pkgnames:
426 metadatas.append(None)
427 for pkgname, appname, iconname, metadata in zip(pkgnames, appnames, iconnames, metadatas):
428- yield self.install(pkgname, appname, iconname, metadata)
429+ yield self.install(pkgname, appname, iconname, metadata=metadata)
430+
431+ @inline_callbacks
432+ def apply_changes(self, pkgname, appname, iconname, addons_install=None, addons_remove=None, metadata=None):
433+ """ install and remove add-ons """
434+ self.emit("transaction-started")
435+ try:
436+ trans = yield self.aptd_client.commit_packages(addons_install, [], addons_remove, [], [])
437+ yield self._run_transaction(trans, pkgname, appname, iconname)
438+ except Exception, error:
439+ self._on_trans_error(error)
440
441 @inline_callbacks
442 def reload(self, metadata=None):
443
444=== modified file 'softwarecenter/db/application.py'
445--- softwarecenter/db/application.py 2010-08-13 14:25:11 +0000
446+++ softwarecenter/db/application.py 2010-08-16 20:56:42 +0000
447@@ -456,11 +456,18 @@
448 source_to_enable = self.component
449 if source_to_enable:
450 sources = source_to_enable.split('&')
451- warning = _("Available from the \"%s\"") % sources[0]
452- if len(sources) > 1:
453- for source in sources[1:]:
454- warning += _(", or from the \"%s\"") % source
455- warning += _(" source.")
456+ sources_length = len(sources)
457+ if sources_length == 1:
458+ warning = _("Available from the \"%s\" source.") % sources[0]
459+ elif sources_length > 1:
460+ # Translators: the visible string is constructed concatenating
461+ # the following 3 strings like this:
462+ # Available from the following sources: %s, ... %s, %s.
463+ warning = _("Available from the following sources: ")
464+ # Cycle through all, but the last
465+ for source in sources[:-1]:
466+ warning += _("\"%s\", ") % source
467+ warning += _("\"%s\".") % sources[sources_length - 1]
468 return warning
469
470 @property
471
472=== modified file 'softwarecenter/enums.py'
473--- softwarecenter/enums.py 2010-08-10 08:31:32 +0000
474+++ softwarecenter/enums.py 2010-08-16 20:56:42 +0000
475@@ -126,6 +126,7 @@
476 APP_ACTION_INSTALL = "install"
477 APP_ACTION_REMOVE = "remove"
478 APP_ACTION_UPGRADE = "upgrade"
479+APP_ACTION_APPLY = "apply_changes"
480
481 from version import *
482 USER_AGENT="Software Center/%s (N;) %s/%s (%s)" % (VERSION,
483
484=== modified file 'softwarecenter/view/appdetailsview.py'
485--- softwarecenter/view/appdetailsview.py 2010-08-09 14:17:41 +0000
486+++ softwarecenter/view/appdetailsview.py 2010-08-16 20:56:42 +0000
487@@ -22,7 +22,7 @@
488 import urllib
489 import gobject
490
491-from softwarecenter.enums import MISSING_APP_ICON
492+from softwarecenter.apt.aptcache import PackageAddonsManager
493 from softwarecenter.db.application import AppDetails
494 from softwarecenter.backend import get_install_backend
495 from softwarecenter.enums import *
496@@ -35,8 +35,17 @@
497 __gsignals__ = {
498 "application-request-action" : (gobject.SIGNAL_RUN_LAST,
499 gobject.TYPE_NONE,
500- (gobject.TYPE_PYOBJECT, str),
501+ (gobject.TYPE_PYOBJECT,
502+ gobject.TYPE_PYOBJECT,
503+ gobject.TYPE_PYOBJECT,
504+ str),
505 ),
506+ "navigation-request" : ( gobject.SIGNAL_RUN_LAST,
507+ gobject.TYPE_NONE,
508+ (str,
509+ ),
510+ ),
511+
512 }
513
514 def __init__(self, db, distro, icons, cache, history, datadir):
515@@ -45,10 +54,13 @@
516 self.icons = icons
517 self.cache = cache
518 self.cache.connect("cache-ready", self._on_cache_ready)
519+ self.addons_manager = PackageAddonsManager(cache)
520 self.history = history
521 self.datadir = datadir
522 self.app = None
523 self.appdetails = None
524+ self.addons_install = []
525+ self.addons_remove = []
526 # aptdaemon
527 self.backend = get_install_backend()
528 self._logger = logging.getLogger(__name__)
529@@ -58,6 +70,22 @@
530 you need to overwrite
531 """
532 pass
533+
534+ # add-on handling
535+ def _set_addon_install(self, addon):
536+ pkg = self.cache[addon]
537+ if addon not in self.addons_install and pkg.installed == None:
538+ self.addons_install.append(addon)
539+ if addon in self.addons_remove:
540+ self.addons_remove.remove(addon)
541+
542+ def _set_addon_remove(self, addon):
543+ pkg = self.cache[addon]
544+ if addon not in self.addons_remove and pkg.installed != None:
545+ self.addons_remove.append(addon)
546+ if addon in self.addons_install:
547+ self.addons_install.remove(addon)
548+
549 # public API
550 def show_app(self, app):
551 """ show the given application """
552@@ -75,13 +103,16 @@
553 self.backend.reload()
554 def install(self):
555 """ install the current application, fire an action request """
556- self.emit("application-request-action", self.app, APP_ACTION_INSTALL)
557+ self.emit("application-request-action", self.app, self.addons_install, self.addons_remove, APP_ACTION_INSTALL)
558 def remove(self):
559 """ remove the current application, , fire an action request """
560- self.emit("application-request-action", self.app, APP_ACTION_REMOVE)
561+ self.emit("application-request-action", self.app, self.addons_install, self.addons_remove, APP_ACTION_REMOVE)
562 def upgrade(self):
563 """ upgrade the current application, fire an action request """
564- self.emit("application-request-action", self.app, APP_ACTION_UPGRADE)
565+ self.emit("application-request-action", self.app, self.addons_install, self.addons_remove, APP_ACTION_UPGRADE)
566+ def apply_changes(self):
567+ """ apply changes concerning add-ons """
568+ self.emit("application-request-action", self.app, self.addons_install, self.addons_remove, APP_ACTION_APPLY)
569
570 def buy_app(self):
571 """ initiate the purchase transaction """
572@@ -107,10 +138,10 @@
573 get_install_backend().add_repo_add_key_and_install_app(deb_line,
574 signing_key_id,
575 self.app)
576-
577 # internal callbacks
578 def _on_cache_ready(self, cache):
579 # re-show the application if the cache changes, it may affect the
580 # current application
581 self._logger.debug("on_cache_ready")
582 self.show_app(self.app)
583+ self.addons_manager = PackageAddonsManager(cache)
584
585=== modified file 'softwarecenter/view/appdetailsview_gtk.py'
586--- softwarecenter/view/appdetailsview_gtk.py 2010-08-13 14:25:11 +0000
587+++ softwarecenter/view/appdetailsview_gtk.py 2010-08-16 20:56:42 +0000
588@@ -33,8 +33,10 @@
589 import cairo
590
591 from gettext import gettext as _
592+import apt_pkg
593 from softwarecenter.backend import get_install_backend
594-from softwarecenter.db.application import AppDetails
595+from softwarecenter.db.application import AppDetails, Application
596+from softwarecenter.apt.aptcache import AptCache
597 from softwarecenter.enums import *
598 from softwarecenter.paths import SOFTWARE_CENTER_ICON_CACHE_DIR
599 from softwarecenter.utils import ImageDownloader
600@@ -150,7 +152,8 @@
601 if state in (PKG_STATE_INSTALLING,
602 PKG_STATE_INSTALLING_PURCHASED,
603 PKG_STATE_REMOVING,
604- PKG_STATE_UPGRADING):
605+ PKG_STATE_UPGRADING,
606+ APP_ACTION_APPLY):
607 self.button.hide()
608 self.show()
609 elif state == PKG_STATE_NOT_FOUND:
610@@ -159,8 +162,8 @@
611 self.button.set_sensitive(False)
612 self.button.show()
613 self.show()
614- else:
615 state = app_details.pkg_state
616+ else:
617 self.button.set_sensitive(True)
618 self.button.show()
619 self.show()
620@@ -202,7 +205,7 @@
621 if app_details.price:
622 self.set_label(app_details.price)
623 else:
624- self.set_label("")
625+ self.set_label("Free")
626 self.set_button_label(_('Install'))
627 elif state == PKG_STATE_REINSTALLABLE:
628 if app_details.price:
629@@ -213,6 +216,11 @@
630 elif state == PKG_STATE_UPGRADABLE:
631 self.set_label(_('Upgrade Available'))
632 self.set_button_label(_('Upgrade'))
633+ elif state == APP_ACTION_APPLY:
634+ self.set_label(_('Changing add-ons...'))
635+ elif state == PKG_STATE_UNKNOWN:
636+ self.set_button_label("")
637+ self.set_label(_("Error"))
638 elif state == PKG_STATE_ERROR:
639 # this is used when the pkg can not be installed
640 # we display the error in the description field
641@@ -411,78 +419,51 @@
642 self.show_all()
643 return
644
645-
646-class PackageInfoTable(gtk.VBox):
647-
648- def __init__(self):
649- gtk.VBox.__init__(self, spacing=mkit.SPACING_MED)
650-
651- self.version_label = gtk.Label()
652- self.license_label = gtk.Label()
653- self.support_label = gtk.Label()
654-
655- self.version_label.set_selectable(True)
656-
657+class PackageInfo(gtk.HBox):
658+
659+ def __init__(self, key):
660+ gtk.HBox.__init__(self, spacing=mkit.SPACING_XLARGE)
661+ self.key = key
662+ self.value_object = gtk.Label()
663 self.connect('realize', self._on_realize)
664 return
665
666 def _on_realize(self, widget):
667+ # key
668+ k = gtk.Label()
669 dark = self.style.dark[self.state].to_string()
670 key_markup = '<b><span color="%s">%s</span></b>'
671- max_lw = 0 # max key label width
672-
673- for kstr, v in [(_('Version:'), self.version_label),
674- (_('License:'), self.license_label),
675- (_('Updates:'), self.support_label)]:
676-
677- k = gtk.Label()
678- k.set_markup(key_markup % (dark, kstr))
679- v.set_line_wrap(True)
680- max_lw = max(max_lw, k.get_layout().get_pixel_extents()[1][2])
681-
682- a = gtk.Alignment(1.0, 0.0)
683- a.add(k)
684-
685- # we need this extra box to avoid orca repeating itself
686- b = gtk.Alignment(0.0, 0.0)
687- b.add(v)
688-
689- row = gtk.HBox(spacing=mkit.SPACING_XLARGE)
690- row.pack_start(a, False)
691- row.pack_start(b, False)
692-
693- # a11y stuff
694- row.set_property("can-focus", True)
695- row.a11y = row.get_accessible()
696- row.a11y.set_name(kstr)
697-
698- self.pack_start(row, False)
699-
700- for row in self.get_children():
701- k, v = row.get_children()
702- k.set_size_request(max_lw+3*mkit.EM, -1)
703+ k.set_markup(key_markup % (dark, self.key))
704+ a = gtk.Alignment(1.0, 0.0)
705+ # the line below is 'wrong', but in reality it works quite ok
706+ a.set_size_request(100, -1)
707+ a.add(k)
708+ self.pack_start(a, False)
709+
710+ # value
711+ v = self.value_object
712+ v.set_line_wrap(True)
713+ v.set_selectable(True)
714+ b = gtk.Alignment(0.0, 0.0)
715+ b.add(v)
716+ self.pack_start(b, False)
717+
718+ # a11y stuff
719+ self.set_property("can-focus", True)
720+ self.a11y = self.get_accessible()
721
722 self.show_all()
723 return
724
725 def set_width(self, width):
726- for row in self.get_children():
727- k, v = row.get_children()
728- v.set_size_request(width-k.allocation.width-row.get_spacing(), -1)
729+ if self.get_children():
730+ k, v = self.get_children()
731+ v.set_size_request(width-k.allocation.width-self.get_spacing(), -1)
732 return
733
734- def configure(self, version, license, updates):
735-
736- # set labels
737- self.version_label.set_text(version)
738- self.license_label.set_text(license)
739- self.support_label.set_text(updates)
740-
741- # set a11y texts
742- for row in self.get_children():
743- key = row.a11y.get_name().split(":")[0]
744- value = row.get_children()[1].get_children()[0].get_text()
745- row.a11y.set_name(key + ': ' + value)
746+ def set_value(self, value):
747+ self.value_object.set_text(value)
748+ self.a11y.set_name(self.key + ' ' + value)
749
750 class ScreenshotView(gtk.Alignment):
751
752@@ -784,6 +765,215 @@
753 cr.fill()
754 return
755
756+class AddonCheckButton(gtk.HBox):
757+ """ A widget that represents an add-on:
758+ |CheckButton|Icon|Description| """
759+
760+ __gsignals__ = {'toggled': (gobject.SIGNAL_RUN_FIRST,
761+ gobject.TYPE_NONE,
762+ ()),
763+ }
764+
765+ def __init__(self, db, icons, pkgname):
766+ gtk.HBox.__init__(self, spacing=mkit.SPACING_LARGE)
767+ self.app_details = AppDetails(db,
768+ application=Application("None", pkgname))
769+ # the checkbutton
770+ self.checkbutton = gtk.CheckButton()
771+ self.checkbutton.connect("toggled", self._on_checkbutton_toggled)
772+ self.pack_start(self.checkbutton, False)
773+ # the hbox inside the checkbutton that contains the icon and description
774+ hbox = gtk.HBox(spacing=mkit.SPACING_MED)
775+ image = gtk.Image()
776+ icon = self.app_details.icon
777+ if not icon or not icons.has_icon(icon):
778+ icon = MISSING_APP_ICON
779+ try:
780+ pixbuf = icons.load_icon(icon, 22, ()).scale_simple(22, 22, gtk.gdk.INTERP_BILINEAR)
781+ image.set_from_pixbuf(pixbuf)
782+ except TypeError:
783+ logging.warning("cant set icon for '%s' " % pkgname)
784+ hbox.pack_start(image, False, False)
785+ # the display_name
786+ display_name = _("%(summary)s (%(pkgname)s)") % {'summary': self.app_details.display_name.capitalize(),
787+ 'pkgname': pkgname}
788+ label = gtk.Label(display_name)
789+ hbox.pack_start(label, False)
790+ # and put it into the the checkbox
791+ self.checkbutton.add(hbox)
792+ # this is the addon_pkgname
793+ #self.addon_pkgname = gtk.Label(_(" (%(pkgname)s)") % {
794+ # 'pkgname' : pkgname } )
795+ #hbox.pack_start(self.addon_pkgname, False)
796+
797+
798+ def _on_checkbutton_toggled(self, checkbutton):
799+ self.emit("toggled")
800+ def get_active(self):
801+ return self.checkbutton.get_active()
802+ def set_active(self, is_active):
803+ self.checkbutton.set_active(is_active)
804+ def get_addon(self):
805+ return self.app_details.pkgname
806+
807+
808+class AddonView(gtk.VBox):
809+ """ A widget that handles the application add-ons """
810+ # TODO: sort add-ons in alphabetical order
811+
812+ __gsignals__ = {'toggled':(gobject.SIGNAL_RUN_FIRST,
813+ gobject.TYPE_NONE,
814+ (str, gobject.TYPE_PYOBJECT)),
815+ 'description-clicked':(gobject.SIGNAL_RUN_FIRST,
816+ gobject.TYPE_NONE,
817+ (str, )),
818+ }
819+
820+ def __init__(self, cache, db, icons):
821+ gtk.VBox.__init__(self, False, mkit.SPACING_MED)
822+ self.cache = cache
823+ self.db = db
824+ self.icons = icons
825+ self.recommended_addons = None
826+ self.suggested_addons = None
827+
828+ self.label = gtk.Label(_("<b>Choose add-ons:</b>"))
829+ self.label.set_use_markup(True)
830+ self.label.set_alignment(0, 0.5)
831+ self.pack_start(self.label, False, False)
832+
833+ def set_addons(self, app_details, recommended, suggested):
834+ if len(recommended) == 0 and len(suggested) == 0:
835+ return
836+ self.recommended_addons = recommended
837+ self.suggested_addons = suggested
838+ self.app_details = app_details
839+
840+ for widget in self:
841+ if widget != self.label:
842+ self.remove(widget)
843+
844+ for addon in recommended:
845+ try:
846+ pkg = self.cache[addon]
847+ except KeyError:
848+ continue
849+ checkbutton = AddonCheckButton(self.db, self.icons, addon)
850+ #checkbutton.addon_pkgname.connect(
851+ # "clicked", self._on_description_clicked, addon)
852+ checkbutton.set_active(pkg.installed != None)
853+ checkbutton.connect("toggled", self._on_checkbutton_toggled)
854+ self.pack_start(checkbutton, False)
855+ for addon in suggested:
856+ try:
857+ pkg = self.cache[addon]
858+ except KeyError:
859+ continue
860+ checkbutton = AddonCheckButton(self.db, self.icons, addon)
861+ #checkbutton.addon_pkgname.connect(
862+ # "clicked", self._on_description_clicked, addon)
863+ checkbutton.set_active(pkg.installed != None)
864+ checkbutton.connect("toggled", self._on_checkbutton_toggled)
865+ self.pack_start(checkbutton, False)
866+ self.show_all()
867+ return False
868+
869+ def _on_checkbutton_toggled(self, checkbutton):
870+ addon = checkbutton.get_addon()
871+ self.emit("toggled", addon, checkbutton.get_active())
872+
873+ def _on_description_clicked(self, label, addon):
874+ self.emit("description-clicked", addon)
875+
876+class AddonsStateBar(gtk.Alignment):
877+ __gsignals__ = {'changes-canceled': (gobject.SIGNAL_RUN_FIRST,
878+ gobject.TYPE_NONE,
879+ ()),
880+ }
881+
882+ def __init__(self, cache, view):
883+ gtk.Alignment.__init__(self, xscale=1.0, yscale=1.0)
884+ self.set_redraw_on_allocate(False)
885+ self.set_padding(mkit.SPACING_LARGE,
886+ mkit.SPACING_LARGE,
887+ mkit.SPACING_SMALL+2,
888+ mkit.SPACING_SMALL)
889+
890+ self.hbox = gtk.HBox(spacing=mkit.SPACING_LARGE)
891+ self.add(self.hbox)
892+
893+ self.cache = cache
894+ self.view = view
895+ self.applying = False
896+
897+ self.label_price = gtk.Label()
898+ self.label_price.set_line_wrap(True)
899+ self.hbox.pack_start(self.label_price, False, False)
900+
901+ self.hbuttonbox = gtk.HButtonBox()
902+ self.hbuttonbox.set_layout(gtk.BUTTONBOX_END)
903+ self.button_apply = gtk.Button(_("Apply Changes"))
904+ self.button_apply.connect("clicked", self._on_button_apply_clicked)
905+ self.button_cancel = gtk.Button(_("Cancel"))
906+ self.button_cancel.connect("clicked", self._on_button_cancel_clicked)
907+ self.hbuttonbox.pack_start(self.button_cancel, False)
908+ self.hbuttonbox.pack_start(self.button_apply, False)
909+ self.hbox.pack_start(self.hbuttonbox)
910+
911+ self.fill_color = COLOR_GREEN_FILL
912+ self.line_color = COLOR_GREEN_OUTLINE
913+
914+ def configure(self, app_details, addons_install, addons_remove):
915+ if not addons_install and not addons_remove:
916+ self.hide()
917+ return
918+ if app_details.price:
919+ self.label_price.set_label(app_details.price)
920+ else:
921+ self.label_price.set_label(_("Free"))
922+ self.show()
923+
924+ def draw(self, cr, a, expose_area):
925+ if mkit.not_overlapping(a, expose_area): return
926+
927+ cr.save()
928+ rr = mkit.ShapeRoundedRectangle()
929+ rr.layout(cr,
930+ a.x-1, a.y-1,
931+ a.x+a.width, a.y+a.height,
932+ radius=mkit.CORNER_RADIUS)
933+
934+ cr.set_source_rgb(*mkit.floats_from_string(self.fill_color))
935+# cr.set_source_rgb(*mkit.floats_from_string(self.line_color))
936+ cr.fill()
937+
938+ cr.set_line_width(1)
939+ cr.translate(0.5, 0.5)
940+
941+ rr.layout(cr,
942+ a.x-1, a.y-1,
943+ a.x+a.width, a.y+a.height,
944+ radius=mkit.CORNER_RADIUS)
945+
946+ cr.set_source_rgb(*mkit.floats_from_string(self.line_color))
947+ cr.stroke()
948+ cr.restore()
949+ return
950+
951+ def get_applying(self):
952+ return self.applying
953+ def set_applying(self, applying):
954+ self.applying = applying
955+
956+ def _on_button_apply_clicked(self, button):
957+ self.applying = True
958+ self.button_apply.set_sensitive(False)
959+ self.button_cancel.set_sensitive(False)
960+ AppDetailsViewBase.apply_changes(self.view)
961+
962+ def _on_button_cancel_clicked(self, button):
963+ self.emit("changes-canceled")
964+
965
966 class AppDetailsViewGtk(gtk.Viewport, AppDetailsViewBase):
967
968@@ -804,8 +994,14 @@
969 (gobject.TYPE_PYOBJECT,)),
970 'application-request-action' : (gobject.SIGNAL_RUN_LAST,
971 gobject.TYPE_NONE,
972- (gobject.TYPE_PYOBJECT, str),
973+ (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, str),
974 ),
975+ "navigation-request" : ( gobject.SIGNAL_RUN_LAST,
976+ gobject.TYPE_NONE,
977+ (str,
978+ ),
979+ ),
980+
981 }
982
983
984@@ -854,7 +1050,9 @@
985 for pt in self.app_desc.points:
986 pt.set_size_request(w-7*mkit.EM-166, -1)
987
988- self.info_table.set_width(w-6*mkit.EM)
989+ self.version_info.set_width(w-6*mkit.EM)
990+ self.license_info.set_width(w-6*mkit.EM)
991+ self.support_info.set_width(w-6*mkit.EM)
992
993 self._full_redraw() # ewww
994 return
995@@ -886,6 +1084,11 @@
996 self.action_bar.draw(cr,
997 self.action_bar.allocation,
998 event.area)
999+
1000+ if self.addons_bar.get_property('visible'):
1001+ self.addons_bar.draw(cr,
1002+ self.addons_bar.hbox.allocation,
1003+ event.area)
1004
1005 if self.screenshot.get_property('visible'):
1006 self.screenshot.draw(cr, self.screenshot.allocation, expose_area)
1007@@ -957,17 +1160,17 @@
1008
1009 # we have our own viewport so we know when the viewport grows/shrinks
1010 self.vbox.set_redraw_on_allocate(False)
1011-
1012+
1013 # framed section that contains all app details
1014 self.app_info = mkit.FramedSection()
1015 self.app_info.image.set_size_request(84, 84)
1016- self.app_info.set_spacing(mkit.SPACING_LARGE)
1017+ self.app_info.set_spacing(mkit.SPACING_XLARGE)
1018 self.app_info.header.set_spacing(mkit.SPACING_XLARGE)
1019 self.app_info.header_alignment.set_padding(mkit.SPACING_SMALL,
1020 mkit.SPACING_SMALL,
1021 0, 0)
1022
1023- self.app_info.body.set_spacing(mkit.SPACING_LARGE)
1024+ self.app_info.body.set_spacing(mkit.SPACING_MED)
1025 self.vbox.pack_start(self.app_info, False)
1026
1027 # a11y for name/summary
1028@@ -977,9 +1180,9 @@
1029 # controls which are displayed if the app is installed
1030 self.action_bar = PackageStatusBar(self)
1031 self.app_info.body.pack_start(self.action_bar, False)
1032-
1033+
1034 # FramedSection which contains the app description
1035- self.desc_section = mkit.FramedSection(xpadding=mkit.SPACING_LARGE)
1036+ self.desc_section = mkit.FramedSection(xpadding=mkit.SPACING_XLARGE)
1037 self.desc_section.header_alignment.set_padding(0,0,0,0)
1038
1039 self.app_info.body.pack_start(self.desc_section, False)
1040@@ -998,7 +1201,7 @@
1041 # screenshot
1042 self.screenshot = ScreenshotView(self.distro, self.icons)
1043 app_desc_hb.pack_end(self.screenshot)
1044-
1045+
1046 # homepage link button
1047 self.homepage_btn = mkit.HLinkButton(_('Website'))
1048 self.homepage_btn.connect('clicked', self._on_homepage_clicked)
1049@@ -1014,9 +1217,32 @@
1050 self.share_btn.connect('clicked', self._on_share_clicked)
1051 self.app_desc.footer.pack_start(self.share_btn, False)
1052
1053- # package info table
1054- self.info_table = PackageInfoTable()
1055- self.app_info.body.pack_start(self.info_table, False)
1056+ alignment = gtk.Alignment()
1057+ alignment.set_padding(mkit.SPACING_LARGE, 0, 0, 0)
1058+ self.desc_section.body.pack_start(alignment, False)
1059+
1060+ # add-on handling
1061+ self.addon_view = AddonView(self.cache, self.db, self.icons)
1062+ self.addon_view.connect("toggled", self._on_addon_view_toggled)
1063+ self.addon_view.connect("description-clicked", self._on_addon_view_description_clicked)
1064+ alignment.add(self.addon_view)
1065+
1066+ self.totalsize_info = PackageInfo(_("Total size:"))
1067+ self.app_info.body.pack_start(self.totalsize_info, False)
1068+
1069+ self.addons_bar = AddonsStateBar(self.cache, self)
1070+ self.addons_bar.connect("changes-canceled", self._on_addonsbar_changescanceled)
1071+ self.app_info.body.pack_start(self.addons_bar, False)
1072+
1073+ # package info
1074+ self.version_info = PackageInfo(_("Version:"))
1075+ self.app_info.body.pack_start(self.version_info, False)
1076+
1077+ self.license_info = PackageInfo(_("License:"))
1078+ self.app_info.body.pack_start(self.license_info, False)
1079+
1080+ self.support_info = PackageInfo(_("Updates:"))
1081+ self.app_info.body.pack_start(self.support_info, False)
1082
1083 self.show_all()
1084 return
1085@@ -1053,12 +1279,16 @@
1086
1087 # if we have an error or if we need to enable a source, then hide everything else
1088 if app_details.pkg_state in (PKG_STATE_NOT_FOUND, PKG_STATE_NEEDS_SOURCE):
1089- self.info_table.hide()
1090 self.screenshot.hide()
1091+ self.version_info.hide()
1092+ self.license_info.hide()
1093+ self.support_info.hide()
1094 self.desc_section.hide()
1095 else:
1096 self.desc_section.show()
1097- self.info_table.show()
1098+ self.version_info.show()
1099+ self.license_info.show()
1100+ self.support_info.show()
1101 self.screenshot.show()
1102
1103 # depending on pkg install state set action labels
1104@@ -1075,14 +1305,14 @@
1105 self.app_desc.body.a11y.set_name("Description: " + description)
1106
1107 # show or hide the homepage button and set uri if homepage specified
1108- if app_details.website and self.info_table.get_property('visible'):
1109+ if app_details.website:
1110 self.homepage_btn.show()
1111 self.homepage_btn.set_tooltip_text(app_details.website)
1112 else:
1113 self.homepage_btn.hide()
1114
1115 # check if gwibber-poster is available, if so display Share... btn
1116- if self._gwibber_is_available and self.info_table.get_property('visible'):
1117+ if self._gwibber_is_available:
1118 self.share_btn.show()
1119 else:
1120 self.share_btn.hide()
1121@@ -1107,7 +1337,21 @@
1122 support = app_details.maintenance_status
1123 else:
1124 support = _("Unknown")
1125- self.info_table.configure(version, license, support)
1126+
1127+ self.version_info.set_value(version)
1128+ self.license_info.set_value(license)
1129+ self.support_info.set_value(support)
1130+
1131+ # Update add-on interface
1132+ self.addon_view.hide_all()
1133+ gobject.idle_add(self.addon_view.set_addons, self.app_details, self.recommended, self.suggested)
1134+
1135+ # Update total size label
1136+ self.totalsize_info.hide_all()
1137+ gobject.idle_add(self.update_totalsize)
1138+
1139+ # Update addons state bar
1140+ self.addons_bar.configure(self.app_details, self.addons_install, self.addons_remove)
1141 return
1142
1143 # public API
1144@@ -1129,6 +1373,14 @@
1145 # init data
1146 self.app = app
1147 self.app_details = app.get_details(self.db)
1148+ self.recommended = self.addons_manager.recommended_addons(app.pkgname)
1149+ self.suggested = self.addons_manager.suggested_addons(app.pkgname)
1150+ LOG.debug("AppDetailsView.show_app recommended '%s'" % self.recommended)
1151+ LOG.debug("AppDetailsView.show_app suggested '%s'" % self.suggested)
1152+
1153+ self.addons_install = []
1154+ self.addons_remove = []
1155+
1156 # for compat with the base class
1157 self.appdetails = self.app_details
1158 #print "AppDetailsViewGtk:"
1159@@ -1150,7 +1402,31 @@
1160 def _update_interface_on_trans_ended(self, result):
1161 self.action_bar.button.set_sensitive(True)
1162 self.action_bar.button.show()
1163+ self.addons_bar.button_apply.set_sensitive(True)
1164+ self.addons_bar.button_cancel.set_sensitive(True)
1165+ state = self.action_bar.pkg_state
1166+ pkg_state = None
1167+ if state == PKG_STATE_INSTALLING or state == PKG_STATE_UPGRADING \
1168+ or self.addons_bar.applying:
1169+ self.addons_bar.show_all()
1170+ pkg_state = PKG_STATE_INSTALLED
1171+ else:
1172+ self.addons_bar.hide_all()
1173+ pkg_state = PKG_STATE_UNINSTALLED
1174
1175+ if self.addons_bar.applying:
1176+ self.action_bar.configure(self.app_details, pkg_state)
1177+ self.addons_install = []
1178+ self.addons_remove = []
1179+ self.addons_bar.configure(self.app_details, self.addons_install, self.addons_remove)
1180+ self.addons_bar.applying = False
1181+
1182+ for widget in self.addon_view:
1183+ if widget != self.addon_view.label:
1184+ addon = widget.get_addon()
1185+ widget.set_active(self.cache[addon].installed != None)
1186+ return False
1187+
1188 state = self.action_bar.pkg_state
1189 # handle purchase: install purchased has multiple steps
1190 if (state == PKG_STATE_INSTALLING_PURCHASED and
1191@@ -1164,14 +1440,24 @@
1192 # normal states
1193 elif state == PKG_STATE_REMOVING:
1194 self.action_bar.configure(self.app_details, PKG_STATE_UNINSTALLED)
1195+ self.addons_install = []
1196+ self.addons_remove = []
1197 elif state == PKG_STATE_INSTALLING:
1198 self.action_bar.configure(self.app_details, PKG_STATE_INSTALLED)
1199+ self.addons_install = []
1200+ self.addons_remove = []
1201+ self.addons_bar.configure(self.app_details, self.addons_install, self.addons_remove)
1202 elif state == PKG_STATE_UPGRADING:
1203 self.action_bar.configure(self.app_details, PKG_STATE_INSTALLED)
1204 return False
1205
1206 def _on_transaction_started(self, backend):
1207 self.action_bar.button.hide()
1208+
1209+ if self.addons_bar.get_applying():
1210+ self.action_bar.configure(self.app_details, APP_ACTION_APPLY)
1211+ return
1212+
1213 state = self.action_bar.pkg_state
1214 LOG.debug("_on_transaction_stated %s" % state)
1215 if state == PKG_STATE_NEEDS_PURCHASE:
1216@@ -1205,6 +1491,8 @@
1217 gobject.idle_add(self._show_prog_idle_cb)
1218 if pkgname in backend.pending_transactions:
1219 self.action_bar.progress.set_fraction(progress/100.0)
1220+ if progress == 100:
1221+ self.action_bar.progress.set_fraction(1)
1222 return
1223
1224 def _show_prog_idle_cb(self):
1225@@ -1293,18 +1581,143 @@
1226 return self.icons.load_icon(MISSING_APP_ICON, 84, 0)
1227 elif app_details.icon_needs_download:
1228 self._logger.debug("did not find the icon locally, must download it")
1229-
1230- def on_image_download_complete(downloader, image_file_path):
1231- # when the download is complete, replace the icon in the view with the downloaded one
1232- pb = gtk.gdk.pixbuf_new_from_file(image_file_path)
1233- self.app_info.set_icon_from_pixbuf(pb)
1234-
1235- icon_file_path = os.path.join(SOFTWARE_CENTER_ICON_CACHE_DIR, app_details.icon_file_name)
1236- image_downloader = ImageDownloader()
1237- image_downloader.connect('image-download-complete', on_image_download_complete)
1238- image_downloader.download_image(app_details.icon_url, icon_file_path)
1239+ else:
1240+ return self.icons.load_icon(MISSING_APP_ICON, 84, 0)
1241+ else:
1242+ return self.icons.load_icon(MISSING_APP_ICON, 84, 0)
1243+
1244+ def _on_addon_view_description_clicked(self, button, pkgname):
1245+ self.emit("navigation-request", pkgname)
1246+ return
1247+
1248+ def _on_addon_view_toggled(self, view, addon, isActive):
1249+ if isActive:
1250+ self._set_addon_install(addon)
1251+ else:
1252+ self._set_addon_remove(addon)
1253+ if self.app_details.pkg_state == PKG_STATE_INSTALLED:
1254+ self.addons_bar.configure(self.app_details, self.addons_install, self.addons_remove)
1255+ gobject.idle_add(self.update_totalsize)
1256+
1257+ def _on_addonsbar_changescanceled(self, widget):
1258+ self.addons_install = []
1259+ self.addons_remove = []
1260+ self.addon_view.set_addons(self.app_details,
1261+ self.recommended,
1262+ self.suggested)
1263+ self.addons_bar.configure(self.app_details, self.addons_install, self.addons_remove)
1264+ gobject.idle_add(self.update_totalsize)
1265+
1266+ def get_icon_filename(self, iconname, iconsize):
1267+ iconinfo = self.icons.lookup_icon(iconname, iconsize, 0)
1268+ if not iconinfo:
1269+ iconinfo = self.icons.lookup_icon(MISSING_APP_ICON, iconsize, 0)
1270+ return iconinfo.get_filename()
1271+
1272+ def on_image_download_complete(downloader, image_file_path):
1273+ # when the download is complete, replace the icon in the view with the downloaded one
1274+ pb = gtk.gdk.pixbuf_new_from_file(image_file_path)
1275+ self.app_info.set_icon_from_pixbuf(pb)
1276+
1277+ icon_file_path = os.path.join(SOFTWARE_CENTER_ICON_CACHE_DIR, app_details.icon_file_name)
1278+ image_downloader = ImageDownloader()
1279+ image_downloader.connect('image-download-complete', on_image_download_complete)
1280+ image_downloader.download_image(app_details.icon_url, icon_file_path)
1281
1282 return self.icons.load_icon(MISSING_APP_ICON, 84, 0)
1283+
1284+ def update_totalsize(self):
1285+ def pkg_downloaded(pkg_version):
1286+ filename = os.path.basename(pkg_version.filename)
1287+ # FIXME: use relative path here
1288+ return os.path.exists("/var/cache/apt/archives/" + filename)
1289+
1290+ while gtk.events_pending():
1291+ gtk.main_iteration()
1292+
1293+ pkgs_to_install = []
1294+ pkgs_to_remove = []
1295+ total_download_size = 0 # in kB
1296+ total_install_size = 0 # in kB
1297+ label_string = ""
1298+
1299+ try:
1300+ pkg = self.cache[self.app_details.pkgname]
1301+ except KeyError:
1302+ self.totalsize_info.hide_all()
1303+ return False
1304+ version = pkg.installed
1305+ if version == None:
1306+ version = max(pkg.versions)
1307+ pkgs_to_install.append(version)
1308+ deps_inst = self.cache.get_all_deps_installing(pkg)
1309+ for dep in deps_inst:
1310+ if self.cache[dep].installed == None:
1311+ version = max(self.cache[dep].versions)
1312+ pkgs_to_install.append(version)
1313+ deps_remove = self.cache.get_all_deps_removing(pkg)
1314+ for dep in deps_remove:
1315+ if self.cache[dep].installed != None:
1316+ version = self.cache[dep].installed
1317+ pkgs_to_remove.append(version)
1318+
1319+ for addon in self.addons_install:
1320+ version = max(self.cache[addon].versions)
1321+ pkgs_to_install.append(version)
1322+ deps_inst = self.cache.get_all_deps_installing(self.cache[addon])
1323+ for dep in deps_inst:
1324+ if self.cache[dep].installed == None:
1325+ version = max(self.cache[dep].versions)
1326+ pkgs_to_install.append(version)
1327+ deps_remove = self.cache.get_all_deps_removing(self.cache[addon])
1328+ for dep in deps_remove:
1329+ if self.cache[dep].installed != None:
1330+ version = self.cache[dep].installed
1331+ pkgs_to_remove.append(version)
1332+ for addon in self.addons_remove:
1333+ version = self.cache[addon].installed
1334+ pkgs_to_remove.append(version)
1335+ deps_inst = self.cache.get_all_deps_installing(self.cache[addon])
1336+ for dep in deps_inst:
1337+ if self.cache[dep].installed == None:
1338+ version = max(self.cache[dep].versions)
1339+ pkgs_to_install.append(version)
1340+ deps_remove = self.cache.get_all_deps_removing(self.cache[addon])
1341+ for dep in deps_remove:
1342+ if self.cache[dep].installed != None:
1343+ version = self.cache[dep].installed
1344+ pkgs_to_remove.append(version)
1345+
1346+ for pkg in pkgs_to_install:
1347+ if pkgs_to_install.count(pkg) > 1:
1348+ pkgs_to_install.remove(pkg)
1349+ for pkg in pkgs_to_remove:
1350+ if pkgs_to_remove.count(pkg) > 1:
1351+ pkgs_to_remove.remove(pkg)
1352+
1353+ for pkg in pkgs_to_install:
1354+ if not pkg_downloaded(pkg):
1355+ total_download_size += pkg.size
1356+ total_install_size += pkg.installed_size
1357+ for pkg in pkgs_to_remove:
1358+ total_install_size -= pkg.installed_size
1359+
1360+ if total_download_size > 0:
1361+ download_size = apt_pkg.size_to_str(total_download_size)
1362+ label_string += _("%sB to download, " % (download_size))
1363+ if total_install_size > 0:
1364+ install_size = apt_pkg.size_to_str(total_install_size)
1365+ label_string += _("%sB when installed" % (install_size))
1366+ elif total_install_size < 0:
1367+ remove_size = apt_pkg.size_to_str(-total_install_size)
1368+ label_string += _("%sB to be freed" % (remove_size))
1369+
1370+ if label_string == "":
1371+ self.totalsize_info.hide_all()
1372+ else:
1373+ self.totalsize_info.set_value(label_string)
1374+ self.totalsize_info.show_all()
1375+ return False
1376
1377
1378 if __name__ == "__main__":
1379
1380=== modified file 'test/test_appdetails_view.py'
1381--- test/test_appdetails_view.py 2010-08-11 19:43:05 +0000
1382+++ test/test_appdetails_view.py 2010-08-16 20:56:42 +0000
1383@@ -65,6 +65,27 @@
1384 for i in range(PKG_STATE_UNKNOWN):
1385 mock_app_details.pkg_state = i
1386 self.appdetails.show_app(app)
1387+
1388+ def test_show_app_addons(self):
1389+ app = Application("Web browser", "firefox")
1390+ mock_app_details = mock.Mock(AppDetails)
1391+ mock_app_details.pkgname = "firefox"
1392+ mock_app_details.appname = "Web browser"
1393+ mock_app_details.display_name = "display_name"
1394+ mock_app_details.display_summary = "display_summary"
1395+ mock_app_details.error = None
1396+ mock_app_details.warning = None
1397+ mock_app_details.description = "description"
1398+ mock_app_details.website = "website"
1399+ mock_app_details.thumbnail = None
1400+ mock_app_details.license = "license"
1401+ mock_app_details.maintenance_status = "support_status"
1402+ mock_app_details.purchase_date = "purchase_date"
1403+ mock_app_details.installation_date = "installation_date"
1404+ mock_app_details.price = "price"
1405+ mock_app_details._error_not_found = ""
1406+ app.get_details = lambda db: mock_app_details
1407+ self.appdetails.show_app(app)
1408
1409
1410 if __name__ == "__main__":
1411
1412=== modified file 'test/test_aptd.py'
1413--- test/test_aptd.py 2010-07-08 15:13:27 +0000
1414+++ test/test_aptd.py 2010-08-16 20:56:42 +0000
1415@@ -41,6 +41,14 @@
1416 keyserver = "keyserver.ubuntu.com"
1417 self.aptd.aptd_client.add_vendor_key_from_keyserver = self._monkey_patched_add_vendor_key_from_keyserver
1418 self.aptd.add_vendor_key_from_keyserver(keyid, keyserver)
1419+
1420+ def test_apply_changes(self):
1421+ pkgname = "gimp"
1422+ appname = "The GIMP app"
1423+ iconname = "icon-gimp"
1424+ addons_install = ["gimp-data-extras", "gimp-gutenprint"]
1425+ addons_remove = ["gimp-plugin-registry"]
1426+ yield self.aptd.apply_changes(pkgname, appname ,iconname, addons_install, addons_remove)
1427
1428
1429 if __name__ == "__main__":
1430
1431=== modified file 'test/test_gui.py'
1432--- test/test_gui.py 2010-08-10 09:10:35 +0000
1433+++ test/test_gui.py 2010-08-16 20:56:42 +0000
1434@@ -166,7 +166,9 @@
1435 self.app.show_available_packages(["i-dont-exit"])
1436 self._p()
1437 self.assertFalse(self.app.available_pane.app_details.screenshot.get_property("visible"))
1438- self.assertFalse(self.app.available_pane.app_details.info_table.get_property("visible"))
1439+ self.assertFalse(self.app.available_pane.app_details.version_info.get_property("visible"))
1440+ self.assertFalse(self.app.available_pane.app_details.license_info.get_property("visible"))
1441+ self.assertFalse(self.app.available_pane.app_details.support_info.get_property("visible"))
1442 self.assertFalse(self.app.available_pane.app_details.desc_section.get_property("visible"))
1443
1444 # helper stuff

Subscribers

People subscribed via source and target branches