Merge lp:~pitti/aptdaemon/gtk3gi into lp:aptdaemon

Proposed by Martin Pitt on 2010-11-29
Status: Merged
Merged at revision: 527
Proposed branch: lp:~pitti/aptdaemon/gtk3gi
Merge into: lp:aptdaemon
Diff against target: 1325 lines (+1295/-2)
4 files modified
NEWS (+8/-0)
aptdaemon/core.py (+2/-2)
aptdaemon/gtk3widgets.py (+1119/-0)
gtk3-demo.py (+166/-0)
To merge this branch: bzr merge lp:~pitti/aptdaemon/gtk3gi
Reviewer Review Type Date Requested Status
Aptdaemon Developers 2010-11-29 Pending
Review via email: mp+42088@code.launchpad.net

Description of the Change

This adds a GTK3 widgets module and corresponding demo. We still need the pygtk2 one for software-center (until this gets ported), but I need a GTK3/pygi variant for language-selector.

Please note that "Install file.." is currently broken, but that's unrelated to the GTK 3 port and happens with gtk-demo as well:

Traceback (most recent call last):
  File "/home/martin/upstream/python-defer/defer/__init__.py", line 428, in defer
    result = func(*args, **kwargs)
TypeError: InstallFile() takes exactly 4 non-keyword arguments (2 given)

(from the aptd daemon output). This is not as trivial to fix as merely adding a "False" to the InstallFile() argument list in client.py; I'm afraid I'm not quite sure what the intention was here.

To post a comment you must log in.
lp:~pitti/aptdaemon/gtk3gi updated on 2010-12-02
529. By Martin Pitt on 2010-11-30

gtk3widgets.py: Put back text tagging

530. By Martin Pitt on 2010-11-30

Fix NEWS entry

531. By Martin Pitt on 2010-12-02

remove workaround for GNOME#636043, fixed now

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2010-11-18 07:08:08 +0000
3+++ NEWS 2010-12-02 16:30:34 +0000
4@@ -1,3 +1,11 @@
5+API CHANGES (UNRELEASED):
6+
7+* gtkwidgets Python module:
8+ - There is now a gtk3widgets module which uses GTK 3.0 and pygobject (with
9+ introspection). gtk3-demo.py is a corresponding port of gtk-demo.py.
10+ Note that this currently does not provide the AptTerminal class, as our
11+ libvte still uses GTK 2.0.
12+
13 API CHANGES 0.40:
14
15 * General:
16
17=== modified file 'aptdaemon/core.py'
18--- aptdaemon/core.py 2010-10-22 07:12:59 +0000
19+++ aptdaemon/core.py 2010-12-02 16:30:34 +0000
20@@ -483,8 +483,8 @@
21 self.PropertyChanged("Packages", self._packages)
22
23 packages = property(_get_packages, _set_packages,
24- doc="Packages which will be explictly install, "
25- "upgraded, removed, purged or reinstalled.")
26+ doc="Packages which will be explictly installed, "
27+ "reinstalled, removed, purged, upgraded, or downgraded.")
28
29 def _get_unauthenticated(self):
30 return self._unauthenticated
31
32=== added file 'aptdaemon/gtk3widgets.py'
33--- aptdaemon/gtk3widgets.py 1970-01-01 00:00:00 +0000
34+++ aptdaemon/gtk3widgets.py 2010-12-02 16:30:34 +0000
35@@ -0,0 +1,1119 @@
36+#!/usr/bin/env python
37+# -*- coding: utf-8 -*-
38+"""
39+This module provides widgets to use aptdaemon in a GTK application.
40+"""
41+# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de>
42+#
43+# Licensed under the GNU General Public License Version 2
44+#
45+# This program is free software; you can redistribute it and/or modify
46+# it under the terms of the GNU General Public License as published by
47+# the Free Software Foundation; either version 2 of the License, or
48+# (at your option) any later version.
49+#
50+# This program is distributed in the hope that it will be useful,
51+# but WITHOUT ANY WARRANTY; without even the implied warranty of
52+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
53+# GNU General Public License for more details.
54+#
55+# You should have received a copy of the GNU General Public License
56+# along with this program; if not, write to the Free Software
57+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
58+
59+__author__ = "Sebastian Heinlein <devel@glatzor.de>"
60+
61+__all__ = ("AptConfigFileConflictDialog", "AptCancelButton", "AptConfirmDialog",
62+ "AptProgressDialog", "AptTerminalExpander", "AptStatusIcon",
63+ "AptRoleIcon", "AptStatusAnimation", "AptRoleLabel",
64+ "AptStatusLabel", "AptMediumRequiredDialog", "AptMessageDialog",
65+ "AptErrorDialog", "AptProgressBar", "DiffView",
66+ #"AptTerminal"
67+ )
68+
69+import difflib
70+import gettext
71+import os
72+import pty
73+import re
74+
75+import apt_pkg
76+import dbus
77+import dbus.mainloop.glib
78+import gobject
79+from gi.repository import Gdk
80+from gi.repository import Gtk
81+from gi.repository import Pango
82+#from gi.repository import Vte #TODO: does not currently exist
83+
84+import client
85+from enums import *
86+from defer import inline_callbacks, return_value
87+
88+dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
89+
90+_ = lambda msg: gettext.dgettext("aptdaemon", msg)
91+
92+(COLUMN_ID,
93+ COLUMN_PACKAGE) = range(2)
94+
95+
96+class AptStatusIcon(Gtk.Image):
97+ """
98+ Provides a Gtk.Image which shows an icon representing the status of a
99+ aptdaemon transaction
100+ """
101+ def __init__(self, transaction=None, size=Gtk.IconSize.DIALOG):
102+ gobject.GObject.__init__(self)
103+ self.icon_size = size
104+ self.icon_name = None
105+ self._signals = []
106+ self.set_alignment(0, 0)
107+ if transaction != None:
108+ self.set_transaction(transaction)
109+
110+ def set_transaction(self, transaction):
111+ """Connect to the given transaction"""
112+ for sig in self._signals:
113+ gobject.source_remove(sig)
114+ self._signals = []
115+ self._signals.append(transaction.connect("status-changed",
116+ self._on_status_changed))
117+
118+ def set_icon_size(self, size):
119+ """Set the icon size to gtk stock icon size value"""
120+ self.icon_size = size
121+
122+ def _on_status_changed(self, transaction, status):
123+ """Set the status icon according to the changed status"""
124+ icon_name = get_status_icon_name_from_enum(status)
125+ if icon_name is None:
126+ icon_name = Gtk.STOCK_MISSING_IMAGE
127+ if icon_name != self.icon_name:
128+ self.set_from_icon_name(icon_name, self.icon_size)
129+ self.icon_name = icon_name
130+
131+
132+class AptRoleIcon(AptStatusIcon):
133+ """
134+ Provides a Gtk.Image which shows an icon representing the role of an
135+ aptdaemon transaction
136+ """
137+ def set_transaction(self, transaction):
138+ for sig in self._signals:
139+ gobject.source_remove(sig)
140+ self._signals = []
141+ self._signals.append(transaction.connect("role-changed",
142+ self._on_role_changed))
143+ self._on_role_changed(transaction, transaction.role)
144+
145+ def _on_role_changed(self, transaction, role_enum):
146+ """Show an icon representing the role"""
147+ icon_name = get_role_icon_name_from_enum(role_enum)
148+ if icon_name is None:
149+ icon_name = Gtk.STOCK_MISSING_IMAGE
150+ if icon_name != self.icon_name:
151+ self.set_from_icon_name(icon_name, self.icon_size)
152+ self.icon_name = icon_name
153+
154+
155+class AptStatusAnimation(AptStatusIcon):
156+ """
157+ Provides a Gtk.Image which shows an animation representing the
158+ transaction status
159+ """
160+ def __init__(self, transaction=None, size=Gtk.IconSize.DIALOG):
161+ AptStatusIcon.__init__(self, transaction, size)
162+ self.animation = []
163+ self.ticker = 0
164+ self.frame_counter = 0
165+ self.iter = 0
166+ name = get_status_animation_name_from_enum(STATUS_WAITING)
167+ fallback = get_status_icon_name_from_enum(STATUS_WAITING)
168+ self.set_animation(name, fallback)
169+
170+ def set_animation(self, name, fallback=None, size=None):
171+ """Show and start the animation of the given name and size"""
172+ if name == self.icon_name:
173+ return
174+ if size is not None:
175+ self.icon_size = size
176+ self.stop_animation()
177+ animation = []
178+ (width, height) = Gtk.icon_size_lookup(self.icon_size)
179+ theme = Gtk.IconTheme.get_default()
180+ if name is not None and theme.has_icon(name):
181+ pixbuf = theme.load_icon(name, width, 0)
182+ rows = pixbuf.get_height() / height
183+ cols = pixbuf.get_width() / width
184+ for r in range(rows):
185+ for c in range(cols):
186+ animation.append(pixbuf.subpixbuf(c * width, r * height,
187+ width, height))
188+ if len(animation) > 0:
189+ self.animation = animation
190+ self.iter = 0
191+ self.set_from_pixbuf(self.animation[0])
192+ self.start_animation()
193+ else:
194+ self.set_from_pixbuf(pixbuf)
195+ self.icon_name = name
196+ elif fallback is not None and theme.has_icon(fallback):
197+ self.set_from_icon_name(fallback, self.icon_size)
198+ self.icon_name = fallback
199+ else:
200+ self.set_from_icon_name(Gtk.STOCK_MISSING_IMAGE)
201+
202+ def start_animation(self):
203+ """Start the animation"""
204+ if self.ticker == 0:
205+ self.ticker = gobject.timeout_add(200, self._advance)
206+
207+ def stop_animation(self):
208+ """Stop the animation"""
209+ if self.ticker != 0:
210+ gobject.source_remove(self.ticker)
211+ self.ticker = 0
212+
213+ def _advance(self):
214+ """
215+ Show the next frame of the animation and stop the animation if the
216+ widget is no longer visible
217+ """
218+ if self.get_property("visible") == False:
219+ self.ticker = 0
220+ return False
221+ self.iter = self.iter + 1
222+ if self.iter >= len(self.animation):
223+ self.iter = 0
224+ self.set_from_pixbuf(self.animation[self.iter])
225+ return True
226+
227+ def _on_status_changed(self, transaction, status):
228+ """
229+ Set the animation according to the changed status
230+ """
231+ name = get_status_animation_name_from_enum(status)
232+ fallback = get_status_icon_name_from_enum(status)
233+ self.set_animation(name, fallback)
234+
235+
236+class AptRoleLabel(Gtk.Label):
237+ """
238+ Status label for the running aptdaemon transaction
239+ """
240+ def __init__(self, transaction=None):
241+ gobject.GObject.__init__(self)
242+ self.set_alignment(0, 0)
243+ self.set_ellipsize(Pango.EllipsizeMode.END)
244+ self._signals = []
245+ if transaction != None:
246+ self.set_transaction(transaction)
247+
248+ def set_transaction(self, transaction):
249+ """Connect the status label to the given aptdaemon transaction"""
250+ for sig in self._signals:
251+ gobject.source_remove(sig)
252+ self._signals = []
253+ self._on_role_changed(transaction, transaction.role)
254+ self._signals.append(transaction.connect("role-changed",
255+ self._on_role_changed))
256+
257+ def _on_role_changed(self, transaction, role):
258+ """Set the role text."""
259+ self.set_markup(get_role_localised_present_from_enum(role))
260+
261+
262+class AptStatusLabel(Gtk.Label):
263+ """
264+ Status label for the running aptdaemon transaction
265+ """
266+ def __init__(self, transaction=None):
267+ gobject.GObject.__init__(self)
268+ self.set_alignment(0, 0)
269+ self.set_ellipsize(Pango.EllipsizeMode.END)
270+ self._signals = []
271+ if transaction != None:
272+ self.set_transaction(transaction)
273+
274+ def set_transaction(self, transaction):
275+ """Connect the status label to the given aptdaemon transaction"""
276+ for sig in self._signals:
277+ gobject.source_remove(sig)
278+ self._signals = []
279+ self._signals.append(transaction.connect("status-changed",
280+ self._on_status_changed))
281+ self._signals.append(transaction.connect("status-details-changed",
282+ self._on_status_details_changed))
283+
284+ def _on_status_changed(self, transaction, status):
285+ """Set the status text according to the changed status"""
286+ self.set_markup(get_status_string_from_enum(status))
287+
288+ def _on_status_details_changed(self, transaction, text):
289+ """Set the status text to the one reported by apt"""
290+ self.set_markup(text)
291+
292+
293+class AptProgressBar(Gtk.ProgressBar):
294+ """
295+ Provides a Gtk.Progress which represents the progress of an aptdaemon
296+ transactions
297+ """
298+ def __init__(self, transaction=None):
299+ gobject.GObject.__init__(self)
300+ self.set_ellipsize(Pango.EllipsizeMode.END)
301+ self.set_text(" ")
302+ self.set_pulse_step(0.05)
303+ self._signals = []
304+ if transaction != None:
305+ self.set_transaction(transaction)
306+
307+ def set_transaction(self, transaction):
308+ """Connect the progress bar to the given aptdaemon transaction"""
309+ for sig in self._signals:
310+ gobject.source_remove(sig)
311+ self._signals = []
312+ self._signals.append(transaction.connect("finished", self._on_finished))
313+ self._signals.append(transaction.connect("progress-changed",
314+ self._on_progress_changed))
315+ self._signals.append(transaction.connect("progress-details-changed",
316+ self._on_progress_details))
317+
318+ def _on_progress_changed(self, transaction, progress):
319+ """
320+ Update the progress according to the latest progress information
321+ """
322+ if progress > 100:
323+ self.pulse()
324+ else:
325+ self.set_fraction(progress/100.0)
326+
327+ def _on_progress_details(self, transaction, items_done, items_total,
328+ bytes_done, bytes_total, speed, eta):
329+ """
330+ Update the progress bar text according to the latest progress details
331+ """
332+ if items_total == 0 and bytes_total == 0:
333+ self.set_text(" ")
334+ return
335+ if speed != 0:
336+ self.set_text(_("Downloaded %sB of %sB "
337+ "at %sB/s") % (apt_pkg.size_to_str(bytes_done),
338+ apt_pkg.size_to_str(bytes_total),
339+ apt_pkg.size_to_str(speed)))
340+ else:
341+ self.set_text(_("Downloaded %sB "
342+ "of %sB") % (apt_pkg.size_to_str(bytes_done),
343+ apt_pkg.size_to_str(bytes_total)))
344+
345+ def _on_finished(self, transaction, exit):
346+ """Set the progress to 100% when the transaction is complete"""
347+ self.set_fraction(1)
348+
349+
350+class AptDetailsExpander(Gtk.Expander):
351+
352+ def __init__(self, transaction=None, terminal=True):
353+ gobject.GObject.__init__(self, label=_("Details"))
354+ self.show_terminal = terminal
355+ self._signals = []
356+ self.set_sensitive(False)
357+ self.set_expanded(False)
358+ if self.show_terminal:
359+ # TODO: re-enable once GIR for VTE is available
360+ #self.terminal = AptTerminal()
361+ self.terminal = None
362+ else:
363+ self.terminal = None
364+ self.download_view = AptDownloadsView()
365+ self.download_scrolled = Gtk.ScrolledWindow()
366+ self.download_scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
367+ self.download_scrolled.set_policy(Gtk.PolicyType.NEVER,
368+ Gtk.PolicyType.AUTOMATIC)
369+ self.download_scrolled.add(self.download_view)
370+ hbox = Gtk.HBox()
371+ hbox.pack_start(self.download_scrolled, True, True, 0)
372+ if self.terminal:
373+ hbox.pack_start(self.terminal, True, True, 0)
374+ self.add(hbox)
375+ if transaction != None:
376+ self.set_transaction(transaction)
377+
378+ def set_transaction(self, transaction):
379+ """Connect the status label to the given aptdaemon transaction"""
380+ for sig in self._signals:
381+ gobject.source_remove(sig)
382+ self._signals.append(transaction.connect("status-changed",
383+ self._on_status_changed))
384+ self._signals.append(transaction.connect("terminal-attached-changed",
385+ self._on_terminal_attached_changed))
386+ if self.terminal:
387+ self.terminal.set_transaction(transaction)
388+ self.download_view.set_transaction(transaction)
389+
390+ def _on_status_changed(self, trans, status):
391+ if status == STATUS_DOWNLOADING:
392+ self.set_sensitive(True)
393+ self.download_scrolled.show()
394+ if self.terminal:
395+ self.terminal.hide()
396+ elif status == STATUS_COMMITTING:
397+ self.download_scrolled.hide()
398+ if self.terminal:
399+ self.terminal.show()
400+ self.set_sensitive(True)
401+ else:
402+ self.set_expanded(False)
403+ self.set_sensitive(False)
404+ else:
405+ self.download_scrolled.hide()
406+ if self.terminal:
407+ self.terminal.hide()
408+ self.set_sensitive(False)
409+ self.set_expanded(False)
410+
411+ def _on_terminal_attached_changed(self, transaction, attached):
412+ """Connect the terminal to the pty device"""
413+ if attached and self.terminal:
414+ self.set_sensitive(True)
415+
416+
417+# TODO: needs GIR for vte
418+#class AptTerminal(vte.Terminal):
419+#
420+# def __init__(self, transaction=None):
421+# vte.Terminal.__init__(self)
422+# self._signals = []
423+# self._master, self._slave = pty.openpty()
424+# self._ttyname = os.ttyname(self._slave)
425+# self.set_size(80, 24)
426+# self.set_pty(self._master)
427+# if transaction != None:
428+# self.set_transaction(transaction)
429+#
430+# def set_transaction(self, transaction):
431+# """Connect the status label to the given aptdaemon transaction"""
432+# for sig in self._signals:
433+# gobject.source_remove(sig)
434+# self._signals.append(transaction.connect("terminal-attached-changed",
435+# self._on_terminal_attached_changed))
436+# self._transaction = transaction
437+# self._transaction.set_terminal(self._ttyname)
438+#
439+# def _on_terminal_attached_changed(self, transaction, attached):
440+# """Show the terminal"""
441+# self.set_sensitive(attached)
442+
443+class AptCancelButton(Gtk.Button):
444+ """
445+ Provides a Gtk.Button which allows to cancel a running aptdaemon
446+ transaction
447+ """
448+ def __init__(self, transaction=None):
449+ gobject.GObject.__init__(self)
450+ self.set_use_stock(True)
451+ self.set_label(Gtk.STOCK_CANCEL)
452+ self.set_sensitive(True)
453+ self._signals = []
454+ if transaction != None:
455+ self.set_transaction(transaction)
456+
457+ def set_transaction(self, transaction):
458+ """Connect the status label to the given aptdaemon transaction"""
459+ for sig in self._signals:
460+ gobject.source_remove(sig)
461+ self._signals = []
462+ self._signals.append(transaction.connect("finished", self._on_finished))
463+ self._signals.append(transaction.connect("cancellable-changed",
464+ self._on_cancellable_changed))
465+ self.connect("clicked", self._on_clicked, transaction)
466+
467+ def _on_cancellable_changed(self, transaction, cancellable):
468+ """
469+ Enable the button if cancel is allowed and disable it in the other case
470+ """
471+ self.set_sensitive(cancellable)
472+
473+ def _on_finished(self, transaction, status):
474+ self.set_sensitive(False)
475+
476+ def _on_clicked(self, button, transaction):
477+ transaction.cancel()
478+ self.set_sensitive(False)
479+
480+
481+class AptDownloadsView(Gtk.TreeView):
482+
483+ """A Gtk.TreeView which displays the progress and status of each dowload
484+ of a transaction.
485+ """
486+
487+ COL_TEXT, COL_PROGRESS, COL_URI = range(3)
488+
489+ def __init__(self, transaction=None):
490+ gobject.GObject.__init__(self)
491+ model = Gtk.ListStore.newv([gobject.TYPE_STRING, gobject.TYPE_INT,
492+ gobject.TYPE_STRING])
493+ self.set_model(model)
494+ self.props.headers_visible = False
495+ self.set_rules_hint(True)
496+ self._download_map = {}
497+ self._signals = []
498+ if transaction != None:
499+ self.set_transaction(transaction)
500+ cell_uri = Gtk.CellRendererText()
501+ cell_uri.props.ellipsize = Pango.EllipsizeMode.END
502+ column_download = Gtk.TreeViewColumn(_("File"))
503+ column_download.pack_start(cell_uri, True)
504+ column_download.add_attribute(cell_uri, "markup", self.COL_TEXT)
505+ cell_progress = Gtk.CellRendererProgress()
506+ #TRANSLATORS: header of the progress download column
507+ column_progress = Gtk.TreeViewColumn(_("%"))
508+ column_progress.pack_start(cell_progress, True)
509+ column_progress.set_cell_data_func(cell_progress, self._data_progress, None)
510+ self.append_column(column_progress)
511+ self.append_column(column_download)
512+ self.set_tooltip_column(self.COL_URI)
513+
514+ def set_transaction(self, transaction):
515+ """Connect the download view to the given aptdaemon transaction"""
516+ for sig in self._signals:
517+ gobject.source_remove(sig)
518+ self._signals = []
519+ self._signals.append(transaction.connect("progress-download-changed",
520+ self._on_download_changed))
521+
522+ def _on_download_changed(self, transaction, uri, status, desc, full_size,
523+ downloaded, message):
524+ """Callback for a changed download progress."""
525+ try:
526+ progress = downloaded * 100 / full_size
527+ except ZeroDivisionError:
528+ progress = -1
529+ if status == DOWNLOAD_DONE:
530+ progress = 100
531+ if progress > 100:
532+ progress = 100
533+ text = desc[:]
534+ text += "\n<small>"
535+ #TRANSLATORS: %s is the full size in Bytes, e.g. 198M
536+ if status == DOWNLOAD_FETCHING:
537+ text += _("Downloaded %sB of %sB") % \
538+ (apt_pkg.size_to_str(downloaded),
539+ apt_pkg.size_to_str(full_size))
540+ elif status == DOWNLOAD_DONE:
541+ if full_size != 0:
542+ text += _("Downloaded %sB") % apt_pkg.size_to_str(full_size)
543+ else:
544+ text += _("Downloaded")
545+ else:
546+ text += get_download_status_from_enum(status)
547+ text += "</small>"
548+ model = self.get_model()
549+ try:
550+ iter = self._download_map[uri]
551+ except KeyError:
552+ adj = self.get_vadjustment()
553+ is_scrolled_down = adj.value + adj.page_size == adj.upper
554+ iter = model.append((text, progress, uri))
555+ self._download_map[uri] = iter
556+ if is_scrolled_down:
557+ # If the treeview was scrolled to the end, do this again
558+ # after appending a new item
559+ self.scroll_to_cell(model.get_path(iter), None, False, False, False)
560+ else:
561+ model.set_value(iter, self.COL_TEXT, text)
562+ model.set_value(iter, self.COL_PROGRESS, progress)
563+
564+ def _data_progress(self, column, cell, model, iter, data):
565+ progress = model.get_value(iter, self.COL_PROGRESS)
566+ if progress == -1:
567+ cell.props.pulse = progress
568+ else:
569+ cell.props.value = progress
570+
571+
572+class AptProgressDialog(Gtk.Dialog):
573+ """
574+ Complete progress dialog for long taking aptdaemon transactions, which
575+ features a progress bar, cancel button, status icon and label
576+ """
577+
578+ __gsignals__ = {"finished": (gobject.SIGNAL_RUN_FIRST,
579+ gobject.TYPE_NONE, ())}
580+
581+ def __init__(self, transaction=None, parent=None, terminal=True,
582+ debconf=True):
583+ gobject.GObject.__init__(self, parent=parent)
584+ self._expanded_size = None
585+ self.debconf = debconf
586+ # Setup the dialog
587+ self.set_border_width(6)
588+ self.set_resizable(False)
589+ self.get_content_area().set_spacing(6)
590+ # Setup the cancel button
591+ self.button_cancel = AptCancelButton(transaction)
592+ self.get_action_area().pack_start(self.button_cancel, False, False, 0)
593+ # Setup the status icon, label and progressbar
594+ hbox = Gtk.HBox()
595+ hbox.set_spacing(12)
596+ hbox.set_border_width(6)
597+ self.icon = AptRoleIcon()
598+ hbox.pack_start(self.icon, False, True, 0)
599+ vbox = Gtk.VBox()
600+ vbox.set_spacing(12)
601+ self.label_role = Gtk.Label()
602+ self.label_role.set_alignment(0, 0)
603+ vbox.pack_start(self.label_role, False, True, 0)
604+ vbox_progress = Gtk.VBox()
605+ vbox_progress.set_spacing(6)
606+ self.progress = AptProgressBar()
607+ vbox_progress.pack_start(self.progress, False, True, 0)
608+ self.label = AptStatusLabel()
609+ self.label._on_status_changed(None, STATUS_WAITING)
610+ vbox_progress.pack_start(self.label, False, True, 0)
611+ vbox.pack_start(vbox_progress, False, True, 0)
612+ hbox.pack_start(vbox, True, True, 0)
613+ self.expander = AptDetailsExpander(terminal=terminal)
614+ self.expander.connect("notify::expanded", self._on_expanded)
615+ vbox.pack_start(self.expander, True, True, 0)
616+ self.get_content_area().pack_start(hbox, True, True, 0)
617+ self._transaction = None
618+ self._signals = []
619+ self.set_title("")
620+ self.realize()
621+ self.progress.set_size_request(350, -1)
622+ self.get_window().set_functions(Gdk.WMFunction.MOVE|Gdk.WMFunction.RESIZE)
623+ if transaction != None:
624+ self.set_transaction(transaction)
625+ # catch ESC and behave as if cancel was clicked
626+ self.connect("delete-event", self._on_dialog_delete_event)
627+
628+ def _on_dialog_delete_event(self, dialog, event):
629+ self.button_cancel.clicked()
630+ return True
631+
632+ def _on_expanded(self, expander, param):
633+ # Make the dialog resizable if the expander is expanded
634+ # try to restore a previous size
635+ if not expander.get_expanded():
636+ self._expanded_size = self.get_size()
637+ self.set_resizable(False)
638+ elif self._expanded_size:
639+ self.set_resizable(True)
640+ self.resize(*self._expanded_size)
641+ else:
642+ self.set_resizable(True)
643+
644+ def run(self, attach=False, close_on_finished=True, show_error=True,
645+ reply_handler=None, error_handler=None):
646+ """Run the transaction and show the progress in the dialog.
647+
648+ Keyword arguments:
649+ attach -- do not start the transaction but instead only monitor
650+ an already running one
651+ close_on_finished -- if the dialog should be closed when the
652+ transaction is complete
653+ show_error -- show a dialog with the error message
654+ """
655+ try:
656+ self._run(attach, close_on_finished, show_error)
657+ except Exception, error:
658+ if error_handler:
659+ error_handler(error)
660+ else:
661+ raise
662+ if reply_handler:
663+ reply_handler()
664+
665+ @inline_callbacks
666+ def _run(self, attach, close_on_finished, show_error):
667+ parent = self.get_transient_for()
668+ sig = self._transaction.connect("finished", self._on_finished,
669+ close_on_finished, show_error)
670+ self._signals.append(sig)
671+ if attach:
672+ yield self._transaction.attach()
673+ else:
674+ if self.debconf:
675+ yield self._transaction.set_debconf_frontend("gnome")
676+ yield self._transaction.run()
677+ self.show_all()
678+
679+ def _on_role_changed(self, transaction, role_enum):
680+ """Show the role of the transaction in the dialog interface"""
681+ role = get_role_localised_present_from_enum(role_enum)
682+ self.set_title(role)
683+ self.label_role.set_markup("<big><b>%s</b></big>" % role)
684+
685+ def set_transaction(self, transaction):
686+ """Connect the dialog to the given aptdaemon transaction"""
687+ for sig in self._signals:
688+ gobject.source_remove(sig)
689+ self._signals = []
690+ self._signals.append(transaction.connect("role-changed",
691+ self._on_role_changed))
692+ self._signals.append(transaction.connect("medium-required",
693+ self._on_medium_required))
694+ self._signals.append(transaction.connect("config-file-conflict",
695+ self._on_config_file_conflict))
696+ self._on_role_changed(transaction, transaction.role)
697+ self.progress.set_transaction(transaction)
698+ self.icon.set_transaction(transaction)
699+ self.label.set_transaction(transaction)
700+ self.expander.set_transaction(transaction)
701+ self._transaction = transaction
702+
703+ def _on_medium_required(self, transaction, medium, drive):
704+ dialog = AptMediumRequiredDialog(medium, drive, self)
705+ res = dialog.run()
706+ dialog.hide()
707+ if res == Gtk.ResponseType.OK:
708+ self._transaction.provide_medium(medium)
709+ else:
710+ self._transaction.cancel()
711+
712+ def _on_config_file_conflict(self, transaction, old, new):
713+ dialog = AptConfigFileConflictDialog(old, new, self)
714+ res = dialog.run()
715+ dialog.hide()
716+ if res == Gtk.ResponseType.YES:
717+ self._transaction.resolve_config_file_conflict(old, "replace")
718+ else:
719+ self._transaction.resolve_config_file_conflict(old, "keep")
720+
721+ def _on_finished(self, transaction, status, close, show_error):
722+ if close:
723+ self.hide()
724+ if status == EXIT_FAILED and show_error:
725+ err_dia = AptErrorDialog(self._transaction.error, self)
726+ err_dia.run()
727+ err_dia.hide()
728+ self.emit("finished")
729+
730+
731+class _ExpandableDialog(Gtk.Dialog):
732+
733+ """Dialog with an expander."""
734+
735+ def __init__(self, parent=None, stock_type=None, expanded_child=None,
736+ expander_label=None, title=None, message=None, buttons=None):
737+ """Return an _AptDaemonDialog instance.
738+
739+ Keyword arguments:
740+ parent -- set the dialog transient for the given Gtk.Window
741+ stock_type -- type of the Dialog, defaults to Gtk.STOCK_DIALOG_QUESTION
742+ expanded_child -- Widget which should be expanded
743+ expander_label -- label for the expander
744+ title -- a news header like title of the dialog
745+ message -- the message which should be shown in the dialog
746+ buttons -- tuple containing button text/reponse id pairs, defaults
747+ to a close button
748+ """
749+ if not buttons:
750+ buttons = (Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
751+ gobject.GObject.__init__(self, parent=parent)
752+ self.add_buttons(*buttons)
753+ self.set_resizable(True)
754+ self.set_border_width(6)
755+ self.get_content_area().set_spacing(12)
756+ if not stock_type:
757+ stock_type = Gtk.STOCK_DIALOG_QUESTION
758+ icon = Gtk.Image.new_from_stock(stock_type, Gtk.IconSize.DIALOG)
759+ icon.set_alignment(0 ,0)
760+ hbox_base = Gtk.HBox()
761+ hbox_base.set_spacing(12)
762+ hbox_base.set_border_width(6)
763+ vbox_left = Gtk.VBox()
764+ vbox_left.set_spacing(12)
765+ hbox_base.pack_start(icon, False, True, 0)
766+ hbox_base.pack_start(vbox_left, True, True, 0)
767+ self.label = Gtk.Label()
768+ self.label.set_selectable(True)
769+ self.label.set_alignment(0, 0)
770+ self.label.set_line_wrap(True)
771+ vbox_left.pack_start(self.label, False, True, 0)
772+ self.get_content_area().pack_start(hbox_base, True, True, 0)
773+ # The expander widget
774+ self.expander = Gtk.Expander(label=expander_label)
775+ self.expander.set_spacing(6)
776+ self.expander.set_use_underline(True)
777+ self.expander.connect("notify::expanded", self._on_expanded)
778+ vbox_left.pack_start(self.expander, True, True, 0)
779+ self._expanded_size = self.get_size()
780+ # Set some initial data
781+ text = ""
782+ if title:
783+ text = "<b><big>%s</big></b>" % title
784+ if message:
785+ if text:
786+ text += "\n\n"
787+ text += message
788+ self.label.set_markup(text)
789+ if expanded_child:
790+ self.expander.add(expanded_child)
791+
792+ def _on_expanded(self, expander, param):
793+ if expander.get_expanded():
794+ self.set_resizable(True)
795+ self.resize(*self._expanded_size)
796+ else:
797+ self._expanded_size = self.get_size()
798+ self.set_resizable(False)
799+
800+
801+class AptMediumRequiredDialog(Gtk.MessageDialog):
802+
803+ """Dialog to ask for medium change."""
804+
805+ def __init__(self, medium, drive, parent=None):
806+ gobject.GObject.__init__(self, parent=parent,
807+ type=Gtk.MessageType.INFO)
808+ #TRANSLATORS: %s represents the name of a CD or DVD
809+ text = _("CD/DVD '%s' is required") % medium
810+ #TRANSLATORS: %s is the name of the CD/DVD drive
811+ desc = _("Please insert the above CD/DVD into the drive '%s' to "
812+ "install software packages from it.") % drive
813+ self.set_markup("<big><b>%s</b></big>\n\n%s" % (text, desc))
814+ self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
815+ _("C_ontinue"), Gtk.ResponseType.OK)
816+ self.set_default_response(Gtk.ResponseType.OK)
817+
818+
819+class AptConfirmDialog(Gtk.Dialog):
820+
821+ """Dialog to confirm the changes that would be required by a transaction."""
822+
823+ def __init__(self, trans, cache=None, parent=None):
824+ """Return an AptConfirmDialog instance.
825+
826+ Keyword arguments:
827+ trans -- the transaction of which the dependencies should be shown
828+ cache -- an optional apt.cache.Cache() instance to provide more details
829+ about packages
830+ parent -- set the dialog transient for the given Gtk.Window
831+ """
832+ gobject.GObject.__init__(self)
833+ self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
834+ self.add_button(_("C_ontinue"), Gtk.ResponseType.OK)
835+ self.cache = cache
836+ self.trans = trans
837+ if isinstance(parent, Gdk.Window):
838+ self.realize()
839+ self.window.set_transient_for(parent)
840+ else:
841+ self.set_transient_for(parent)
842+ self.set_resizable(True)
843+ self.set_border_width(6)
844+ self.get_content_area().set_spacing(12)
845+ icon = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_QUESTION,
846+ Gtk.IconSize.DIALOG)
847+ icon.set_alignment(0 ,0)
848+ hbox_base = Gtk.HBox()
849+ hbox_base.set_spacing(12)
850+ hbox_base.set_border_width(6)
851+ vbox_left = Gtk.VBox()
852+ vbox_left.set_spacing(12)
853+ hbox_base.pack_start(icon, False, True, 0)
854+ hbox_base.pack_start(vbox_left, True, True, 0)
855+ self.label = Gtk.Label()
856+ self.label.set_selectable(True)
857+ self.label.set_alignment(0, 0)
858+ vbox_left.pack_start(self.label, False, True, 0)
859+ self.get_content_area().pack_start(hbox_base, True, True, 0)
860+ self.treestore = Gtk.TreeStore(gobject.TYPE_STRING)
861+ self.treeview = Gtk.TreeView.new_with_model(self.treestore)
862+ self.treeview.set_headers_visible(False)
863+ self.treeview.set_rules_hint(True)
864+ self.column = Gtk.TreeViewColumn()
865+ self.treeview.append_column(self.column)
866+ cell_icon = Gtk.CellRendererPixbuf()
867+ self.column.pack_start(cell_icon, False)
868+ self.column.set_cell_data_func(cell_icon, self.render_package_icon, None)
869+ cell_desc = Gtk.CellRendererText()
870+ self.column.pack_start(cell_desc, True)
871+ self.column.set_cell_data_func(cell_desc, self.render_package_desc, None)
872+ self.scrolled = Gtk.ScrolledWindow()
873+ self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
874+ self.scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
875+ self.scrolled.add(self.treeview)
876+ vbox_left.pack_start(self.scrolled, True, True, 0)
877+ self.set_default_response(Gtk.ResponseType.CANCEL)
878+
879+ def _show_changes(self):
880+ """Show a message and the dependencies in the dialog."""
881+ self.treestore.clear()
882+ for index, msg in enumerate([_("Install"),
883+ _("Reinstall"),
884+ _("Remove"),
885+ _("Purge"),
886+ _("Upgrade"),
887+ _("Downgrade"),
888+ _("Skip upgrade")]):
889+ if self.trans.dependencies[index]:
890+ piter = self.treestore.append(None, ["<b>%s</b>" % msg])
891+ for pkg in self.trans.dependencies[index]:
892+ for object in self.map_package(pkg):
893+ self.treestore.append(piter, [str(object)])
894+ # If there is only one type of changes (e.g. only installs) expand the
895+ # tree
896+ #FIXME: adapt the title and message accordingly
897+ #FIXME: Should we have different modes? Only show dependencies, only
898+ # initial packages or both?
899+ msg = _("Please take a look at the list of changes below.")
900+ if len(self.treestore) == 1:
901+ filtered_store = self.treestore.filter_new(Gtk.TreePath.new_first())
902+ self.treeview.expand_all()
903+ self.treeview.set_model(filtered_store)
904+ self.treeview.set_show_expanders(False)
905+ if self.trans.dependencies[PKGS_INSTALL]:
906+ title = _("Additional software has to be installed")
907+ elif self.trans.dependencies[PKGS_REINSTALL]:
908+ title = _("Additional software has to be re-installed")
909+ elif self.trans.dependencies[PKGS_REMOVE]:
910+ title = _("Additional software has to be removed")
911+ elif self.trans.dependencies[PKGS_PURGE]:
912+ title = _("Additional software has to be purged")
913+ elif self.trans.dependencies[PKGS_UPGRADE]:
914+ title = _("Additional software has to be upgraded")
915+ elif self.trans.dependencies[PKGS_DOWNGRADE]:
916+ title = _("Additional software has to be downgraded")
917+ elif self.trans.dependencies[PKGS_KEEP]:
918+ title = _("Updates will be skipped")
919+ if len(filtered_store) < 6:
920+ self.set_resizable(False)
921+ self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER)
922+ else:
923+ self.treeview.set_size_request(350, 200)
924+ else:
925+ title = _("Additional changes are required")
926+ self.treeview.set_size_request(350, 200)
927+ self.treeview.collapse_all()
928+ if self.trans.download:
929+ msg += "\n"
930+ msg += _("%sB will be downloaded in total.") % \
931+ apt_pkg.size_to_str(self.trans.download)
932+ if self.trans.space < 0:
933+ msg += "\n"
934+ msg += _("%sB of disk space will be freed.") % \
935+ apt_pkg.size_to_str(self.trans.space)
936+ elif self.trans.space > 0:
937+ msg += "\n"
938+ msg += _("%sB more disk space will be used.") % \
939+ apt_pkg.size_to_str(self.trans.space)
940+ self.label.set_markup("<b><big>%s</big></b>\n\n%s" % (title, msg))
941+
942+
943+ def map_package(self, pkg):
944+ """Map a package to a different object type, e.g. applications
945+ and return a list of those.
946+
947+ By default return the package itself inside a list.
948+
949+ Override this method if you don't want to store package names
950+ in the treeview.
951+ """
952+ return [pkg]
953+
954+ def render_package_icon(self, column, cell, model, iter, data):
955+ """Data func for the Gtk.CellRendererPixbuf which shows the package.
956+
957+ Override this method if you want to show custom icons for
958+ a package or map it to applications.
959+ """
960+ path = model.get_path(iter)
961+ if path.get_depth() == 0:
962+ cell.props.visible = False
963+ else:
964+ cell.props.visible = True
965+ cell.props.icon_name = "applications-other"
966+
967+ def render_package_desc(self, column, cell, model, iter, data):
968+ """Data func for the Gtk.CellRendererText which shows the package.
969+
970+ Override this method if you want to show more information about
971+ a package or map it to applications.
972+ """
973+ value = model.get_value(iter, 0)
974+ if not value:
975+ return
976+ try:
977+ pkg_name, pkg_version = value.split("=")[0:2]
978+ except ValueError:
979+ pkg_name = value
980+ pkg_version = None
981+ try:
982+ if pkg_version:
983+ text = "%s (%s)\n<small>%s</small>" % (pkg_name, pkg_version,
984+ self.cache[pkg_name].summary)
985+ else:
986+ text = "%s\n<small>%s</small>" % (pkg_name,
987+ self.cache[pkg_name].summary)
988+ except (KeyError, TypeError):
989+ if pkg_version:
990+ text = "%s (%s)" % (pkg_name, pkg_version)
991+ else:
992+ text = "%s" % pkg_name
993+ cell.set_property("markup", text)
994+
995+ def run(self):
996+ self._show_changes()
997+ self.show_all()
998+ return Gtk.Dialog.run(self)
999+
1000+
1001+class AptConfigFileConflictDialog(_ExpandableDialog):
1002+
1003+ """Dialog to resolve conflicts between local and shipped
1004+ configuration files.
1005+ """
1006+
1007+ def __init__(self, from_path, to_path, parent=None):
1008+ self.from_path = from_path
1009+ self.to_path = to_path
1010+ #TRANSLATORS: %s is a file path
1011+ title = _("Replace your changes in '%s' with a later version of "
1012+ "the configuration file?") % from_path
1013+ msg = _("If you don't know why the file is there already, it is "
1014+ "usually safe to replace it.")
1015+ scrolled = Gtk.ScrolledWindow()
1016+ scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
1017+ scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
1018+ self.diffview = DiffView()
1019+ self.diffview.set_size_request(-1, 200)
1020+ scrolled.add(self.diffview)
1021+ _ExpandableDialog.__init__(self, parent=parent,
1022+ expander_label=_("_Changes"),
1023+ expanded_child=scrolled,
1024+ title=title, message=msg,
1025+ buttons=(_("_Keep"), Gtk.ResponseType.NO,
1026+ _("_Replace"), Gtk.ResponseType.YES))
1027+ self.set_default_response(Gtk.ResponseType.NO)
1028+
1029+ def run(self):
1030+ self.show_all()
1031+ self.diffview.show_diff(self.from_path, self.to_path)
1032+ return _ExpandableDialog.run(self)
1033+
1034+
1035+class DiffView(Gtk.TextView):
1036+
1037+ """Shows the difference between two files."""
1038+
1039+ REGEX_RANGE = "^@@ \-(?P<from_start>[0-9]+),(?P<from_context>[0-9]+) " \
1040+ "\+(?P<to_start>[0-9]+),(?P<to_context>[0-9]+) @@"
1041+ ELLIPSIS = "[…]\n"
1042+
1043+ def __init__(self):
1044+ self.buffer = Gtk.TextBuffer()
1045+ gobject.GObject.__init__(self, buffer=self.buffer)
1046+ self.set_property("editable", False)
1047+ self.set_cursor_visible(False)
1048+ tags = self.buffer.get_tag_table()
1049+ #FIXME: How to get better colors?
1050+ tag_default = Gtk.TextTag.new("default")
1051+ tag_default.set_properties(font="Mono")
1052+ tags.add(tag_default)
1053+ tag_add = Gtk.TextTag.new("add")
1054+ tag_add.set_properties(font="Mono",
1055+ background='#8ae234')
1056+ tags.add(tag_add)
1057+ tag_remove = Gtk.TextTag.new("remove")
1058+ tag_remove.set_properties(font="Mono",
1059+ background='#ef2929')
1060+ tags.add(tag_remove)
1061+ tag_num = Gtk.TextTag.new("number")
1062+ tag_num.set_properties(font="Mono",
1063+ background='#eee')
1064+ tags.add(tag_num)
1065+
1066+ def show_diff(self, from_path, to_path):
1067+ """Show the difference between two files."""
1068+ #FIXME: Use gio
1069+ try:
1070+ from_lines = open(from_path).readlines()
1071+ to_lines = open(to_path).readlines()
1072+ except IOError:
1073+ return
1074+
1075+ # helper function to work around current un-introspectability of
1076+ # varargs methods like insert_with_tags_by_name()
1077+ def insert_tagged_text(iter, text, tag):
1078+ #self.buffer.insert_with_tags_by_name(iter, text, tag)
1079+ offset = iter.get_offset()
1080+ self.buffer.insert(iter, text)
1081+ self.buffer.apply_tag_by_name(tag,
1082+ self.buffer.get_iter_at_offset(offset), iter)
1083+
1084+ iter = self.buffer.get_start_iter()
1085+ for line in difflib.unified_diff(from_lines, to_lines, lineterm=""):
1086+ if line.startswith("@@"):
1087+ match = re.match(self.REGEX_RANGE, line)
1088+ line_number = int(match.group("from_start"))
1089+ if line_number > 1:
1090+ insert_tagged_text(iter, self.ELLIPSIS, "default")
1091+ elif line.startswith("---") or line.startswith("+++"):
1092+ continue
1093+ elif line.startswith(" "):
1094+ line_number += 1
1095+ insert_tagged_text(iter, str(line_number), "number")
1096+ insert_tagged_text(iter, line, "default")
1097+ elif line.startswith("-"):
1098+ line_number += 1
1099+ insert_tagged_text(iter, str(line_number), "number")
1100+ insert_tagged_text(iter, line, "remove")
1101+ elif line.startswith("+"):
1102+ spaces = " " * len(str(line_number))
1103+ insert_tagged_text(iter, spaces, "number")
1104+ insert_tagged_text(iter, line, "add")
1105+
1106+
1107+class _DetailsExpanderMessageDialog(Gtk.MessageDialog):
1108+ """
1109+ Common base class for Apt*Dialog
1110+ """
1111+ def __init__(self, text, desc, type, details=None, parent=None):
1112+ gobject.GObject.__init__(self, parent=parent,
1113+ message_type=type,
1114+ buttons=Gtk.ButtonsType.CLOSE)
1115+ self.set_markup("<big><b>%s</b></big>\n\n%s" % (text, desc))
1116+ self.set_details(details)
1117+
1118+ def set_details(self, details):
1119+ if not details:
1120+ return
1121+ #TRANSLATORS: expander label in the error dialog
1122+ expander = Gtk.Expander.new_with_mnemonic(_("_Details"))
1123+ expander.set_spacing(6)
1124+ scrolled = Gtk.ScrolledWindow()
1125+ scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
1126+ scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
1127+ textview = Gtk.TextView()
1128+ buffer = textview.get_buffer()
1129+ buffer.insert_at_cursor(details)
1130+ scrolled.add(textview)
1131+ expander.add(scrolled)
1132+ self.get_message_area().add(expander)
1133+ expander.show_all()
1134+
1135+
1136+class AptMessageDialog(_DetailsExpanderMessageDialog):
1137+ """
1138+ Dialog for aptdaemon messages with details in an expandable text view
1139+ """
1140+ def __init__(self, enum, details=None, parent=None):
1141+ text = get_msg_string_from_enum(enum)
1142+ desc = get_msg_description_from_enum(enum)
1143+ _DetailsExpanderMessageDialog.__init__(self, text, desc,
1144+ Gtk.MessageType.INFO, details, parent)
1145+
1146+class AptErrorDialog(_DetailsExpanderMessageDialog):
1147+ """
1148+ Dialog for aptdaemon errors with details in an expandable text view
1149+ """
1150+ def __init__(self, error=None, parent=None):
1151+ text = get_error_string_from_enum(error.code)
1152+ desc = get_error_description_from_enum(error.code)
1153+ _DetailsExpanderMessageDialog.__init__(self, text, desc,
1154+ Gtk.MessageType.ERROR, error.details, parent)
1155
1156=== added file 'gtk3-demo.py'
1157--- gtk3-demo.py 1970-01-01 00:00:00 +0000
1158+++ gtk3-demo.py 2010-12-02 16:30:34 +0000
1159@@ -0,0 +1,166 @@
1160+#!/usr/bin/env python
1161+# -*- coding: utf-8 -*-
1162+"""
1163+Provides a graphical demo application for aptdaemon
1164+"""
1165+# Copyright (C) 2008-2009 Sebastian Heinlein <sevel@glatzor.de>
1166+#
1167+# Licensed under the GNU General Public License Version 2
1168+#
1169+# This program is free software; you can redistribute it and/or modify
1170+# it under the terms of the GNU General Public License as published by
1171+# the Free Software Foundation; either version 2 of the License, or
1172+# (at your option) any later version.
1173+#
1174+# This program is distributed in the hope that it will be useful,
1175+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1176+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1177+# GNU General Public License for more details.
1178+#
1179+# You should have received a copy of the GNU General Public License
1180+# along with this program; if not, write to the Free Software
1181+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
1182+
1183+__author__ = "Sebastian Heinlein <devel@glatzor.de>"
1184+
1185+from optparse import OptionParser
1186+import logging
1187+
1188+import gobject
1189+from gi.repository import Gtk
1190+Gtk.require_version('3.0')
1191+
1192+import aptdaemon.client
1193+from aptdaemon.enums import *
1194+from aptdaemon.gtk3widgets import AptErrorDialog, \
1195+ AptConfirmDialog, \
1196+ AptProgressDialog
1197+import aptdaemon.errors
1198+
1199+
1200+class AptDaemonDemo:
1201+
1202+ """Provides a graphical test application."""
1203+
1204+ def _run_transaction(self, transaction):
1205+ dia = AptProgressDialog(transaction, parent=self.win)
1206+ dia.run(close_on_finished=True, show_error=True,
1207+ reply_handler=lambda: True,
1208+ error_handler=self._on_error)
1209+
1210+ def _simulate_trans(self, trans):
1211+ trans.simulate(reply_handler=lambda: self._confirm_deps(trans),
1212+ error_handler=self._on_error)
1213+
1214+ def _confirm_deps(self, trans):
1215+ if [pkgs for pkgs in trans.dependencies if pkgs]:
1216+ dia = AptConfirmDialog(trans, parent=self.win)
1217+ res = dia.run()
1218+ dia.hide()
1219+ if res != Gtk.ResponseType.OK:
1220+ return
1221+ self._run_transaction(trans)
1222+
1223+ def _on_error(self, error):
1224+ try:
1225+ raise error
1226+ except aptdaemon.errors.NotAuthorizedError:
1227+ # Silently ignore auth failures
1228+ return
1229+ except aptdaemon.errors.TransactionFailed, error:
1230+ pass
1231+ except Exception, error:
1232+ error = aptdaemon.errors.TransactionFailed(ERROR_UNKNOWN,
1233+ str(error))
1234+ dia = AptErrorDialog(error)
1235+ dia.run()
1236+ dia.hide()
1237+
1238+ def _on_upgrade_clicked(self, *args):
1239+ self.ac.upgrade_system(safe_mode=False,
1240+ reply_handler=self._simulate_trans,
1241+ error_handler=self._on_error)
1242+
1243+ def _on_update_clicked(self, *args):
1244+ self.ac.update_cache(reply_handler=self._run_transaction,
1245+ error_handler=self._on_error)
1246+
1247+ def _on_install_clicked(self, *args):
1248+ self.ac.install_packages([self.package],
1249+ reply_handler=self._simulate_trans,
1250+ error_handler=self._on_error)
1251+
1252+ def _on_install_file_clicked(self, *args):
1253+ chooser = Gtk.FileChooserDialog(parent=self.win,
1254+ action=Gtk.FileChooserAction.OPEN,
1255+ buttons=(Gtk.STOCK_CANCEL,
1256+ Gtk.ResponseType.CANCEL,
1257+ Gtk.STOCK_OPEN,
1258+ Gtk.ResponseType.OK))
1259+ chooser.set_local_only(True)
1260+ chooser.run()
1261+ chooser.hide()
1262+ path = chooser.get_filename()
1263+ self.ac.install_file(path, reply_handler=self._simulate_trans,
1264+ error_handler=self._on_error)
1265+
1266+ def _on_remove_clicked(self, *args):
1267+ self.ac.remove_packages([self.package],
1268+ reply_handler=self._simulate_trans,
1269+ error_handler=self._on_error)
1270+
1271+ def __init__(self, package):
1272+ self.win = Gtk.Window()
1273+ self.package = package
1274+ self.win.set_resizable(False)
1275+ self.win.set_title("Aptdaemon Demo")
1276+ icon_theme = Gtk.IconTheme.get_default()
1277+ try:
1278+ Gtk.window_set_default_icon(icon_theme.load_icon("aptdaemon-setup",
1279+ 32, 0))
1280+ except (gobject.GError, AttributeError):
1281+ pass
1282+ button_update = Gtk.Button.new_with_mnemonic("_Update")
1283+ button_install = Gtk.Button.new_with_mnemonic("_Install %s" % self.package)
1284+ button_install_file = Gtk.Button.new_with_mnemonic("Install _file...")
1285+ button_remove = Gtk.Button.new_with_mnemonic("_Remove %s" % self.package)
1286+ button_upgrade = Gtk.Button.new_with_mnemonic("Upgrade _System")
1287+ button_update.connect("clicked", self._on_update_clicked)
1288+ button_install.connect("clicked", self._on_install_clicked)
1289+ button_install_file.connect("clicked", self._on_install_file_clicked)
1290+ button_remove.connect("clicked", self._on_remove_clicked)
1291+ button_upgrade.connect("clicked", self._on_upgrade_clicked)
1292+ vbox = Gtk.VBox()
1293+ vbox.add(button_update)
1294+ vbox.add(button_install)
1295+ vbox.add(button_install_file)
1296+ vbox.add(button_remove)
1297+ vbox.add(button_upgrade)
1298+ self.win.add(vbox)
1299+ self.loop = gobject.MainLoop()
1300+ self.win.connect("delete-event", lambda w, e: self.loop.quit())
1301+ self.win.show_all()
1302+ self.ac = aptdaemon.client.AptClient()
1303+
1304+ def run(self):
1305+ self.loop.run()
1306+
1307+
1308+def main():
1309+ parser = OptionParser()
1310+ parser.add_option("-p", "--package", default="cw", action="store",
1311+ dest="package",
1312+ help="Use this package for installation and removal")
1313+ parser.add_option("-d", "--debug", default=False, action="store_true",
1314+ help="Verbose debugging")
1315+ options, args = parser.parse_args()
1316+ if options.debug:
1317+ logging.basicConfig(level=logging.DEBUG)
1318+
1319+ demo = AptDaemonDemo(options.package)
1320+ demo.run()
1321+
1322+if __name__ == "__main__":
1323+ main()
1324+
1325+# vim:ts=4:sw=4:et

Subscribers

People subscribed via source and target branches

to status/vote changes: