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

Proposed by Mohamed Amine Ilidrissi on 2010-07-26
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 2010-07-26 Approve on 2010-08-16
Mohamed Amine Ilidrissi (community) Resubmit on 2010-08-05
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 on 2010-08-02

merge with trunk

935. By Mohamed Amine Ilidrissi on 2010-08-02

Modified changelog.

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
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> on 2010-08-04

Fixed a bunch of things at mpt's request.

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

review: Resubmit
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 on 2010-08-11

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

938. By Mohamed Amine Ilidrissi on 2010-08-11

merge with trunk (argh conflicts).

939. By Mohamed Amine Ilidrissi on 2010-08-12

bunch of fixes.

940. By Mohamed Amine Ilidrissi on 2010-08-12

bunch of fixes... again.

941. By Mohamed Amine Ilidrissi on 2010-08-12

merge mvo's branch, many thanks.

942. By Mohamed Amine Ilidrissi on 2010-08-12

Thank you mvo for your efforts :)

943. By Mohamed Amine Ilidrissi on 2010-08-12

Renamed a function.

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 on 2010-08-13

merge with trunk.

945. By Mohamed Amine Ilidrissi on 2010-08-13

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

946. By Mohamed Amine Ilidrissi on 2010-08-13

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 on 2010-08-13

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 on 2010-08-13

2 fixes for 2 small bugs.

949. By Mohamed Amine Ilidrissi on 2010-08-13

Fixed debian/changelog fail.

950. By Mohamed Amine Ilidrissi on 2010-08-15

merge kiwinote's branch, many thanks :)

951. By Mohamed Amine Ilidrissi on 2010-08-15

Fixed the bar padding, and the total size text.

952. By Mohamed Amine Ilidrissi on 2010-08-15

some fixes.

953. By Mohamed Amine Ilidrissi on 2010-08-16

Polishing.

954. By Mohamed Amine Ilidrissi on 2010-08-16

Fixed padding and added a :

955. By Mohamed Amine Ilidrissi on 2010-08-16

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

956. By Mohamed Amine Ilidrissi on 2010-08-16

Renamed TotalSizeBar to AddonsStateBar.

957. By Mohamed Amine Ilidrissi on 2010-08-16

Fixed padding issues.

958. By Mohamed Amine Ilidrissi on 2010-08-16

Fixed an implementation error.

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 on 2010-08-16

Fixed padding issues.

960. By Mohamed Amine Ilidrissi on 2010-08-16

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