diff --git a/resources/recipes/sueddeutsche.recipe b/resources/recipes/sueddeutsche.recipe index 8f4d7e5892..9a5b00fc6c 100644 --- a/resources/recipes/sueddeutsche.recipe +++ b/resources/recipes/sueddeutsche.recipe @@ -19,10 +19,10 @@ class Sueddeutsche(BasicNewsRecipe): no_stylesheets = True language = 'de' - encoding = 'iso-8859-15' + encoding = 'utf-8' remove_javascript = True - + remove_tags = [ dict(name='link'), dict(name='iframe'), dict(name='div', attrs={'id':["bookmarking","themenbox","artikelfoot","CAD_AD", "SKY_AD","NT1_AD","navbar1","sdesiteheader"]}), diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index 81d67ff7dd..3bd554e891 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -7,16 +7,17 @@ __docformat__ = 'restructuredtext en' Module to implement the Cover Flow feature ''' -import sys, os +import sys, os, time -from PyQt4.QtGui import QImage, QSizePolicy -from PyQt4.QtCore import Qt, QSize, SIGNAL, QObject +from PyQt4.Qt import QImage, QSizePolicy, QTimer, QDialog, Qt, QSize, \ + QStackedLayout from calibre import plugins -from calibre.gui2 import config +from calibre.gui2 import config, available_height, available_width pictureflow, pictureflowerror = plugins['pictureflow'] if pictureflow is not None: + class EmptyImageList(pictureflow.FlowImages): def __init__(self): pictureflow.FlowImages.__init__(self) @@ -51,7 +52,7 @@ if pictureflow is not None: def __init__(self, model, buffer=20): pictureflow.FlowImages.__init__(self) self.model = model - QObject.connect(self.model, SIGNAL('modelReset()'), self.reset) + self.model.modelReset.connect(self.reset) def count(self): return self.model.count() @@ -66,7 +67,7 @@ if pictureflow is not None: return ans def reset(self): - self.emit(SIGNAL('dataChanged()')) + self.dataChanged.emit() def image(self, index): return self.model.cover(index) @@ -74,13 +75,14 @@ if pictureflow is not None: class CoverFlow(pictureflow.PictureFlow): - def __init__(self, height=300, parent=None, text_height=25): + def __init__(self, parent=None): pictureflow.PictureFlow.__init__(self, parent, config['cover_flow_queue_length']+1) - self.setSlideSize(QSize(int(2/3. * height), height)) - self.setMinimumSize(QSize(int(2.35*0.67*height), (5/3.)*height+text_height)) + self.setMinimumSize(QSize(10, 10)) self.setFocusPolicy(Qt.WheelFocus) - self.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) + self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, + QSizePolicy.Expanding)) + self.setZoomFactor(150) def wheelEvent(self, ev): ev.accept() @@ -95,6 +97,104 @@ else: DatabaseImages = 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): return 0 @@ -103,12 +203,12 @@ if __name__ == '__main__': app = QApplication([]) w = QMainWindow() cf = CoverFlow() - cf.resize(cf.minimumSize()) - w.resize(cf.minimumSize()+QSize(30, 20)) + cf.resize(int(available_width()/1.5), available_height()-60) + w.resize(cf.size()+QSize(30, 20)) path = sys.argv[1] model = FileSystemImages(sys.argv[1]) + cf.currentChanged[int].connect(model.currentChanged) cf.setImages(model) - cf.connect(cf, SIGNAL('currentChanged(int)'), model.currentChanged) w.setCentralWidget(cf) w.show() diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py new file mode 100644 index 0000000000..a991c4d1f8 --- /dev/null +++ b/src/calibre/gui2/init.py @@ -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 ' +__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)) + + + + # }}} + diff --git a/src/calibre/gui2/jobs.py b/src/calibre/gui2/jobs.py index 0b54ce2406..adf938c435 100644 --- a/src/calibre/gui2/jobs.py +++ b/src/calibre/gui2/jobs.py @@ -7,11 +7,14 @@ __docformat__ = 'restructuredtext en' Job management. ''' +import re + from Queue import Empty, Queue from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \ - QTimer, SIGNAL, QIcon, QDialog, QAbstractItemDelegate, QApplication, \ - QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip + QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \ + QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \ + QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication from calibre.utils.ipc.server import Server 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 import __appname__ from calibre.gui2.dialogs.job_view_ui import Ui_Dialog +from calibre.gui2.progress_indicator import ProgressIndicator class JobManager(QAbstractTableModel): + job_added = pyqtSignal(int) + job_done = pyqtSignal(int) + def __init__(self): QAbstractTableModel.__init__(self) self.wait_icon = QVariant(QIcon(I('jobs.svg'))) @@ -37,8 +44,7 @@ class JobManager(QAbstractTableModel): self.changed_queue = Queue() self.timer = QTimer(self) - self.connect(self.timer, SIGNAL('timeout()'), self.update, - Qt.QueuedConnection) + self.timer.timeout.connect(self.update, type=Qt.QueuedConnection) self.timer.start(1000) def columnCount(self, parent=QModelIndex()): @@ -130,8 +136,7 @@ class JobManager(QAbstractTableModel): for i, j in enumerate(self.jobs): if j.run_state == j.RUNNING: idx = self.index(i, 3) - self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), - idx, idx) + self.dataChanged.emit(idx, idx) # Update parallel jobs jobs = set([]) @@ -157,20 +162,19 @@ class JobManager(QAbstractTableModel): self.jobs.sort() self.reset() if job.is_finished: - self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs())) + self.job_done.emit(len(self.unfinished_jobs())) else: for job in jobs: idx = self.jobs.index(job) - self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), + self.dataChanged.emit( self.index(idx, 0), self.index(idx, 3)) def _add_job(self, job): - self.emit(SIGNAL('layoutAboutToBeChanged()')) + self.layoutAboutToBeChanged.emit() self.jobs.append(job) self.jobs.sort() - self.emit(SIGNAL('job_added(int)'), len(self.unfinished_jobs())) - self.emit(SIGNAL('layoutChanged()')) + self.job_added.emit(len(self.unfinished_jobs())) def done_jobs(self): return [j for j in self.jobs if j.is_finished] @@ -266,6 +270,76 @@ class DetailView(QDialog, Ui_Dialog): if more: 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(''+_('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): @@ -278,14 +352,9 @@ class JobsDialog(QDialog, Ui_JobsDialog): self.model = model self.setWindowModality(Qt.NonModal) self.setWindowTitle(__appname__ + _(' - Jobs')) - self.connect(self.kill_button, SIGNAL('clicked()'), - self.kill_job) - self.connect(self.details_button, SIGNAL('clicked()'), - 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.kill_button.clicked.connect(self.kill_job) + self.details_button.clicked.connect(self.show_details) + self.stop_all_jobs_button.clicked.connect(self.kill_all_jobs) self.pb_delegate = ProgressBarDelegate(self) self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate) self.jobs_view.doubleClicked.connect(self.show_job_details) @@ -304,18 +373,18 @@ class JobsDialog(QDialog, Ui_JobsDialog): d.exec_() d.timer.stop() - def kill_job(self): + def kill_job(self, *args): for index in self.jobs_view.selectedIndexes(): row = index.row() self.model.kill_job(row, self) return - def show_details(self): + def show_details(self, *args): for index in self.jobs_view.selectedIndexes(): self.show_job_details(index) return - def kill_all_jobs(self): + def kill_all_jobs(self, *args): self.model.kill_all_jobs() def closeEvent(self, e): diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index 6c3f04828e..109b001925 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -426,6 +426,14 @@ class BooksView(QTableView): # {{{ if dy != 0: self.column_header.update() + def scroll_to_row(self, row): + if row > -1: + h = self.horizontalHeader() + for i in range(h.count()): + if not h.isSectionHidden(i): + self.scrollTo(self.model().index(row, i)) + break + def close(self): self._model.close() diff --git a/src/calibre/gui2/main.ui b/src/calibre/gui2/main.ui index 1abaf53d3c..b4bafc3b79 100644 --- a/src/calibre/gui2/main.ui +++ b/src/calibre/gui2/main.ui @@ -329,7 +329,7 @@ 0 - + @@ -389,37 +389,43 @@ - - - - 100 - 10 - - - - true - - - true - - - false - - - QAbstractItemView::DragDrop - - - true - - - QAbstractItemView::SelectRows - - - false - - - false - + + + + + + + 100 + 10 + + + + true + + + true + + + false + + + QAbstractItemView::DragDrop + + + true + + + QAbstractItemView::SelectRows + + + false + + + false + + + + diff --git a/src/calibre/gui2/pictureflow/pictureflow.cpp b/src/calibre/gui2/pictureflow/pictureflow.cpp index 1b8d3300f4..9bb9a0954c 100644 --- a/src/calibre/gui2/pictureflow/pictureflow.cpp +++ b/src/calibre/gui2/pictureflow/pictureflow.cpp @@ -85,6 +85,8 @@ typedef long PFreal; typedef unsigned short QRgb565; +#define FONT_SIZE 18 + #define RGB565_RED_MASK 0xF800 #define RGB565_GREEN_MASK 0x07E0 #define RGB565_BLUE_MASK 0x001F @@ -540,6 +542,8 @@ void PictureFlowPrivate::showSlide(int index) void PictureFlowPrivate::resize(int w, int h) { + slideHeight = int(float(h)/2.); + slideWidth = int(float(slideHeight) * 2/3.); recalc(w, h); resetSlides(); triggerRender(); @@ -709,14 +713,14 @@ void PictureFlowPrivate::render() QPainter painter; painter.begin(&buffer); - QFont font("Arial", 14); + QFont font("Arial", FONT_SIZE); font.setBold(true); painter.setFont(font); painter.setPen(Qt::white); //painter.setPen(QColor(255,255,255,127)); if (centerIndex < slideCount() && centerIndex > -1) - painter.drawText( QRect(0,0, buffer.width(), (buffer.height() - slideSize().height())/2), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-FONT_SIZE*3), Qt::AlignCenter, slideImages->caption(centerIndex)); painter.end(); @@ -759,7 +763,7 @@ void PictureFlowPrivate::render() QPainter painter; painter.begin(&buffer); - QFont font("Arial", 14); + QFont font("Arial", FONT_SIZE); font.setBold(true); painter.setFont(font); @@ -768,12 +772,12 @@ void PictureFlowPrivate::render() painter.setPen(QColor(255,255,255, (255-fade) )); if (leftTextIndex < sc && leftTextIndex > -1) - painter.drawText( QRect(0,0, buffer.width(), (buffer.height() - slideSize().height())/2), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3), Qt::AlignCenter, slideImages->caption(leftTextIndex)); painter.setPen(QColor(255,255,255, fade)); if (leftTextIndex+1 < sc && leftTextIndex > -2) - painter.drawText( QRect(0,0, buffer.width(), (buffer.height() - slideSize().height())/2), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3), Qt::AlignCenter, slideImages->caption(leftTextIndex+1)); diff --git a/src/calibre/gui2/pictureflow/pictureflow.h b/src/calibre/gui2/pictureflow/pictureflow.h index 3e0b606d8a..8cce025180 100644 --- a/src/calibre/gui2/pictureflow/pictureflow.h +++ b/src/calibre/gui2/pictureflow/pictureflow.h @@ -115,7 +115,8 @@ public: QSize slideSize() const; /*! - Sets the dimension of each slide (in pixels). + Sets the dimension of each slide (in pixels). Do not use this method directly + instead use resize which automatically sets an appropriate slide size. */ void setSlideSize(QSize size); diff --git a/src/calibre/gui2/pictureflow/pictureflow.sip b/src/calibre/gui2/pictureflow/pictureflow.sip index c636529e05..9202dd8ad5 100644 --- a/src/calibre/gui2/pictureflow/pictureflow.sip +++ b/src/calibre/gui2/pictureflow/pictureflow.sip @@ -16,6 +16,10 @@ public: virtual int count(); virtual QImage image(int index); virtual QString caption(int index); + +signals: + void dataChanged(); + }; diff --git a/src/calibre/gui2/sidebar.py b/src/calibre/gui2/sidebar.py index bd305912a0..6710b5d471 100644 --- a/src/calibre/gui2/sidebar.py +++ b/src/calibre/gui2/sidebar.py @@ -5,78 +5,13 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re from functools import partial 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 -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(''+_('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): toggle_texts = { @@ -114,8 +49,6 @@ class SideBar(QToolBar): self.spacer = QWidget(self) self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) 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.hide_cover_browser = partial(self._toggle_cover_browser, @@ -124,9 +57,8 @@ class SideBar(QToolBar): if isinstance(ch, QToolButton): 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): - self.jobs_button.initialize(jobs_dialog) self.cover_browser, self.do_toggle_cover_browser = cover_browser, \ toggle_cover_browser if self.cover_browser is None: @@ -166,6 +98,7 @@ class SideBar(QToolBar): 'book_info'), type=Qt.QueuedConnection) self.horizontal_splitter.state_changed.connect(partial(self.view_status_changed, 'tag_browser'), type=Qt.QueuedConnection) + self.addWidget(jobs_button) @@ -211,30 +144,4 @@ class SideBar(QToolBar): def _toggle_book_info(self, show=None): 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) - diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index e8f3068d35..b5ced8d626 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -18,6 +18,8 @@ from calibre.utils.config import prefs from calibre.library.field_metadata import TagsIcons from calibre.utils.search_query_parser import saved_searches 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): # {{{ @@ -29,12 +31,16 @@ class TagsView(QTreeView): # {{{ tag_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal() - def __init__(self, *args): - QTreeView.__init__(self, *args) + def __init__(self, parent=None): + QTreeView.__init__(self, parent=None) + self.tag_match = None self.setUniformRowHeights(True) self.setCursor(Qt.PointingHandCursor) 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): 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() + +# }}} + diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index cfbcb794f3..54ef0f9f60 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -16,9 +16,9 @@ from threading import Thread from functools import partial from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \ QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ - QToolButton, QDialog, QDesktopServices, \ + QDialog, QDesktopServices, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \ - QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\ + QMessageBox, QHelpEvent, QInputDialog,\ QThread, pyqtSignal from PyQt4.QtSvg import QSvgRenderer @@ -35,10 +35,9 @@ from calibre.gui2 import warning_dialog, choose_files, error_dialog, \ question_dialog,\ pixmap_to_data, choose_dir, \ Dispatcher, gprefs, \ - available_height, \ max_available_height, config, info_dialog, \ - available_width, GetMetadata -from calibre.gui2.cover_flow import CoverFlow, DatabaseImages, pictureflowerror + GetMetadata +from calibre.gui2.cover_flow import pictureflowerror, CoverFlowMixin from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS from calibre.gui2.wizard import move_library 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_ui import Ui_MainWindow 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_bulk import MetadataBulkDialog 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.caches import CoverCache 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.tag_view import TagBrowserMixin +from calibre.gui2.init import ToolbarMixin, LibraryViewMixin -class SaveMenu(QMenu): - - 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): +class Listener(Thread): # {{{ def __init__(self, listener): Thread.__init__(self) @@ -102,7 +88,9 @@ class Listener(Thread): except: pass -class SystemTrayIcon(QSystemTrayIcon): +# }}} + +class SystemTrayIcon(QSystemTrayIcon): # {{{ def __init__(self, icon, parent): QSystemTrayIcon.__init__(self, icon, parent) @@ -115,7 +103,10 @@ class SystemTrayIcon(QSystemTrayIcon): return True return QSystemTrayIcon.event(self, ev) -class Main(MainWindow, Ui_MainWindow, DeviceGUI): +# }}} + +class Main(MainWindow, Ui_MainWindow, DeviceGUI, ToolbarMixin, + TagBrowserMixin, CoverFlowMixin, LibraryViewMixin): 'The main GUI' def set_default_thumbnail(self, height): @@ -234,7 +225,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.connect(self.system_tray_icon, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), 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 ######################## 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) ####################### Setup Toolbar ##################### - md = QMenu() - 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) + ToolbarMixin.__init__(self) ####################### Library view ######################## - 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.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) + LibraryViewMixin.__init__(self, db) self.show() + if self.system_tray_icon.isVisible() and opts.start_in_tray: self.hide_windows() 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.cover_cache = CoverCache(self.library_path) self.cover_cache.start() self.library_view.model().cover_cache = self.cover_cache self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit) 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, self.restriction_count_changed): self.library_view.model().count_changed_signal.connect(x) @@ -578,46 +311,29 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): type=Qt.QueuedConnection) ########################### Tags Browser ############################## + TagBrowserMixin.__init__(self, db) self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon) self.search_restriction.setMinimumContentsLength(10) ########################### Cover Flow ################################ - 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 - 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.library.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) + + CoverFlowMixin.__init__(self) self._calculated_available_height = min(max_available_height()-15, self.height()) self.resize(self.width(), self._calculated_available_height) self.search.setMaximumWidth(self.width()-150) + # Jobs Button {{{ + self.jobs_button = JobsButton() + self.jobs_button.initialize(self.jobs_dialog, self.job_manager) + # }}} + ####################### 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.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']: @@ -638,40 +354,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.connect(self.scheduler, SIGNAL('start_recipe_fetch(PyQt_PyObject)'), self.download_scheduled_recipe, Qt.QueuedConnection) - self.library_view.verticalHeader().sectionClicked.connect(self.view_specific_book) - - 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._add_filesystem_book = Dispatcher(self.__add_filesystem_book) 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): d = SavedSearchEditor(self, search) @@ -761,81 +449,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): error_dialog(self, _('Failed to start content server'), 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.scrollTo(self.library_view.currentIndex()) - 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.scrollTo(self.library_view.currentIndex()) - 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. Adding and deleting books creates a complexity. When added, they are @@ -906,32 +519,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.search_restriction.setCurrentIndex(0) 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 - 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): try: @@ -1309,21 +896,21 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): Dispatcher(self._files_added), spare_server=self.spare_server) 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 recursively assuming one book per folder. ''' 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 recursively assuming multiple books per folder. ''' 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 from a book file. @@ -1383,7 +970,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): def add_filesystem_book(self, paths, allow_device=True): 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. ''' @@ -2298,12 +1885,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): d.exec_() self.content_server = d.server 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.tool_bar.setToolButtonStyle( - Qt.ToolButtonTextUnderIcon if \ - config['show_text_in_toolbar'] else \ - Qt.ToolButtonIconOnly) self.save_menu.actions()[2].setText( _('Save only %s format to disk')% prefs['output_format'].upper()) @@ -2460,12 +2043,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): geometry = config['main_window_geometry'] if geometry is not None: self.restoreGeometry(geometry) - self.tool_bar.setIconSize(config['toolbar_icon_size']) - self.tool_bar.setToolButtonStyle( - Qt.ToolButtonTextUnderIcon if \ - config['show_text_in_toolbar'] else \ - Qt.ToolButtonIconOnly) - + self.read_toolbar_settings() def write_settings(self): config.set('main_window_geometry', self.saveGeometry())