~ubuntu-core-dev/update-manager/main

« back to all changes in this revision

Viewing changes to UpdateManager/UpdatesAvailable.py

  • Committer: Michael Vogt
  • Date: 2005-11-15 14:44:23 UTC
  • Revision ID: egon@top-20051115144423-5d01f44d63b99ecc
* build-system tweaks

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# UpdatesAvailable.py
2
 
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
3
 
#
4
 
#  Copyright (c) 2004-2013 Canonical
5
 
#                2004 Michiel Sikkes
6
 
#                2005 Martin Willemoes Hansen
7
 
#                2010 Mohamed Amine IL Idrissi
8
 
#
9
 
#  Author: Michiel Sikkes <michiel@eyesopened.nl>
10
 
#          Michael Vogt <mvo@debian.org>
11
 
#          Martin Willemoes Hansen <mwh@sysrq.dk>
12
 
#          Mohamed Amine IL Idrissi <ilidrissiamine@gmail.com>
13
 
#          Alex Launi <alex.launi@canonical.com>
14
 
#          Michael Terry <michael.terry@canonical.com>
15
 
#          Dylan McCall <dylanmccall@ubuntu.com>
16
 
#
17
 
#  This program is free software; you can redistribute it and/or
18
 
#  modify it under the terms of the GNU General Public License as
19
 
#  published by the Free Software Foundation; either version 2 of the
20
 
#  License, or (at your option) any later version.
21
 
#
22
 
#  This program is distributed in the hope that it will be useful,
23
 
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
24
 
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25
 
#  GNU General Public License for more details.
26
 
#
27
 
#  You should have received a copy of the GNU General Public License
28
 
#  along with this program; if not, write to the Free Software
29
 
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
30
 
#  USA
31
 
 
32
 
from __future__ import absolute_import, print_function
33
 
 
34
 
import gi
35
 
gi.require_version("Gtk", "3.0")
36
 
from gi.repository import GLib
37
 
from gi.repository import Gtk
38
 
from gi.repository import Gdk
39
 
from gi.repository import GObject
40
 
from gi.repository import Gio
41
 
from gi.repository import Pango
42
 
 
43
 
import warnings
44
 
warnings.filterwarnings("ignore", "Accessed deprecated property",
45
 
                        DeprecationWarning)
46
 
 
47
 
import apt_pkg
48
 
 
49
 
import os
50
 
import re
51
 
import logging
52
 
import time
53
 
import threading
54
 
 
55
 
from gettext import gettext as _
56
 
from gettext import ngettext
57
 
 
58
 
from .Core.utils import humanize_size
59
 
from .Core.AlertWatcher import AlertWatcher
60
 
from .Core.UpdateList import UpdateSystemGroup
61
 
from .Dialogs import InternalDialog
62
 
 
63
 
from DistUpgrade.DistUpgradeCache import NotEnoughFreeSpaceError
64
 
 
65
 
from .ChangelogViewer import ChangelogViewer
66
 
from .UnitySupport import UnitySupport
67
 
 
68
 
 
69
 
#import pdb
70
 
 
71
 
# FIXME:
72
 
# - 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
75
 
 
76
 
# list constants
77
 
(LIST_NAME, LIST_UPDATE_DATA, LIST_SIZE, LIST_TOGGLE_ACTIVE,
78
 
 LIST_SENSITIVE) = range(5)
79
 
 
80
 
# NetworkManager enums
81
 
from .Core.roam import NetworkManagerHelper
82
 
 
83
 
 
84
 
class UpdateData():
85
 
    def __init__(self, groups, group, item):
86
 
        self.groups = groups if groups else []
87
 
        self.group = group
88
 
        self.item = item
89
 
 
90
 
 
91
 
class CellAreaPackage(Gtk.CellAreaBox):
92
 
    """This CellArea lays our package cells side by side, without allocating
93
 
       width for a cell if it isn't present (like icons for header labels).
94
 
       We assume that the last cell should be expanded to fill remaining space,
95
 
       and other cells have a fixed width.
96
 
    """
97
 
 
98
 
    def __init__(self, indent_toplevel=False):
99
 
        Gtk.CellAreaBox.__init__(self)
100
 
        self.indent_toplevel = indent_toplevel
101
 
        self.column = None
102
 
        self.cached_cell_size = {}
103
 
 
104
 
    def do_foreach_alloc(self, context, widget, cell_area_in, bg_area_in,
105
 
                         callback):
106
 
        cells = []
107
 
 
108
 
        def gather(cell, data):
109
 
            cells.append(cell)
110
 
 
111
 
        self.foreach(gather, None)
112
 
 
113
 
        cell_is_hidden = {}
114
 
 
115
 
        # Record the space required by each cell
116
 
        for cell_number, cell in enumerate(cells):
117
 
            # Detect if this cell should be allocated space
118
 
            if isinstance(cell, Gtk.CellRendererPixbuf):
119
 
                gicon = cell.get_property("gicon")
120
 
                hide_cell = gicon is None
121
 
            else:
122
 
                hide_cell = False
123
 
            cell_is_hidden[cell_number] = hide_cell
124
 
 
125
 
            if not hide_cell and cell_number not in self.cached_cell_size:
126
 
                min_size, natural_size = cell.get_preferred_width(widget)
127
 
                self.cached_cell_size[cell_number] = natural_size
128
 
 
129
 
        cell_area = cell_area_in.copy()
130
 
        bg_area = bg_area_in.copy()
131
 
        spacing = self.get_property("spacing")
132
 
        cell_start = self.get_cell_start(widget)
133
 
        orig_end = cell_area.width + cell_area.x
134
 
        cur_path = self.get_current_path_string()
135
 
        depth = Gtk.TreePath.new_from_string(cur_path).get_depth()
136
 
 
137
 
        # And finally, start handling each cell
138
 
        extra_cell_width = 0
139
 
        cell_area.x = cell_start
140
 
        cell_area.width = 0
141
 
 
142
 
        last_cell_number = len(cells) - 1
143
 
        for cell_number, cell in enumerate(cells):
144
 
            is_last_cell = cell_number == last_cell_number
145
 
            cell_size = self.cached_cell_size.get(cell_number, 0)
146
 
 
147
 
            if cell_area.width > 0 and extra_cell_width == 0:
148
 
                cell_area.x += cell_area.width + spacing
149
 
 
150
 
            if cell_number == 0:
151
 
                # The first cell is affected by its depth in the tree
152
 
                if not cell_is_hidden[1] and self.indent_toplevel:
153
 
                    # if not a header, align with header rows
154
 
                    depth += 1
155
 
                if depth > 1:
156
 
                    indent = max(0, depth - 1)
157
 
                    indent_size = cell_size * indent
158
 
                    if depth == 2:
159
 
                        indent_extra = spacing
160
 
                    elif depth == 3:
161
 
                        indent_extra = spacing + 1
162
 
                    else:
163
 
                        indent_extra = spacing * indent
164
 
                    cell_area.x += indent_size + indent_extra
165
 
 
166
 
            if is_last_cell:
167
 
                cell_size = max(cell_size, orig_end - cell_area.x)
168
 
            if not cell_is_hidden[cell_number]:
169
 
                cell_area.width = cell_size + extra_cell_width
170
 
                extra_cell_width = 0
171
 
            else:
172
 
                cell_area.width = 0
173
 
                extra_cell_width = cell_size + spacing
174
 
 
175
 
            if callback(cell, cell_area.copy(), bg_area.copy()):
176
 
                return
177
 
 
178
 
    def do_event(self, context, widget, event, cell_area, flags):
179
 
        # This override is just to trick our parent implementation into
180
 
        # allowing clicks on toggle cells when they are where the expanders
181
 
        # usually are.  It doesn't expect that, so we expand the cell_area
182
 
        # here to be equivalent to bg_area.
183
 
        cell_start = self.get_cell_start(widget)
184
 
        cell_area.width = cell_area.width + cell_area.x - cell_start
185
 
        cell_area.x = cell_start
186
 
        return Gtk.CellAreaBox.do_event(self, context, widget, event,
187
 
                                        cell_area, flags)
188
 
 
189
 
    def get_cell_start(self, widget):
190
 
        if not self.column:
191
 
            return 0
192
 
        else:
193
 
            val = GObject.Value()
194
 
            val.init(int)
195
 
            widget.style_get_property("horizontal-separator", val)
196
 
            h_sep = val.get_int()
197
 
            widget.style_get_property("grid-line-width", val)
198
 
            line_width = val.get_int()
199
 
            cell_start = self.column.get_x_offset() - h_sep - line_width
200
 
            if not self.indent_toplevel:  # i.e. if no headers
201
 
                widget.style_get_property("expander-size", val)
202
 
                spacing = self.get_property("spacing")
203
 
                # Hardcode 4 because GTK+ hardcodes 4 internally
204
 
                cell_start = cell_start + val.get_int() + 4 + spacing
205
 
            return cell_start
206
 
 
207
 
 
208
 
class UpdatesAvailable(InternalDialog):
209
 
    APP_INSTALL_ICONS_PATH = "/usr/share/app-install/icons"
210
 
 
211
 
    def __init__(self, window_main, header=None, desc=None,
212
 
                 need_reboot=False):
213
 
        InternalDialog.__init__(self, window_main)
214
 
 
215
 
        self.window_main = window_main
216
 
        self.datadir = window_main.datadir
217
 
        self.cache = window_main.cache
218
 
 
219
 
        self.custom_header = header
220
 
        self.custom_desc = desc
221
 
        self.need_reboot = need_reboot
222
 
 
223
 
        content_ui_path = os.path.join(self.datadir,
224
 
                                       "gtkbuilder/UpdateManager.ui")
225
 
        self._load_ui(content_ui_path, "pane_updates_available")
226
 
        self.set_content_widget(self.pane_updates_available)
227
 
 
228
 
        self.dl_size = 0
229
 
        self.connected = True
230
 
 
231
 
        # Used for inhibiting power management
232
 
        self.sleep_cookie = None
233
 
 
234
 
        self.settings = Gio.Settings.new("com.ubuntu.update-manager")
235
 
 
236
 
        # Special icon theme for looking up app-install-data icons
237
 
        self.app_icons = Gtk.IconTheme.get_default()
238
 
        self.app_icons.append_search_path(self.APP_INSTALL_ICONS_PATH)
239
 
 
240
 
        # Create Unity launcher quicklist
241
 
        # FIXME: instead of passing parent we really should just send signals
242
 
        self.unity = UnitySupport(parent=self)
243
 
 
244
 
        # setup the help viewer and disable the help button if there
245
 
        # is no viewer available
246
 
        #self.help_viewer = HelpViewer("update-manager")
247
 
        #if self.help_viewer.check() == False:
248
 
        #    self.button_help.set_sensitive(False)
249
 
 
250
 
        self.add_settings_button()
251
 
        self.button_close = self.add_button(Gtk.STOCK_CANCEL,
252
 
                                            self.window_main.close)
253
 
        self.button_install = self.add_button(_("Install Now"),
254
 
                                              self.on_button_install_clicked)
255
 
        self.focus_button = self.button_install
256
 
 
257
 
        # create text view
258
 
        self.textview_changes = ChangelogViewer()
259
 
        self.textview_changes.show()
260
 
        self.scrolledwindow_changes.add(self.textview_changes)
261
 
        changes_buffer = self.textview_changes.get_buffer()
262
 
        changes_buffer.create_tag("versiontag", weight=Pango.Weight.BOLD)
263
 
 
264
 
        # the treeview (move into it's own code!)
265
 
        self.store = Gtk.TreeStore(str, GObject.TYPE_PYOBJECT, str, bool, bool)
266
 
        self.treeview_update.set_model(None)
267
 
 
268
 
        self.image_restart.set_from_gicon(self.get_restart_icon(),
269
 
                                          Gtk.IconSize.BUTTON)
270
 
 
271
 
        restart_icon_renderer = Gtk.CellRendererPixbuf()
272
 
        restart_icon_renderer.set_property("xpad", 4)
273
 
        restart_icon_renderer.set_property("ypad", 2)
274
 
        restart_icon_renderer.set_property("stock-size", Gtk.IconSize.MENU)
275
 
        restart_icon_renderer.set_property("follow-state", True)
276
 
        restart_column = Gtk.TreeViewColumn(None, restart_icon_renderer)
277
 
        restart_column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
278
 
        restart_column.set_fixed_width(20)
279
 
        self.treeview_update.append_column(restart_column)
280
 
        restart_column.set_cell_data_func(restart_icon_renderer,
281
 
                                          self.restart_icon_renderer_data_func)
282
 
 
283
 
        self.pkg_cell_area = CellAreaPackage(False)
284
 
        pkg_column = Gtk.TreeViewColumn.new_with_area(self.pkg_cell_area)
285
 
        self.pkg_cell_area.column = pkg_column
286
 
        pkg_column.set_title(_("Install or remove"))
287
 
        pkg_column.set_property("spacing", 4)
288
 
        pkg_column.set_expand(True)
289
 
        self.treeview_update.append_column(pkg_column)
290
 
 
291
 
        pkg_toggle_renderer = Gtk.CellRendererToggle()
292
 
        pkg_toggle_renderer.set_property("ypad", 2)
293
 
        pkg_toggle_renderer.connect("toggled", self.on_update_toggled)
294
 
        pkg_column.pack_start(pkg_toggle_renderer, False)
295
 
        pkg_column.add_attribute(pkg_toggle_renderer,
296
 
                                 'active', LIST_TOGGLE_ACTIVE)
297
 
        pkg_column.add_attribute(pkg_toggle_renderer,
298
 
                                 'sensitive', LIST_SENSITIVE)
299
 
        pkg_column.set_cell_data_func(pkg_toggle_renderer,
300
 
                                      self.pkg_toggle_renderer_data_func)
301
 
 
302
 
        pkg_icon_renderer = Gtk.CellRendererPixbuf()
303
 
        pkg_icon_renderer.set_property("ypad", 2)
304
 
        pkg_icon_renderer.set_property("stock-size", Gtk.IconSize.MENU)
305
 
        pkg_column.pack_start(pkg_icon_renderer, False)
306
 
        pkg_column.set_cell_data_func(pkg_icon_renderer,
307
 
                                      self.pkg_icon_renderer_data_func)
308
 
 
309
 
        pkg_label_renderer = Gtk.CellRendererText()
310
 
        pkg_label_renderer.set_property("ypad", 2)
311
 
        pkg_label_renderer.set_property("ellipsize", Pango.EllipsizeMode.END)
312
 
        pkg_column.pack_start(pkg_label_renderer, True)
313
 
        pkg_column.set_cell_data_func(pkg_label_renderer,
314
 
                                      self.pkg_label_renderer_data_func)
315
 
 
316
 
        size_renderer = Gtk.CellRendererText()
317
 
        size_renderer.set_property("xpad", 6)
318
 
        size_renderer.set_property("ypad", 0)
319
 
        size_renderer.set_property("xalign", 1)
320
 
        # 1.0/1.2 == PANGO.Scale.SMALL. Constant is not (yet) introspected.
321
 
        size_renderer.set_property("scale", 1.0 / 1.2)
322
 
        size_column = Gtk.TreeViewColumn(_("Download"), size_renderer,
323
 
                                         text=LIST_SIZE)
324
 
        size_column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
325
 
        size_column.add_attribute(size_renderer,
326
 
                                  'sensitive', LIST_SENSITIVE)
327
 
        self.treeview_update.append_column(size_column)
328
 
 
329
 
        self.treeview_update.set_headers_visible(True)
330
 
        self.treeview_update.set_headers_clickable(False)
331
 
        self.treeview_update.set_direction(Gtk.TextDirection.LTR)
332
 
        self.treeview_update.set_fixed_height_mode(False)
333
 
        self.treeview_update.set_expander_column(pkg_column)
334
 
        self.treeview_update.set_search_column(LIST_NAME)
335
 
        self.treeview_update.connect("button-press-event",
336
 
                                     self.on_treeview_button_press)
337
 
 
338
 
        # init show version
339
 
        self.show_versions = self.settings.get_boolean("show-versions")
340
 
        # init summary_before_name
341
 
        self.summary_before_name = self.settings.get_boolean(
342
 
            "summary-before-name")
343
 
 
344
 
        # expander
345
 
        self.expander_details.set_expanded(
346
 
            self.settings.get_boolean("show-details"))
347
 
        self.expander_details.connect("activate", self.pre_activate_details)
348
 
        self.expander_details.connect("notify::expanded",
349
 
                                      self.activate_details)
350
 
        self.expander_desc.connect("notify::expanded", self.activate_desc)
351
 
 
352
 
        # If auto-updates are on, change cancel label
353
 
        self.notifier_settings = Gio.Settings.new("com.ubuntu.update-notifier")
354
 
        self.notifier_settings.connect(
355
 
            "changed::auto-launch",
356
 
            lambda s, p: self.update_close_button())
357
 
        self.update_close_button()
358
 
 
359
 
        # Alert watcher
360
 
        self.alert_watcher = AlertWatcher()
361
 
        self.alert_watcher.connect("network-alert", self._on_network_alert)
362
 
        self.alert_watcher.connect("battery-alert", self._on_battery_alert)
363
 
        self.alert_watcher.connect("network-3g-alert",
364
 
                                   self._on_network_3g_alert)
365
 
 
366
 
    def stop(self):
367
 
        InternalDialog.stop(self)
368
 
        self._save_state()
369
 
 
370
 
    def start(self):
371
 
        InternalDialog.start(self)
372
 
        self.set_update_list(self.window_main.update_list)
373
 
        self.alert_watcher.check_alert_state()
374
 
        self._restore_state()
375
 
 
376
 
    def is_auto_update(self):
377
 
        update_days = apt_pkg.config.find_i(
378
 
            "APT::Periodic::Update-Package-Lists")
379
 
        return update_days >= 1
380
 
 
381
 
    def update_close_button(self):
382
 
        if self.is_auto_update():
383
 
            self.button_close.set_label(_("_Remind Me Later"))
384
 
            self.button_close.set_use_stock(False)
385
 
            self.button_close.set_use_underline(True)
386
 
        else:
387
 
            self.button_close.set_label(Gtk.STOCK_CANCEL)
388
 
            self.button_close.set_use_stock(True)
389
 
            self.button_close.set_use_underline(False)
390
 
 
391
 
    def install_all_updates(self, menu, menuitem, data):
392
 
        self.select_all_upgrades(None)
393
 
        self.on_button_install_clicked()
394
 
 
395
 
    def pkg_requires_restart(self, pkg):
396
 
        if pkg is None or pkg.candidate is None:
397
 
            return False
398
 
        restart_condition = pkg.candidate.record.get('Restart-Required')
399
 
        return restart_condition == 'system'
400
 
 
401
 
    def get_restart_icon(self):
402
 
        # FIXME: Non-standard, incorrect icon name (from app category).
403
 
        # Theme support for what we want seems to be lacking.
404
 
        restart_icon_names = ['view-refresh-symbolic',
405
 
                              'system-restart',
406
 
                              'system-reboot']
407
 
        return Gio.ThemedIcon.new_from_names(restart_icon_names)
408
 
 
409
 
    def restart_icon_renderer_data_func(self, cell_layout, renderer, model,
410
 
                                        iter, user_data):
411
 
        data = model.get_value(iter, LIST_UPDATE_DATA)
412
 
        sensitive = model.get_value(iter, LIST_SENSITIVE)
413
 
        path = model.get_path(iter)
414
 
 
415
 
        renderer.set_sensitive(sensitive)
416
 
 
417
 
        requires_restart = False
418
 
        if data.item and data.item.pkg:
419
 
            requires_restart = self.pkg_requires_restart(data.item.pkg)
420
 
        elif data.group:
421
 
            if not self.treeview_update.row_expanded(path):
422
 
                # A package in the group requires restart
423
 
                for group_item in data.group.items:
424
 
                    if group_item.pkg and self.pkg_requires_restart(
425
 
                            group_item.pkg):
426
 
                        requires_restart = True
427
 
                        break
428
 
 
429
 
        if requires_restart:
430
 
            gicon = self.get_restart_icon()
431
 
        else:
432
 
            gicon = None
433
 
        renderer.set_property("gicon", gicon)
434
 
 
435
 
    def pkg_toggle_renderer_data_func(self, cell_layout, renderer, model,
436
 
                                      iter, user_data):
437
 
        data = model.get_value(iter, LIST_UPDATE_DATA)
438
 
 
439
 
        activatable = False
440
 
        inconsistent = False
441
 
        if data.item:
442
 
            activatable = data.item.pkg.name not in self.list.held_back
443
 
            inconsistent = False
444
 
            renderer.set_sensitive(data.item.sensitive)
445
 
        elif data.group:
446
 
            activatable = True
447
 
            inconsistent = data.group.selection_is_inconsistent()
448
 
        elif data.groups:
449
 
            activatable = True
450
 
            inconsistent = False
451
 
            saw_install = None
452
 
            for group in data.groups:
453
 
                for item in group.items:
454
 
                    this_install = item.is_selected()
455
 
                    if saw_install is not None and saw_install != this_install:
456
 
                        inconsistent = True
457
 
                        break
458
 
                    saw_install = this_install
459
 
                if inconsistent:
460
 
                    break
461
 
 
462
 
        # The "active" attribute is already set via LIST_TOGGLE_ACTIVE in the
463
 
        # tree model, so we don't set it here.
464
 
        renderer.set_property("activatable", activatable)
465
 
        renderer.set_property("inconsistent", inconsistent)
466
 
 
467
 
    def pkg_icon_renderer_data_func(self, cell_layout, renderer, model,
468
 
                                    iter, user_data):
469
 
        data = model.get_value(iter, LIST_UPDATE_DATA)
470
 
 
471
 
        gicon = None
472
 
        if data.group:
473
 
            gicon = self.get_app_install_icon(data.group.icon)
474
 
        elif data.item:
475
 
            gicon = self.get_app_install_icon(data.item.icon)
476
 
 
477
 
        renderer.set_property("gicon", gicon)
478
 
 
479
 
    def get_app_install_icon(self, icon):
480
 
        """Any application icon is coming from app-install-data's desktop
481
 
           files, which refer to icons from app-install-data's icon directory.
482
 
           So we look them up here."""
483
 
 
484
 
        if not isinstance(icon, Gio.ThemedIcon):
485
 
            return icon  # shouldn't happen
486
 
 
487
 
        info = self.app_icons.choose_icon(icon.get_names(), 16,
488
 
                                          Gtk.IconLookupFlags.FORCE_SIZE)
489
 
        if info is not None:
490
 
            return Gio.FileIcon.new(Gio.File.new_for_path(info.get_filename()))
491
 
        else:
492
 
            return icon  # Assume it's in one of the user's themes
493
 
 
494
 
    def pkg_label_renderer_data_func(self, cell_layout, renderer, model,
495
 
                                     iter, user_data):
496
 
        data = model.get_value(iter, LIST_UPDATE_DATA)
497
 
        sensitive = model.get_value(iter, LIST_SENSITIVE)
498
 
        name = GLib.markup_escape_text(model.get_value(iter, LIST_NAME))
499
 
 
500
 
        renderer.set_sensitive(sensitive)
501
 
 
502
 
        if data.group:
503
 
            markup = name
504
 
        elif data.item:
505
 
            markup = name
506
 
        else:  # header
507
 
            markup = "<b>%s</b>" % name
508
 
 
509
 
        renderer.set_property("markup", markup)
510
 
 
511
 
    def set_changes_buffer(self, changes_buffer, text, name, srcpkg):
512
 
        changes_buffer.set_text("")
513
 
        lines = text.split("\n")
514
 
        if len(lines) == 1:
515
 
            changes_buffer.set_text(text)
516
 
            return
517
 
 
518
 
        for line in lines:
519
 
            end_iter = changes_buffer.get_end_iter()
520
 
            version_match = re.match(
521
 
                r'^%s \((.*)\)(.*)\;.*$' % re.escape(srcpkg), line)
522
 
            #bullet_match = re.match("^.*[\*-]", line)
523
 
            author_match = re.match("^.*--.*<.*@.*>.*$", line)
524
 
            if version_match:
525
 
                version = version_match.group(1)
526
 
                #upload_archive = version_match.group(2).strip()
527
 
                version_text = _("Version %s: \n") % version
528
 
                changes_buffer.insert_with_tags_by_name(end_iter, version_text,
529
 
                                                        "versiontag")
530
 
            elif (author_match):
531
 
                pass
532
 
            else:
533
 
                changes_buffer.insert(end_iter, line + "\n")
534
 
 
535
 
    def on_treeview_update_cursor_changed(self, widget):
536
 
        path = widget.get_cursor()[0]
537
 
        # check if we have a path at all
538
 
        if path is None:
539
 
            return
540
 
        model = widget.get_model()
541
 
        iter = model.get_iter(path)
542
 
 
543
 
        # set descr
544
 
        data = model.get_value(iter, LIST_UPDATE_DATA)
545
 
        item = data.item
546
 
        if (item is None and data.group is not None
547
 
                and data.group.core_item is not None):
548
 
            item = data.group.core_item
549
 
        if (item is None or item.pkg is None
550
 
                or item.pkg.candidate is None
551
 
                or item.pkg.candidate.description is None):
552
 
            changes_buffer = self.textview_changes.get_buffer()
553
 
            changes_buffer.set_text("")
554
 
            desc_buffer = self.textview_descr.get_buffer()
555
 
            desc_buffer.set_text("")
556
 
            self.notebook_details.set_sensitive(False)
557
 
            return
558
 
        long_desc = item.pkg.candidate.description
559
 
        self.notebook_details.set_sensitive(True)
560
 
        # do some regular expression magic on the description
561
 
        # Add a newline before each bullet
562
 
        p = re.compile(r'^(\s|\t)*(\*|0|-)', re.MULTILINE)
563
 
        long_desc = p.sub('\n*', long_desc)
564
 
        # replace all newlines by spaces
565
 
        p = re.compile(r'\n', re.MULTILINE)
566
 
        long_desc = p.sub(" ", long_desc)
567
 
        # replace all multiple spaces by newlines
568
 
        p = re.compile(r'\s\s+', re.MULTILINE)
569
 
        long_desc = p.sub("\n", long_desc)
570
 
        long_desc = ("Package: %s\n%s" %
571
 
                     (item.pkg.name, long_desc))
572
 
 
573
 
        desc_buffer = self.textview_descr.get_buffer()
574
 
        desc_buffer.set_text(long_desc)
575
 
 
576
 
        # now do the changelog
577
 
        name = item.pkg.name
578
 
        if name is None:
579
 
            return
580
 
 
581
 
        changes_buffer = self.textview_changes.get_buffer()
582
 
 
583
 
        # check if we have the changes already and if so, display them
584
 
        # (even if currently disconnected)
585
 
        if name in self.cache.all_changes:
586
 
            changes = self.cache.all_changes[name]
587
 
            srcpkg = self.cache[name].candidate.source_name
588
 
            self.set_changes_buffer(changes_buffer, changes, name, srcpkg)
589
 
        # if not connected, do not even attempt to get the changes
590
 
        elif not self.connected:
591
 
            changes_buffer.set_text(
592
 
                _("No network connection detected, you can not download "
593
 
                  "changelog information."))
594
 
        # else, get it from the entwork
595
 
        elif self.expander_details.get_expanded():
596
 
            lock = threading.Lock()
597
 
            lock.acquire()
598
 
            changelog_thread = threading.Thread(
599
 
                target=self.cache.get_news_and_changelog, args=(name, lock))
600
 
            changelog_thread.start()
601
 
            changes_buffer.set_text("%s\n" %
602
 
                                    _("Downloading list of changes..."))
603
 
            iter = changes_buffer.get_iter_at_line(1)
604
 
            anchor = changes_buffer.create_child_anchor(iter)
605
 
            button = Gtk.Button(stock="gtk-cancel")
606
 
            self.textview_changes.add_child_at_anchor(button, anchor)
607
 
            button.show()
608
 
            id = button.connect("clicked",
609
 
                                lambda w, lock: lock.release(), lock)
610
 
            # wait for the dl-thread
611
 
            while lock.locked():
612
 
                time.sleep(0.01)
613
 
                while Gtk.events_pending():
614
 
                    Gtk.main_iteration()
615
 
            # download finished (or canceld, or time-out)
616
 
            button.hide()
617
 
            if button.handler_is_connected(id):
618
 
                button.disconnect(id)
619
 
        # check if we still are in the right pkg (the download may have taken
620
 
        # some time and the user may have clicked on a new pkg)
621
 
        now_path = widget.get_cursor()[0]
622
 
        if now_path is None:
623
 
            return
624
 
        if path != now_path:
625
 
            return
626
 
        # display NEWS.Debian first, then the changelog
627
 
        changes = ""
628
 
        srcpkg = self.cache[name].candidate.source_name
629
 
        if name in self.cache.all_news:
630
 
            changes += self.cache.all_news[name]
631
 
        if name in self.cache.all_changes:
632
 
            changes += self.cache.all_changes[name]
633
 
        if changes:
634
 
            self.set_changes_buffer(changes_buffer, changes, name, srcpkg)
635
 
 
636
 
    def on_treeview_button_press(self, widget, event):
637
 
        """
638
 
        Show a context menu if a right click was performed on an update entry
639
 
        """
640
 
        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:
641
 
            # need to keep a reference here of menu, otherwise it gets
642
 
            # deleted when it goes out of scope and no menu is visible
643
 
            # (bug #806949)
644
 
            self.menu = menu = Gtk.Menu()
645
 
            item_select_none = \
646
 
                Gtk.MenuItem.new_with_mnemonic(_("_Deselect All"))
647
 
            item_select_none.connect("activate", self.select_none_upgrades)
648
 
            menu.append(item_select_none)
649
 
            num_updates = self.cache.install_count + self.cache.del_count
650
 
            if num_updates == 0:
651
 
                item_select_none.set_property("sensitive", False)
652
 
            item_select_all = Gtk.MenuItem.new_with_mnemonic(_("Select _All"))
653
 
            item_select_all.connect("activate", self.select_all_upgrades)
654
 
            menu.append(item_select_all)
655
 
            menu.show_all()
656
 
            menu.popup_for_device(
657
 
                None, None, None, None, None, event.button, event.time)
658
 
            menu.show()
659
 
            return True
660
 
 
661
 
    # we need this for select all/unselect all
662
 
    def _toggle_group_headers(self, new_selection_value):
663
 
        """ small helper that will set/unset the group headers
664
 
        """
665
 
        model = self.treeview_update.get_model()
666
 
        for row in model:
667
 
            data = model.get_value(row.iter, LIST_UPDATE_DATA)
668
 
            if data.groups is not None or data.group is not None:
669
 
                model.set_value(row.iter, LIST_TOGGLE_ACTIVE,
670
 
                                new_selection_value)
671
 
 
672
 
    def select_all_upgrades(self, widget):
673
 
        """
674
 
        Select all updates
675
 
        """
676
 
        self.setBusy(True)
677
 
        self.cache.saveDistUpgrade()
678
 
        self._toggle_group_headers(True)
679
 
        self.treeview_update.queue_draw()
680
 
        self.updates_changed()
681
 
        self.setBusy(False)
682
 
 
683
 
    def select_none_upgrades(self, widget):
684
 
        """
685
 
        Select none updates
686
 
        """
687
 
        self.setBusy(True)
688
 
        self.cache.clear()
689
 
        self._toggle_group_headers(False)
690
 
        self.treeview_update.queue_draw()
691
 
        self.updates_changed()
692
 
        self.setBusy(False)
693
 
 
694
 
    def setBusy(self, flag):
695
 
        """ Show a watch cursor if the app is busy for more than 0.3 sec.
696
 
        Furthermore provide a loop to handle user interface events """
697
 
        if self.window_main.get_window() is None:
698
 
            return
699
 
        if flag:
700
 
            self.window_main.get_window().set_cursor(
701
 
                Gdk.Cursor.new(Gdk.CursorType.WATCH))
702
 
        else:
703
 
            self.window_main.get_window().set_cursor(None)
704
 
        while Gtk.events_pending():
705
 
            Gtk.main_iteration()
706
 
 
707
 
    def _mark_selected_updates(self):
708
 
        def foreach_cb(model, path, iter, data):
709
 
            data = model.get_value(iter, LIST_UPDATE_DATA)
710
 
            active = False
711
 
            if data.item:
712
 
                active = data.item.is_selected()
713
 
            elif data.group:
714
 
                active = data.group.packages_are_selected()
715
 
            elif data.groups:
716
 
                active = any([g.packages_are_selected() for g in data.groups])
717
 
            model.set_value(iter, LIST_TOGGLE_ACTIVE, active)
718
 
        self.store.foreach(foreach_cb, None)
719
 
 
720
 
    def _check_for_required_restart(self):
721
 
        requires_restart = False
722
 
 
723
 
        def foreach_cb(model, path, iter, data):
724
 
            data = model.get_value(iter, LIST_UPDATE_DATA)
725
 
            active = model.get_value(iter, LIST_TOGGLE_ACTIVE)
726
 
            if not active:
727
 
                return
728
 
            pkg = None
729
 
            if data.item:
730
 
                pkg = data.item.pkg
731
 
            elif data.group and data.group.core_item:
732
 
                pkg = data.group.core_item.pkg
733
 
            if pkg and self.pkg_requires_restart(pkg):
734
 
                nonlocal requires_restart
735
 
                requires_restart = True
736
 
 
737
 
        self.store.foreach(foreach_cb, None)
738
 
        self.hbox_restart.set_visible(requires_restart)
739
 
 
740
 
    def _refresh_updates_count(self):
741
 
        self.button_install.set_sensitive(self.cache.install_count
742
 
                                          + self.cache.del_count)
743
 
        try:
744
 
            inst_count = self.cache.install_count + self.cache.del_count
745
 
            self.dl_size = self.cache.required_download
746
 
            download_str = ""
747
 
            if self.dl_size != 0:
748
 
                download_str = _("%s will be downloaded.") % (
749
 
                    humanize_size(self.dl_size))
750
 
                self.image_downsize.set_sensitive(True)
751
 
                # do not set the buttons to sensitive/insensitive until NM
752
 
                # can deal with dialup connections properly
753
 
                #if self.alert_watcher.network_state != NM_STATE_CONNECTED:
754
 
                #    self.button_install.set_sensitive(False)
755
 
                #else:
756
 
                #    self.button_install.set_sensitive(True)
757
 
                self.button_install.set_sensitive(True)
758
 
                self.unity.set_install_menuitem_visible(True)
759
 
            else:
760
 
                if inst_count > 0:
761
 
                    download_str = ngettext(
762
 
                        "The update has already been downloaded.",
763
 
                        "The updates have already been downloaded.",
764
 
                        inst_count)
765
 
                    self.button_install.set_sensitive(True)
766
 
                    self.unity.set_install_menuitem_visible(True)
767
 
                else:
768
 
                    download_str = _("There are no updates to install.")
769
 
                    self.button_install.set_sensitive(False)
770
 
                    self.unity.set_install_menuitem_visible(False)
771
 
                self.image_downsize.set_sensitive(False)
772
 
            self.label_downsize.set_text(download_str)
773
 
            self.hbox_downsize.show()
774
 
            self.vbox_alerts.show()
775
 
        except SystemError as e:
776
 
            print("required_download could not be calculated: %s" % e)
777
 
            self.label_downsize.set_markup(_("Unknown download size."))
778
 
            self.image_downsize.set_sensitive(False)
779
 
            self.hbox_downsize.show()
780
 
            self.vbox_alerts.show()
781
 
 
782
 
    def updates_changed(self):
783
 
        self._mark_selected_updates()
784
 
        self._check_for_required_restart()
785
 
        self._refresh_updates_count()
786
 
 
787
 
    def update_count(self):
788
 
        """activate or disable widgets and show dialog texts corresponding to
789
 
           the number of available updates"""
790
 
        self.updates_changed()
791
 
 
792
 
        text_header = None
793
 
        text_desc = None
794
 
 
795
 
        if self.custom_header is not None:
796
 
            text_header = self.custom_header
797
 
        if self.custom_desc is not None:
798
 
            text_desc = self.custom_desc
799
 
 
800
 
        # show different text on first run (UX team suggestion)
801
 
        elif self.settings.get_boolean("first-run"):
802
 
            flavor = self.window_main.meta_release.flavor_name
803
 
            version = self.window_main.meta_release.current_dist_version
804
 
            text_header = _("Updated software has been issued since %s %s "
805
 
                            "was released. Do you want to install "
806
 
                            "it now?") % (flavor, version)
807
 
            self.settings.set_boolean("first-run", False)
808
 
        else:
809
 
            text_header = _("Updated software is available for this "
810
 
                            "computer. Do you want to install it now?")
811
 
            if not self.hbox_restart.get_visible() and self.need_reboot:
812
 
                text_desc = _("The computer also needs to restart "
813
 
                              "to finish installing previous updates.")
814
 
 
815
 
        self.notebook_details.set_sensitive(True)
816
 
        self.treeview_update.set_sensitive(True)
817
 
        self.set_header(text_header)
818
 
        self.set_desc(text_desc)
819
 
 
820
 
        return True
821
 
 
822
 
    # Before we shrink the window, capture the size
823
 
    def pre_activate_details(self, expander):
824
 
        expanded = self.expander_details.get_expanded()
825
 
        if expanded:
826
 
            self._save_state()
827
 
 
828
 
    def activate_details(self, expander, data):
829
 
        expanded = self.expander_details.get_expanded()
830
 
        self.settings.set_boolean("show-details", expanded)
831
 
        if expanded:
832
 
            self.on_treeview_update_cursor_changed(self.treeview_update)
833
 
        self._restore_state()
834
 
 
835
 
    def activate_desc(self, expander, data):
836
 
        expanded = self.expander_desc.get_expanded()
837
 
        self.expander_desc.set_vexpand(expanded)
838
 
 
839
 
    def on_button_install_clicked(self):
840
 
        self.unity.set_install_menuitem_visible(False)
841
 
        # print("on_button_install_clicked")
842
 
        err_sum = _("Not enough free disk space")
843
 
        err_msg = _("The upgrade needs a total of %s free space on "
844
 
                    "disk '%s'. "
845
 
                    "Please free at least an additional %s of disk "
846
 
                    "space on '%s'. %s")
847
 
        # specific ways to resolve lack of free space
848
 
        remedy_archivedir = _("Remove temporary packages of former "
849
 
                              "installations using 'sudo apt clean'.")
850
 
        remedy_boot = _("You can remove old kernels using "
851
 
                        "'sudo apt autoremove', and you could also "
852
 
                        "set COMPRESS=xz in "
853
 
                        "/etc/initramfs-tools/initramfs.conf to "
854
 
                        "reduce the size of your initramfs.")
855
 
        remedy_root = _("Empty your trash and remove temporary "
856
 
                        "packages of former installations using "
857
 
                        "'sudo apt clean'.")
858
 
        remedy_tmp = _("Reboot to clean up files in /tmp.")
859
 
        remedy_usr = _("")
860
 
        # check free space and error if its not enough
861
 
        try:
862
 
            self.cache.checkFreeSpace()
863
 
        except NotEnoughFreeSpaceError as e:
864
 
            # CheckFreeSpace examines where packages are cached
865
 
            archivedir = apt_pkg.config.find_dir("Dir::Cache::archives")
866
 
            err_long = ""
867
 
            for req in e.free_space_required_list:
868
 
                if err_long != "":
869
 
                    err_long += " "
870
 
                if req.dir == archivedir:
871
 
                    err_long += err_msg % (req.size_total, req.dir,
872
 
                                           req.size_needed, req.dir,
873
 
                                           remedy_archivedir)
874
 
                elif req.dir == "/boot":
875
 
                    err_long += err_msg % (req.size_total, req.dir,
876
 
                                           req.size_needed, req.dir,
877
 
                                           remedy_boot)
878
 
                elif req.dir == "/":
879
 
                    err_long += err_msg % (req.size_total, req.dir,
880
 
                                           req.size_needed, req.dir,
881
 
                                           remedy_root)
882
 
                elif req.dir == "/tmp":
883
 
                    err_long += err_msg % (req.size_total, req.dir,
884
 
                                           req.size_needed, req.dir,
885
 
                                           remedy_tmp)
886
 
                elif req.dir == "/usr":
887
 
                    err_long += err_msg % (req.size_total, req.dir,
888
 
                                           req.size_needed, req.dir,
889
 
                                           remedy_usr)
890
 
            self.window_main.start_error(False, err_sum, err_long)
891
 
            return
892
 
        except SystemError:
893
 
            logging.exception("free space check failed")
894
 
        self.window_main.start_install()
895
 
 
896
 
    def _on_network_alert(self, watcher, state):
897
 
        # do not set the buttons to sensitive/insensitive until NM
898
 
        # can deal with dialup connections properly
899
 
        if state in NetworkManagerHelper.NM_STATE_CONNECTING_LIST:
900
 
            self.label_offline.set_text(_("Connecting..."))
901
 
            self.updates_changed()
902
 
            self.hbox_offline.show()
903
 
            self.vbox_alerts.show()
904
 
            self.connected = False
905
 
        # in doubt (STATE_UNKNOWN), assume connected
906
 
        elif (state in NetworkManagerHelper.NM_STATE_CONNECTED_LIST
907
 
              or state == NetworkManagerHelper.NM_STATE_UNKNOWN):
908
 
            self.updates_changed()
909
 
            self.hbox_offline.hide()
910
 
            self.connected = True
911
 
            # trigger re-showing the current app to get changelog info (if
912
 
            # needed)
913
 
            self.on_treeview_update_cursor_changed(self.treeview_update)
914
 
        else:
915
 
            self.connected = False
916
 
            self.label_offline.set_text(_("You may not be able to check for "
917
 
                                          "updates or download new updates."))
918
 
            self.updates_changed()
919
 
            self.hbox_offline.show()
920
 
            self.vbox_alerts.show()
921
 
 
922
 
    def _on_battery_alert(self, watcher, on_battery):
923
 
        if on_battery:
924
 
            self.hbox_battery.show()
925
 
            self.vbox_alerts.show()
926
 
        else:
927
 
            self.hbox_battery.hide()
928
 
 
929
 
    def _on_network_3g_alert(self, watcher, on_3g, is_roaming):
930
 
        #print("on 3g: %s; roaming: %s" % (on_3g, is_roaming))
931
 
        if is_roaming:
932
 
            self.hbox_roaming.show()
933
 
            self.hbox_on_3g.hide()
934
 
        elif on_3g:
935
 
            self.hbox_on_3g.show()
936
 
            self.hbox_roaming.hide()
937
 
        else:
938
 
            self.hbox_on_3g.hide()
939
 
            self.hbox_roaming.hide()
940
 
 
941
 
    def on_update_toggled(self, renderer, path):
942
 
        """ a toggle button in the listview was toggled """
943
 
        iter = self.store.get_iter(path)
944
 
        data = self.store.get_value(iter, LIST_UPDATE_DATA)
945
 
        # make sure that we don't allow to toggle deactivated updates
946
 
        # this is needed for the call by the row activation callback
947
 
        if data.groups:
948
 
            self.toggle_from_items([item for group in data.groups
949
 
                                    for item in group.items])
950
 
        elif data.group:
951
 
            self.toggle_from_items(data.group.items)
952
 
        else:
953
 
            self.toggle_from_items([data.item])
954
 
 
955
 
    def on_treeview_update_row_activated(self, treeview, path, column, *args):
956
 
        """
957
 
        If an update row was activated (by pressing space), toggle the
958
 
        install check box
959
 
        """
960
 
        self.on_update_toggled(None, path)
961
 
 
962
 
    def toggle_from_items(self, items):
963
 
        self.setBusy(True)
964
 
        actiongroup = apt_pkg.ActionGroup(self.cache._depcache)
965
 
 
966
 
        # Deselect all updates if any are selected
967
 
        keep_packages = any([item.is_selected() for item in items])
968
 
        for item in items:
969
 
            try:
970
 
                if keep_packages:
971
 
                    item.pkg.mark_keep()
972
 
                elif item.pkg.name not in self.list.held_back:
973
 
                    if not item.to_remove:
974
 
                        item.pkg.mark_install()
975
 
                    else:
976
 
                        item.pkg.mark_delete()
977
 
            except SystemError:
978
 
                pass
979
 
 
980
 
        # check if we left breakage
981
 
        if self.cache._depcache.broken_count:
982
 
            Fix = apt_pkg.ProblemResolver(self.cache._depcache)
983
 
            Fix.resolve_by_keep()
984
 
        self.updates_changed()
985
 
        self.treeview_update.queue_draw()
986
 
        del actiongroup
987
 
        self.setBusy(False)
988
 
 
989
 
    def _save_state(self):
990
 
        """ save the state  (window-size for now) """
991
 
        if self.expander_details.get_expanded():
992
 
            (w, h) = self.window_main.get_size()
993
 
            self.settings.set_int("window-width", w)
994
 
            self.settings.set_int("window-height", h)
995
 
 
996
 
    def _restore_state(self):
997
 
        """ restore the state (window-size for now) """
998
 
        w = self.settings.get_int("window-width")
999
 
        h = self.settings.get_int("window-height")
1000
 
        expanded = self.expander_details.get_expanded()
1001
 
        if expanded:
1002
 
            self.window_main.begin_user_resizable(w, h)
1003
 
        else:
1004
 
            self.window_main.end_user_resizable()
1005
 
        return False
1006
 
 
1007
 
    def _add_header(self, name, groups):
1008
 
        total_size = 0
1009
 
        for group in groups:
1010
 
            total_size = total_size + group.get_total_size()
1011
 
        header_row = [
1012
 
            name,
1013
 
            UpdateData(groups, None, None),
1014
 
            humanize_size(total_size),
1015
 
            True,
1016
 
            True
1017
 
        ]
1018
 
        return self.store.append(None, header_row)
1019
 
 
1020
 
    def _add_groups(self, groups):
1021
 
        # Each row contains:
1022
 
        #  row label (for screen reader),
1023
 
        #  update data tuple (is_toplevel, group object, package object),
1024
 
        #  update size,
1025
 
        #  update selection state
1026
 
        for group in groups:
1027
 
            if not group.items:
1028
 
                continue
1029
 
 
1030
 
            group_is_item = None
1031
 
            if not isinstance(group, UpdateSystemGroup) and \
1032
 
                    len(group.items) == 1:
1033
 
                group_is_item = group.items[0]
1034
 
 
1035
 
            group_row = [
1036
 
                group.name,
1037
 
                UpdateData(None, group, group_is_item),
1038
 
                humanize_size(group.get_total_size()),
1039
 
                True,
1040
 
                group.sensitive
1041
 
            ]
1042
 
            group_iter = self.store.append(None, group_row)
1043
 
 
1044
 
            if group_is_item:
1045
 
                continue
1046
 
            for item in group.items:
1047
 
                item_row = [
1048
 
                    item.name,
1049
 
                    UpdateData(None, None, item),
1050
 
                    humanize_size(getattr(item.pkg.candidate, "size", 0)),
1051
 
                    True,
1052
 
                    group.sensitive
1053
 
                ]
1054
 
                self.store.append(group_iter, item_row)
1055
 
 
1056
 
    def set_update_list(self, update_list):
1057
 
        self.list = update_list
1058
 
 
1059
 
        # use the watch cursor
1060
 
        self.setBusy(True)
1061
 
        # disconnect the view first
1062
 
        self.treeview_update.set_model(None)
1063
 
        self.store.clear()
1064
 
        # clean most objects
1065
 
        self.dl_size = 0
1066
 
 
1067
 
        self.scrolledwindow_update.show()
1068
 
 
1069
 
        # add security and update groups to self.store
1070
 
        if self.list.oem_groups:
1071
 
            self._add_header(_("Improved hardware support"),
1072
 
                             self.list.oem_groups)
1073
 
            self._add_groups(self.list.oem_groups)
1074
 
        if self.list.security_groups:
1075
 
            self._add_header(_("Security updates"), self.list.security_groups)
1076
 
            self._add_groups(self.list.security_groups)
1077
 
        if ((self.list.security_groups or self.list.oem_groups)
1078
 
                and self.list.update_groups):
1079
 
            self._add_header(_("Other updates"), self.list.update_groups)
1080
 
        elif self.list.update_groups and (self.list.kernel_autoremove_groups
1081
 
                                          or self.list.duplicate_groups):
1082
 
            self._add_header(_("Updates"), self.list.update_groups)
1083
 
        if self.list.update_groups:
1084
 
            self._add_groups(self.list.update_groups)
1085
 
        if self.list.kernel_autoremove_groups:
1086
 
            self._add_header(
1087
 
                _("Unused kernel updates to be removed"),
1088
 
                self.list.kernel_autoremove_groups)
1089
 
            self._add_groups(self.list.kernel_autoremove_groups)
1090
 
        if self.list.duplicate_groups:
1091
 
            self._add_header(
1092
 
                _("Duplicate packages to be removed"),
1093
 
                self.list.duplicate_groups)
1094
 
            self._add_groups(self.list.duplicate_groups)
1095
 
        if self.list.ubuntu_pro_groups:
1096
 
            self._add_groups(self.list.ubuntu_pro_groups)
1097
 
 
1098
 
        self.treeview_update.set_model(self.store)
1099
 
        self.pkg_cell_area.indent_toplevel = (
1100
 
            bool(self.list.security_groups)
1101
 
            or bool(self.list.kernel_autoremove_groups)
1102
 
            or bool(self.list.duplicate_groups))
1103
 
        self.update_close_button()
1104
 
        self.update_count()
1105
 
        self.setBusy(False)
1106
 
        while Gtk.events_pending():
1107
 
            Gtk.main_iteration()
1108
 
        self.updates_changed()
1109
 
        return False