Improved GUI display of AssertionErrors
[zeroinstall/solver.git] / zeroinstall / 0launch-gui / mainwindow.py
blobda857eba50a25202b150e6d0897a035d8ba6147a
1 # Copyright (C) 2009, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import gtk
5 import sys
6 from logging import info, warn
8 from zeroinstall import _, translation
9 from zeroinstall import SafeException
10 from zeroinstall.support import tasks, pretty_size
11 from zeroinstall.injector import download, iface_cache
12 from iface_browser import InterfaceBrowser
13 import dialog
14 from zeroinstall.gtkui import gtkutils
15 from zeroinstall.gtkui import help_box
17 ngettext = translation.ngettext
19 SHOW_PREFERENCES = 0
21 class MainWindow:
22 progress = None
23 progress_area = None
24 browser = None
25 window = None
26 cancel_download_and_run = None
27 driver = None
28 comment = None
29 systray_icon = None
30 systray_icon_blocker = None
32 def __init__(self, driver, widgets, download_only, select_only = False):
33 self.driver = driver
34 self.select_only = select_only
36 def update_ok_state():
37 self.window.set_response_sensitive(gtk.RESPONSE_OK, driver.solver.ready)
38 if driver.solver.ready and self.window.get_focus() is None:
39 run_button.grab_focus()
40 driver.watchers.append(update_ok_state)
42 self.window = widgets.get_widget('main')
43 self.window.set_default_size(gtk.gdk.screen_width() * 2 / 5, 300)
45 self.progress = widgets.get_widget('progress')
46 self.progress_area = widgets.get_widget('progress_area')
47 self.comment = widgets.get_widget('comment')
49 widgets.get_widget('stop').connect('clicked', lambda b: driver.config.handler.abort_all_downloads())
51 self.refresh_button = widgets.get_widget('refresh')
53 # Tree view
54 self.browser = InterfaceBrowser(driver, widgets)
56 prefs = widgets.get_widget('preferences')
57 self.window.get_action_area().set_child_secondary(prefs, True)
59 # Glade won't let me add this to the template!
60 if select_only:
61 run_button = dialog.MixedButton(_("_Select"), gtk.STOCK_EXECUTE, button = gtk.ToggleButton())
62 elif download_only:
63 run_button = dialog.MixedButton(_("_Download"), gtk.STOCK_EXECUTE, button = gtk.ToggleButton())
64 else:
65 run_button = dialog.MixedButton(_("_Run"), gtk.STOCK_EXECUTE, button = gtk.ToggleButton())
66 self.window.add_action_widget(run_button, gtk.RESPONSE_OK)
67 run_button.show_all()
68 run_button.set_can_default(True)
69 self.run_button = run_button
71 run_button.grab_focus()
73 def response(dialog, resp):
74 if resp in (gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT):
75 self.window.destroy()
76 sys.exit(1)
77 elif resp == gtk.RESPONSE_OK:
78 if self.cancel_download_and_run:
79 self.cancel_download_and_run.trigger()
80 if run_button.get_active():
81 self.cancel_download_and_run = tasks.Blocker("cancel downloads")
82 self.download_and_run(run_button, self.cancel_download_and_run)
83 elif resp == gtk.RESPONSE_HELP:
84 gui_help.display()
85 elif resp == SHOW_PREFERENCES:
86 import preferences
87 preferences.show_preferences(driver.config, notify_cb = lambda: driver.solve_with_downloads())
88 self.window.connect('response', response)
89 self.window.realize() # Make busy pointer work, even with --systray
91 def destroy(self):
92 self.window.destroy()
94 def show(self):
95 self.window.show()
97 def set_response_sensitive(self, response, sensitive):
98 self.window.set_response_sensitive(response, sensitive)
100 @tasks.async
101 def download_and_run(self, run_button, cancelled):
102 try:
103 if not self.select_only:
104 downloaded = self.driver.download_uncached_implementations()
106 if downloaded:
107 # We need to wait until everything is downloaded...
108 blockers = [downloaded, cancelled]
109 yield blockers
110 tasks.check(blockers)
112 if cancelled.happened:
113 return
115 uncached = self.driver.get_uncached_implementations()
116 else:
117 uncached = None # (we don't care)
119 if uncached:
120 missing = '\n- '.join([_('%(iface_name)s %(impl_version)s') % {'iface_name': iface.get_name(), 'impl_version': impl.get_version()} for iface, impl in uncached])
121 dialog.alert(self.window, _('Not all downloads succeeded; cannot run program.\n\nFailed to get:') + '\n- ' + missing)
122 else:
123 sels = self.driver.solver.selections
124 doc = sels.toDOM()
125 reply = doc.toxml('utf-8')
126 if sys.version_info[0] > 2:
127 stdout = sys.stdout.buffer
128 else:
129 stdout = sys.stdout
130 stdout.write(('Length:%8x\n' % len(reply)).encode('utf-8') + reply)
131 self.window.destroy()
132 sys.exit(0) # Success
133 except SystemExit:
134 raise
135 except download.DownloadAborted as ex:
136 run_button.set_active(False)
137 # Don't bother reporting this to the user
138 except Exception as ex:
139 run_button.set_active(False)
140 self.report_exception(ex)
142 def update_download_status(self, only_update_visible = False):
143 """Called at regular intervals while there are downloads in progress,
144 and once at the end. Update the display."""
145 monitored_downloads = self.driver.config.handler.monitored_downloads
147 self.browser.update_download_status(only_update_visible)
149 if not monitored_downloads:
150 self.progress_area.hide()
151 self.window.get_window().set_cursor(None)
152 return
154 if not self.progress_area.get_property('visible'):
155 self.progress_area.show()
156 self.window.get_window().set_cursor(gtkutils.get_busy_pointer())
158 any_known = False
159 done = total = self.driver.config.handler.total_bytes_downloaded # Completed downloads
160 n_downloads = self.driver.config.handler.n_completed_downloads
161 # Now add downloads in progress...
162 for x in monitored_downloads:
163 if x.status != download.download_fetching: continue
164 n_downloads += 1
165 if x.expected_size:
166 any_known = True
167 so_far = x.get_bytes_downloaded_so_far()
168 total += x.expected_size or max(4096, so_far) # Guess about 4K for feeds/icons
169 done += so_far
171 progress_text = '%s / %s' % (pretty_size(done), pretty_size(total))
172 self.progress.set_text(
173 ngettext('Downloading one file (%(progress)s)',
174 'Downloading %(number)d files (%(progress)s)', n_downloads)
175 % {'progress': progress_text, 'number': n_downloads})
177 if total == 0 or (n_downloads < 2 and not any_known):
178 self.progress.pulse()
179 else:
180 self.progress.set_fraction(float(done) / total)
182 def set_message(self, message):
183 import pango
184 self.comment.set_text(message)
185 attrs = pango.AttrList()
186 attrs.insert(pango.AttrWeight(pango.WEIGHT_BOLD, end_index = len(message)))
187 self.comment.set_attributes(attrs)
188 self.comment.show()
190 def use_systray_icon(self):
191 try:
192 self.systray_icon = gtk.status_icon_new_from_icon_name("zeroinstall")
193 except Exception as ex:
194 info(_("No system tray support: %s"), ex)
195 else:
196 root_iface = iface_cache.iface_cache.get_interface(self.driver.requirements.interface_uri)
197 self.systray_icon.set_tooltip(_('Checking for updates for %s') % root_iface.get_name())
198 self.systray_icon.connect('activate', self.remove_systray_icon)
199 self.systray_icon_blocker = tasks.Blocker('Tray icon clicked')
201 def remove_systray_icon(self, i = None):
202 assert self.systray_icon, i
203 self.show()
204 self.systray_icon.set_visible(False)
205 self.systray_icon = None
206 self.systray_icon_blocker.trigger()
207 self.systray_icon_blocker = None
209 def report_exception(self, ex, tb = None):
210 if not isinstance(ex, SafeException):
211 if isinstance(ex, AssertionError):
212 # Assertions often don't say that they're errors (and are frequently
213 # blank).
214 ex = repr(ex)
215 if tb is None:
216 warn(ex, exc_info = True)
217 else:
218 warn(ex, exc_info = (type(ex), ex, tb))
219 if self.systray_icon:
220 self.systray_icon.set_blinking(True)
221 self.systray_icon.set_tooltip(str(ex) + '\n' + _('(click for details)'))
222 else:
223 dialog.alert(self.window, str(ex) or repr(ex))
225 gui_help = help_box.HelpBox(_("Injector Help"),
226 (_('Overview'), '\n' +
227 _("""A program is made up of many different components, typically written by different \
228 groups of people. Each component is available in multiple versions. Zero Install is \
229 used when starting a program. Its job is to decide which implementation of each required \
230 component to use.
232 Zero Install starts with the program you want to run (like 'The Gimp') and chooses an \
233 implementation (like 'The Gimp 2.2.0'). However, this implementation \
234 will in turn depend on other components, such as 'GTK' (which draws the menus \
235 and buttons). Thus, it must choose implementations of \
236 each dependency (each of which may require further components, and so on).""")),
238 (_('List of components'), '\n' +
239 _("""The main window displays all these components, and the version of each chosen \
240 implementation. The top-most one represents the program you tried to run, and each direct \
241 child is a dependency. The 'Fetch' column shows the amount of data that needs to be \
242 downloaded, or '(cached)' if it is already on this computer.
244 If you are happy with the choices shown, click on the Download (or Run) button to \
245 download (and run) the program.""")),
247 (_('Choosing different versions'), '\n' +
248 _("""To control which implementations (versions) are chosen you can click on Preferences \
249 and adjust the network policy and the overall stability policy. These settings affect \
250 all programs run using Zero Install.
252 Alternatively, you can edit the policy of an individual component by clicking on the \
253 button at the end of its line in the table and choosing "Show Versions" from the menu. \
254 See that dialog's help text for more information.""") + '\n'),
256 (_('Reporting bugs'), '\n' +
257 _("""To report a bug, right-click over the component which you think contains the problem \
258 and choose 'Report a Bug...' from the menu. If you don't know which one is the cause, \
259 choose the top one (i.e. the program itself). The program's author can reassign the \
260 bug if necessary, or switch to using a different version of the library.""") + '\n'),
262 (_('The cache'), '\n' +
263 _("""Each version of a program that is downloaded is stored in the Zero Install cache. This \
264 means that it won't need to be downloaded again each time you run the program. The \
265 "0store manage" command can be used to view the cache.""") + '\n'),