1
# Copyright (C) 2008-2009 Canonical Ltd.
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License version 3,
5
# as published by the Free Software Foundation.
7
# This program is distributed in the hope that it will be useful,
8
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
# GNU General Public License for more details.
12
# You should have received a copy of the GNU General Public License
13
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
import subprocess, sys
26
from usbcreator.frontends.base import Frontend
27
from usbcreator.misc import *
29
if 'USBCREATOR_LOCAL' in os.environ:
30
ui_path = os.path.join(os.getcwd(), 'gui/usbcreator-gtk.ui')
31
icon_path = os.path.join(os.getcwd(), 'desktop/usb-creator-gtk.png')
33
ui_path = '/usr/share/usb-creator/usbcreator-gtk.ui'
34
icon_path = '/usr/share/pixmaps/usb-creator-gtk.png'
36
gtk.gdk.threads_init()
38
def thread_wrap(func):
39
'''Decorator for functions that will be called by another thread.'''
41
gtk.gdk.threads_enter()
45
gtk.gdk.threads_leave()
48
class GtkFrontend(Frontend):
50
def startup_failure(cls, message):
51
dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR,
52
gtk.BUTTONS_CLOSE, message)
57
def DBusMainLoop(cls):
58
from dbus.mainloop.glib import DBusGMainLoop
59
DBusGMainLoop(set_as_default=True)
61
def __init__(self, backend, img=None, persistent=True):
63
self.all_widgets = set()
65
self.builder = gtk.Builder()
66
self.builder.set_translation_domain('usbcreator')
67
self.builder.add_from_file(ui_path)
69
for widget in self.builder.get_objects():
70
# Taken from ubiquity:
71
# We generally want labels to be selectable so that people can
72
# easily report problems in them
73
# (https://launchpad.net/bugs/41618), but GTK+ likes to put
74
# selectable labels in the focus chain, and I can't seem to turn
75
# this off in glade and have it stick. Accordingly, make sure
76
# labels are unfocusable here.
77
if isinstance(widget, gtk.Label):
78
widget.set_property('can-focus', False)
79
if issubclass(type(widget), gtk.Widget):
80
self.all_widgets.add(widget)
81
setattr(self, gtk.Widget.get_name(widget), widget)
83
gtk.window_set_default_icon_from_file(icon_path)
84
self.builder.connect_signals (self, None)
85
self.cancelbutton.connect('clicked', lambda x: self.warning_dialog.hide())
86
self.exitbutton.connect('clicked', lambda x: self.quit())
87
self.progress_cancel_button.connect('clicked', lambda x: self.warning_dialog.show())
88
def format_value(scale, value):
89
return format_mb_size(value)
90
self.persist_value.set_adjustment(
91
gtk.Adjustment(0, 0, 100, 1, 10, 0))
92
self.persist_value.connect('format-value', format_value)
93
# TODO evand 2009-08-26: DevKit isn't happy with our format code at the moment.
94
self.format_dest.set_sensitive(False)
96
# Connect to backend signals.
97
self.backend = backend
98
self.backend.source_added_cb = self.add_source
99
self.backend.target_added_cb = self.add_target
100
self.backend.source_removed_cb = self.remove_source
101
self.backend.target_removed_cb = self.remove_target
102
self.backend.failure_cb = self.failure
103
self.backend.success_cb = self.success
104
self.backend.install_progress_cb = self.progress
105
self.backend.install_progress_message_cb = self.progress_message
106
self.backend.retry_cb = self.retry
108
self.setup_sources_treeview()
109
self.setup_targets_treeview()
110
self.persist_vbox.set_sensitive(False)
112
#selection = self.source_treeview.get_selection()
113
#selection.connect('changed', lambda x: self.backend.refresh_targets())
114
#selection = self.dest_treeview.get_selection()
115
#selection.connect('changed', self.dest_selection_changed)
118
self.backend.add_image(img)
121
self.persist_disabled.set_active(True)
122
self.backend.detect_devices()
124
gtk.gdk.threads_enter()
126
gtk.gdk.threads_leave()
128
def add_timeout(self, interval, func, *args):
129
'''Add a new timer for function 'func' with optional arguments. Wraps a
130
similar gobject call timeout_add.'''
132
timer = gobject.timeout_add(interval, func, *args)
135
def delete_timeout(self, timer):
136
'''Remove the specified timer. Wraps gobject source_remove call.'''
138
return gobject.source_remove(timer)
140
def add_source(self, source):
141
logging.debug('add_source: %s' % str(source))
142
model = self.source_treeview.get_model()
143
model.append([source])
145
sel = self.source_treeview.get_selection()
146
m, i = sel.get_selected()
150
def add_target(self, target):
151
logging.debug('add_target: %s' % str(target))
152
model = self.dest_treeview.get_model()
153
model.append([target])
155
sel = self.dest_treeview.get_selection()
156
m, i = sel.get_selected()
160
def remove_source(self, source):
161
model = self.source_treeview.get_model()
162
iterator = model.get_iter_first()
164
while iterator is not None:
165
if model.get_value(iterator, 0) == source:
167
iterator = model.iter_next(iterator)
168
if to_delete is not None:
169
model.remove(to_delete)
171
sel = self.source_treeview.get_selection()
172
m, i = sel.get_selected()
176
def remove_target(self, target):
177
model = self.dest_treeview.get_model()
178
iterator = model.get_iter_first()
180
while iterator is not None:
181
if model.get_value(iterator, 0) == target:
183
iterator = model.iter_next(iterator)
184
if to_delete is not None:
185
model.remove(to_delete)
187
sel = self.dest_treeview.get_selection()
188
m, i = sel.get_selected()
192
def get_source(self):
193
'''Returns the UDI of the selected source image.'''
194
sel = self.source_treeview.get_selection()
195
m, i = sel.get_selected()
199
logging.debug('No source selected.')
202
def get_target(self):
203
'''Returns the UDI of the selected target disk or partition.'''
204
sel = self.dest_treeview.get_selection()
205
m, i = sel.get_selected()
209
logging.debug('No target selected.')
212
def get_persistence(self):
213
if self.persist_enabled.get_active() and \
214
self.persist_enabled.state != gtk.STATE_INSENSITIVE:
215
val = self.persist_value.get_value()
220
def get_gnome_drive(self, udi):
221
monitor = gnomevfs.VolumeMonitor()
222
for drive in monitor.get_connected_drives():
223
if drive.get_hal_udi() == udi:
226
def setup_sources_treeview(self):
227
def column_data_func(layout, cell, model, iterator, column):
230
udi = model[iterator][0]
231
dev = self.backend.sources[udi]
233
drive = self.get_gnome_drive(udi)
235
cell.set_property('text', drive.get_display_name())
237
cell.set_property('text', dev['device'])
239
cell.set_property('text', dev['label'])
241
cell.set_property('text', format_size(dev['size']))
243
def pixbuf_data_func(column, cell, model, iterator):
246
udi = model[iterator][0]
247
drive = self.get_gnome_drive(udi)
248
source_type = self.backend.sources[udi]['type']
250
cell.set_property('icon-name', drive.get_icon())
251
elif source_type == SOURCE_ISO:
252
cell.set_property('stock-id', gtk.STOCK_CDROM)
253
elif source_type == SOURCE_IMG:
254
cell.set_property('stock-id', gtk.STOCK_HARDDISK)
256
cell.set_property('stock-id', None)
258
list_store = gtk.ListStore(str)
259
self.source_treeview.set_model(list_store)
261
cell_name = gtk.CellRendererText()
262
cell_pixbuf = gtk.CellRendererPixbuf()
263
column_name = gtk.TreeViewColumn(_('CD-Drive/Image'))
264
column_name.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
265
column_name.pack_start(cell_pixbuf, expand=False)
266
column_name.pack_start(cell_name, expand=True)
267
self.source_treeview.append_column(column_name)
268
column_name.set_cell_data_func(cell_name, column_data_func, 0)
269
column_name.set_cell_data_func(cell_pixbuf, pixbuf_data_func)
271
cell_version = gtk.CellRendererText()
272
column_name = gtk.TreeViewColumn(_('OS Version'), cell_version)
273
column_name.set_cell_data_func(cell_version, column_data_func, 1)
274
column_name.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
275
self.source_treeview.append_column(column_name)
277
cell_size = gtk.CellRendererText()
278
column_name = gtk.TreeViewColumn(_('Size'), cell_size)
279
column_name.set_cell_data_func(cell_size, column_data_func, 2)
280
column_name.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
281
self.source_treeview.append_column(column_name)
283
# Drag and drop support.
284
# FIXME evand 2009-04-28: Anything can be dropped on the source
285
# treeview. Ideally, the user should only be able to drop ISO and IMG
288
def motion_cb(wid, context, x, y, time):
289
context.drag_status(gtk.gdk.ACTION_COPY, time)
292
def drop_cb(w, context, x, y, time):
293
target_list = w.drag_dest_get_target_list()
294
target = w.drag_dest_find_target(context, target_list)
295
selection = w.drag_get_data(context, target)
296
context.finish(True, True)
299
def data_received_cb(w, context, x, y, selection, target_type, timestamp):
300
# FIXME evand 2009-04-28: Use the GNOME VFS? Test with a sshfs
302
file = selection.data.strip('\r\n\x00')
303
if file.startswith('file://'):
305
elif file.startswith('file:'):
307
self.backend.add_image(file)
309
self.source_treeview.drag_dest_set(gtk.gdk.ACTION_DEFAULT,
310
[('text/uri-list', 0, 600)], gtk.gdk.ACTION_COPY)
311
self.source_treeview.connect('drag_motion', motion_cb)
312
self.source_treeview.connect('drag_drop', drop_cb)
313
self.source_treeview.connect('drag-data-received', data_received_cb)
315
def update_target(self, udi):
316
m = self.dest_treeview.get_model()
317
iterator = m.get_iter_first()
318
while iterator is not None:
319
if m.get_value(iterator, 0) == udi:
320
m.row_changed(m.get_path(iterator), iterator)
322
iterator = m.iter_next(iterator)
324
def dest_selection_changed(self, selection):
325
'''The selected partition has changed and the bounds on the persistence
326
slider need to be changed, or the slider needs to be disabled, to
327
reflect the amount of free space on the partition.'''
329
# XXX evand 2009-05-06: I'm tempted to move this into the backend,
330
# should it get any more complicated, but right now most of the work
331
# here is for the frontend.
333
# TODO evand 2009-05-06: When we factor in the difference in size after
334
# Ubuntu CD directories are removed (casper, pool, etc), we should add
335
# it as a property of the partition, as we'll need to factor it in
337
self.persist_vbox.set_sensitive(False)
338
self.persist_enabled_vbox.set_sensitive(False)
342
model, iterator = selection.get_selected()
345
target_udi = model[iterator][0]
346
source_udi = self.get_source()
349
source = self.backend.sources[source_udi]
350
if target_udi in self.backend.targets:
351
# We're dealing with a partition, therefore we need to calculate
352
# how much, if any, extra space can be used for the persistence
354
target = self.backend.targets[target_udi]
355
persist_max = (target['free'] - source['size']) / 1024 / 1024
356
if persist_max > MIN_PERSISTENCE:
357
self.persist_vbox.set_sensitive(True)
358
self.persist_enabled_vbox.set_sensitive(True)
359
self.persist_value.set_range(MIN_PERSISTENCE, persist_max)
361
def setup_targets_treeview(self):
362
def column_data_func(layout, cell, model, iterator, column):
365
udi = model[iterator][0]
366
dev = self.backend.targets[udi]
367
drive = self.get_gnome_drive(udi)
370
cell.set_property('text', drive.get_display_name() )
372
cell.set_property('text', dev['device'])
374
cell.set_property('text', dev['label'])
376
cell.set_property('text', format_size(dev['capacity']))
378
cell.set_property('text', format_size(dev['free']))
380
def pixbuf_data_func(column, cell, model, iterator):
383
udi = model[iterator][0]
384
status = self.backend.targets[udi]['status']
386
if status == NEED_SPACE:
387
cell.set_property('stock-id', gtk.STOCK_DIALOG_WARNING)
388
elif status == CANNOT_USE:
389
# TODO evand 2009-05-05: Implement disabled rows as a
391
cell.set_property('stock-id', gtk.STOCK_DIALOG_ERROR)
393
drive = self.get_gnome_drive(udi)
395
cell.set_property('icon-name', drive.get_icon())
397
cell.set_property('stock-id', None)
399
list_store = gtk.ListStore(str)
400
self.dest_treeview.set_model(list_store)
402
column_name = gtk.TreeViewColumn()
403
column_name.set_title(_('Device'))
404
cell_name = gtk.CellRendererText()
405
cell_pixbuf = gtk.CellRendererPixbuf()
406
column_name.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
407
column_name.pack_start(cell_pixbuf, expand=False)
408
column_name.pack_start(cell_name, expand=True)
409
self.dest_treeview.append_column(column_name)
410
column_name.set_cell_data_func(cell_name, column_data_func, 0)
411
column_name.set_cell_data_func(cell_pixbuf, pixbuf_data_func)
413
cell_name = gtk.CellRendererText()
414
column_name = gtk.TreeViewColumn(_('Label'), cell_name)
415
column_name.set_cell_data_func(cell_name, column_data_func, 1)
416
column_name.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
417
self.dest_treeview.append_column(column_name)
419
cell_capacity = gtk.CellRendererText()
420
column_name = gtk.TreeViewColumn(_('Capacity'), cell_capacity)
421
column_name.set_cell_data_func(cell_capacity, column_data_func, 2)
422
column_name.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
423
self.dest_treeview.append_column(column_name)
425
cell_free = gtk.CellRendererText()
426
column_name = gtk.TreeViewColumn(_('Free Space'), cell_free)
427
column_name.set_cell_data_func(cell_free, column_data_func, 3)
428
column_name.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
429
self.dest_treeview.append_column(column_name)
431
def add_file_source_dialog(self, *args):
433
chooser = gtk.FileChooserDialog(title=None,action=gtk.FILE_CHOOSER_ACTION_OPEN,
434
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
435
for p, n in (('*.iso', _('CD Images')), ('*.img', _('Disk Images'))):
436
filter = gtk.FileFilter()
437
filter.add_pattern(p)
439
chooser.add_filter(filter)
440
# FIXME evand 2009-04-28: I think there's a bug open about this block
441
# of code. Looks fairly wonky.
442
if 'SUDO_USER' in os.environ:
443
folder = os.path.expanduser('~' + os.environ['SUDO_USER'])
445
folder = os.path.expanduser('~')
446
chooser.set_current_folder(folder)
447
response = chooser.run()
448
if response == gtk.RESPONSE_OK:
449
filename = chooser.get_filename()
451
self.backend.add_image(filename)
453
def install(self, widget):
454
source = self.get_source()
455
target = self.get_target()
456
persist = self.get_persistence()
457
# TODO evand 2009-07-31: Make these the default values in the
459
starting_up = _('Starting up...')
460
self.progress_title.set_markup('<big><b>' + starting_up + '</b></big>')
461
self.progress_info.set_text('')
462
if source and target:
463
self.install_window.show()
465
self.backend.install(source, target, persist)
468
def progress(self, complete, remaining, speed):
469
self.progress_bar.set_fraction(complete / 100.0)
470
if remaining and speed:
471
# TODO evand 2009-07-24: Could use a time formatting function
472
# like our human size function.
473
mins = int(remaining / 60)
474
secs = int(remaining % 60)
475
text = _('%d%% complete (%dm%ss remaining)') % \
476
(complete, mins, secs)
477
self.progress_info.set_text(text)
479
self.progress_info.set_text(_('%d%% complete') % complete)
482
def progress_message(self, message):
483
self.progress_title.set_markup('<big><b>' + message + '</b></big>')
487
# TODO evand 2009-07-28: Implement a retry dialog.
488
raise NotImplementedError
490
def quit(self, *args):
491
self.backend.cancel_install()
495
def failure(self, message=None):
496
logging.critical('Installation failed.')
497
# FIXME: evand 2009-07-28: Do we need this?
498
self.warning_dialog.hide()
499
self.install_window.hide()
501
self.failed_dialog_label.set_text(message)
503
message = _('Installation failed.')
504
self.failed_dialog_label.set_text(message)
505
self.failed_dialog.run()
510
self.warning_dialog.hide()
511
self.install_window.hide()
512
self.finished_dialog.run()
515
def notify(self, message):
516
dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_WARNING,
517
gtk.BUTTONS_CLOSE, message)
521
def format_dest_clicked(self, *args):
522
# FIXME evand 2009-04-30: This needs a big warning dialog.
523
model, iterator = self.dest_treeview.get_selection().get_selected()
526
udi = model[iterator][0]
527
self.backend.format(udi)
529
def open_dest_folder(self, *args):
530
model, iterator = self.dest_treeview.get_selection().get_selected()
532
logging.error('Open button pressed but there was no selection.')
534
disk = model[iterator][0]
535
self.backend.open_mountpoint(udi)
537
# vim: set ai et sts=4 tabstop=4 sw=4: