Buff up jobs management

This commit is contained in:
Kovid Goyal 2007-08-04 23:23:10 +00:00
parent 1461146624
commit ef0df0e877
11 changed files with 327 additions and 72 deletions

View File

@ -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 RC = images_rc.pyc
%_ui.py : %.ui %_ui.py : %.ui

View File

@ -12,9 +12,7 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.Warning ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.Warning
import traceback from PyQt4.QtCore import QThread, SIGNAL, QObject
from PyQt4.QtCore import QThread, SIGNAL, QObject, Qt
from libprs500.devices.prs500.driver import PRS500 from libprs500.devices.prs500.driver import PRS500
@ -45,37 +43,6 @@ class DeviceDetector(QThread):
device[1] ^= True device[1] ^= True
self.msleep(self.sleep_time) 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): class DeviceManager(QObject):
@ -87,6 +54,7 @@ class DeviceManager(QObject):
def info_func(self): def info_func(self):
''' Return callable that returns device information and free space on device''' ''' Return callable that returns device information and free space on device'''
def get_device_information(updater): def get_device_information(updater):
'''Get device information'''
self.device.set_progress_reporter(updater) self.device.set_progress_reporter(updater)
info = self.device.get_device_information(end_session=False) info = self.device.get_device_information(end_session=False)
info = [i.replace('\x00', '').replace('\x01', '') for i in info] info = [i.replace('\x00', '').replace('\x01', '') for i in info]
@ -98,6 +66,7 @@ class DeviceManager(QObject):
def books_func(self): def books_func(self):
'''Return callable that returns the list of books on device as two booklists''' '''Return callable that returns the list of books on device as two booklists'''
def books(updater): def books(updater):
'''Get metadata from device'''
self.device.set_progress_reporter(updater) self.device.set_progress_reporter(updater)
mainlist = self.device.books(oncard=False, end_session=False) mainlist = self.device.books(oncard=False, end_session=False)
cardlist = self.device.books(oncard=True) cardlist = self.device.books(oncard=True)
@ -107,14 +76,17 @@ class DeviceManager(QObject):
def sync_booklists_func(self): def sync_booklists_func(self):
'''Upload booklists to device''' '''Upload booklists to device'''
def sync_booklists(updater, booklists): def sync_booklists(updater, booklists):
'''Sync metadata to device'''
self.device.set_progress_reporter(updater) 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 return sync_booklists
def upload_books_func(self): def upload_books_func(self):
'''Upload books to device''' '''Upload books to device'''
def upload_books(updater, files, names, on_card=False): 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 return upload_books
def add_books_to_metadata(self, locations, metadata, booklists): def add_books_to_metadata(self, locations, metadata, booklists):
@ -123,6 +95,7 @@ class DeviceManager(QObject):
def delete_books_func(self): def delete_books_func(self):
'''Remove books from device''' '''Remove books from device'''
def delete_books(updater, paths): def delete_books(updater, paths):
'''Delete books from device'''
self.device.delete_books(paths, end_session=True) self.device.delete_books(paths, end_session=True)
return delete_books return delete_books

View File

@ -17,10 +17,11 @@
from PyQt4.QtCore import QObject from PyQt4.QtCore import QObject
from PyQt4.QtGui import QDialog from PyQt4.QtGui import QDialog
class ModalDialog(QObject): class Dialog(QObject):
def __init__(self, window): def __init__(self, window):
QObject.__init__(self, window) QObject.__init__(self, window)
self.dialog = QDialog(window) self.dialog = QDialog(window)
self.accept = self.dialog.accept self.accept = self.dialog.accept
self.reject = self.dialog.reject self.reject = self.dialog.reject
self.window = window self.window = window
self.isVisible = self.dialog.isVisible

View File

@ -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()

View File

@ -0,0 +1,50 @@
<ui version="4.0" >
<class>JobsDialog</class>
<widget class="QDialog" name="JobsDialog" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>633</width>
<height>542</height>
</rect>
</property>
<property name="windowTitle" >
<string>Active Jobs</string>
</property>
<property name="windowIcon" >
<iconset resource="../images.qrc" >:/images/jobs.svg</iconset>
</property>
<layout class="QVBoxLayout" >
<item>
<widget class="QTableView" name="jobs_view" >
<property name="contextMenuPolicy" >
<enum>Qt::NoContextMenu</enum>
</property>
<property name="editTriggers" >
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionMode" >
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize" >
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../images.qrc" />
</resources>
<connections/>
</ui>

View File

@ -18,13 +18,13 @@
from PyQt4.QtCore import SIGNAL, QObject from PyQt4.QtCore import SIGNAL, QObject
from libprs500.gui2 import qstring_to_unicode 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 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): def __init__(self, window, rows, db):
Ui_MetadataBulkDialog.__init__(self) Ui_MetadataBulkDialog.__init__(self)
ModalDialog.__init__(self, window) Dialog.__init__(self, window)
self.setupUi(self.dialog) self.setupUi(self.dialog)
self.db = db self.db = db
self.ids = [ db.id(r) for r in rows] self.ids = [ db.id(r) for r in rows]
@ -42,7 +42,7 @@ class MetadataBulkDialog(Ui_MetadataBulkDialog, ModalDialog):
id, name = i id, name = i
self.series.addItem(name) self.series.addItem(name)
self.series.lineEdit().setText('')
self.dialog.exec_() self.dialog.exec_()

View File

@ -24,7 +24,7 @@ from PyQt4.Qt import QObject, QPixmap, QListWidgetItem, QErrorMessage
from libprs500.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \ from libprs500.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \
choose_files, pixmap_to_data, BOOK_EXTENSIONS, choose_images 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 from libprs500.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
class Format(QListWidgetItem): class Format(QListWidgetItem):
@ -34,7 +34,7 @@ class Format(QListWidgetItem):
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
ext.upper(), parent, QListWidgetItem.UserType) ext.upper(), parent, QListWidgetItem.UserType)
class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog): class MetadataSingleDialog(Ui_MetadataSingleDialog, Dialog):
def select_cover(self, checked): def select_cover(self, checked):
files = choose_images(self.window, 'change cover dialog', files = choose_images(self.window, 'change cover dialog',
@ -121,7 +121,7 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, ModalDialog):
def __init__(self, window, row, db): def __init__(self, window, row, db):
Ui_MetadataSingleDialog.__init__(self) Ui_MetadataSingleDialog.__init__(self)
ModalDialog.__init__(self, window) Dialog.__init__(self, window)
self.setupUi(self.dialog) self.setupUi(self.dialog)
self.splitter.setStretchFactor(100, 1) self.splitter.setStretchFactor(100, 1)
self.db = db self.db = db

View File

@ -1380,7 +1380,6 @@
rx="162.459" rx="162.459"
cy="68" cy="68"
cx="181.01601" cx="181.01601"
style="fill:url(#linearGradient11897)"
sodipodi:cx="181.01601" sodipodi:cx="181.01601"
sodipodi:cy="68" sodipodi:cy="68"
sodipodi:rx="162.459" sodipodi:rx="162.459"
@ -2190,7 +2189,7 @@
<path <path
d="M 188.42089,-54.769775 C 170.74798,-54.769775 156.4209,-40.44269 156.4209,-22.76977 C 156.4209,-5.09686 170.74798,9.230225 188.42089,9.230225 C 206.09381,9.230225 220.4209,-5.09686 220.4209,-22.76977 C 220.4209,-40.44269 206.09381,-54.769775 188.42089,-54.769775 z M 188.42089,-11.22879 C 182.04713,-11.22879 176.87991,-16.396005 176.87991,-22.76977 C 176.87991,-29.14407 182.04713,-34.31076 188.42089,-34.31076 C 194.79414,-34.31076 199.96188,-29.14407 199.96188,-22.76977 C 199.96188,-16.396005 194.79414,-11.22879 188.42089,-11.22879 z" d="M 188.42089,-54.769775 C 170.74798,-54.769775 156.4209,-40.44269 156.4209,-22.76977 C 156.4209,-5.09686 170.74798,9.230225 188.42089,9.230225 C 206.09381,9.230225 220.4209,-5.09686 220.4209,-22.76977 C 220.4209,-40.44269 206.09381,-54.769775 188.42089,-54.769775 z M 188.42089,-11.22879 C 182.04713,-11.22879 176.87991,-16.396005 176.87991,-22.76977 C 176.87991,-29.14407 182.04713,-34.31076 188.42089,-34.31076 C 194.79414,-34.31076 199.96188,-29.14407 199.96188,-22.76977 C 199.96188,-16.396005 194.79414,-11.22879 188.42089,-11.22879 z"
id="path27365" id="path27365"
style="fill:url(#linearGradient27367);fill-opacity:1" /> style="fill-opacity:1" />
</clipPath> </clipPath>
<linearGradient <linearGradient
inkscape:collect="always" inkscape:collect="always"

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -12,14 +12,66 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import traceback, textwrap
from PyQt4.QtCore import QAbstractTableModel, QMutex, QObject, SIGNAL from PyQt4.QtCore import QAbstractTableModel, QMutex, QObject, SIGNAL, Qt, \
QVariant, QThread
from PyQt4.QtGui import QIcon
from libprs500.gui2.device import DeviceJob from libprs500.gui2 import NONE
class JobException(Exception): class JobException(Exception):
pass pass
class Job(QThread):
''' Class to run a function in a separate thread with optional mutex based locking.'''
def __init__(self, id, description, mutex, func, *args, **kwargs):
'''
@param id: Number. Id of this thread.
@param description: String. Description of this job.
@param mutex: A QMutex or None. Is locked before function is run.
@param func: A callable that should be executed in this thread.
'''
QThread.__init__(self)
self.id = id
self.func = func
self.description = description if description else 'Device Job #' + str(self.id)
self.args = args
self.kwargs = kwargs
self.mutex = mutex
self.result = None
self.percent_done = 0
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.percent_done = val
self.emit(SIGNAL('status_update(int, int)'), self.id, int(val))
class DeviceJob(Job):
'''
Jobs that involve communication with the device.
'''
def __init__(self, id, description, mutex, func, *args, **kwargs):
Job.__init__(self, id, description, mutex, func, *args, **kwargs)
class JobManager(QAbstractTableModel): class JobManager(QAbstractTableModel):
def __init__(self): def __init__(self):
@ -31,19 +83,33 @@ class JobManager(QAbstractTableModel):
self.device_lock = QMutex() self.device_lock = QMutex()
self.cleanup_lock = QMutex() self.cleanup_lock = QMutex()
self.cleanup = {} self.cleanup = {}
self.device_job_icon = QVariant(QIcon(':/images/reader.svg'))
self.job_icon = QVariant(QIcon(':/images/jobs.svg'))
self.wrapper = textwrap.TextWrapper(width=40)
def create_job(self, job_class, lock, *args, **kwargs): def create_job(self, job_class, description, lock, *args, **kwargs):
self.job_create_lock.lock() self.job_create_lock.lock()
try: try:
self.next_id += 1 self.next_id += 1
job = job_class(self.next_id, lock, *args, **kwargs) job = job_class(self.next_id, description, lock, *args, **kwargs)
QObject.connect(job, SIGNAL('finished()'), self.cleanup_jobs) QObject.connect(job, SIGNAL('finished()'), self.cleanup_jobs)
QObject.connect(job, SIGNAL('status_update(int, int)'), self.status_update)
self.jobs[self.next_id] = job self.jobs[self.next_id] = job
self.emit(SIGNAL('job_added(int)'), self.next_id) self.emit(SIGNAL('job_added(int)'), self.next_id)
self.reset()
return job return job
finally: finally:
self.job_create_lock.unlock() self.job_create_lock.unlock()
def has_device_jobs(self):
for job in self.jobs.values():
if isinstance(job, DeviceJob):
return True
return False
def has_jobs(self):
return len(self.jobs.values()) > 0
def run_device_job(self, slot, callable, *args, **kwargs): def run_device_job(self, slot, callable, *args, **kwargs):
''' '''
Run a job to communicate with the device. Run a job to communicate with the device.
@ -53,7 +119,9 @@ class JobManager(QAbstractTableModel):
@param args: The arguments to pass to callable @param args: The arguments to pass to callable
@param kwargs: The keyword 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)'), QObject.connect(job, SIGNAL('jobdone(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.job_done) self.job_done)
if slot: if slot:
@ -68,15 +136,18 @@ class JobManager(QAbstractTableModel):
''' '''
self.job_remove_lock.lock() self.job_remove_lock.lock()
try: try:
job = self.jobs.pop(id) job = self.jobs.pop(id)
self.reset()
self.cleanup_lock.lock() self.cleanup_lock.lock()
self.cleanup[id] = job self.cleanup[id] = job
self.cleanup_lock.unlock() self.cleanup_lock.unlock()
self.emit(SIGNAL('job_done(int)'), id)
if len(self.jobs.keys()) == 0: if len(self.jobs.keys()) == 0:
self.emit(SIGNAL('no_more_jobs()')) self.emit(SIGNAL('no_more_jobs()'))
finally: finally:
self.job_remove_lock.unlock() self.job_remove_lock.unlock()
def cleanup_jobs(self): def cleanup_jobs(self):
self.cleanup_lock.lock() self.cleanup_lock.lock()
toast = [] toast = []
@ -86,4 +157,53 @@ class JobManager(QAbstractTableModel):
for id in toast: for id in toast:
self.cleanup.pop(id) self.cleanup.pop(id)
self.cleanup_lock.unlock() 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)

View File

@ -16,7 +16,7 @@ import os, tempfile, sys
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QSettings, QVariant, QSize, QThread 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 PyQt4.QtSvg import QSvgRenderer
from libprs500 import __version__, __appname__ 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.jobs import JobManager, JobException
from libprs500.gui2.dialogs.metadata_single import MetadataSingleDialog from libprs500.gui2.dialogs.metadata_single import MetadataSingleDialog
from libprs500.gui2.dialogs.metadata_bulk import MetadataBulkDialog from libprs500.gui2.dialogs.metadata_bulk import MetadataBulkDialog
from libprs500.gui2.dialogs.jobs import JobsDialog
class Main(QObject, Ui_MainWindow): class Main(QObject, Ui_MainWindow):
@ -51,6 +53,7 @@ class Main(QObject, Ui_MainWindow):
self.setupUi(window) self.setupUi(window)
self.read_settings() self.read_settings()
self.job_manager = JobManager() self.job_manager = JobManager()
self.jobs_dialog = JobsDialog(self.window, self.job_manager)
self.device_manager = None self.device_manager = None
self.upload_memory = {} self.upload_memory = {}
self.delete_memory = {} self.delete_memory = {}
@ -64,10 +67,10 @@ class Main(QObject, Ui_MainWindow):
self.vanity.setText(self.vanity_template.arg(' ')) self.vanity.setText(self.vanity_template.arg(' '))
####################### Status Bar ##################### ####################### Status Bar #####################
self.status_bar = StatusBar() self.status_bar = StatusBar(self.jobs_dialog)
self.window.setStatusBar(self.status_bar) 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('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 ##################### ####################### Setup Toolbar #####################
sm = QMenu() sm = QMenu()
@ -195,6 +198,8 @@ class Main(QObject, Ui_MainWindow):
if exception: if exception:
self.job_exception(id, exception, formatted_traceback) self.job_exception(id, exception, formatted_traceback)
return 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. Upload books to device.
@param files: List of either paths to files or file like objects @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, id = self.job_manager.run_device_job(self.books_uploaded,
self.device_manager.upload_books_func(), 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 self.upload_memory[id] = metadata
@ -434,8 +441,21 @@ class Main(QObject, Ui_MainWindow):
settings.endGroup() settings.endGroup()
def close_event(self, e): def close_event(self, e):
self.write_settings() msg = 'There are active jobs. Are you sure you want to quit?'
e.accept() if self.job_manager.has_device_jobs():
msg = '<p>'+__appname__ + ' is communicating with the device!<br>'+\
'Quitting may cause corruption on the device.<br>'+\
'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(): def main():
lock = os.path.join(tempfile.gettempdir(),"libprs500_gui_lock") lock = os.path.join(tempfile.gettempdir(),"libprs500_gui_lock")

View File

@ -12,10 +12,14 @@
## You should have received a copy of the GNU General Public License along ## 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., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from libprs500.gui2.dialogs.jobs import JobsDialog
import textwrap 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 PyQt4.QtCore import Qt, QSize
from libprs500.gui2 import qstring_to_unicode
class BookInfoDisplay(QFrame): class BookInfoDisplay(QFrame):
class BookCoverDisplay(QLabel): class BookCoverDisplay(QLabel):
@ -39,7 +43,7 @@ class BookInfoDisplay(QFrame):
def __init__(self): def __init__(self):
QLabel.__init__(self) QLabel.__init__(self)
self.setTextInteractionFlags(Qt.TextSelectableByMouse) self.setTextInteractionFlags(Qt.TextSelectableByMouse)
self.setText('')#<table><tr><td>row 1</td><td>row 2</td></tr><tr><td>fsfdsfsfsfsfsfsdfsffsfsd</td></tr></table>') self.setText('')
def __init__(self, clear_message): def __init__(self, clear_message):
QFrame.__init__(self) QFrame.__init__(self)
@ -74,35 +78,83 @@ class BookInfoDisplay(QFrame):
self.clear_message() self.clear_message()
self.setVisible(True) self.setVisible(True)
class MovieButton(QLabel): class BusyIndicator(QLabel):
def __init__(self, movie): def __init__(self, movie, jobs_dialog):
QLabel.__init__(self) QLabel.__init__(self)
self.movie = movie self.setCursor(Qt.PointingHandCursor)
self.setToolTip('Click to see list of active jobs.')
self.setMovie(movie) self.setMovie(movie)
self.movie.start() movie.start()
self.movie.setPaused(True) 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('<b>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): class StatusBar(QStatusBar):
def __init__(self): def __init__(self, jobs_dialog):
QStatusBar.__init__(self) 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.addPermanentWidget(self.movie_button)
self.book_info = BookInfoDisplay(self.clearMessage) self.book_info = BookInfoDisplay(self.clearMessage)
self.addWidget(self.book_info) 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): 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: if self.movie_button.movie.state() == QMovie.Paused:
self.movie_button.movie.setPaused(False) 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): def no_more_jobs(self):
if self.movie_button.movie.state() == QMovie.Running: if self.movie_button.movie.state() == QMovie.Running:
self.movie_button.movie.setPaused(True) 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__': if __name__ == '__main__':
# Used to create the animated status icon # 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 from subprocess import check_call
import os import os
app = QApplication([]) app = QApplication([])