diff --git a/src/libprs500/gui2/Makefile b/src/libprs500/gui2/Makefile index 1ef49a19be..ed8e636bb4 100644 --- a/src/libprs500/gui2/Makefile +++ b/src/libprs500/gui2/Makefile @@ -1,4 +1,4 @@ -UI = main_ui.py dialogs/metadata_single_ui.py dialogs/metadata_bulk_ui.py +UI = main_ui.py dialogs/metadata_single_ui.py dialogs/metadata_bulk_ui.py dialogs/jobs_ui.py RC = images_rc.pyc %_ui.py : %.ui diff --git a/src/libprs500/gui2/device.py b/src/libprs500/gui2/device.py index 41e021bf01..a3965566be 100644 --- a/src/libprs500/gui2/device.py +++ b/src/libprs500/gui2/device.py @@ -12,9 +12,7 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.Warning -import traceback - -from PyQt4.QtCore import QThread, SIGNAL, QObject, Qt +from PyQt4.QtCore import QThread, SIGNAL, QObject from libprs500.devices.prs500.driver import PRS500 @@ -45,37 +43,6 @@ class DeviceDetector(QThread): device[1] ^= True self.msleep(self.sleep_time) -class DeviceJob(QThread): - ''' - Worker thread that communicates with device. - ''' - def __init__(self, id, mutex, func, *args, **kwargs): - QThread.__init__(self) - self.id = id - self.func = func - self.args = args - self.kwargs = kwargs - self.mutex = mutex - self.result = None - - def run(self): - if self.mutex != None: - self.mutex.lock() - last_traceback, exception = None, None - try: - try: - self.result = self.func(self.progress_update, *self.args, **self.kwargs) - except Exception, err: - exception = err - last_traceback = traceback.format_exc() - finally: - if self.mutex != None: - self.mutex.unlock() - self.emit(SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), - self.id, self.result, exception, last_traceback) - - def progress_update(self, val): - self.emit(SIGNAL('status_update(int)'), int(val)) class DeviceManager(QObject): @@ -87,6 +54,7 @@ class DeviceManager(QObject): def info_func(self): ''' Return callable that returns device information and free space on device''' def get_device_information(updater): + '''Get device information''' self.device.set_progress_reporter(updater) info = self.device.get_device_information(end_session=False) info = [i.replace('\x00', '').replace('\x01', '') for i in info] @@ -98,6 +66,7 @@ class DeviceManager(QObject): def books_func(self): '''Return callable that returns the list of books on device as two booklists''' def books(updater): + '''Get metadata from device''' self.device.set_progress_reporter(updater) mainlist = self.device.books(oncard=False, end_session=False) cardlist = self.device.books(oncard=True) @@ -107,14 +76,17 @@ class DeviceManager(QObject): def sync_booklists_func(self): '''Upload booklists to device''' def sync_booklists(updater, booklists): + '''Sync metadata to device''' self.device.set_progress_reporter(updater) - self.device.sync_booklists(booklists) + self.device.sync_booklists(booklists, end_session=False) + return self.device.card_prefix(end_session=False), self.device.free_space() return sync_booklists def upload_books_func(self): '''Upload books to device''' def upload_books(updater, files, names, on_card=False): - return self.device.upload_books(files, names, on_card, end_session=True) + '''Upload books to device: ''' + return self.device.upload_books(files, names, on_card, end_session=False) return upload_books def add_books_to_metadata(self, locations, metadata, booklists): @@ -123,6 +95,7 @@ class DeviceManager(QObject): def delete_books_func(self): '''Remove books from device''' def delete_books(updater, paths): + '''Delete books from device''' self.device.delete_books(paths, end_session=True) return delete_books diff --git a/src/libprs500/gui2/dialogs/__init__.py b/src/libprs500/gui2/dialogs/__init__.py index ab1d9f6e26..789efa5570 100644 --- a/src/libprs500/gui2/dialogs/__init__.py +++ b/src/libprs500/gui2/dialogs/__init__.py @@ -17,10 +17,11 @@ from PyQt4.QtCore import QObject from PyQt4.QtGui import QDialog -class ModalDialog(QObject): +class Dialog(QObject): def __init__(self, window): QObject.__init__(self, window) self.dialog = QDialog(window) self.accept = self.dialog.accept self.reject = self.dialog.reject - self.window = window \ No newline at end of file + self.window = window + self.isVisible = self.dialog.isVisible diff --git a/src/libprs500/gui2/dialogs/jobs.py b/src/libprs500/gui2/dialogs/jobs.py new file mode 100644 index 0000000000..86778fc517 --- /dev/null +++ b/src/libprs500/gui2/dialogs/jobs.py @@ -0,0 +1,40 @@ +## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +'''Display active jobs''' + +from PyQt4.QtCore import Qt, QObject, SIGNAL + +from libprs500.gui2.dialogs import Dialog +from libprs500.gui2.dialogs.jobs_ui import Ui_JobsDialog +from libprs500 import __appname__ + +class JobsDialog(Ui_JobsDialog, Dialog): + def __init__(self, window, model): + Ui_JobsDialog.__init__(self) + Dialog.__init__(self, window) + self.setupUi(self.dialog) + self.jobs_view.setModel(model) + self.model = model + self.dialog.setWindowModality(Qt.NonModal) + self.dialog.setWindowTitle(__appname__ + ' - Active Jobs') + QObject.connect(self.jobs_view.model(), SIGNAL('modelReset()'), + self.jobs_view.resizeColumnsToContents) + + def show(self): + self.dialog.show() + self.jobs_view.resizeColumnsToContents() + + def hide(self): + self.dialog.hide() diff --git a/src/libprs500/gui2/dialogs/jobs.ui b/src/libprs500/gui2/dialogs/jobs.ui new file mode 100644 index 0000000000..59e34bba4f --- /dev/null +++ b/src/libprs500/gui2/dialogs/jobs.ui @@ -0,0 +1,50 @@ + + JobsDialog + + + + 0 + 0 + 633 + 542 + + + + Active Jobs + + + :/images/jobs.svg + + + + + + Qt::NoContextMenu + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + 32 + 32 + + + + + + + + + + + diff --git a/src/libprs500/gui2/dialogs/metadata_bulk.py b/src/libprs500/gui2/dialogs/metadata_bulk.py index 923670cb16..6a31696344 100644 --- a/src/libprs500/gui2/dialogs/metadata_bulk.py +++ b/src/libprs500/gui2/dialogs/metadata_bulk.py @@ -18,13 +18,13 @@ from PyQt4.QtCore import SIGNAL, QObject from libprs500.gui2 import qstring_to_unicode -from libprs500.gui2.dialogs import ModalDialog +from libprs500.gui2.dialogs import Dialog from libprs500.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog -class MetadataBulkDialog(Ui_MetadataBulkDialog, ModalDialog): +class MetadataBulkDialog(Ui_MetadataBulkDialog, Dialog): def __init__(self, window, rows, db): Ui_MetadataBulkDialog.__init__(self) - ModalDialog.__init__(self, window) + Dialog.__init__(self, window) self.setupUi(self.dialog) self.db = db self.ids = [ db.id(r) for r in rows] @@ -42,7 +42,7 @@ class MetadataBulkDialog(Ui_MetadataBulkDialog, ModalDialog): id, name = i self.series.addItem(name) - + self.series.lineEdit().setText('') self.dialog.exec_() diff --git a/src/libprs500/gui2/dialogs/metadata_single.py b/src/libprs500/gui2/dialogs/metadata_single.py index 66ee796176..fa022ef2c4 100644 --- a/src/libprs500/gui2/dialogs/metadata_single.py +++ b/src/libprs500/gui2/dialogs/metadata_single.py @@ -24,7 +24,7 @@ from PyQt4.Qt import QObject, QPixmap, QListWidgetItem, QErrorMessage from libprs500.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \ choose_files, pixmap_to_data, BOOK_EXTENSIONS, choose_images -from libprs500.gui2.dialogs import ModalDialog +from libprs500.gui2.dialogs import Dialog from libprs500.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog class Format(QListWidgetItem): @@ -34,7 +34,7 @@ class Format(QListWidgetItem): QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), ext.upper(), parent, QListWidgetItem.UserType) -class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog): +class MetadataSingleDialog(Ui_MetadataSingleDialog, Dialog): def select_cover(self, checked): files = choose_images(self.window, 'change cover dialog', @@ -121,7 +121,7 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog): def __init__(self, window, row, db): Ui_MetadataSingleDialog.__init__(self) - ModalDialog.__init__(self, window) + Dialog.__init__(self, window) self.setupUi(self.dialog) self.splitter.setStretchFactor(100, 1) self.db = db diff --git a/src/libprs500/gui2/images/trash.svg b/src/libprs500/gui2/images/trash.svg index 9a0c7c2bbc..1ff9c4d874 100644 --- a/src/libprs500/gui2/images/trash.svg +++ b/src/libprs500/gui2/images/trash.svg @@ -1380,7 +1380,6 @@ rx="162.459" cy="68" cx="181.01601" - style="fill:url(#linearGradient11897)" sodipodi:cx="181.01601" sodipodi:cy="68" sodipodi:rx="162.459" @@ -2190,7 +2189,7 @@ + style="fill-opacity:1" /> 0 + def run_device_job(self, slot, callable, *args, **kwargs): ''' Run a job to communicate with the device. @@ -53,7 +119,9 @@ class JobManager(QAbstractTableModel): @param args: The arguments to pass to callable @param kwargs: The keyword arguments to pass to callable ''' - job = self.create_job(DeviceJob, self.device_lock, callable, *args, **kwargs) + desc = callable.__doc__ if callable.__doc__ else '' + desc += kwargs.pop('job_extra_description', '') + job = self.create_job(DeviceJob, desc, self.device_lock, callable, *args, **kwargs) QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), self.job_done) if slot: @@ -68,15 +136,18 @@ class JobManager(QAbstractTableModel): ''' self.job_remove_lock.lock() try: - job = self.jobs.pop(id) + job = self.jobs.pop(id) + self.reset() self.cleanup_lock.lock() self.cleanup[id] = job self.cleanup_lock.unlock() + self.emit(SIGNAL('job_done(int)'), id) if len(self.jobs.keys()) == 0: self.emit(SIGNAL('no_more_jobs()')) + finally: self.job_remove_lock.unlock() - + def cleanup_jobs(self): self.cleanup_lock.lock() toast = [] @@ -86,4 +157,53 @@ class JobManager(QAbstractTableModel): for id in toast: self.cleanup.pop(id) self.cleanup_lock.unlock() + + + def rowCount(self, parent): + return len(self.jobs) + + def columnCount(self, parent): + return 3 + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return NONE + if orientation == Qt.Horizontal: + if section == 0: text = "Job" + elif section == 1: text = "Status" + elif section == 2: text = "Progress" + return QVariant(self.trUtf8(text)) + else: + return QVariant(section+1) + + def data(self, index, role): + if role not in (Qt.DisplayRole, Qt.DecorationRole): + return NONE + row, col = index.row(), index.column() + keys = self.jobs.keys() + keys.sort() + job = self.jobs[keys[row]] + if role == Qt.DisplayRole: + if col == 0: + return QVariant('\n'.join(self.wrapper.wrap(job.description))) + if col == 1: + status = 'Waiting' + if job.isRunning(): + status = 'Working' + if job.isFinished(): + status = 'Done' + return QVariant(status) + if col == 2: + p = str(job.percent_done) + r'%' + return QVariant(p) + if role == Qt.DecorationRole and col == 0: + return self.device_job_icon if isinstance(job, DeviceJob) else self.job_icon + return NONE + + def status_update(self, id, progress): + keys = self.jobs.keys() + keys.sort() + row = keys.index(id) + index = self.index(row, 2) + self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), index, index) \ No newline at end of file diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py index 5b81ce3cf3..b16bd9b0a2 100644 --- a/src/libprs500/gui2/main.py +++ b/src/libprs500/gui2/main.py @@ -16,7 +16,7 @@ import os, tempfile, sys from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ QSettings, QVariant, QSize, QThread -from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon +from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox from PyQt4.QtSvg import QSvgRenderer from libprs500 import __version__, __appname__ @@ -32,6 +32,8 @@ from libprs500.gui2.status import StatusBar from libprs500.gui2.jobs import JobManager, JobException from libprs500.gui2.dialogs.metadata_single import MetadataSingleDialog from libprs500.gui2.dialogs.metadata_bulk import MetadataBulkDialog +from libprs500.gui2.dialogs.jobs import JobsDialog + class Main(QObject, Ui_MainWindow): @@ -51,6 +53,7 @@ class Main(QObject, Ui_MainWindow): self.setupUi(window) self.read_settings() self.job_manager = JobManager() + self.jobs_dialog = JobsDialog(self.window, self.job_manager) self.device_manager = None self.upload_memory = {} self.delete_memory = {} @@ -64,10 +67,10 @@ class Main(QObject, Ui_MainWindow): self.vanity.setText(self.vanity_template.arg(' ')) ####################### Status Bar ##################### - self.status_bar = StatusBar() + self.status_bar = StatusBar(self.jobs_dialog) self.window.setStatusBar(self.status_bar) QObject.connect(self.job_manager, SIGNAL('job_added(int)'), self.status_bar.job_added) - QObject.connect(self.job_manager, SIGNAL('no_more_jobs()'), self.status_bar.no_more_jobs) + QObject.connect(self.job_manager, SIGNAL('job_done(int)'), self.status_bar.job_done) ####################### Setup Toolbar ##################### sm = QMenu() @@ -195,6 +198,8 @@ class Main(QObject, Ui_MainWindow): if exception: self.job_exception(id, exception, formatted_traceback) return + cp, fs = result + self.location_view.model().update_devices(cp, fs) ############################################################################ @@ -236,9 +241,11 @@ class Main(QObject, Ui_MainWindow): Upload books to device. @param files: List of either paths to files or file like objects ''' + titles = ', '.join([i['title'] for i in metadata]) id = self.job_manager.run_device_job(self.books_uploaded, self.device_manager.upload_books_func(), - files, names, on_card=on_card + files, names, on_card=on_card, + job_extra_description=titles ) self.upload_memory[id] = metadata @@ -434,8 +441,21 @@ class Main(QObject, Ui_MainWindow): settings.endGroup() def close_event(self, e): - self.write_settings() - e.accept() + msg = 'There are active jobs. Are you sure you want to quit?' + if self.job_manager.has_device_jobs(): + msg = '

'+__appname__ + ' is communicating with the device!
'+\ + 'Quitting may cause corruption on the device.
'+\ + 'Are you sure you want to quit?' + if self.job_manager.has_jobs(): + d = QMessageBox(QMessageBox.Warning, 'WARNING: Active jobs', msg, + QMessageBox.Yes|QMessageBox.No, self.window) + d.setIconPixmap(QPixmap(':/images/dialog_warning.svg')) + d.setDefaultButton(QMessageBox.No) + if d.exec_() == QMessageBox.Yes: + self.write_settings() + e.accept() + else: + e.ignore() def main(): lock = os.path.join(tempfile.gettempdir(),"libprs500_gui_lock") diff --git a/src/libprs500/gui2/status.py b/src/libprs500/gui2/status.py index b7fd162c2c..6b0e8e8580 100644 --- a/src/libprs500/gui2/status.py +++ b/src/libprs500/gui2/status.py @@ -12,10 +12,14 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from libprs500.gui2.dialogs.jobs import JobsDialog + import textwrap -from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap +from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \ + QVBoxLayout, QSizePolicy from PyQt4.QtCore import Qt, QSize +from libprs500.gui2 import qstring_to_unicode class BookInfoDisplay(QFrame): class BookCoverDisplay(QLabel): @@ -39,7 +43,7 @@ class BookInfoDisplay(QFrame): def __init__(self): QLabel.__init__(self) self.setTextInteractionFlags(Qt.TextSelectableByMouse) - self.setText('')#
row 1row 2
fsfdsfsfsfsfsfsdfsffsfsd
') + self.setText('') def __init__(self, clear_message): QFrame.__init__(self) @@ -74,35 +78,83 @@ class BookInfoDisplay(QFrame): self.clear_message() self.setVisible(True) -class MovieButton(QLabel): - def __init__(self, movie): +class BusyIndicator(QLabel): + def __init__(self, movie, jobs_dialog): QLabel.__init__(self) - self.movie = movie + self.setCursor(Qt.PointingHandCursor) + self.setToolTip('Click to see list of active jobs.') self.setMovie(movie) - self.movie.start() - self.movie.setPaused(True) + movie.start() + movie.setPaused(True) + self.jobs_dialog = jobs_dialog + + + def mouseReleaseEvent(self, event): + if self.jobs_dialog.isVisible(): + self.jobs_dialog.hide() + else: + self.jobs_dialog.show() + + +class MovieButton(QFrame): + def __init__(self, movie, jobs_dialog): + QFrame.__init__(self) + self.setLayout(QVBoxLayout()) + self.movie_widget = BusyIndicator(movie, jobs_dialog) + self.movie = movie + self.layout().addWidget(self.movie_widget) + self.jobs = QLabel('Jobs: 0') + self.jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom) + self.layout().addWidget(self.jobs) + self.layout().setAlignment(self.jobs, Qt.AlignHCenter) + self.jobs.setMargin(0) + self.layout().setMargin(0) + self.jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + class StatusBar(QStatusBar): - def __init__(self): + def __init__(self, jobs_dialog): QStatusBar.__init__(self) - self.movie_button = MovieButton(QMovie(':/images/jobs-animated.mng')) + self.movie_button = MovieButton(QMovie(':/images/jobs-animated.mng'), jobs_dialog) self.addPermanentWidget(self.movie_button) self.book_info = BookInfoDisplay(self.clearMessage) self.addWidget(self.book_info) + + def jobs(self): + src = qstring_to_unicode(self.movie_button.jobs.text()) + return int(src.rpartition(':')[2].lstrip()) + def job_added(self, id): + jobs = self.movie_button.jobs + src = qstring_to_unicode(jobs.text()) + num = self.jobs() + nnum = num+1 + text = src.replace(str(num), str(nnum)) + jobs.setText(text) if self.movie_button.movie.state() == QMovie.Paused: self.movie_button.movie.setPaused(False) + def job_done(self, id): + jobs = self.movie_button.jobs + src = qstring_to_unicode(jobs.text()) + num = self.jobs() + nnum = num-1 + text = src.replace(str(num), str(nnum)) + jobs.setText(text) + if nnum == 0: + self.no_more_jobs() + def no_more_jobs(self): if self.movie_button.movie.state() == QMovie.Running: self.movie_button.movie.setPaused(True) - self.movie_button.movie.jumpToFrame(0) # This causes MNG error 11, but seems to work regardless + # This causes MNG error 11 + #self.movie_button.movie.jumpToFrame(0) if __name__ == '__main__': # Used to create the animated status icon - from PyQt4.Qt import QApplication, QPainter, QSvgRenderer, QPixmap, QColor + from PyQt4.Qt import QApplication, QPainter, QSvgRenderer, QColor from subprocess import check_call import os app = QApplication([])