Refactoring. Note cover browser is broken

This commit is contained in:
Kovid Goyal 2010-06-09 01:17:38 -06:00
commit 47158c8f6d
8 changed files with 576 additions and 591 deletions

View File

@ -7,16 +7,17 @@ __docformat__ = 'restructuredtext en'
Module to implement the Cover Flow feature Module to implement the Cover Flow feature
''' '''
import sys, os import sys, os, time
from PyQt4.QtGui import QImage, QSizePolicy from PyQt4.Qt import QImage, QSizePolicy, QTimer, QDialog, Qt, QSize, \
from PyQt4.QtCore import Qt, QSize, SIGNAL, QObject QStackedLayout
from calibre import plugins from calibre import plugins
from calibre.gui2 import config from calibre.gui2 import config, available_height, available_width
pictureflow, pictureflowerror = plugins['pictureflow'] pictureflow, pictureflowerror = plugins['pictureflow']
if pictureflow is not None: if pictureflow is not None:
class EmptyImageList(pictureflow.FlowImages): class EmptyImageList(pictureflow.FlowImages):
def __init__(self): def __init__(self):
pictureflow.FlowImages.__init__(self) pictureflow.FlowImages.__init__(self)
@ -51,7 +52,7 @@ if pictureflow is not None:
def __init__(self, model, buffer=20): def __init__(self, model, buffer=20):
pictureflow.FlowImages.__init__(self) pictureflow.FlowImages.__init__(self)
self.model = model self.model = model
QObject.connect(self.model, SIGNAL('modelReset()'), self.reset) self.model.modelReset.connect(self.reset)
def count(self): def count(self):
return self.model.count() return self.model.count()
@ -66,7 +67,7 @@ if pictureflow is not None:
return ans return ans
def reset(self): def reset(self):
self.emit(SIGNAL('dataChanged()')) self.dataChanged.emit()
def image(self, index): def image(self, index):
return self.model.cover(index) return self.model.cover(index)
@ -74,13 +75,13 @@ if pictureflow is not None:
class CoverFlow(pictureflow.PictureFlow): class CoverFlow(pictureflow.PictureFlow):
def __init__(self, height=300, parent=None, text_height=25): def __init__(self, parent=None):
pictureflow.PictureFlow.__init__(self, parent, pictureflow.PictureFlow.__init__(self, parent,
config['cover_flow_queue_length']+1) config['cover_flow_queue_length']+1)
self.setSlideSize(QSize(int(2/3. * height), height)) self.setMinimumSize(QSize(10, 10))
self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+text_height))
self.setFocusPolicy(Qt.WheelFocus) self.setFocusPolicy(Qt.WheelFocus)
self.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding))
def wheelEvent(self, ev): def wheelEvent(self, ev):
ev.accept() ev.accept()
@ -95,6 +96,104 @@ else:
DatabaseImages = None DatabaseImages = None
FileSystemImages = None FileSystemImages = None
class CoverFlowMixin(object):
def __init__(self):
self.cover_flow = None
if CoverFlow is not None:
self.cf_last_updated_at = None
self.cover_flow_sync_timer = QTimer(self)
self.cover_flow_sync_timer.timeout.connect(self.cover_flow_do_sync)
self.cover_flow_sync_flag = True
self.cover_flow = CoverFlow(parent=self)
self.cover_flow.setVisible(False)
if not config['separate_cover_flow']:
self.cb_layout.addWidget(self.cover_flow)
self.cover_flow.currentChanged.connect(self.sync_listview_to_cf)
self.library_view.selectionModel().currentRowChanged.connect(
self.sync_cf_to_listview)
self.db_images = DatabaseImages(self.library_view.model())
self.cover_flow.setImages(self.db_images)
ah, aw = available_height(), available_width()
self._cb_layout_is_horizontal = float(aw)/ah >= 1.4
self.cb_layout.setDirection(self.cb_layout.LeftToRight if
self._cb_layout_is_horizontal else
self.cb_layout.TopToBottom)
def toggle_cover_flow_visibility(self, show):
if config['separate_cover_flow']:
if show:
d = QDialog(self)
ah, aw = available_height(), available_width()
d.resize(int(aw/1.5), ah-60)
d._layout = QStackedLayout()
d.setLayout(d._layout)
d.setWindowTitle(_('Browse by covers'))
d.layout().addWidget(self.cover_flow)
self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason)
d.show()
d.finished.connect(self.sidebar.external_cover_flow_finished)
self.cf_dialog = d
else:
cfd = getattr(self, 'cf_dialog', None)
if cfd is not None:
self.cover_flow.setVisible(False)
cfd.hide()
self.cf_dialog = None
else:
if show:
self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason)
else:
self.cover_flow.setVisible(False)
def toggle_cover_flow(self, show):
if show:
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
self.library_view.setCurrentIndex(
self.library_view.currentIndex())
self.cover_flow_sync_timer.start(500)
self.library_view.scroll_to_row(self.library_view.currentIndex().row())
else:
self.cover_flow_sync_timer.stop()
idx = self.library_view.model().index(self.cover_flow.currentSlide(), 0)
if idx.isValid():
sm = self.library_view.selectionModel()
sm.select(idx, sm.ClearAndSelect|sm.Rows)
self.library_view.setCurrentIndex(idx)
self.library_view.scroll_to_row(idx.row())
self.toggle_cover_flow_visibility(show)
def sync_cf_to_listview(self, current, previous):
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \
self.cover_flow.currentSlide() != current.row():
self.cover_flow.setCurrentSlide(current.row())
self.cover_flow_sync_flag = True
def cover_flow_do_sync(self):
self.cover_flow_sync_flag = True
try:
if self.cover_flow.isVisible() and self.cf_last_updated_at is not None and \
time.time() - self.cf_last_updated_at > 0.5:
self.cf_last_updated_at = None
row = self.cover_flow.currentSlide()
m = self.library_view.model()
index = m.index(row, 0)
if self.library_view.currentIndex().row() != row and index.isValid():
self.cover_flow_sync_flag = False
self.library_view.scroll_to_row(index.row())
sm = self.library_view.selectionModel()
sm.select(index, sm.ClearAndSelect|sm.Rows)
self.library_view.setCurrentIndex(index)
except:
pass
def sync_listview_to_cf(self, row):
self.cf_last_updated_at = time.time()
def main(args=sys.argv): def main(args=sys.argv):
return 0 return 0
@ -103,12 +202,12 @@ if __name__ == '__main__':
app = QApplication([]) app = QApplication([])
w = QMainWindow() w = QMainWindow()
cf = CoverFlow() cf = CoverFlow()
cf.resize(cf.minimumSize()) cf.resize(int(available_width()/1.5), available_height()-60)
w.resize(cf.minimumSize()+QSize(30, 20)) w.resize(cf.size()+QSize(30, 20))
path = sys.argv[1] path = sys.argv[1]
model = FileSystemImages(sys.argv[1]) model = FileSystemImages(sys.argv[1])
cf.currentChanged[int].connect(model.currentChanged)
cf.setImages(model) cf.setImages(model)
cf.connect(cf, SIGNAL('currentChanged(int)'), model.currentChanged)
w.setCentralWidget(cf) w.setCentralWidget(cf)
w.show() w.show()

281
src/calibre/gui2/init.py Normal file
View File

@ -0,0 +1,281 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import functools
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import isosx
from calibre.gui2 import config
_keep_refs = []
def partial(*args, **kwargs):
ans = functools.partial(*args, **kwargs)
_keep_refs.append(ans)
return ans
class SaveMenu(QMenu): # {{{
save_fmt = pyqtSignal(object)
def __init__(self, parent):
QMenu.__init__(self, _('Save single format to disk...'), parent)
for ext in sorted(BOOK_EXTENSIONS):
action = self.addAction(ext.upper())
setattr(self, 'do_'+ext, partial(self.do, ext))
action.triggered.connect(
getattr(self, 'do_'+ext))
def do(self, ext, *args):
self.save_fmt.emit(ext)
# }}}
class ToolbarMixin(object): # {{{
def __init__(self):
md = QMenu()
md.addAction(_('Edit metadata individually'),
partial(self.edit_metadata, False))
md.addSeparator()
md.addAction(_('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True))
md.addSeparator()
md.addAction(_('Download metadata and covers'),
partial(self.download_metadata, False, covers=True))
md.addAction(_('Download only metadata'),
partial(self.download_metadata, False, covers=False))
md.addAction(_('Download only covers'),
partial(self.download_metadata, False, covers=True,
set_metadata=False, set_social_metadata=False))
md.addAction(_('Download only social metadata'),
partial(self.download_metadata, False, covers=False,
set_metadata=False, set_social_metadata=True))
self.metadata_menu = md
mb = QMenu()
mb.addAction(_('Merge into first selected book - delete others'),
self.merge_books)
mb.addSeparator()
mb.addAction(_('Merge into first selected book - keep others'),
partial(self.merge_books, safe_merge=True))
self.merge_menu = mb
self.action_merge.setMenu(mb)
md.addSeparator()
md.addAction(self.action_merge)
self.add_menu = QMenu()
self.add_menu.addAction(_('Add books from a single directory'),
self.add_books)
self.add_menu.addAction(_('Add books from directories, including '
'sub-directories (One book per directory, assumes every ebook '
'file is the same book in a different format)'),
self.add_recursive_single)
self.add_menu.addAction(_('Add books from directories, including '
'sub directories (Multiple books per directory, assumes every '
'ebook file is a different book)'), self.add_recursive_multiple)
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
'formats)'), self.add_empty)
self.action_add.setMenu(self.add_menu)
self.action_add.triggered.connect(self.add_books)
self.action_del.triggered.connect(self.delete_books)
self.action_edit.triggered.connect(self.edit_metadata)
self.action_merge.triggered.connect(self.merge_books)
self.action_save.triggered.connect(self.save_to_disk)
self.save_menu = QMenu()
self.save_menu.addAction(_('Save to disk'), partial(self.save_to_disk,
False))
self.save_menu.addAction(_('Save to disk in a single directory'),
partial(self.save_to_single_dir, False))
self.save_menu.addAction(_('Save only %s format to disk')%
prefs['output_format'].upper(),
partial(self.save_single_format_to_disk, False))
self.save_menu.addAction(
_('Save only %s format to disk in a single directory')%
prefs['output_format'].upper(),
partial(self.save_single_fmt_to_single_dir, False))
self.save_sub_menu = SaveMenu(self)
self.save_menu.addMenu(self.save_sub_menu)
self.save_sub_menu.save_fmt.connect(self.save_specific_format_disk)
self.action_view.triggered.connect(self.view_book)
self.view_menu = QMenu()
self.view_menu.addAction(_('View'), partial(self.view_book, False))
ac = self.view_menu.addAction(_('View specific format'))
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
self.action_view.setMenu(self.view_menu)
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
self.delete_menu = QMenu()
self.delete_menu.addAction(_('Remove selected books'), self.delete_books)
self.delete_menu.addAction(
_('Remove files of a specific format from selected books..'),
self.delete_selected_formats)
self.delete_menu.addAction(
_('Remove all formats from selected books, except...'),
self.delete_all_but_selected_formats)
self.delete_menu.addAction(
_('Remove covers from selected books'), self.delete_covers)
self.action_del.setMenu(self.delete_menu)
self.action_open_containing_folder.setShortcut(Qt.Key_O)
self.addAction(self.action_open_containing_folder)
self.action_sync.setShortcut(Qt.Key_D)
self.action_sync.setEnabled(True)
self.create_device_menu()
self.action_sync.triggered.connect(
self._sync_action_triggered)
self.action_edit.setMenu(md)
self.action_save.setMenu(self.save_menu)
cm = QMenu()
cm.addAction(_('Convert individually'), partial(self.convert_ebook,
False, bulk=False))
cm.addAction(_('Bulk convert'),
partial(self.convert_ebook, False, bulk=True))
cm.addSeparator()
ac = cm.addAction(
_('Create catalog of books in your calibre library'))
ac.triggered.connect(self.generate_catalog)
self.action_convert.setMenu(cm)
self.action_convert.triggered.connect(self.convert_ebook)
self.convert_menu = cm
pm = QMenu()
ap = self.action_preferences
pm.addAction(ap)
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
self.run_wizard)
self.action_preferences.setMenu(pm)
self.preferences_menu = pm
for x in (self.preferences_action, self.action_preferences):
x.triggered.connect(self.do_config)
for x in ('news', 'edit', 'sync', 'convert', 'save', 'add', 'view',
'del', 'preferences'):
w = self.tool_bar.widgetForAction(getattr(self, 'action_'+x))
w.setPopupMode(w.MenuButtonPopup)
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
for ch in self.tool_bar.children():
if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor)
self.tool_bar.contextMenuEvent = self.no_op
def read_toolbar_settings(self):
self.tool_bar.setIconSize(config['toolbar_icon_size'])
self.tool_bar.setToolButtonStyle(
Qt.ToolButtonTextUnderIcon if \
config['show_text_in_toolbar'] else \
Qt.ToolButtonIconOnly)
# }}}
class LibraryViewMixin(object): # {{{
def __init__(self, db):
similar_menu = QMenu(_('Similar books...'))
similar_menu.addAction(self.action_books_by_same_author)
similar_menu.addAction(self.action_books_in_this_series)
similar_menu.addAction(self.action_books_with_the_same_tags)
similar_menu.addAction(self.action_books_by_this_publisher)
self.action_books_by_same_author.setShortcut(Qt.ALT + Qt.Key_A)
self.action_books_in_this_series.setShortcut(Qt.ALT + Qt.Key_S)
self.action_books_by_this_publisher.setShortcut(Qt.ALT + Qt.Key_P)
self.action_books_with_the_same_tags.setShortcut(Qt.ALT+Qt.Key_T)
self.addAction(self.action_books_by_same_author)
self.addAction(self.action_books_by_this_publisher)
self.addAction(self.action_books_in_this_series)
self.addAction(self.action_books_with_the_same_tags)
self.similar_menu = similar_menu
self.action_books_by_same_author.triggered.connect(
partial(self.show_similar_books, 'authors'))
self.action_books_in_this_series.triggered.connect(
partial(self.show_similar_books, 'series'))
self.action_books_with_the_same_tags.triggered.connect(
partial(self.show_similar_books, 'tag'))
self.action_books_by_this_publisher.triggered.connect(
partial(self.show_similar_books, 'publisher'))
self.library_view.set_context_menu(self.action_edit, self.action_sync,
self.action_convert, self.action_view,
self.action_save,
self.action_open_containing_folder,
self.action_show_book_details,
self.action_del,
similar_menu=similar_menu)
self.memory_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.card_a_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
for func, args in [
('connect_to_search_box', (self.search,
self.search_done)),
('connect_to_book_display',
(self.status_bar.book_info.show_data,)),
]:
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
getattr(view, func)(*args)
self.memory_view.connect_dirtied_signal(self.upload_booklists)
self.card_a_view.connect_dirtied_signal(self.upload_booklists)
self.card_b_view.connect_dirtied_signal(self.upload_booklists)
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
self.library_view.model().set_book_on_device_func(self.book_on_device)
prefs['library_path'] = self.library_path
for view in ('library', 'memory', 'card_a', 'card_b'):
view = getattr(self, view+'_view')
view.verticalHeader().sectionDoubleClicked.connect(self.view_specific_book)
def show_similar_books(self, type, *args):
search, join = [], ' '
idx = self.library_view.currentIndex()
if not idx.isValid():
return
row = idx.row()
if type == 'series':
series = idx.model().db.series(row)
if series:
search = ['series:"'+series+'"']
elif type == 'publisher':
publisher = idx.model().db.publisher(row)
if publisher:
search = ['publisher:"'+publisher+'"']
elif type == 'tag':
tags = idx.model().db.tags(row)
if tags:
search = ['tag:"='+t+'"' for t in tags.split(',')]
elif type in ('author', 'authors'):
authors = idx.model().db.authors(row)
if authors:
search = ['author:"='+a.strip().replace('|', ',')+'"' \
for a in authors.split(',')]
join = ' or '
if search:
self.search.set_search_string(join.join(search))
# }}}

View File

@ -7,11 +7,14 @@ __docformat__ = 'restructuredtext en'
Job management. Job management.
''' '''
import re
from Queue import Empty, Queue from Queue import Empty, Queue
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \ from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
QTimer, SIGNAL, QIcon, QDialog, QAbstractItemDelegate, QApplication, \ QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \
QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication
from calibre.utils.ipc.server import Server from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob from calibre.utils.ipc.job import ParallelJob
@ -20,9 +23,13 @@ from calibre.gui2.device import DeviceJob
from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
from calibre import __appname__ from calibre import __appname__
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
from calibre.gui2.progress_indicator import ProgressIndicator
class JobManager(QAbstractTableModel): class JobManager(QAbstractTableModel):
job_added = pyqtSignal(int)
job_done = pyqtSignal(int)
def __init__(self): def __init__(self):
QAbstractTableModel.__init__(self) QAbstractTableModel.__init__(self)
self.wait_icon = QVariant(QIcon(I('jobs.svg'))) self.wait_icon = QVariant(QIcon(I('jobs.svg')))
@ -37,8 +44,7 @@ class JobManager(QAbstractTableModel):
self.changed_queue = Queue() self.changed_queue = Queue()
self.timer = QTimer(self) self.timer = QTimer(self)
self.connect(self.timer, SIGNAL('timeout()'), self.update, self.timer.timeout.connect(self.update, type=Qt.QueuedConnection)
Qt.QueuedConnection)
self.timer.start(1000) self.timer.start(1000)
def columnCount(self, parent=QModelIndex()): def columnCount(self, parent=QModelIndex()):
@ -130,8 +136,7 @@ class JobManager(QAbstractTableModel):
for i, j in enumerate(self.jobs): for i, j in enumerate(self.jobs):
if j.run_state == j.RUNNING: if j.run_state == j.RUNNING:
idx = self.index(i, 3) idx = self.index(i, 3)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), self.dataChanged.emit(idx, idx)
idx, idx)
# Update parallel jobs # Update parallel jobs
jobs = set([]) jobs = set([])
@ -157,20 +162,19 @@ class JobManager(QAbstractTableModel):
self.jobs.sort() self.jobs.sort()
self.reset() self.reset()
if job.is_finished: if job.is_finished:
self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs())) self.job_done.emit(len(self.unfinished_jobs()))
else: else:
for job in jobs: for job in jobs:
idx = self.jobs.index(job) idx = self.jobs.index(job)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), self.dataChanged.emit(
self.index(idx, 0), self.index(idx, 3)) self.index(idx, 0), self.index(idx, 3))
def _add_job(self, job): def _add_job(self, job):
self.emit(SIGNAL('layoutAboutToBeChanged()')) self.layoutAboutToBeChanged.emit()
self.jobs.append(job) self.jobs.append(job)
self.jobs.sort() self.jobs.sort()
self.emit(SIGNAL('job_added(int)'), len(self.unfinished_jobs())) self.job_added.emit(len(self.unfinished_jobs()))
self.emit(SIGNAL('layoutChanged()'))
def done_jobs(self): def done_jobs(self):
return [j for j in self.jobs if j.is_finished] return [j for j in self.jobs if j.is_finished]
@ -266,6 +270,76 @@ class DetailView(QDialog, Ui_Dialog):
if more: if more:
self.log.appendPlainText(more.decode('utf-8', 'replace')) self.log.appendPlainText(more.decode('utf-8', 'replace'))
class JobsButton(QFrame):
def __init__(self, horizontal=False, size=48, parent=None):
QFrame.__init__(self, parent)
self.pi = ProgressIndicator(self, size)
self._jobs = QLabel('<b>'+_('Jobs:')+' 0')
if horizontal:
self.setLayout(QHBoxLayout())
else:
self.setLayout(QVBoxLayout())
self._jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom)
self.layout().addWidget(self.pi)
self.layout().addWidget(self._jobs)
if not horizontal:
self.layout().setAlignment(self._jobs, Qt.AlignHCenter)
self._jobs.setMargin(0)
self.layout().setMargin(0)
self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.setCursor(Qt.PointingHandCursor)
self.setToolTip(_('Click to see list of active jobs.'))
def initialize(self, jobs_dialog, job_manager):
self.jobs_dialog = jobs_dialog
job_manager.job_added.connect(self.job_added)
job_manager.job_done.connect(self.job_done)
def mouseReleaseEvent(self, event):
if self.jobs_dialog.isVisible():
self.jobs_dialog.hide()
else:
self.jobs_dialog.show()
@property
def is_running(self):
return self.pi.isAnimated()
def start(self):
self.pi.startAnimation()
def stop(self):
self.pi.stopAnimation()
def jobs(self):
src = unicode(self._jobs.text())
return int(re.search(r'\d+', src).group())
def job_added(self, nnum):
jobs = self._jobs
src = unicode(jobs.text())
num = self.jobs()
text = src.replace(str(num), str(nnum))
jobs.setText(text)
self.start()
def job_done(self, nnum):
jobs = self._jobs
src = unicode(jobs.text())
num = self.jobs()
text = src.replace(str(num), str(nnum))
jobs.setText(text)
if nnum == 0:
self.no_more_jobs()
def no_more_jobs(self):
if self.is_running:
self.stop()
QCoreApplication.instance().alert(self, 5000)
class JobsDialog(QDialog, Ui_JobsDialog): class JobsDialog(QDialog, Ui_JobsDialog):
@ -278,14 +352,9 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.model = model self.model = model
self.setWindowModality(Qt.NonModal) self.setWindowModality(Qt.NonModal)
self.setWindowTitle(__appname__ + _(' - Jobs')) self.setWindowTitle(__appname__ + _(' - Jobs'))
self.connect(self.kill_button, SIGNAL('clicked()'), self.kill_button.clicked.connect(self.kill_job)
self.kill_job) self.details_button.clicked.connect(self.show_details)
self.connect(self.details_button, SIGNAL('clicked()'), self.stop_all_jobs_button.clicked.connect(self.kill_all_jobs)
self.show_details)
self.connect(self.stop_all_jobs_button, SIGNAL('clicked()'),
self.kill_all_jobs)
self.connect(self, SIGNAL('kill_job(int, PyQt_PyObject)'),
self.jobs_view.model().kill_job)
self.pb_delegate = ProgressBarDelegate(self) self.pb_delegate = ProgressBarDelegate(self)
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate) self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
self.jobs_view.doubleClicked.connect(self.show_job_details) self.jobs_view.doubleClicked.connect(self.show_job_details)
@ -304,18 +373,18 @@ class JobsDialog(QDialog, Ui_JobsDialog):
d.exec_() d.exec_()
d.timer.stop() d.timer.stop()
def kill_job(self): def kill_job(self, *args):
for index in self.jobs_view.selectedIndexes(): for index in self.jobs_view.selectedIndexes():
row = index.row() row = index.row()
self.model.kill_job(row, self) self.model.kill_job(row, self)
return return
def show_details(self): def show_details(self, *args):
for index in self.jobs_view.selectedIndexes(): for index in self.jobs_view.selectedIndexes():
self.show_job_details(index) self.show_job_details(index)
return return
def kill_all_jobs(self): def kill_all_jobs(self, *args):
self.model.kill_all_jobs() self.model.kill_all_jobs()
def closeEvent(self, e): def closeEvent(self, e):

View File

@ -540,6 +540,8 @@ void PictureFlowPrivate::showSlide(int index)
void PictureFlowPrivate::resize(int w, int h) void PictureFlowPrivate::resize(int w, int h)
{ {
slideHeight = int(float(h)/2.);
slideWidth = int(float(slideHeight) * 2/3.);
recalc(w, h); recalc(w, h);
resetSlides(); resetSlides();
triggerRender(); triggerRender();

View File

@ -16,6 +16,10 @@ public:
virtual int count(); virtual int count();
virtual QImage image(int index); virtual QImage image(int index);
virtual QString caption(int index); virtual QString caption(int index);
signals:
void dataChanged();
}; };

View File

@ -5,78 +5,13 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re
from functools import partial from functools import partial
from PyQt4.Qt import QToolBar, Qt, QIcon, QSizePolicy, QWidget, \ from PyQt4.Qt import QToolBar, Qt, QIcon, QSizePolicy, QWidget, \
QFrame, QVBoxLayout, QLabel, QSize, QCoreApplication, QToolButton QSize, QToolButton
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2 import dynamic from calibre.gui2 import dynamic
class JobsButton(QFrame):
def __init__(self, parent):
QFrame.__init__(self, parent)
self.setLayout(QVBoxLayout())
self.pi = ProgressIndicator(self)
self.layout().addWidget(self.pi)
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.Minimum)
self.setCursor(Qt.PointingHandCursor)
self.setToolTip(_('Click to see list of active jobs.'))
def initialize(self, jobs_dialog):
self.jobs_dialog = jobs_dialog
def mouseReleaseEvent(self, event):
if self.jobs_dialog.isVisible():
self.jobs_dialog.hide()
else:
self.jobs_dialog.show()
@property
def is_running(self):
return self.pi.isAnimated()
def start(self):
self.pi.startAnimation()
def stop(self):
self.pi.stopAnimation()
class Jobs(ProgressIndicator):
def initialize(self, jobs_dialog):
self.jobs_dialog = jobs_dialog
def mouseClickEvent(self, event):
if self.jobs_dialog.isVisible():
self.jobs_dialog.jobs_view.write_settings()
self.jobs_dialog.hide()
else:
self.jobs_dialog.jobs_view.read_settings()
self.jobs_dialog.show()
self.jobs_dialog.jobs_view.restore_column_widths()
@property
def is_running(self):
return self.isAnimated()
def start(self):
self.startAnimation()
def stop(self):
self.stopAnimation()
class SideBar(QToolBar): class SideBar(QToolBar):
toggle_texts = { toggle_texts = {
@ -114,8 +49,6 @@ class SideBar(QToolBar):
self.spacer = QWidget(self) self.spacer = QWidget(self)
self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
self.addWidget(self.spacer) self.addWidget(self.spacer)
self.jobs_button = JobsButton(self)
self.addWidget(self.jobs_button)
self.show_cover_browser = partial(self._toggle_cover_browser, show=True) self.show_cover_browser = partial(self._toggle_cover_browser, show=True)
self.hide_cover_browser = partial(self._toggle_cover_browser, self.hide_cover_browser = partial(self._toggle_cover_browser,
@ -124,9 +57,8 @@ class SideBar(QToolBar):
if isinstance(ch, QToolButton): if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor) ch.setCursor(Qt.PointingHandCursor)
def initialize(self, jobs_dialog, cover_browser, toggle_cover_browser, def initialize(self, jobs_button, cover_browser, toggle_cover_browser,
cover_browser_error, vertical_splitter, horizontal_splitter): cover_browser_error, vertical_splitter, horizontal_splitter):
self.jobs_button.initialize(jobs_dialog)
self.cover_browser, self.do_toggle_cover_browser = cover_browser, \ self.cover_browser, self.do_toggle_cover_browser = cover_browser, \
toggle_cover_browser toggle_cover_browser
if self.cover_browser is None: if self.cover_browser is None:
@ -166,6 +98,7 @@ class SideBar(QToolBar):
'book_info'), type=Qt.QueuedConnection) 'book_info'), type=Qt.QueuedConnection)
self.horizontal_splitter.state_changed.connect(partial(self.view_status_changed, self.horizontal_splitter.state_changed.connect(partial(self.view_status_changed,
'tag_browser'), type=Qt.QueuedConnection) 'tag_browser'), type=Qt.QueuedConnection)
self.addWidget(jobs_button)
@ -211,30 +144,4 @@ class SideBar(QToolBar):
def _toggle_book_info(self, show=None): def _toggle_book_info(self, show=None):
self.vertical_splitter.toggle_side_index() self.vertical_splitter.toggle_side_index()
def jobs(self):
src = unicode(self.jobs_button.jobs.text())
return int(re.search(r'\d+', src).group())
def job_added(self, nnum):
jobs = self.jobs_button.jobs
src = unicode(jobs.text())
num = self.jobs()
text = src.replace(str(num), str(nnum))
jobs.setText(text)
self.jobs_button.start()
def job_done(self, nnum):
jobs = self.jobs_button.jobs
src = unicode(jobs.text())
num = self.jobs()
text = src.replace(str(num), str(nnum))
jobs.setText(text)
if nnum == 0:
self.no_more_jobs()
def no_more_jobs(self):
if self.jobs_button.is_running:
self.jobs_button.stop()
QCoreApplication.instance().alert(self, 5000)

View File

@ -18,6 +18,8 @@ from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
class TagsView(QTreeView): # {{{ class TagsView(QTreeView): # {{{
@ -29,12 +31,16 @@ class TagsView(QTreeView): # {{{
tag_item_renamed = pyqtSignal() tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal()
def __init__(self, *args): def __init__(self, parent=None):
QTreeView.__init__(self, *args) QTreeView.__init__(self, parent=None)
self.tag_match = None
self.setUniformRowHeights(True) self.setUniformRowHeights(True)
self.setCursor(Qt.PointingHandCursor) self.setCursor(Qt.PointingHandCursor)
self.setIconSize(QSize(30, 30)) self.setIconSize(QSize(30, 30))
self.tag_match = None self.setTabKeyNavigation(True)
self.setAlternatingRowColors(True)
self.setAnimated(True)
self.setHeaderHidden(True)
def set_database(self, db, tag_match, popularity): def set_database(self, db, tag_match, popularity):
self.hidden_categories = config['tag_browser_hidden_categories'] self.hidden_categories = config['tag_browser_hidden_categories']
@ -588,3 +594,42 @@ class TagsModel(QAbstractItemModel): # {{{
# }}} # }}}
class TagBrowserMixin(object): # {{{
def __init__(self, db):
self.tags_view.set_database(self.library_view.model().db,
self.tag_match, self.popularity)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
def do_user_categories_edit(self, on_category=None):
d = TagCategories(self, self.library_view.model().db, on_category)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.set_new_model()
self.tags_view.recount()
def do_tags_list_edit(self, tag, category):
d = TagListEditor(self, self.library_view.model().db, tag, category)
d.exec_()
if d.result() == d.Accepted:
# Clean up everything, as information could have changed for many books.
self.library_view.model().refresh()
self.tags_view.set_new_model()
self.tags_view.recount()
self.saved_search.clear_to_help()
self.search.clear_to_help()
def do_tag_item_renamed(self):
# Clean up library view and search
self.library_view.model().refresh()
self.saved_search.clear_to_help()
self.search.clear_to_help()
# }}}

View File

@ -16,9 +16,9 @@ from threading import Thread
from functools import partial from functools import partial
from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \ from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
QToolButton, QDialog, QDesktopServices, \ QDialog, QDesktopServices, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \
QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\ QMessageBox, QHelpEvent, QInputDialog,\
QThread, pyqtSignal QThread, pyqtSignal
from PyQt4.QtSvg import QSvgRenderer from PyQt4.QtSvg import QSvgRenderer
@ -35,10 +35,9 @@ from calibre.gui2 import warning_dialog, choose_files, error_dialog, \
question_dialog,\ question_dialog,\
pixmap_to_data, choose_dir, \ pixmap_to_data, choose_dir, \
Dispatcher, gprefs, \ Dispatcher, gprefs, \
available_height, \
max_available_height, config, info_dialog, \ max_available_height, config, info_dialog, \
available_width, GetMetadata GetMetadata
from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror from calibre.gui2.cover_flow import pictureflowerror, CoverFlowMixin
from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS
from calibre.gui2.wizard import move_library from calibre.gui2.wizard import move_library
from calibre.gui2.dialogs.scheduler import Scheduler from calibre.gui2.dialogs.scheduler import Scheduler
@ -46,7 +45,7 @@ from calibre.gui2.update import CheckForUpdates
from calibre.gui2.main_window import MainWindow from calibre.gui2.main_window import MainWindow
from calibre.gui2.main_ui import Ui_MainWindow from calibre.gui2.main_ui import Ui_MainWindow
from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceGUI, Emailer from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceGUI, Emailer
from calibre.gui2.jobs import JobManager, JobsDialog from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
@ -60,24 +59,11 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
from calibre.library.caches import CoverCache from calibre.library.caches import CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
from calibre.gui2.tag_view import TagBrowserMixin
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin
class SaveMenu(QMenu): class Listener(Thread): # {{{
def __init__(self, parent):
QMenu.__init__(self, _('Save single format to disk...'), parent)
for ext in sorted(BOOK_EXTENSIONS):
action = self.addAction(ext.upper())
setattr(self, 'do_'+ext, partial(self.do, ext))
self.connect(action, SIGNAL('triggered(bool)'),
getattr(self, 'do_'+ext))
def do(self, ext, *args):
self.emit(SIGNAL('save_fmt(PyQt_PyObject)'), ext)
class Listener(Thread):
def __init__(self, listener): def __init__(self, listener):
Thread.__init__(self) Thread.__init__(self)
@ -102,7 +88,9 @@ class Listener(Thread):
except: except:
pass pass
class SystemTrayIcon(QSystemTrayIcon): # }}}
class SystemTrayIcon(QSystemTrayIcon): # {{{
def __init__(self, icon, parent): def __init__(self, icon, parent):
QSystemTrayIcon.__init__(self, icon, parent) QSystemTrayIcon.__init__(self, icon, parent)
@ -115,7 +103,10 @@ class SystemTrayIcon(QSystemTrayIcon):
return True return True
return QSystemTrayIcon.event(self, ev) return QSystemTrayIcon.event(self, ev)
class Main(MainWindow, Ui_MainWindow, DeviceGUI): # }}}
class Main(MainWindow, Ui_MainWindow, DeviceGUI, ToolbarMixin,
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin):
'The main GUI' 'The main GUI'
def set_default_thumbnail(self, height): def set_default_thumbnail(self, height):
@ -234,7 +225,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.connect(self.system_tray_icon, self.connect(self.system_tray_icon,
SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), SIGNAL('activated(QSystemTrayIcon::ActivationReason)'),
self.system_tray_icon_activated) self.system_tray_icon_activated)
self.tool_bar.contextMenuEvent = self.no_op QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
self.do_advanced_search)
####################### Start spare job server ######################## ####################### Start spare job server ########################
QTimer.singleShot(1000, self.add_spare_server) QTimer.singleShot(1000, self.add_spare_server)
@ -277,281 +269,22 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.status_bar.files_dropped.connect(self.files_dropped_on_book) self.status_bar.files_dropped.connect(self.files_dropped_on_book)
####################### Setup Toolbar ##################### ####################### Setup Toolbar #####################
md = QMenu() ToolbarMixin.__init__(self)
md.addAction(_('Edit metadata individually'))
md.addSeparator()
md.addAction(_('Edit metadata in bulk'))
md.addSeparator()
md.addAction(_('Download metadata and covers'))
md.addAction(_('Download only metadata'))
md.addAction(_('Download only covers'))
md.addAction(_('Download only social metadata'))
self.metadata_menu = md
mb = QMenu()
mb.addAction(_('Merge into first selected book - delete others'))
mb.addSeparator()
mb.addAction(_('Merge into first selected book - keep others'))
self.merge_menu = mb
self.action_merge.setMenu(mb)
md.addSeparator()
md.addAction(self.action_merge)
self.add_menu = QMenu()
self.add_menu.addAction(_('Add books from a single directory'))
self.add_menu.addAction(_('Add books from directories, including '
'sub-directories (One book per directory, assumes every ebook '
'file is the same book in a different format)'))
self.add_menu.addAction(_('Add books from directories, including '
'sub directories (Multiple books per directory, assumes every '
'ebook file is a different book)'))
self.add_menu.addAction(_('Add Empty book. (Book entry with no '
'formats)'))
self.action_add.setMenu(self.add_menu)
QObject.connect(self.action_add, SIGNAL("triggered(bool)"),
self.add_books)
QObject.connect(self.add_menu.actions()[0], SIGNAL("triggered(bool)"),
self.add_books)
QObject.connect(self.add_menu.actions()[1], SIGNAL("triggered(bool)"),
self.add_recursive_single)
QObject.connect(self.add_menu.actions()[2], SIGNAL("triggered(bool)"),
self.add_recursive_multiple)
QObject.connect(self.add_menu.actions()[3], SIGNAL('triggered(bool)'),
self.add_empty)
QObject.connect(self.action_del, SIGNAL("triggered(bool)"),
self.delete_books)
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"),
self.edit_metadata)
self.__em1__ = partial(self.edit_metadata, bulk=False)
QObject.connect(md.actions()[0], SIGNAL('triggered(bool)'),
self.__em1__)
self.__em2__ = partial(self.edit_metadata, bulk=True)
QObject.connect(md.actions()[2], SIGNAL('triggered(bool)'),
self.__em2__)
self.__em3__ = partial(self.download_metadata, covers=True)
QObject.connect(md.actions()[4], SIGNAL('triggered(bool)'),
self.__em3__)
self.__em4__ = partial(self.download_metadata, covers=False)
QObject.connect(md.actions()[5], SIGNAL('triggered(bool)'),
self.__em4__)
self.__em5__ = partial(self.download_metadata, covers=True,
set_metadata=False, set_social_metadata=False)
QObject.connect(md.actions()[6], SIGNAL('triggered(bool)'),
self.__em5__)
self.__em6__ = partial(self.download_metadata, covers=False,
set_metadata=False, set_social_metadata=True)
QObject.connect(md.actions()[7], SIGNAL('triggered(bool)'),
self.__em6__)
QObject.connect(self.action_merge, SIGNAL("triggered(bool)"),
self.merge_books)
QObject.connect(mb.actions()[0], SIGNAL('triggered(bool)'),
self.merge_books)
self.__mb1__ = partial(self.merge_books, safe_merge=True)
QObject.connect(mb.actions()[2], SIGNAL('triggered(bool)'),
self.__mb1__)
self.save_menu = QMenu()
self.save_menu.addAction(_('Save to disk'))
self.save_menu.addAction(_('Save to disk in a single directory'))
self.save_menu.addAction(_('Save only %s format to disk')%
prefs['output_format'].upper())
self.save_menu.addAction(
_('Save only %s format to disk in a single directory')%
prefs['output_format'].upper())
self.save_sub_menu = SaveMenu(self)
self.save_menu.addMenu(self.save_sub_menu)
self.connect(self.save_sub_menu, SIGNAL('save_fmt(PyQt_PyObject)'),
self.save_specific_format_disk)
self.view_menu = QMenu()
self.view_menu.addAction(_('View'))
ac = self.view_menu.addAction(_('View specific format'))
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
self.action_view.setMenu(self.view_menu)
self.delete_menu = QMenu()
self.delete_menu.addAction(_('Remove selected books'))
self.delete_menu.addAction(
_('Remove files of a specific format from selected books..'))
self.delete_menu.addAction(
_('Remove all formats from selected books, except...'))
self.delete_menu.addAction(
_('Remove covers from selected books'))
self.action_del.setMenu(self.delete_menu)
QObject.connect(self.action_save, SIGNAL("triggered(bool)"),
self.save_to_disk)
QObject.connect(self.save_menu.actions()[0], SIGNAL("triggered(bool)"),
self.save_to_disk)
QObject.connect(self.save_menu.actions()[1], SIGNAL("triggered(bool)"),
self.save_to_single_dir)
QObject.connect(self.save_menu.actions()[2], SIGNAL("triggered(bool)"),
self.save_single_format_to_disk)
QObject.connect(self.save_menu.actions()[3], SIGNAL("triggered(bool)"),
self.save_single_fmt_to_single_dir)
QObject.connect(self.action_view, SIGNAL("triggered(bool)"),
self.view_book)
QObject.connect(self.view_menu.actions()[0],
SIGNAL("triggered(bool)"), self.view_book)
QObject.connect(self.view_menu.actions()[1],
SIGNAL("triggered(bool)"), self.view_specific_format,
Qt.QueuedConnection)
self.connect(self.action_open_containing_folder,
SIGNAL('triggered(bool)'), self.view_folder)
self.delete_menu.actions()[0].triggered.connect(self.delete_books)
self.delete_menu.actions()[1].triggered.connect(self.delete_selected_formats)
self.delete_menu.actions()[2].triggered.connect(self.delete_all_but_selected_formats)
self.delete_menu.actions()[3].triggered.connect(self.delete_covers)
self.action_open_containing_folder.setShortcut(Qt.Key_O)
self.addAction(self.action_open_containing_folder)
self.action_sync.setShortcut(Qt.Key_D)
self.action_sync.setEnabled(True)
self.create_device_menu()
self.connect(self.action_sync, SIGNAL('triggered(bool)'),
self._sync_action_triggered)
self.action_edit.setMenu(md)
self.action_save.setMenu(self.save_menu)
cm = QMenu()
cm.addAction(_('Convert individually'))
cm.addAction(_('Bulk convert'))
cm.addSeparator()
ac = cm.addAction(
_('Create catalog of books in your calibre library'))
ac.triggered.connect(self.generate_catalog)
self.action_convert.setMenu(cm)
self._convert_single_hook = partial(self.convert_ebook, bulk=False)
QObject.connect(cm.actions()[0],
SIGNAL('triggered(bool)'), self._convert_single_hook)
self._convert_bulk_hook = partial(self.convert_ebook, bulk=True)
QObject.connect(cm.actions()[1],
SIGNAL('triggered(bool)'), self._convert_bulk_hook)
QObject.connect(self.action_convert,
SIGNAL('triggered(bool)'), self.convert_ebook)
self.convert_menu = cm
pm = QMenu()
ap = self.action_preferences
pm.addAction(ap.icon(), ap.text())
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'))
self.connect(pm.actions()[0], SIGNAL('triggered(bool)'),
self.do_config)
self.connect(pm.actions()[1], SIGNAL('triggered(bool)'),
self.run_wizard)
self.action_preferences.setMenu(pm)
self.preferences_menu = pm
self.tool_bar.widgetForAction(self.action_news).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_edit).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_sync).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_convert).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_save).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_add).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_view).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_del).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.widgetForAction(self.action_preferences).\
setPopupMode(QToolButton.MenuButtonPopup)
self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
self.connect(self.preferences_action, SIGNAL('triggered(bool)'),
self.do_config)
self.connect(self.action_preferences, SIGNAL('triggered(bool)'),
self.do_config)
QObject.connect(self.advanced_search_button, SIGNAL('clicked(bool)'),
self.do_advanced_search)
for ch in self.tool_bar.children():
if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor)
####################### Library view ######################## ####################### Library view ########################
similar_menu = QMenu(_('Similar books...')) LibraryViewMixin.__init__(self, db)
similar_menu.addAction(self.action_books_by_same_author)
similar_menu.addAction(self.action_books_in_this_series)
similar_menu.addAction(self.action_books_with_the_same_tags)
similar_menu.addAction(self.action_books_by_this_publisher)
self.action_books_by_same_author.setShortcut(Qt.ALT + Qt.Key_A)
self.action_books_in_this_series.setShortcut(Qt.ALT + Qt.Key_S)
self.action_books_by_this_publisher.setShortcut(Qt.ALT + Qt.Key_P)
self.action_books_with_the_same_tags.setShortcut(Qt.ALT+Qt.Key_T)
self.addAction(self.action_books_by_same_author)
self.addAction(self.action_books_by_this_publisher)
self.addAction(self.action_books_in_this_series)
self.addAction(self.action_books_with_the_same_tags)
self.similar_menu = similar_menu
self.connect(self.action_books_by_same_author, SIGNAL('triggered()'),
lambda : self.show_similar_books('author'))
self.connect(self.action_books_in_this_series, SIGNAL('triggered()'),
lambda : self.show_similar_books('series'))
self.connect(self.action_books_with_the_same_tags,
SIGNAL('triggered()'),
lambda : self.show_similar_books('tag'))
self.connect(self.action_books_by_this_publisher, SIGNAL('triggered()'),
lambda : self.show_similar_books('publisher'))
self.library_view.set_context_menu(self.action_edit, self.action_sync,
self.action_convert, self.action_view,
self.action_save,
self.action_open_containing_folder,
self.action_show_book_details,
self.action_del,
similar_menu=similar_menu)
self.memory_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.card_a_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
for func, args in [
('connect_to_search_box', (self.search,
self.search_done)),
('connect_to_book_display',
(self.status_bar.book_info.show_data,)),
]:
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
getattr(view, func)(*args)
self.memory_view.connect_dirtied_signal(self.upload_booklists)
self.card_a_view.connect_dirtied_signal(self.upload_booklists)
self.card_b_view.connect_dirtied_signal(self.upload_booklists)
self.show() self.show()
if self.system_tray_icon.isVisible() and opts.start_in_tray: if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows() self.hide_windows()
self.stack.setCurrentIndex(0) self.stack.setCurrentIndex(0)
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
self.library_view.model().set_book_on_device_func(self.book_on_device)
prefs['library_path'] = self.library_path
self.search.setFocus(Qt.OtherFocusReason) self.search.setFocus(Qt.OtherFocusReason)
self.cover_cache = CoverCache(self.library_path) self.cover_cache = CoverCache(self.library_path)
self.cover_cache.start() self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache self.library_view.model().cover_cache = self.cover_cache
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit) self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
self.search_restriction.activated[str].connect(self.apply_search_restriction) self.search_restriction.activated[str].connect(self.apply_search_restriction)
self.tags_view.set_database(db, self.tag_match, self.popularity)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
for x in (self.location_view.count_changed, self.tags_view.recount, for x in (self.location_view.count_changed, self.tags_view.recount,
self.restriction_count_changed): self.restriction_count_changed):
self.library_view.model().count_changed_signal.connect(x) self.library_view.model().count_changed_signal.connect(x)
@ -578,46 +311,29 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
########################### Tags Browser ############################## ########################### Tags Browser ##############################
TagBrowserMixin.__init__(self, db)
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon) self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10) self.search_restriction.setMinimumContentsLength(10)
########################### Cover Flow ################################ ########################### Cover Flow ################################
self.cover_flow = None
if CoverFlow is not None: CoverFlowMixin.__init__(self)
self.cf_last_updated_at = None
self.cover_flow_sync_timer = QTimer(self)
self.cover_flow_sync_timer.timeout.connect(self.cover_flow_do_sync)
self.cover_flow_sync_flag = True
text_height = 40 if config['separate_cover_flow'] else 25
ah = available_height()
cfh = ah-100
cfh = 3./5 * cfh - text_height
if not config['separate_cover_flow']:
cfh = 220 if ah > 950 else 170 if ah > 850 else 140
self.cover_flow = CoverFlow(height=cfh, text_height=text_height)
self.cover_flow.setVisible(False)
if not config['separate_cover_flow']:
self.cb_layout.addWidget(self.cover_flow)
self.cover_flow.currentChanged.connect(self.sync_listview_to_cf)
self.library_view.selectionModel().currentRowChanged.connect(
self.sync_cf_to_listview)
self.db_images = DatabaseImages(self.library_view.model())
self.cover_flow.setImages(self.db_images)
self._calculated_available_height = min(max_available_height()-15, self._calculated_available_height = min(max_available_height()-15,
self.height()) self.height())
self.resize(self.width(), self._calculated_available_height) self.resize(self.width(), self._calculated_available_height)
self.search.setMaximumWidth(self.width()-150) self.search.setMaximumWidth(self.width()-150)
# Jobs Button {{{
self.jobs_button = JobsButton()
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
# }}}
####################### Side Bar ############################### ####################### Side Bar ###############################
self.sidebar.initialize(self.jobs_dialog, self.cover_flow, self.sidebar.initialize(self.jobs_button, self.cover_flow,
self.toggle_cover_flow, pictureflowerror, self.toggle_cover_flow, pictureflowerror,
self.vertical_splitter, self.horizontal_splitter) self.vertical_splitter, self.horizontal_splitter)
QObject.connect(self.job_manager, SIGNAL('job_added(int)'),
self.sidebar.job_added, Qt.QueuedConnection)
QObject.connect(self.job_manager, SIGNAL('job_done(int)'),
self.sidebar.job_done, Qt.QueuedConnection)
if config['autolaunch_server']: if config['autolaunch_server']:
@ -639,38 +355,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
SIGNAL('start_recipe_fetch(PyQt_PyObject)'), SIGNAL('start_recipe_fetch(PyQt_PyObject)'),
self.download_scheduled_recipe, Qt.QueuedConnection) self.download_scheduled_recipe, Qt.QueuedConnection)
for view in ('library', 'memory', 'card_a', 'card_b'):
view = getattr(self, view+'_view')
view.verticalHeader().sectionDoubleClicked.connect(self.view_specific_book)
self.location_view.setCurrentIndex(self.location_view.model().index(0)) self.location_view.setCurrentIndex(self.location_view.model().index(0))
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book) self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
def do_user_categories_edit(self, on_category=None):
d = TagCategories(self, self.library_view.model().db, on_category)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.set_new_model()
self.tags_view.recount()
def do_tags_list_edit(self, tag, category):
d = TagListEditor(self, self.library_view.model().db, tag, category)
d.exec_()
if d.result() == d.Accepted:
# Clean up everything, as information could have changed for many books.
self.library_view.model().refresh()
self.tags_view.set_new_model()
self.tags_view.recount()
self.saved_search.clear_to_help()
self.search.clear_to_help()
def do_tag_item_renamed(self):
# Clean up library view and search
self.library_view.model().refresh()
self.saved_search.clear_to_help()
self.search.clear_to_help()
def do_saved_search_edit(self, search): def do_saved_search_edit(self, search):
d = SavedSearchEditor(self, search) d = SavedSearchEditor(self, search)
@ -760,81 +449,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
error_dialog(self, _('Failed to start content server'), error_dialog(self, _('Failed to start content server'),
unicode(self.content_server.exception)).exec_() unicode(self.content_server.exception)).exec_()
def show_similar_books(self, type):
search, join = [], ' '
idx = self.library_view.currentIndex()
if not idx.isValid():
return
row = idx.row()
if type == 'series':
series = idx.model().db.series(row)
if series:
search = ['series:"'+series+'"']
elif type == 'publisher':
publisher = idx.model().db.publisher(row)
if publisher:
search = ['publisher:"'+publisher+'"']
elif type == 'tag':
tags = idx.model().db.tags(row)
if tags:
search = ['tag:"='+t+'"' for t in tags.split(',')]
elif type == 'author':
authors = idx.model().db.authors(row)
if authors:
search = ['author:"='+a.strip().replace('|', ',')+'"' \
for a in authors.split(',')]
join = ' or '
if search:
self.search.set_search_string(join.join(search))
def toggle_cover_flow(self, show):
if config['separate_cover_flow']:
if show:
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
d = QDialog(self)
ah, aw = available_height(), available_width()
d.resize(int(aw/2.), ah-60)
d._layout = QStackedLayout()
d.setLayout(d._layout)
d.setWindowTitle(_('Browse by covers'))
d.layout().addWidget(self.cover_flow)
self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason)
self.library_view.scroll_to_row(self.library_view.currentIndex().row())
d.show()
d.finished.connect(self.sidebar.external_cover_flow_finished)
self.cf_dialog = d
self.cover_flow_sync_timer.start(500)
else:
self.cover_flow_sync_timer.stop()
idx = self.library_view.model().index(self.cover_flow.currentSlide(), 0)
if idx.isValid():
sm = self.library_view.selectionModel()
sm.select(idx, sm.ClearAndSelect|sm.Rows)
self.library_view.setCurrentIndex(idx)
cfd = getattr(self, 'cf_dialog', None)
if cfd is not None:
self.cover_flow.setVisible(False)
cfd.hide()
self.cf_dialog = None
else:
if show:
self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row())
self.library_view.setCurrentIndex(
self.library_view.currentIndex())
self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason)
self.library_view.scroll_to_row(self.library_view.currentIndex().row())
self.cover_flow_sync_timer.start(500)
else:
self.cover_flow_sync_timer.stop()
self.cover_flow.setVisible(False)
idx = self.library_view.model().index(self.cover_flow.currentSlide(), 0)
if idx.isValid():
sm = self.library_view.selectionModel()
sm.select(idx, sm.ClearAndSelect|sm.Rows)
self.library_view.setCurrentIndex(idx)
''' '''
Restrictions. Restrictions.
Adding and deleting books creates a complexity. When added, they are Adding and deleting books creates a complexity. When added, they are
@ -905,33 +519,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.search_restriction.setCurrentIndex(0) self.search_restriction.setCurrentIndex(0)
self.apply_search_restriction('') self.apply_search_restriction('')
def sync_cf_to_listview(self, current, previous):
if self.cover_flow_sync_flag and self.cover_flow.isVisible() and \
self.cover_flow.currentSlide() != current.row():
self.cover_flow.setCurrentSlide(current.row())
self.cover_flow_sync_flag = True
def cover_flow_do_sync(self):
self.cover_flow_sync_flag = True
try:
if self.cover_flow.isVisible() and self.cf_last_updated_at is not None and \
time.time() - self.cf_last_updated_at > 0.5:
self.cf_last_updated_at = None
row = self.cover_flow.currentSlide()
m = self.library_view.model()
index = m.index(row, 0)
if self.library_view.currentIndex().row() != row and index.isValid():
self.cover_flow_sync_flag = False
self.library_view.scroll_to_row(index.row())
sm = self.library_view.selectionModel()
sm.select(index, sm.ClearAndSelect|sm.Rows)
self.library_view.setCurrentIndex(index)
except:
pass
def sync_listview_to_cf(self, row):
self.cf_last_updated_at = time.time()
def another_instance_wants_to_talk(self): def another_instance_wants_to_talk(self):
try: try:
@ -1309,21 +896,21 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
Dispatcher(self._files_added), spare_server=self.spare_server) Dispatcher(self._files_added), spare_server=self.spare_server)
self._adder.add_recursive(root, single) self._adder.add_recursive(root, single)
def add_recursive_single(self, checked): def add_recursive_single(self, *args):
''' '''
Add books from the local filesystem to either the library or the device Add books from the local filesystem to either the library or the device
recursively assuming one book per folder. recursively assuming one book per folder.
''' '''
self.add_recursive(True) self.add_recursive(True)
def add_recursive_multiple(self, checked): def add_recursive_multiple(self, *args):
''' '''
Add books from the local filesystem to either the library or the device Add books from the local filesystem to either the library or the device
recursively assuming multiple books per folder. recursively assuming multiple books per folder.
''' '''
self.add_recursive(False) self.add_recursive(False)
def add_empty(self, checked): def add_empty(self, *args):
''' '''
Add an empty book item to the library. This does not import any formats Add an empty book item to the library. This does not import any formats
from a book file. from a book file.
@ -1383,7 +970,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
def add_filesystem_book(self, paths, allow_device=True): def add_filesystem_book(self, paths, allow_device=True):
self._add_filesystem_book(paths, allow_device=allow_device) self._add_filesystem_book(paths, allow_device=allow_device)
def add_books(self, checked): def add_books(self, *args):
''' '''
Add books from the local filesystem to either the library or the device. Add books from the local filesystem to either the library or the device.
''' '''
@ -2289,12 +1876,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
d.exec_() d.exec_()
self.content_server = d.server self.content_server = d.server
if d.result() == d.Accepted: if d.result() == d.Accepted:
self.tool_bar.setIconSize(config['toolbar_icon_size']) self.read_toolbar_settings()
self.search.search_as_you_type(config['search_as_you_type']) self.search.search_as_you_type(config['search_as_you_type'])
self.tool_bar.setToolButtonStyle(
Qt.ToolButtonTextUnderIcon if \
config['show_text_in_toolbar'] else \
Qt.ToolButtonIconOnly)
self.save_menu.actions()[2].setText( self.save_menu.actions()[2].setText(
_('Save only %s format to disk')% _('Save only %s format to disk')%
prefs['output_format'].upper()) prefs['output_format'].upper())
@ -2451,12 +2034,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
geometry = config['main_window_geometry'] geometry = config['main_window_geometry']
if geometry is not None: if geometry is not None:
self.restoreGeometry(geometry) self.restoreGeometry(geometry)
self.tool_bar.setIconSize(config['toolbar_icon_size']) self.read_toolbar_settings()
self.tool_bar.setToolButtonStyle(
Qt.ToolButtonTextUnderIcon if \
config['show_text_in_toolbar'] else \
Qt.ToolButtonIconOnly)
def write_settings(self): def write_settings(self):
config.set('main_window_geometry', self.saveGeometry()) config.set('main_window_geometry', self.saveGeometry())