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
%_ui.py : %.ui

View File

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

View File

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

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, \
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

View File

@ -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 @@
<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"
id="path27365"
style="fill:url(#linearGradient27367);fill-opacity:1" />
style="fill-opacity:1" />
</clipPath>
<linearGradient
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
## with this program; if not, write to the Free Software Foundation, Inc.,
## 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):
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):
def __init__(self):
@ -31,19 +83,33 @@ class JobManager(QAbstractTableModel):
self.device_lock = QMutex()
self.cleanup_lock = QMutex()
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()
try:
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('status_update(int, int)'), self.status_update)
self.jobs[self.next_id] = job
self.emit(SIGNAL('job_added(int)'), self.next_id)
self.reset()
return job
finally:
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):
'''
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:
@ -69,11 +137,14 @@ class JobManager(QAbstractTableModel):
self.job_remove_lock.lock()
try:
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()
@ -87,3 +158,52 @@ class JobManager(QAbstractTableModel):
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)

View File

@ -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 = '<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():
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
## 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('')#<table><tr><td>row 1</td><td>row 2</td></tr><tr><td>fsfdsfsfsfsfsfsdfsffsfsd</td></tr></table>')
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('<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):
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([])