3
# Copyright (C) 2009 Roderick B. Greening <roderick.greening@gmail.com>
4
# Copyright (C) 2014 Harald Sitter <apachelogger@kubuntu.org>
6
# Based in part on work by:
7
# David Edmundson <kde@davidedmundson.co.uk>
8
# Canonical Ltd. USB Creator Team
10
# This program is free software: you can redistribute it and/or modify
11
# it under the terms of the GNU General Public License version 3,
12
# as published by the Free Software Foundation.
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU General Public License for more details.
19
# You should have received a copy of the GNU General Public License
20
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
# TODO: When pykf5 becomes available qmessagebox should be ported back to kmessagebox
28
from PyQt5.QtCore import *
29
from PyQt5.QtGui import *
30
from PyQt5.QtWidgets import *
32
from usbcreator.frontends.kde.translate import translate
33
uic.properties.Properties._string = translate
37
from usbcreator.frontends.base import Frontend
38
from usbcreator import misc
40
from queue import Queue
42
from Queue import Queue
45
def thread_wrap(func):
46
'''Decorator for functions that will be called by another thread.'''
48
queue.put(lambda: func(*args))
51
class KdeFrontend(Frontend, QObject):
53
def startup_failure(cls, message):
54
#KMessageBox.error(None, message)
55
QMessageBox.critical(None, '', message)
59
def DBusMainLoop(cls):
60
from dbus.mainloop.qt import DBusQtMainLoop
61
DBusQtMainLoop(set_as_default=True)
63
def __init__(self, backend, img=None,
64
allow_system_internal=False):
65
QObject.__init__(self)
67
#our passed vars - keep them private
69
self.__allow_system_internal = allow_system_internal
71
# Perform some initialization
72
self.__initPrivateVars()
76
self.__backend = backend
78
# Connect to backend signals.
79
self.__backend.source_added_cb = self.add_source
80
self.__backend.target_added_cb = self.add_target
81
self.__backend.source_removed_cb = self.remove_source
82
self.__backend.target_removed_cb = self.remove_target
83
self.__backend.failure_cb = self.failure
84
self.__backend.success_cb = self.success
85
self.__backend.install_progress_cb = self.progress
86
self.__backend.install_progress_message_cb = self.progress_message
87
self.__backend.retry_cb = self.retry
88
self.__backend.target_changed_cb = self.update_target
90
#add any file sources passed
91
if self.__img is not None:
92
self.__backend.add_image(misc.text_type(self.__img))
94
downloadsDir = QDir(QStandardPaths.standardLocations(QStandardPaths.DownloadLocation)[0])
96
isoFilter.append("*.iso")
97
for iso in downloadsDir.entryList(isoFilter, QDir.Files):
98
self.__backend.add_image(misc.text_type(downloadsDir.absoluteFilePath(iso)))
101
if not queue.empty():
102
func = queue.get_nowait()
106
self.queue_processor = self.add_timeout(500, test_func, None)
108
self.__backend.detect_devices()
110
self.update_loop = self.add_timeout(2000, self.__backend.update_free)
112
def __initPrivateVars(self):
113
"""Initialize Private Variables"""
116
self.__mainWindow = QDialog()
119
self.__mainWindow_ui = "usbcreator-kde.ui"
121
# init Backend to None - easier to debug...
122
self.__backend = None
124
# Set by add_file_source_dialog, used to auto-select a manually added
126
self.__recently_added_image = None
129
"""Initialize the interface"""
131
# Locate the ui for the main window and load it.
132
if 'USBCREATOR_LOCAL' in os.environ:
133
appdir = os.path.join(os.getcwd(), 'gui')
135
file = QStandardPaths.locate(QStandardPaths.DataLocation, self.__mainWindow_ui)
136
appdir = file[:file.rfind("/")]
137
uic.loadUi(misc.text_type(appdir + "/" + self.__mainWindow_ui), self.__mainWindow)
139
#hide sources if an argument was provided
141
self.__mainWindow.ui_source_list.hide()
142
self.__mainWindow.ui_add_source.hide()
143
self.__mainWindow.source_label.hide()
145
#disable the start button by default
146
self.__mainWindow.ui_start_button.setEnabled(False)
149
self.__mainWindow.ui_quit_button.setIcon(QIcon.fromTheme("application-exit"))
150
self.__mainWindow.ui_start_button.setIcon(QIcon.fromTheme("dialog-ok-apply"))
151
self.__mainWindow.ui_add_source.setIcon(QIcon.fromTheme("media-optical"))
154
self.__mainWindow.ui_add_source.clicked.connect(self.add_file_source_dialog)
155
self.__mainWindow.ui_quit_button.clicked.connect(self.quit)
156
self.__mainWindow.ui_start_button.clicked.connect(self.install)
157
self.__mainWindow.ui_dest_list.currentItemChanged.connect(self.dest_selection_changed)
158
self.__mainWindow.ui_source_list.currentItemChanged.connect(self.source_selection_changed)
160
# FIXME: we need a custom delegate and elide the iso column on the left rather than the right
161
# otherwise long paths will take up the entire space while in fact the image name is the useful bit of information 90% of the time
163
self.__mainWindow.ui_source_list.setSortingEnabled(True)
164
self.__mainWindow.ui_source_list.sortByColumn(0, Qt.AscendingOrder)
165
# Last column dictates width.
166
# Size column gets fixed to whatever the contents is. Since this is
167
# formatted size a la '1 TiB' it pretty much stays within a certain
169
# Image and Version columns are scaled respectively to maximize space
171
# Neither colum is resizable by the user, so additional tooltips are
172
# enabled for the widgetitems (see add_source).
173
self.__mainWindow.ui_source_list.header().setSectionResizeMode(0, QHeaderView.Stretch)
174
self.__mainWindow.ui_source_list.header().setSectionResizeMode(1, QHeaderView.Stretch)
175
self.__mainWindow.ui_source_list.header().setSectionResizeMode(2, QHeaderView.ResizeToContents)
176
self.__mainWindow.ui_dest_list.setSortingEnabled(True)
177
self.__mainWindow.ui_dest_list.sortByColumn(0, Qt.AscendingOrder)
178
# For destinations only stretch the device column.
179
self.__mainWindow.ui_dest_list.header().setSectionResizeMode(0, QHeaderView.Stretch)
180
self.__mainWindow.ui_dest_list.header().setSectionResizeMode(1, QHeaderView.ResizeToContents)
181
self.__mainWindow.ui_dest_list.header().setSectionResizeMode(2, QHeaderView.ResizeToContents)
184
self.__mainWindow.show()
186
def __timeout_callback(self, func, *args):
187
'''Private callback wrapper used by add_timeout'''
189
timer = self.sender()
194
def __fail(self, message=None):
195
'''Handle Failed Install Gracefully'''
197
logging.exception('Installation failed.')
198
self.progress_bar.hide()
200
message = _('Installation failed.')
201
#KMessageBox.error(self.__mainWindow, message)
202
QMessageBox.critical(self.__mainWindow, '', message)
205
def add_timeout(self, interval, func, *args):
206
'''Add a new timer for function 'func' with optional arguments. Mirrors a
207
similar gobject call timeout_add.'''
209
# FIXME: now that we are part of a Qt object, we may be able to alter for builtin timers
211
timer.timeout.connect(lambda: self.__timeout_callback(func, *args))
212
timer.start(interval)
216
def delete_timeout(self, timer):
217
'''Remove the specified timer'''
224
def add_target(self, target):
225
logging.debug('add_target: %s' % misc.text_type(target))
226
new_item = QTreeWidgetItem(self.__mainWindow.ui_dest_list)
227
new_item.setData(0,Qt.UserRole,target)
229
# the new_item lines should be auto triggered onChange to the
230
# TreeWidget when new_item is appended.
231
new_item.setText(0,target)
232
new_item.setIcon(0,QIcon.fromTheme("drive-removable-media-usb-pendrive"))
234
item = self.__mainWindow.ui_dest_list.currentItem()
236
item = self.__mainWindow.ui_dest_list.topLevelItem(0)
238
self.__mainWindow.ui_dest_list.setCurrentItem(item,True)
240
# populate from device data
241
if self.__backend is not None:
242
dev = self.__backend.targets[target]
243
pretty_name = "%s %s (%s)" % (dev['vendor'], dev['model'], dev['device'])
244
new_item.setText(0,pretty_name)
245
new_item.setToolTip(0, new_item.text(0))
246
new_item.setText(1,dev['label'])
247
new_item.setToolTip(1, new_item.text(1))
248
new_item.setText(2,misc.format_size(dev['capacity']))
249
new_item.setToolTip(2, new_item.text(2))
251
def remove_target(self, target):
252
for i in range(0,self.__mainWindow.ui_dest_list.topLevelItemCount()):
253
item = self.__mainWindow.ui_dest_list.topLevelItem(i)
254
if item.data(0,Qt.UserRole) == target:
255
self.__mainWindow.ui_dest_list.takeTopLevelItem(i)
258
if not self.__mainWindow.ui_dest_list.currentItem():
259
item = self.__mainWindow.ui_dest_list.topLevelItem(0)
261
self.__mainWindow.ui_dest_list.setCurrentItem(item,True)
263
def add_source(self, source):
264
logging.debug('add_source: %s' % misc.text_type(source))
265
new_item = QTreeWidgetItem(self.__mainWindow.ui_source_list)
266
new_item.setData(0,Qt.UserRole,source)
268
# the new_item lines should be auto triggered onChange to the TreeWidget
269
# when new_item is appended.
270
new_item.setText(0,source)
271
new_item.setIcon(0,QIcon.fromTheme("media-optical"))
273
item = self.__mainWindow.ui_source_list.currentItem()
275
item = self.__mainWindow.ui_source_list.topLevelItem(0)
277
self.__mainWindow.ui_source_list.setCurrentItem(item,True)
279
# how does this all get added? here or elsewhere...
280
# populate from device data
281
if self.__backend is not None:
282
new_item.setText(0,self.__backend.sources[source]['device'])
283
new_item.setToolTip(0, new_item.text(0))
284
# Strip as some derivates like to have whitespaces/newlines (e.g. netrunner)
285
new_item.setText(1,self.__backend.sources[source]['label'].strip())
286
new_item.setToolTip(1, new_item.text(1))
287
new_item.setText(2,misc.format_size(self.__backend.sources[source]['size']))
288
new_item.setToolTip(2, new_item.text(2))
290
# Iff the new_item was recently added by add_file_source_dialog,
291
# make it the current item.
292
if (self.__recently_added_image != None and
293
self.__backend.sources[source]['device'] == self.__recently_added_image):
294
self.__mainWindow.ui_source_list.setCurrentItem(new_item,True)
295
self.__recently_added_image = None
297
def remove_source(self, source):
298
for i in range(0,self.__mainWindow.ui_source_list.topLevelItemCount()):
299
item = self.__mainWindow.ui_source_list.topLevelItem(i)
300
if item.data(0,Qt.UserRole) == source:
301
self.__mainWindow.ui_source_list.removeItemWidget(item,0)
304
if not self.__mainWindow.ui_source_list.currentItem():
305
item = self.__mainWindow.ui_source_list.topLevelItem(0)
307
self.__mainWindow.ui_source_list.setCurrentItem(item,True)
309
def get_source(self):
310
'''Returns the UDI of the selected source image.'''
311
item = self.__mainWindow.ui_source_list.currentItem()
313
# Must deal in unicode and not QString for backend
314
source = misc.text_type(item.data(0,Qt.UserRole))
317
logging.debug('No source selected.')
320
def get_target(self):
321
'''Returns the UDI of the selected target disk or partition.'''
322
item = self.__mainWindow.ui_dest_list.currentItem()
324
# Must deal in unicode and not QString for backend
325
dest = misc.text_type(item.data(0,Qt.UserRole))
328
logging.debug('No target selected.')
331
def update_target(self, udi):
332
for i in range(0,self.__mainWindow.ui_dest_list.topLevelItemCount()):
333
item = self.__mainWindow.ui_dest_list.topLevelItem(i)
334
if misc.text_type(item.data(0,Qt.UserRole)) == udi:
335
# FIXME: pyqt5 entirely bypasses qt's signals and apparently fails
336
# to do so correctly so the following yields an error
337
# even though it should work just fine (i.e. the signal exists
339
self.__mainWindow.ui_dest_list.itemChanged.emit(item, 0)
341
target = self.__backend.targets[udi]
342
# Update install button state.
343
status = target['status']
344
source = self.__backend.get_current_source()
347
stype = self.__backend.sources[source]['type']
348
if (status == misc.CAN_USE or
349
(self.__mainWindow.ui_start_button.isEnabled() and stype == misc.SOURCE_IMG)):
350
self.__mainWindow.ui_start_button.setEnabled(True)
352
self.__mainWindow.ui_start_button.setEnabled(False)
353
# Update the destination status message.
354
if status == misc.CANNOT_USE:
355
msg = _('The device is not large enough to hold this image.')
358
self.__mainWindow.ui_dest_status.setText(msg)
360
def source_selection_changed(self, current_item, prev_item):
361
'''The selected image has changed we need to refresh targets'''
362
if not self.__backend:
364
if current_item is not None:
365
udi = misc.text_type(current_item.data(0,Qt.UserRole))
368
self.__backend.set_current_source(udi)
369
item = self.__mainWindow.ui_dest_list.currentItem()
370
self.dest_selection_changed(item, None)
372
def dest_selection_changed(self, current_item, prev_item):
373
if not self.__backend:
376
if current_item is None:
379
udi = misc.text_type(current_item.data(0,Qt.UserRole))
380
self.update_target(udi)
382
def add_file_source_dialog(self):
383
self.__recently_added_image = None
385
# This here filter is for kfiledialog, no clue if it will ever make a return
386
#filter = '*.iso|' + _('CD Images') + '\n*.img|' + _('Disk Images')
387
filter = _('CD Images') + '(*.iso)' + ';;' + _('Disk Images') + '(*.img)'
389
downloadPath = QStandardPaths.standardLocations(QStandardPaths.DownloadLocation)[0]
390
openFileName = QFileDialog.getOpenFileName(self.__mainWindow,
395
openFileName = openFileName[0]
396
filename = misc.text_type(openFileName)
401
# If the file is already in the model, simply mark it as selected.
402
for i in range(0, self.__mainWindow.ui_source_list.topLevelItemCount()):
403
item = self.__mainWindow.ui_source_list.topLevelItem(i)
404
if item.text(0) == filename:
405
self.__mainWindow.ui_source_list.setCurrentItem(item, True)
408
self.__recently_added_image = filename
409
self.__backend.add_image(filename)
412
source = self.get_source()
413
target = self.get_target()
414
if (source and target):
415
msgbox = QMessageBox(self.__mainWindow)
416
msgbox.setIcon(QMessageBox.Warning)
417
msgbox.setText(_('Are you sure you want to write the disc image to the device?'))
418
msgbox.setInformativeText(_('All existing data will be lost.'))
419
msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
421
if res != QMessageBox.Yes:
424
self.__mainWindow.hide()
425
self.delete_timeout(self.update_loop)
427
self.progress_bar = QProgressDialog("",_('Cancel'),0,100,self.__mainWindow)
428
#set title of progress window (same as gtk frontend)
429
self.progress_bar.setWindowTitle(_('Installing'))
430
#prevent progress bar from emitting reset on reaching max value (and auto closing)
431
self.progress_bar.setAutoReset(False)
432
#force immediate showing, rather than waiting...
433
self.progress_bar.setMinimumDuration(0)
434
#must disconnect the canceled() SIGNAL, otherwise the progress bar is actually destroyed
435
self.progress_bar.canceled.disconnect(self.progress_bar.cancel)
436
#now we connect our own signal to display a warning dialog instead
437
self.progress_bar.canceled.connect(self.warning_dialog)
438
starting_up = _('Starting up')
439
self.progress_bar.setLabelText(starting_up)
440
self.progress_bar.show()
442
self.__backend.install(source, target,
443
allow_system_internal=self.__allow_system_internal)
447
message = _('You must select both source image and target device first.')
451
def progress(self, complete):
452
# Updating value cause dialog to re-appear from hidden (dunno why)
453
if not self.progress_bar.isHidden():
456
self.progress_bar.setValue(int(complete))
458
self.progress_bar.setLabelText(_('Finishing...'))
461
def progress_message(self, message):
462
self.progress_bar.setLabelText(message)
464
def quit(self, *args):
465
self.__backend.cancel_install()
469
def failure(self, message=None):
475
'''Install completed'''
476
self.progress_bar.hide()
477
text = _('The installation is complete. You may now reboot your '
478
'computer with this device inserted to try or install '
481
QMessageBox.information(self.__mainWindow, '', text)
482
self.__backend.shutdown()
486
def retry(self, message):
489
caption = _('Retry?')
491
#res = KMessageBox.warningYesNo(self.__mainWindow,message,caption)
492
res = QMessageBox.warning(self.__mainWindow, caption, message,
493
QMessageBox.Yes, QMessageBox.No)
495
#return res == KMessageBox.Yes
496
return res == QMessageBox.Yes
498
def notify(self,title):
499
#KMessageBox.sorry(self.__mainWindow,title)
500
QMessageBox.warning(self.__mainWindow, '', title)
502
def warning_dialog(self):
503
'''A warning dialog to show when progress dialog cancel is pressed'''
505
caption = _('Quit the installation?')
506
text = _('Do you really want to quit the installation now?')
508
#hide the progress bar - install will still continue in bg
509
self.progress_bar.hide()
511
#res = KMessageBox.warningYesNo(self.__mainWindow,text,caption)
512
res = QMessageBox.warning(self.__mainWindow, caption, text,
513
QMessageBox.Yes, QMessageBox.No)
515
#if res == KMessageBox.Yes:
516
if res == QMessageBox.Yes:
519
#user chose not to quit, so re-show progress bar
520
self.progress_bar.show()
522
def format_dest_clicked(self):