Merge lp:~dylanmccall/update-manager/group-by-applications into lp:update-manager

Proposed by Dylan McCall
Status: Merged
Merged at revision: 2582
Proposed branch: lp:~dylanmccall/update-manager/group-by-applications
Merge into: lp:update-manager
Diff against target: 859 lines (+405/-239)
2 files modified
UpdateManager/Core/UpdateList.py (+175/-100)
UpdateManager/UpdatesAvailable.py (+230/-139)
To merge this branch: bzr merge lp:~dylanmccall/update-manager/group-by-applications
Reviewer Review Type Date Requested Status
Dylan McCall (community) Needs Resubmitting
Barry Warsaw (community) Needs Fixing
Matthew Paul Thomas Pending
Review via email: mp+112678@code.launchpad.net

Description of the change

This is a first step towards grouping updates according to the specification at https://wiki.ubuntu.com/SoftwareUpdates.

There are some details still to be worked out, but the feature so far is functional :)

Changes:
 - Changed the list of updates to a treeview, where updates are collapsed under application names.
 - UpdateList links updated packages to applications and groups them according to dependencies.
 - Restart required indicator is shown for packages with "XB-Restart-Required: system." There is a temporary override for linux-meta for testing purposes.
 - Update origins are not displayed.

Known issues, to be resolved with future work:
 - Indentation in the update tree view is presently hard coded and probably belongs (at least partly) in the light-themes package.
 - The 'Ubuntu base' update group should list specific core packages, not 'everything else.' The specification is unclear on what to do with everything else, so it's like this for the time being.
 - The Updates Required icon does not have its corresponding UI element below the download size text.
 - The Download column does not yet show a check mark for downloaded packages.
 - "Terminal" has an expander, with "gnome-terminal" as a child. Instead, group headers should simply be packages instead of their own things.

So, it isn't _finished_, but it does seem to be in a working state. The reason I'm interested in merging with trunk is because I want to keep our code in sync. Most of the crazy merge conflicts are probably done with now (especially since we seem to be standardized on 4 space indents). However, my diff spans _a lot_ of UpdateList.py and UpdatesAvailable.py, so any conflicts can be a real pain to deal with.

To post a comment you must log in.
2442. By Dylan McCall

Attempt to load a symbolic icon for 'restart required'.
Don't select second item in update list.

Revision history for this message
Barry Warsaw (barry) wrote :

See detailed review sent separately.

review: Needs Fixing
Revision history for this message
Barry Warsaw (barry) wrote :
Download full text (43.2 KiB)

Hi Dylan,

Thanks for your contribution to Update Manager, and to Ubuntu!

I've provided some detailed comments on the code below.

One thing I'd really like to see is some unittests, especially for the new
code in UpdateList.py. Would you like to take a crack at adding a few for
some of the new methods and classes you're adding? Have you run the existing
unittests to see if your changes have broken any of them? While Update
Manager doesn't have a fantastic test suite, I'm not crazy about making things
worse by adding more untested code.

Someone should also review the UI changes before this branch lands, probably
mpt or someone else on the design team. I'll request a review from mpt to
start with.

I did notice one UI problem when I ran your branch on Quantal. I see an
update for Ubuntu base (which I gather is a catch-all for anything that
doesn't fit into any other category). However, when I expand the arrow for
that group, I'm still left with a very short window, one row that contains the
"Ubuntu base" string, and a very long and mostly unusable scroll bar. Maybe
the window should expand vertically to fit the sub-package contents better, or
maybe the details window should be taller to start with.

Other than that, I think the groupings are a nice refactoring of the
information presented by Update Manager!

I'll mark the branch Needs Fixing for now, but I will be happy to re-review it
if you make the suggested changes.

=== modified file 'UpdateManager/Core/UpdateList.py'
--- UpdateManager/Core/UpdateList.py 2012-06-28 00:10:23 +0000
+++ UpdateManager/Core/UpdateList.py 2012-07-17 18:03:22 +0000
> @@ -1,25 +1,29 @@
> -# UpdateList.py
> +# UpdateList.py
> # -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
> -#
> +#

You need to be really careful about adding extra whitespace, as your patch
does here. There are several reason for this, but probably the most important
one is that such whitespace-only changes are just noise that distract from the
substantive changes, and reviews thereof. There are many options for ensuring
"whitespace normalization" in Python code, some of which can be accomplished
by your editor. E.g. I have nice functions for Emacs:

http://mail.python.org/pipermail/python-dev/2007-April/072773.html

> # Copyright (c) 2004-2008 Canonical
> -#
> +#
> # Author: Michael Vogt <email address hidden>
> -#
> -# This program is free software; you can redistribute it and/or
> -# modify it under the terms of the GNU General Public License as
> +#
> +# This program is free software; you can redistribute it and/or
> +# modify it under the terms of the GNU General Public License as
> # published by the Free Software Foundation; either version 2 of the
> # License, or (at your option) any later version.
> -#
> +#
> # This program is distributed in the hope that it will be useful,
> # but WITHOUT ANY WARRANTY; without even the implied warranty of
> # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> # GNU General Public License for more details.
> -#
> +#
> # You should have received a copy of the GNU General Public License
> # along with this program; if not, wri...

2443. By Dylan McCall

Merged changes from trunk.
Removed unnecessary TODO notes in UpdateList.py

2444. By Dylan McCall

Fixed unintended formatting changes.

2445. By Dylan McCall

Merged changes from trunk.

2446. By Dylan McCall

Fixed some code style errors in UpdateList.

Revision history for this message
Dylan McCall (dylanmccall) wrote :

Okay, I'm finally working on this and I'll let you know when it's all done. (Couple days, I hope). A lot of the weirder issues are due to me manually merging changes, apparently to the wrong version of UpdateList.py. Unfortunate waste of time, so thank you for being so patient, Barry! :)

2447. By Dylan McCall

Fixed lines longer than 79 characters in UpdateList.py.

2448. By Dylan McCall

Fixed style and commenting issues in UpdatesAvailable.py.

2449. By Dylan McCall

Merged changes from trunk

Revision history for this message
Dylan McCall (dylanmccall) wrote :

There, I think that covers the immediate problems. Those bizarre whitespace changes were because mterry had cleaned stuff up beautifully (along with the Python 3 update) between me starting and finishing this, and then I merged changes in the worst possible way. I'm sorry for taking so very long to get back to you.

Replacing pkg with package in UpdateList.py:
I'd like to leave "pkg" be in this branch, if that's okay, just because pkg seems to appear consistently across the application and in the previous version of UpdateList.py (even in the apt.pkg library we import). I agree it would be lovely to clean up, though. I honestly do cringe every time I abbreviate something :)

You were wondering about that weird function called _rate_application_for_package. Originally I had expected to need some fancier heuristics, but we seem to be getting along fine with what we have here. I agree with you, and I'll deal with that soon.

get_application_for_package:
GIO has a lovely function to parse a .desktop file, spitting out a class with a (localized) name, icon and such. The important bit is it's the same code we use in places like the Unity dash or the Applications menu in Gnome Panel, so it's all nice and familiar. Adding some comments to explain the purpose...

APP_INSTALL_PATH: Right now that is effectively the same thing that happens in Software Centre. I'm happy to turn it into a config parameter :)

I have some refactoring to do (and some new designs to implement!), so I think I will do that, and I will try to do tests and documentation alongside that work. I will resubmit then.

review: Needs Resubmitting
Revision history for this message
Marco Biscaro (marcobiscaro2112) wrote :

I tried to merge trunk and got conflicts. You should merge trunk again and fix them (in UpdateManager/Core/UpdateList.py).

Some details:

=== modified file 'UpdateManager/Core/UpdateList.py'
--- UpdateManager/Core/UpdateList.py 2012-08-20 15:32:45 +0000
+++ UpdateManager/Core/UpdateList.py 2012-11-10 06:29:19 +0000
@@ -1,7 +1,7 @@
 # UpdateList.py
 # -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
 #
-# Copyright (c) 2004-2012 Canonical
+# Copyright (c) 2004-2008 Canonical

I think this shouldn't be modified.

 #
 # Author: Michael Vogt <email address hidden>
 #

You modified this file a lot, I think your name should be here too. :)

@@ -22,53 +22,103 @@

 from __future__ import print_function

+import warnings
+warnings.filterwarnings("ignore", "Accessed deprecated property",
+ DeprecationWarning)
+
 from gettext import gettext as _
-
-import apt
-import logging
 import operator
-import random
+import itertools
 import subprocess
+import os
+import glob
 import sys

What about alphabetical order here?

Revision history for this message
Michael Terry (mterry) wrote :

Dylan, I wanted to help push this over the hump. I've got a merged-with-trunk branch here: lp:~mterry/update-manager/group-by-application

I'll look into adding some unittests next and fixing the single-package headers next. Is there a team we could put a shared branch under?

Revision history for this message
Dylan McCall (dylanmccall) wrote :

Cool! There is not one at the moment, but feel free to add me to one. (I'll
be able to do that in about 8 hours).

I have a few local changes that I'm aiming to finish up and push by Sunday,
so we might want to chat about those over email or IRC or something, too.
They could make things a lot easier (once they are not broken),
particularly with single packages and packages that aren't applications;)

On Mon, Dec 17, 2012 at 11:04 AM, Michael Terry <<email address hidden>
> wrote:

> Dylan, I wanted to help push this over the hump. I've got a
> merged-with-trunk branch here:
> lp:~mterry/update-manager/group-by-application
>
> I'll look into adding some unittests next and fixing the single-package
> headers next. Is there a team we could put a shared branch under?
> --
>
> https://code.launchpad.net/~dylanmccall/update-manager/group-by-applications/+merge/112678
> You are the owner of lp:~dylanmccall/update-manager/group-by-applications.
>

Revision history for this message
Marco Biscaro (marcobiscaro2112) wrote :

I can help too!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'UpdateManager/Core/UpdateList.py'
--- UpdateManager/Core/UpdateList.py 2012-08-20 15:32:45 +0000
+++ UpdateManager/Core/UpdateList.py 2012-11-10 06:29:19 +0000
@@ -1,7 +1,7 @@
1# UpdateList.py1# UpdateList.py
2# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-2# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
3#3#
4# Copyright (c) 2004-2012 Canonical4# Copyright (c) 2004-2008 Canonical
5#5#
6# Author: Michael Vogt <mvo@debian.org>6# Author: Michael Vogt <mvo@debian.org>
7#7#
@@ -22,53 +22,103 @@
2222
23from __future__ import print_function23from __future__ import print_function
2424
25import warnings
26warnings.filterwarnings("ignore", "Accessed deprecated property",
27 DeprecationWarning)
28
25from gettext import gettext as _29from gettext import gettext as _
26
27import apt
28import logging
29import operator30import operator
30import random31import itertools
31import subprocess32import subprocess
33import os
34import glob
32import sys35import sys
3336
3437from gi.repository import Gio
35class UpdateOrigin(object):38
39APP_INSTALL_PATH = "/usr/share/app-install/desktop"
40
41class UpdateGroup():
42 def __init__(self, core_pkgs, name, icon):
43 self._core_pkgs = set(core_pkgs)
44 self._extra_pkgs = set()
45 self.name = name
46 self.icon = icon
47
48 @property
49 def pkgs(self):
50 all_pkgs = []
51 all_pkgs.extend(self._core_pkgs)
52 all_pkgs.extend(self._extra_pkgs)
53 return sorted(all_pkgs, key=operator.attrgetter('name'))
54
55 def add_extra(self, extra_pkg):
56 if self._contains(extra_pkg):
57 return True
58 elif self._is_dependency(extra_pkg):
59 self._extra_pkgs.add(extra_pkg)
60 return True
61 else:
62 return False
63
64 def _contains(self, pkg):
65 return pkg in self._core_pkgs or pkg in self._extra_pkgs
66
67 def _is_dependency(self, pkg):
68 for core_pkg in self._core_pkgs:
69 candidate = core_pkg.candidate
70 dependencies = candidate.get_dependencies('Depends', 'Recommends')
71 for dependency_pkg in itertools.chain.from_iterable(dependencies):
72 if dependency_pkg.name == pkg.name:
73 return True
74 return False
75
76 def packages_are_selected(self):
77 for pkg in self.pkgs:
78 if pkg.marked_install or pkg.marked_upgrade:
79 return True
80 return False
81
82 def selection_is_inconsistent(self):
83 pkgs_installing = [pkg for pkg in self.pkgs
84 if pkg.marked_install or pkg.marked_upgrade]
85 return (len(pkgs_installing) > 0 and
86 len(pkgs_installing) < len(self.pkgs))
87
88 def get_total_size(self):
89 size = 0
90 for pkg in self.pkgs:
91 size += getattr(pkg.candidate, "size", 0)
92 return size
93
94
95class UpdateApplicationGroup(UpdateGroup):
96 def __init__(self, pkgs, application):
97 name = application.get_display_name()
98 icon = application.get_icon()
99 super(UpdateApplicationGroup, self).__init__(pkgs, name, icon)
100
101
102class UpdateSystemGroup(UpdateGroup):
103 def __init__(self, system_pkgs):
104 name = _("Ubuntu base")
105 icon = Gio.ThemedIcon.new("distributor-logo")
106 super(UpdateSystemGroup, self).__init__(system_pkgs, name, icon)
107
108
109class UpdateOrigin():
36 def __init__(self, desc, importance):110 def __init__(self, desc, importance):
37 self.packages = []111 self.packages = []
38 self.importance = importance112 self.importance = importance
39 self.description = desc113 self.description = desc
40114
41115
42class OriginsImportance:116class UpdateList():
43 # filed in by us
44 SECURITY = 10
45 UPDATES = 9
46 PROPOSED = 8
47 BACKPORTS = 7
48 ARCHIVE = 6
49 # this is filed in by MyCache
50 OTHER = 0
51 # this is used by us
52 OTHER_UNKNOWN = -1
53
54
55class UpdateList(object):
56 """117 """
57 class that contains the list of available updates in118 class that contains the list of available updates in
58 self.pkgs[origin] where origin is the user readable string119 self.pkgs[origin] where origin is the user readable string
59 """120 """
60121
61 # the key in the debian/control file used to add the phased
62 # updates percentage
63 PHASED_UPDATES_KEY = "Phased-Update-Percentage"
64
65 # the file that contains the uniq machine id
66 UNIQ_MACHINE_ID_FILE = "/var/lib/dbus/machine-id"
67
68 # the configuration key to turn phased-updates always on
69 ALWAYS_INCLUDE_PHASED_UPDATES = (
70 "Update-Manager::Always-Include-Phased-Updates")
71
72 def __init__(self, parent):122 def __init__(self, parent):
73 # a map of packages under their origin123 # a map of packages under their origin
74 try:124 try:
@@ -81,59 +131,64 @@
81 "you are using.") % e)131 "you are using.") % e)
82 sys.exit(1)132 sys.exit(1)
83 self.distUpgradeWouldDelete = 0133 self.distUpgradeWouldDelete = 0
84 self.pkgs = {}134 self.pkg_groups = []
85 self.num_updates = 0135 self.num_updates = 0
86 self.matcher = self.initMatcher(dist)136 data_dirs = os.environ['XDG_DATA_DIRS'].split(':')
87 self.random = random.Random()137 self.application_dirs = [os.path.join(base, 'applications')
88 # a stable machine uniq id138 for base in data_dirs]
89 with open(self.UNIQ_MACHINE_ID_FILE) as f:139 self.current_desktop = os.environ.get('XDG_CURRENT_DESKTOP')
90 self.machine_uniq_id = f.read()140
91141 def _file_is_application(self, file_path):
92 def initMatcher(self, dist):142 file_path = os.path.abspath(file_path)
93 # (origin, archive, description, importance)143 is_application = False
94 matcher_templates = [144 for app_dir in self.application_dirs:
95 ("%s-security" % dist, "Ubuntu", _("Important security updates"),145 is_application = is_application or file_path.startswith(app_dir)
96 OriginsImportance.SECURITY),146 extension = os.path.splitext(file_path)[1]
97 ("%s-updates" % dist, "Ubuntu", _("Recommended updates"),147 is_application = is_application and (extension == '.desktop')
98 OriginsImportance.UPDATES),148 return is_application
99 ("%s-proposed" % dist, "Ubuntu", _("Proposed updates"),149
100 OriginsImportance.PROPOSED),150 def _rate_application_for_package(self, application, pkg):
101 ("%s-backports" % dist, "Ubuntu", _("Backports"),151 score = 0
102 OriginsImportance.BACKPORTS),152 desktop_file = os.path.basename(application.get_filename())
103 (dist, "Ubuntu", _("Distribution updates"),153 application_id = os.path.splitext(desktop_file)[0]
104 OriginsImportance.ARCHIVE),154
105 (None, None, _("Other updates"), OriginsImportance.OTHER_UNKNOWN)155 if application.should_show():
106 ]156 score += 1
107 matcher = {}157
108 for (origin, archive, desc, importance) in matcher_templates:158 if application_id == pkg.name:
109 matcher[(origin, archive)] = UpdateOrigin(desc, importance)159 score += 5
110 return matcher160
111161 return score
112 def is_ignored_phased_update(self, pkg):162
113 """ This will test if the pkg is a phased updated and if163 def _get_application_for_package(self, pkg):
114 it needs to get installed or ignored.164 desktop_files = []
115165 rated_applications = []
116 :return: True if the updates should be ignored166
117 """167 for installed_file in pkg.installed_files:
118 # allow the admin to override this168 if self._file_is_application(installed_file):
119 if apt.apt_pkg.config.find_b(169 desktop_files.append(installed_file)
120 self.ALWAYS_INCLUDE_PHASED_UPDATES, False):170
121 return False171 app_install_pattern = os.path.join(APP_INSTALL_PATH, '%s:*' % pkg.name)
122172 for desktop_file in glob.glob(app_install_pattern):
123 if self.PHASED_UPDATES_KEY in pkg.candidate.record:173 desktop_files.append(desktop_file)
124 # its important that we always get the same result on174
125 # multiple runs of the update-manager, so we need to175 for desktop_file in desktop_files:
126 # feed a seed that is a combination of the pkg/ver/machine176 try:
127 self.random.seed("%s-%s-%s" % (177 application = Gio.DesktopAppInfo \
128 pkg.name, pkg.candidate.version,178 .new_from_filename(desktop_file)
129 self.machine_uniq_id))179 application.set_desktop_env(self.current_desktop)
130 threshold = pkg.candidate.record[self.PHASED_UPDATES_KEY]180 except Exception as e:
131 percentage = self.random.randint(0, 100)181 print("Error loading .desktop file %s: %s" % installed_file, e)
132 if percentage > int(threshold):182 continue
133 logging.info("holding back phased update (%s < %s)" % (183 score = self._rate_application_for_package(application, pkg)
134 threshold, percentage))184 if score > 0:
135 return True185 rated_applications.append((score, application))
136 return False186
187 rated_applications.sort(key=lambda app: app[0], reverse=True)
188 if len(rated_applications) > 0:
189 return rated_applications[0][1]
190 else:
191 return None
137192
138 def update(self, cache):193 def update(self, cache):
139 self.held_back = []194 self.held_back = []
@@ -141,9 +196,11 @@
141 # do the upgrade196 # do the upgrade
142 self.distUpgradeWouldDelete = cache.saveDistUpgrade()197 self.distUpgradeWouldDelete = cache.saveDistUpgrade()
143198
144 #dselect_upgrade_origin = UpdateOrigin(_("Previous selected"), 1)199 upgrade_pkgs = []
200 pkgs_by_source = {}
201 ungrouped_pkgs = []
145202
146 # sort by origin203 # Find all upgradable packages
147 for pkg in cache:204 for pkg in cache:
148 if pkg.is_upgradable or pkg.marked_install:205 if pkg.is_upgradable or pkg.marked_install:
149 if getattr(pkg.candidate, "origins", None) is None:206 if getattr(pkg.candidate, "origins", None) is None:
@@ -152,22 +209,40 @@
152 print("WARNING: upgradable but no candidate.origins?!?: ",209 print("WARNING: upgradable but no candidate.origins?!?: ",
153 pkg.name)210 pkg.name)
154 continue211 continue
155 # check where the package belongs212
156 origin_node = cache.match_package_origin(pkg, self.matcher)213 upgrade_pkgs.append(pkg)
157
158 # see if its a phased update and *not* a security update
159 # or shadowing a security update
160 if (origin_node.importance != OriginsImportance.SECURITY and
161 self.is_ignored_phased_update(pkg)):
162 continue
163
164 if origin_node not in self.pkgs:
165 self.pkgs[origin_node] = []
166 self.pkgs[origin_node].append(pkg)
167 self.num_updates = self.num_updates + 1214 self.num_updates = self.num_updates + 1
215
168 if pkg.is_upgradable and not (pkg.marked_upgrade or216 if pkg.is_upgradable and not (pkg.marked_upgrade or
169 pkg.marked_install):217 pkg.marked_install):
170 self.held_back.append(pkg.name)218 self.held_back.append(pkg.name)
171 for l in self.pkgs.keys():219
172 self.pkgs[l].sort(key=operator.attrgetter("name"))220 # Index packages by source package name
221 for pkg in upgrade_pkgs:
222 srcpkg = pkg.candidate.source_name
223 pkgs_by_source.setdefault(srcpkg, []).append(pkg)
224
225 for srcpkg, pkgs in pkgs_by_source.items():
226 for pkg in pkgs:
227 app = self._get_application_for_package(pkg)
228 if app is not None:
229 app_group = UpdateApplicationGroup([pkg], app)
230 self.pkg_groups.append(app_group)
231 else:
232 ungrouped_pkgs.append(pkg)
233 self.pkg_groups.sort(key=operator.attrgetter('name'))
234
235 # Stick together packages and their immediate dependencies
236 for pkg in upgrade_pkgs:
237 for group in self.pkg_groups:
238 result = group.add_extra(pkg)
239 if result and pkg in ungrouped_pkgs:
240 ungrouped_pkgs.remove(pkg)
241
242 # FIXME: system_group should only have packages that are dependencies
243 # of ubuntu-meta
244 system_group = UpdateSystemGroup(ungrouped_pkgs)
245 self.pkg_groups.append(system_group)
246
173 self.keepcount = cache._depcache.keep_count247 self.keepcount = cache._depcache.keep_count
248
174249
=== modified file 'UpdateManager/UpdatesAvailable.py'
--- UpdateManager/UpdatesAvailable.py 2012-10-10 08:13:13 +0000
+++ UpdateManager/UpdatesAvailable.py 2012-11-10 06:29:19 +0000
@@ -70,10 +70,12 @@
7070
71# FIXME:71# FIXME:
72# - kill "all_changes" and move the changes into the "Update" class72# - kill "all_changes" and move the changes into the "Update" class
73# - screen reader does not read update toggle state
74# - screen reader does not say "Downloaded" for downloaded updates
7375
74# list constants76# list constants
75(LIST_CONTENTS, LIST_NAME, LIST_PKG,77(LIST_NAME, LIST_UPDATE_DATA, LIST_SIZE, LIST_TOGGLE_ACTIVE) = range(4)
76 LIST_ORIGIN, LIST_TOGGLE_CHECKED) = range(5)78#LIST_UPDATE_DATA is a tuple of (is_toplevel, group, pkg)
7779
78# NetworkManager enums80# NetworkManager enums
79from .Core.roam import NetworkManagerHelper81from .Core.roam import NetworkManagerHelper
@@ -134,38 +136,71 @@
134 self.update_close_button()136 self.update_close_button()
135137
136 # the treeview (move into it's own code!)138 # the treeview (move into it's own code!)
137 self.store = Gtk.ListStore(str, str, GObject.TYPE_PYOBJECT,139 self.store = Gtk.TreeStore(str, GObject.TYPE_PYOBJECT, str, bool)
138 GObject.TYPE_PYOBJECT, bool)
139 self.treeview_update.set_model(self.store)140 self.treeview_update.set_model(self.store)
140 self.treeview_update.set_headers_clickable(True)141
142 restart_icon_renderer = Gtk.CellRendererPixbuf()
143 restart_icon_renderer.set_property("xpad", 4)
144 restart_icon_renderer.set_property("ypad", 2)
145 restart_icon_renderer.set_property("stock-size", Gtk.IconSize.MENU)
146 restart_icon_renderer.set_property("follow-state", True)
147 restart_column = Gtk.TreeViewColumn(None, restart_icon_renderer)
148 restart_column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
149 self.treeview_update.append_column(restart_column)
150 restart_column.set_cell_data_func(restart_icon_renderer,
151 self.restart_icon_renderer_data_func)
152
153 pkg_column = Gtk.TreeViewColumn()
154 pkg_column.set_title(_("Install"))
155 pkg_column.set_property("spacing", 4)
156 pkg_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
157 pkg_column.set_expand(True)
158 self.treeview_update.append_column(pkg_column)
159
160 pkg_toggle_renderer = Gtk.CellRendererToggle()
161 pkg_toggle_renderer.set_property("ypad", 2)
162 pkg_toggle_renderer.connect("toggled", self.on_update_toggled)
163 pkg_column.pack_start(pkg_toggle_renderer, False)
164 pkg_column.add_attribute(pkg_toggle_renderer,
165 'active', LIST_TOGGLE_ACTIVE)
166 pkg_column.set_cell_data_func(pkg_toggle_renderer,
167 self.pkg_toggle_renderer_data_func)
168
169 pkg_icon_renderer = Gtk.CellRendererPixbuf()
170 pkg_icon_renderer.set_property("ypad", 2)
171 pkg_icon_renderer.set_property("stock-size", Gtk.IconSize.MENU)
172 pkg_column.pack_start(pkg_icon_renderer, False)
173 pkg_column.set_cell_data_func(pkg_icon_renderer,
174 self.pkg_icon_renderer_data_func)
175
176 pkg_label_renderer = Gtk.CellRendererText()
177 pkg_label_renderer.set_property("ypad", 2)
178 pkg_column.pack_start(pkg_label_renderer, True)
179 pkg_column.set_cell_data_func(pkg_label_renderer,
180 self.pkg_label_renderer_data_func)
181
182 size_renderer = Gtk.CellRendererText()
183 size_renderer.set_property("xpad", 6)
184 size_renderer.set_property("ypad", 0)
185 size_renderer.set_property("xalign", 1)
186 # 1.0/1.2 == PANGO.Scale.SMALL. Constant is not (yet) introspected.
187 size_renderer.set_property("scale", 1.0/1.2)
188 size_column = Gtk.TreeViewColumn(_("Download"), size_renderer,
189 text=LIST_SIZE)
190 size_column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
191 self.treeview_update.append_column(size_column)
192
193 self.treeview_update.set_headers_visible(True)
194 self.treeview_update.set_headers_clickable(False)
141 self.treeview_update.set_direction(Gtk.TextDirection.LTR)195 self.treeview_update.set_direction(Gtk.TextDirection.LTR)
142
143 tr = Gtk.CellRendererText()
144 tr.set_property("xpad", 6)
145 tr.set_property("ypad", 6)
146 cr = Gtk.CellRendererToggle()
147 cr.set_property("activatable", True)
148 cr.set_property("xpad", 6)
149 cr.connect("toggled", self.toggled)
150
151 column_install = Gtk.TreeViewColumn(_("Install"), cr,
152 active=LIST_TOGGLE_CHECKED)
153 column_install.set_cell_data_func(cr, self.install_column_view_func)
154 column = Gtk.TreeViewColumn(_("Name"), tr, markup=LIST_CONTENTS)
155 column.set_resizable(True)
156
157 column_install.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
158 column_install.set_fixed_width(30)
159 column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
160 column.set_fixed_width(100)
161 self.treeview_update.set_fixed_height_mode(False)196 self.treeview_update.set_fixed_height_mode(False)
162197 self.treeview_update.set_expander_column(pkg_column)
163 self.treeview_update.append_column(column_install)
164 column_install.set_visible(True)
165 self.treeview_update.append_column(column)
166 self.treeview_update.set_search_column(LIST_NAME)198 self.treeview_update.set_search_column(LIST_NAME)
167 self.treeview_update.connect("button-press-event",199 self.treeview_update.connect("button-press-event",
168 self.show_context_menu)200 self.on_treeview_button_press)
201 # Line up row details under expanders.
202 # FIXME: This is arbitrary and should happen in the Gtk theme itself
203 self.treeview_update.set_level_indentation(12)
169204
170 # setup the help viewer and disable the help button if there205 # setup the help viewer and disable the help button if there
171 # is no viewer available206 # is no viewer available
@@ -210,26 +245,83 @@
210 self.button_close.set_use_underline(False)245 self.button_close.set_use_underline(False)
211246
212 def install_all_updates(self, menu, menuitem, data):247 def install_all_updates(self, menu, menuitem, data):
213 self.select_all_updgrades(None)248 self.select_all_upgrades(None)
214 self.on_button_install_clicked(None)249 self.on_button_install_clicked(None)
215250
216 def install_column_view_func(self, cell_layout, renderer, model, iter,251 def restart_icon_renderer_data_func(self, cell_layout, renderer, model,
217 data):252 iter, data):
218 pkg = model.get_value(iter, LIST_PKG)253 # List of packages we know require a restart.
219 if pkg is None:254 # Hack to test the feature while packages lack the needed metadata.
220 renderer.set_property("activatable", True)255 # FIXME: Remove this soon.
221 return256 restart_sourcepkgs = ['linux-meta']
222 current_state = renderer.get_property("active")257
223 to_install = pkg.marked_install or pkg.marked_upgrade258 def pkg_requires_restart(pkg):
224 renderer.set_property("active", to_install)259 restart_condition = pkg.candidate.record.get('XB-Restart-Required')
225 # we need to update the store as well to ensure orca knowns260 restart_override = pkg.candidate.source_name in restart_sourcepkgs
226 # about state changes (it will not read view_func changes)261 return restart_condition == 'system' or restart_override
227 if to_install != current_state:262
228 self.store[iter][LIST_TOGGLE_CHECKED] = to_install263 is_toplevel, group, pkg = model.get_value(iter, LIST_UPDATE_DATA)
229 if pkg.name in self.list.held_back:264 path = model.get_path(iter)
230 renderer.set_property("activatable", False)265
231 else:266 requires_restart = False
232 renderer.set_property("activatable", True)267 if is_toplevel:
268 if not self.treeview_update.row_expanded(path):
269 # A package in the group requires restart
270 for group_pkg in group.pkgs:
271 if pkg_requires_restart(group_pkg):
272 requires_restart = True
273 break
274 else:
275 requires_restart = pkg_requires_restart(pkg)
276
277 # FIXME: Non-standard, incorrect icon name (from app category).
278 # Theme support for what we want seems to be lacking.
279 if requires_restart:
280 restart_icon_names = ['view-refresh-symbolic',
281 'system-restart',
282 'system-reboot']
283 gicon = Gio.ThemedIcon.new_from_names(restart_icon_names)
284 else:
285 gicon = None
286 renderer.set_property("gicon", gicon)
287
288 def pkg_toggle_renderer_data_func(self, cell_layout, renderer, model, iter, data):
289 is_toplevel, group, pkg = model.get_value(iter, LIST_UPDATE_DATA)
290
291 if is_toplevel:
292 activatable = True
293 inconsistent = group.selection_is_inconsistent()
294 elif pkg:
295 activatable = pkg.name not in self.list.held_back
296 inconsistent = False
297
298 # Assumes the "active" attribute is already set, tied directly to LIST_TOGGLE_ACTIVE
299 renderer.set_property("activatable", activatable)
300 renderer.set_property("inconsistent", inconsistent)
301
302 def pkg_icon_renderer_data_func(self, cell_layout, renderer, model, iter, data):
303 is_toplevel, group, pkg = model.get_value(iter, LIST_UPDATE_DATA)
304
305 if is_toplevel:
306 gicon = group.icon
307 else:
308 gicon = Gio.ThemedIcon.new("package")
309
310 renderer.set_property("gicon", gicon)
311
312 def pkg_label_renderer_data_func(self, cell_layout, renderer, model, iter, data):
313 is_toplevel, group, pkg = model.get_value(iter, LIST_UPDATE_DATA)
314
315 if is_toplevel:
316 markup = "<b>%s</b>" % group.name
317 else:
318 pkg_name = xml.sax.saxutils.escape(pkg.name)
319 if not pkg.is_installed:
320 markup = _("%s (New install)") % pkg_name
321 else:
322 markup = pkg_name
323
324 renderer.set_property("markup", markup)
233325
234 def set_changes_buffer(self, changes_buffer, text, name, srcpkg):326 def set_changes_buffer(self, changes_buffer, text, name, srcpkg):
235 changes_buffer.set_text("")327 changes_buffer.set_text("")
@@ -264,7 +356,7 @@
264 iter = model.get_iter(path)356 iter = model.get_iter(path)
265357
266 # set descr358 # set descr
267 pkg = model.get_value(iter, LIST_PKG)359 is_toplevel, group, pkg = model.get_value(iter, LIST_UPDATE_DATA)
268 if (pkg is None or pkg.candidate is None or360 if (pkg is None or pkg.candidate is None or
269 pkg.candidate.description is None):361 pkg.candidate.description is None):
270 changes_buffer = self.textview_changes.get_buffer()362 changes_buffer = self.textview_changes.get_buffer()
@@ -350,7 +442,7 @@
350 if changes:442 if changes:
351 self.set_changes_buffer(changes_buffer, changes, name, srcpkg)443 self.set_changes_buffer(changes_buffer, changes, name, srcpkg)
352444
353 def show_context_menu(self, widget, event):445 def on_treeview_button_press(self, widget, event):
354 """446 """
355 Show a context menu if a right click was performed on an update entry447 Show a context menu if a right click was performed on an update entry
356 """448 """
@@ -361,10 +453,13 @@
361 self.menu = menu = Gtk.Menu()453 self.menu = menu = Gtk.Menu()
362 item_select_none = \454 item_select_none = \
363 Gtk.MenuItem.new_with_mnemonic(_("_Deselect All"))455 Gtk.MenuItem.new_with_mnemonic(_("_Deselect All"))
364 item_select_none.connect("activate", self.select_none_updgrades)456 item_select_none.connect("activate", self.select_none_upgrades)
365 menu.append(item_select_none)457 menu.append(item_select_none)
458 num_updates = self.cache.install_count
459 if num_updates == 0:
460 item_select_none.set_property("sensitive", False)
366 item_select_all = Gtk.MenuItem.new_with_mnemonic(_("Select _All"))461 item_select_all = Gtk.MenuItem.new_with_mnemonic(_("Select _All"))
367 item_select_all.connect("activate", self.select_all_updgrades)462 item_select_all.connect("activate", self.select_all_upgrades)
368 menu.append(item_select_all)463 menu.append(item_select_all)
369 menu.show_all()464 menu.show_all()
370 menu.popup_for_device(465 menu.popup_for_device(
@@ -373,35 +468,35 @@
373 return True468 return True
374469
375 # we need this for select all/unselect all470 # we need this for select all/unselect all
376 def _toggle_origin_headers(self, new_selection_value):471 def _toggle_group_headers(self, new_selection_value):
377 """ small helper that will set/unset the origin headers472 """ small helper that will set/unset the group headers
378 """473 """
379 model = self.treeview_update.get_model()474 model = self.treeview_update.get_model()
380 for row in model:475 for row in model:
381 if not model.get_value(row.iter, LIST_PKG):476 is_toplevel, group, pkg = model.get_value(row.iter, LIST_UPDATE_DATA)
382 model.set_value(row.iter, LIST_TOGGLE_CHECKED,477 if is_toplevel:
383 new_selection_value)478 model.set_value(row.iter, LIST_TOGGLE_ACTIVE, new_selection_value)
384479
385 def select_all_updgrades(self, widget):480 def select_all_upgrades(self, widget):
386 """481 """
387 Select all updates482 Select all updates
388 """483 """
389 self.setBusy(True)484 self.setBusy(True)
390 self.cache.saveDistUpgrade()485 self.cache.saveDistUpgrade()
391 self._toggle_origin_headers(True)486 self._toggle_group_headers(True)
392 self.treeview_update.queue_draw()487 self.treeview_update.queue_draw()
393 self.refresh_updates_count()488 self.updates_changed()
394 self.setBusy(False)489 self.setBusy(False)
395490
396 def select_none_updgrades(self, widget):491 def select_none_upgrades(self, widget):
397 """492 """
398 Select none updates493 Select none updates
399 """494 """
400 self.setBusy(True)495 self.setBusy(True)
401 self.cache.clear()496 self.cache.clear()
402 self._toggle_origin_headers(False)497 self._toggle_group_headers(False)
403 self.treeview_update.queue_draw()498 self.treeview_update.queue_draw()
404 self.refresh_updates_count()499 self.updates_changed()
405 self.setBusy(False)500 self.setBusy(False)
406501
407 def setBusy(self, flag):502 def setBusy(self, flag):
@@ -417,7 +512,17 @@
417 while Gtk.events_pending():512 while Gtk.events_pending():
418 Gtk.main_iteration()513 Gtk.main_iteration()
419514
420 def refresh_updates_count(self):515 def _mark_selected_updates(self):
516 def foreach_cb(model, path, iter, data):
517 is_toplevel, group, pkg = model.get_value(iter, LIST_UPDATE_DATA)
518 if is_toplevel:
519 active = group.packages_are_selected()
520 elif pkg:
521 active = pkg.marked_install or pkg.marked_upgrade
522 model.set_value(iter, LIST_TOGGLE_ACTIVE, active)
523 self.store.foreach(foreach_cb, None)
524
525 def _refresh_updates_count(self):
421 self.button_install.set_sensitive(self.cache.install_count)526 self.button_install.set_sensitive(self.cache.install_count)
422 try:527 try:
423 inst_count = self.cache.install_count528 inst_count = self.cache.install_count
@@ -458,10 +563,15 @@
458 self.hbox_downsize.show()563 self.hbox_downsize.show()
459 self.vbox_alerts.show()564 self.vbox_alerts.show()
460565
566 def updates_changed(self):
567 self._mark_selected_updates()
568 self._refresh_updates_count()
569
461 def update_count(self):570 def update_count(self):
462 """activate or disable widgets and show dialog texts correspoding to571 """activate or disable widgets and show dialog texts correspoding to
463 the number of available updates"""572 the number of available updates"""
464 self.refresh_updates_count()573 self.updates_changed()
574 num_updates = self.cache.install_count
465575
466 text_header = None576 text_header = None
467 text_desc = None577 text_desc = None
@@ -571,14 +681,14 @@
571 # can deal with dialup connections properly681 # can deal with dialup connections properly
572 if state in NetworkManagerHelper.NM_STATE_CONNECTING_LIST:682 if state in NetworkManagerHelper.NM_STATE_CONNECTING_LIST:
573 self.label_offline.set_text(_("Connecting..."))683 self.label_offline.set_text(_("Connecting..."))
574 self.refresh_updates_count()684 self.updates_changed()
575 self.hbox_offline.show()685 self.hbox_offline.show()
576 self.vbox_alerts.show()686 self.vbox_alerts.show()
577 self.connected = False687 self.connected = False
578 # in doubt (STATE_UNKNOWN), assume connected688 # in doubt (STATE_UNKNOWN), assume connected
579 elif (state in NetworkManagerHelper.NM_STATE_CONNECTED_LIST or689 elif (state in NetworkManagerHelper.NM_STATE_CONNECTED_LIST or
580 state == NetworkManagerHelper.NM_STATE_UNKNOWN):690 state == NetworkManagerHelper.NM_STATE_UNKNOWN):
581 self.refresh_updates_count()691 self.updates_changed()
582 self.hbox_offline.hide()692 self.hbox_offline.hide()
583 self.connected = True693 self.connected = True
584 # trigger re-showing the current app to get changelog info (if694 # trigger re-showing the current app to get changelog info (if
@@ -588,7 +698,7 @@
588 self.connected = False698 self.connected = False
589 self.label_offline.set_text(_("You may not be able to check for "699 self.label_offline.set_text(_("You may not be able to check for "
590 "updates or download new updates."))700 "updates or download new updates."))
591 self.refresh_updates_count()701 self.updates_changed()
592 self.hbox_offline.show()702 self.hbox_offline.show()
593 self.vbox_alerts.show()703 self.vbox_alerts.show()
594704
@@ -611,45 +721,15 @@
611 self.hbox_on_3g.hide()721 self.hbox_on_3g.hide()
612 self.hbox_roaming.hide()722 self.hbox_roaming.hide()
613723
614 def row_activated(self, treeview, path, column):724 def on_update_toggled(self, renderer, path):
615 iter = self.store.get_iter(path)
616
617 pkg = self.store.get_value(iter, LIST_PKG)
618 origin = self.store.get_value(iter, LIST_ORIGIN)
619 if pkg is not None:
620 return
621 self.toggle_from_origin(pkg, origin, True)
622
623 def toggle_from_origin(self, pkg, origin, select_all=True):
624 self.setBusy(True)
625 actiongroup = apt_pkg.ActionGroup(self.cache._depcache)
626 for pkg in self.list.pkgs[origin]:
627 if pkg.marked_install or pkg.marked_upgrade:
628 #print("marking keep: ", pkg.name)
629 pkg.mark_keep()
630 elif not (pkg.name in self.list.held_back):
631 #print("marking install: ", pkg.name)
632 pkg.mark_install(auto_fix=False, auto_inst=False)
633 # check if we left breakage
634 if self.cache._depcache.broken_count:
635 Fix = apt_pkg.ProblemResolver(self.cache._depcache)
636 Fix.resolve_by_keep()
637 self.refresh_updates_count()
638 self.treeview_update.queue_draw()
639 del actiongroup
640 self.setBusy(False)
641
642 def toggled(self, renderer, path):
643 """ a toggle button in the listview was toggled """725 """ a toggle button in the listview was toggled """
644 iter = self.store.get_iter(path)726 iter = self.store.get_iter(path)
645 pkg = self.store.get_value(iter, LIST_PKG)727 is_toplevel, group, pkg = self.store.get_value(iter, LIST_UPDATE_DATA)
646 origin = self.store.get_value(iter, LIST_ORIGIN)
647 # make sure that we don't allow to toggle deactivated updates728 # make sure that we don't allow to toggle deactivated updates
648 # this is needed for the call by the row activation callback729 # this is needed for the call by the row activation callback
649 if pkg is None:730 if is_toplevel:
650 toggled_value = not self.store.get_value(iter, LIST_TOGGLE_CHECKED)731 self.toggle_from_group(group)
651 self.toggle_from_origin(pkg, origin, toggled_value)732 self.updates_changed()
652 self.store.set_value(iter, LIST_TOGGLE_CHECKED, toggled_value)
653 self.treeview_update.queue_draw()733 self.treeview_update.queue_draw()
654 return734 return
655 if pkg is None or pkg.name in self.list.held_back:735 if pkg is None or pkg.name in self.list.held_back:
@@ -666,8 +746,8 @@
666 pkg.mark_install()746 pkg.mark_install()
667 except SystemError:747 except SystemError:
668 pass748 pass
749 self.updates_changed()
669 self.treeview_update.queue_draw()750 self.treeview_update.queue_draw()
670 self.refresh_updates_count()
671 self.setBusy(False)751 self.setBusy(False)
672752
673 def on_treeview_update_row_activated(self, treeview, path, column, *args):753 def on_treeview_update_row_activated(self, treeview, path, column, *args):
@@ -675,7 +755,28 @@
675 If an update row was activated (by pressing space), toggle the755 If an update row was activated (by pressing space), toggle the
676 install check box756 install check box
677 """757 """
678 self.toggled(None, path)758 self.on_update_toggled(None, path)
759
760 def toggle_from_group(self, group):
761 self.setBusy(True)
762 actiongroup = apt_pkg.ActionGroup(self.cache._depcache)
763
764 # Deselect all updates if any in group are selected
765 keep_packages = group.packages_are_selected()
766 for pkg in group.pkgs:
767 if keep_packages:
768 pkg.mark_keep()
769 elif not pkg.name in self.list.held_back:
770 pkg.mark_install(auto_fix=False, auto_inst=False)
771
772 # check if we left breakage
773 if self.cache._depcache.broken_count:
774 Fix = apt_pkg.ProblemResolver(self.cache._depcache)
775 Fix.resolve_by_keep()
776 self.updates_changed()
777 self.treeview_update.queue_draw()
778 del actiongroup
779 self.setBusy(False)
679780
680 def save_state(self):781 def save_state(self):
681 """ save the state (window-size for now) """782 """ save the state (window-size for now) """
@@ -704,46 +805,36 @@
704 self.dl_size = 0805 self.dl_size = 0
705806
706 self.scrolledwindow_update.show()807 self.scrolledwindow_update.show()
707 origin_list = sorted(808
708 self.list.pkgs, key=operator.attrgetter("importance"),809 # add update groups and packages to self.store. Each row contains:
709 reverse=True)810 # row label (for screen reader),
710 for origin in origin_list:811 # update data tuple (is_toplevel, group object, package object),
711 self.store.append(['<b><big>%s</big></b>' % origin.description,812 # update size,
712 origin.description, None, origin, True])813 # update selection state
713 for pkg in self.list.pkgs[origin]:814 for group in self.list.pkg_groups:
714 name = xml.sax.saxutils.escape(pkg.name)815 group_row = [
715 if not pkg.is_installed:816 group.name,
716 name += _(" (New install)")817 (True, group, None),
717 summary = xml.sax.saxutils.escape(getattr(pkg.candidate,818 humanize_size(group.get_total_size()),
718 "summary", ""))819 True
719 if self.summary_before_name:820 ]
720 contents = "%s\n<small>%s</small>" % (summary, name)821 tree_parent = self.store.append(None, group_row)
721 else:822
722 contents = "<b>%s</b>\n<small>%s</small>" % (name, summary)823 for pkg in group.pkgs:
723 #TRANSLATORS: the b stands for Bytes824 pkg_row = [
724 size = _("(Size: %s)") % humanize_size(getattr(pkg.candidate,825 pkg.name,
725 "size", 0))826 (False, group, pkg),
726 installed_version = getattr(pkg.installed, "version", None)827 humanize_size(getattr(pkg.candidate, "size", 0)),
727 candidate_version = getattr(pkg.candidate, "version", None)828 True
728 if installed_version is not None:829 ]
729 version = _("From version %(old_version)s "830 self.store.append(tree_parent, pkg_row)
730 "to %(new_version)s") % {831
731 "old_version": installed_version,
732 "new_version": candidate_version}
733 else:
734 version = _("Version %s") % candidate_version
735 if self.show_versions:
736 contents = "%s\n<small>%s %s</small>" % (contents, version,
737 size)
738 else:
739 contents = "%s <small>%s</small>" % (contents, size)
740 self.store.append([contents, pkg.name, pkg, None, True])
741 self.treeview_update.set_model(self.store)832 self.treeview_update.set_model(self.store)
742 self.update_count()833 self.update_count()
743 self.setBusy(False)834 self.setBusy(False)
744 while Gtk.events_pending():835 while Gtk.events_pending():
745 Gtk.main_iteration()836 Gtk.main_iteration()
746 self.refresh_updates_count()837 self.updates_changed()
747 return False838 return False
748839
749 def main(self):840 def main(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: