Merge lp:~ilidrissi.amine/software-center/addons into lp:software-center
- addons
- Merge into trunk
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 |
Related bugs: |
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 |
Commit message
Description of the change
This branch introduces add-on handling as described in https:/
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.
- 934. By Mohamed Amine Ilidrissi
-
merge with trunk
- 935. By Mohamed Amine Ilidrissi
-
Modified changelog.
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.
Mohamed Amine Ilidrissi (ilidrissi.amine) wrote : | # |
Okay, just fixed these issues. Feel free to request any other fixes.
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 PackageAddonsMa
- 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.
Michael Vogt (mvo) wrote : | # |
Thanks, code is good now, I filed a feature-
- 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.
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.
- 959. By Mohamed Amine Ilidrissi
-
Fixed padding issues.
- 960. By Mohamed Amine Ilidrissi
-
fixed AddonsStateBar padding issue.
Preview Diff
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 |
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/SoftwareCen ter#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?