KG updates

This commit is contained in:
GRiker 2010-06-09 12:45:31 -06:00
commit 78ff40cd9b
23 changed files with 742 additions and 684 deletions

View File

@ -19,7 +19,7 @@ class Sueddeutsche(BasicNewsRecipe):
no_stylesheets = True
language = 'de'
encoding = 'iso-8859-15'
encoding = 'utf-8'
remove_javascript = True

View File

@ -289,7 +289,10 @@ class DevicePlugin(Plugin):
word "card" if C{on_card} is not None otherwise it must contain the word "memory".
:files: A list of paths and/or file-like objects. If they are paths and
the paths point to temporary files, they may have an additional
attribute, original_file_path pointing to the originals.
attribute, original_file_path pointing to the originals. They may have
another optional attribute, deleted_after_upload which if True means
that the file pointed to by original_file_path will be deleted after
being uploaded to the device.
:names: A list of file names that the books should have
once uploaded to the device. len(names) == len(files)
:return: A list of 3-element tuples. The list is meant to be passed

View File

@ -143,10 +143,11 @@ class PRS505(USBMS):
if booklists[i] is not None:
blists[i] = booklists[i]
opts = self.settings()
collections = ['series', 'tags']
if opts.extra_customization:
collections = [x.strip() for x in
opts.extra_customization.split(',')]
else:
collections = []
debug_print('PRS505: collection fields:', collections)
c.update(blists, collections)
c.write()

View File

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

View File

@ -599,7 +599,10 @@ class Emailer(Thread): # {{{
# }}}
class DeviceGUI(object):
class DeviceMixin(object):
def __init__(self):
self.db_book_uuid_cache = set()
def dispatch_sync_event(self, dest, delete, specific):
rows = self.library_view.selectionModel().selectedRows()
@ -851,6 +854,7 @@ class DeviceGUI(object):
def sync_news(self, send_ids=None, do_auto_convert=True):
if self.device_connected:
del_on_upload = config['delete_news_from_library_on_upload']
settings = self.device_manager.device.settings()
ids = list(dynamic.get('news_to_be_synced', set([]))) if send_ids is None else send_ids
ids = [id for id in ids if self.library_view.model().db.has_id(id)]
@ -880,6 +884,8 @@ class DeviceGUI(object):
'the device?'), det_msg=autos):
self.auto_convert_news(auto, format)
files = [f for f in files if f is not None]
for f in files:
f.deleted_after_upload = del_on_upload
if not files:
dynamic.set('news_to_be_synced', set([]))
return
@ -897,8 +903,7 @@ class DeviceGUI(object):
'rb').read())
dynamic.set('news_to_be_synced', set([]))
if config['upload_news_to_device'] and files:
remove = ids if \
config['delete_news_from_library_on_upload'] else []
remove = ids if del_on_upload else []
space = { self.location_view.model().free[0] : None,
self.location_view.model().free[1] : 'carda',
self.location_view.model().free[2] : 'cardb' }

View File

@ -38,8 +38,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
self.opt_read_metadata.setChecked(self.settings.read_metadata)
else:
self.opt_read_metadata.hide()
if extra_customization_message and settings.extra_customization:
if extra_customization_message:
self.extra_customization_label.setText(extra_customization_message)
if settings.extra_customization:
self.opt_extra_customization.setText(settings.extra_customization)
else:
self.extra_customization_label.setVisible(False)

View File

@ -3,6 +3,7 @@ __copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
'''Dialog to create a new custom column'''
import re
from functools import partial
from PyQt4.QtCore import SIGNAL
@ -94,8 +95,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
col = unicode(self.column_name_box.text()).lower()
if not col:
return self.simple_error('', _('No lookup name was provided'))
if not col.isalnum() or not col[0].isalpha():
return self.simple_error('', _('The label must contain only letters and digits, and start with a letter'))
if re.match('^\w*$', col) is None or not col[0].isalpha():
return self.simple_error('', _('The label must contain only letters, digits and underscores, and start with a letter'))
col_heading = unicode(self.column_heading_box.text())
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
if col_type == '*text':

View File

@ -403,12 +403,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
ag = QCoreApplication.instance().desktop().availableGeometry(self)
self.cover.MAX_HEIGHT = ag.height()-(25 if (islinux or isfreebsd) else 0)-height_of_rest
self.cover.MAX_WIDTH = ag.width()-(25 if (islinux or isfreebsd) else 0)-width_of_rest
if cover:
pm = QPixmap()
if cover:
pm.loadFromData(cover)
if not pm.isNull():
self.cover.setPixmap(pm)
if pm.isNull():
pm = QPixmap(I('book.svg'))
else:
self.cover_data = cover
self.cover.setPixmap(pm)
self.original_series_name = unicode(self.series.text()).strip()
if len(db.custom_column_label_map) == 0:
self.central_widget.tabBar().setVisible(False)

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.
'''
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('<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):
@ -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):

View File

@ -39,6 +39,7 @@ class FormatPath(unicode):
def __new__(cls, path, orig_file_path):
ans = unicode.__new__(cls, path)
ans.orig_file_path = orig_file_path
ans.deleted_after_upload = False
return ans
class BooksModel(QAbstractTableModel): # {{{

View File

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

View File

@ -329,7 +329,7 @@
<number>0</number>
</property>
<widget class="QWidget" name="library">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="Splitter" name="horizontal_splitter">
<property name="orientation">
@ -389,6 +389,9 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="cb_layout">
<item>
<widget class="BooksView" name="library_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@ -421,6 +424,9 @@
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>

View File

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

View File

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

View File

@ -16,6 +16,10 @@ public:
virtual int count();
virtual QImage image(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>'
__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('<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):
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)

View File

@ -52,10 +52,7 @@ class BookInfoDisplay(QWidget):
QLabel.__init__(self)
self.setMaximumWidth(81)
self.setMaximumHeight(108)
self.default_pixmap = QPixmap(coverpath).scaled(self.maximumWidth(),
self.maximumHeight(),
Qt.IgnoreAspectRatio,
Qt.SmoothTransformation)
self.default_pixmap = QPixmap(coverpath)
self.setScaledContents(True)
self.statusbar_height = 120
self.setPixmap(self.default_pixmap)

View File

@ -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']
@ -63,8 +69,7 @@ class TagsView(QTreeView): # {{{
def sort_changed(self, state):
config.set('sort_by_popularity', state == Qt.Checked)
self.model().refresh()
# self.search_restriction_set()
self.recount()
def set_search_restriction(self, s):
if s:
@ -197,7 +202,9 @@ class TagsView(QTreeView): # {{{
ci = self.indexAt(QPoint(10, 10))
path = self.model().path_for_index(ci) if self.is_visible(ci) else None
try:
self.model().refresh()
if not self.model().refresh(): # categories changed!
self.set_new_model()
path = None
except: #Database connection could be closed if an integrity check is happening
pass
if path:
@ -210,10 +217,16 @@ class TagsView(QTreeView): # {{{
# gone, or if columns have been hidden or restored, we must rebuild the
# model. Reason: it is much easier than reconstructing the browser tree.
def set_new_model(self):
try:
self._model = TagsModel(self.db, parent=self,
hidden_categories=self.hidden_categories,
search_restriction=self.search_restriction)
self.setModel(self._model)
except:
# The DB must be gone. Set the model to None and hope that someone
# will call set_database later. I don't know if this in fact works
self._model = None
self.setModel(None)
# }}}
class TagTreeItem(object): # {{{
@ -323,18 +336,9 @@ class TagsModel(QAbstractItemModel): # {{{
self.tags_view = parent
self.hidden_categories = hidden_categories
self.search_restriction = search_restriction
self.row_map = []
# Reconstruct the user categories, putting them into metadata
tb_cats = self.db.field_metadata
for k in tb_cats.keys():
if tb_cats[k]['kind'] in ['user', 'search']:
del tb_cats[k]
for user_cat in sorted(prefs['user_categories'].keys()):
cat_name = user_cat+':' # add the ':' to avoid name collision
tb_cats.add_user_category(label=cat_name, name=user_cat)
if len(saved_searches.names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
# get_node_tree cannot return None here, because row_map is empty
data = self.get_node_tree(config['sort_by_popularity'])
self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map):
@ -355,9 +359,22 @@ class TagsModel(QAbstractItemModel): # {{{
self.search_restriction = s
def get_node_tree(self, sort):
old_row_map = self.row_map[:]
self.row_map = []
self.categories = []
# Reconstruct the user categories, putting them into metadata
tb_cats = self.db.field_metadata
for k in tb_cats.keys():
if tb_cats[k]['kind'] in ['user', 'search']:
del tb_cats[k]
for user_cat in sorted(prefs['user_categories'].keys()):
cat_name = user_cat+':' # add the ':' to avoid name collision
tb_cats.add_user_category(label=cat_name, name=user_cat)
if len(saved_searches.names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
# Now get the categories
if self.search_restriction:
data = self.db.get_categories(sort_on_count=sort,
icon_map=self.category_icon_map,
@ -367,13 +384,19 @@ class TagsModel(QAbstractItemModel): # {{{
tb_categories = self.db.field_metadata
for category in tb_categories:
if category in data: # They should always be there, but ...
if category in data: # The search category can come and go
self.row_map.append(category)
self.categories.append(tb_categories[category]['name'])
if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map):
# A category has been added or removed. We must force a rebuild of
# the model
return None
return data
def refresh(self):
data = self.get_node_tree(config['sort_by_popularity']) # get category data
if data is None:
return False
row_index = -1
for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
@ -395,6 +418,7 @@ class TagsModel(QAbstractItemModel): # {{{
tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
self.endInsertRows()
return True
def columnCount(self, parent):
return 1
@ -408,6 +432,8 @@ class TagsModel(QAbstractItemModel): # {{{
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return NONE
# set up to position at the category label
path = self.path_for_index(self.parent(index))
val = unicode(value.toString())
if not val:
error_dialog(self.tags_view, _('Item is blank'),
@ -439,7 +465,12 @@ class TagsModel(QAbstractItemModel): # {{{
label=self.db.field_metadata[key]['label'])
self.tags_view.tag_item_renamed.emit()
item.tag.name = val
self.refresh()
self.refresh() # Should work, because no categories can have disappeared
if path:
idx = self.index_for_path(path)
if idx.isValid():
self.tags_view.setCurrentIndex(idx)
self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter)
return True
def headerData(self, *args):
@ -563,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 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,18 +35,17 @@ 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
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.device import DeviceManager, DeviceMenu, DeviceMixin, Emailer
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, DeviceMixin, ToolbarMixin,
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin):
'The main GUI'
def set_default_thumbnail(self, height):
@ -234,7 +225,10 @@ 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)
DeviceMixin.__init__(self)
####################### Start spare job server ########################
QTimer.singleShot(1000, self.add_spare_server)
@ -277,281 +271,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 +313,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 +356,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 +451,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
@ -854,7 +469,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
r = unicode(r)
if r is not None and r != '':
self.restriction_in_effect = True
restriction = "search:%s"%(r)
restriction = 'search:"%s"'%(r)
else:
self.restriction_in_effect = False
restriction = ''
@ -906,32 +521,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 +898,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 +972,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.
'''
@ -1557,7 +1146,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
if not confirm('<p>'+_('The selected books will be '
'<b>permanently deleted</b> '
'from your device. Are you sure?')
+'</p>', 'library_delete_books', self):
+'</p>', 'device_delete_books', self):
return
if self.stack.currentIndex() == 1:
view = self.memory_view
@ -2289,12 +1878,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())
@ -2451,12 +2036,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())

View File

@ -192,7 +192,7 @@ class CustomColumns(object):
# check if item exists
new_id = self.conn.get(
'SELECT id FROM %s WHERE value=?'%table, (new_name,), all=False)
if new_id is None:
if new_id is None or old_id == new_id:
self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, old_id))
else:
# New id exists. If the column is_multiple, then process like

View File

@ -1003,8 +1003,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
new_id = self.conn.get(
'''SELECT id from tags
WHERE name=?''', (new_name,), all=False)
if new_id is None:
# easy case. Simply rename the tag
if new_id is None or old_id == new_id:
# easy cases. Simply rename the tag. Do it even if equal, in case
# there is a change of case
self.conn.execute('''UPDATE tags SET name=?
WHERE id=?''', (new_name, old_id))
else:
@ -1041,7 +1042,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
new_id = self.conn.get(
'''SELECT id from series
WHERE name=?''', (new_name,), all=False)
if new_id is None:
if new_id is None or old_id == new_id:
self.conn.execute('UPDATE series SET name=? WHERE id=?',
(new_name, old_id))
else:
@ -1086,7 +1087,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
new_id = self.conn.get(
'''SELECT id from publishers
WHERE name=?''', (new_name,), all=False)
if new_id is None:
if new_id is None or old_id == new_id:
# New name doesn't exist. Simply change the old name
self.conn.execute('UPDATE publishers SET name=? WHERE id=?', \
(new_name, old_id))
@ -1113,22 +1114,34 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
new_name = new_name.replace(',', '|')
# Get the list of books we must fix up, one way or the other
books = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,))
# Save the list so we can use it twice
bks = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,))
books = []
for (book_id,) in bks:
books.append(book_id)
# check if the new author already exists
new_id = self.conn.get('SELECT id from authors WHERE name=?',
(new_name,), all=False)
if new_id is None:
if new_id is None or old_id == new_id:
# No name clash. Go ahead and update the author's name
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
(new_name, old_id))
else:
# First check for the degenerate case -- changing a value to itself.
# Update it in case there is a change of case, but do nothing else
if old_id == new_id:
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
(new_name, old_id))
self.conn.commit()
return
# Author exists. To fix this, we must replace all the authors
# instead of replacing the one. Reason: db integrity checks can stop
# the rename process, which would leave everything half-done. We
# can't do it the same way as tags (delete and add) because author
# order is important.
for (book_id,) in books:
for book_id in books:
# Get the existing list of authors
authors = self.conn.get('''
SELECT author from books_authors_link
@ -1139,7 +1152,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# with the new one while we are at it
for i,aut in enumerate(authors):
authors[i] = aut[0] if aut[0] != old_id else new_id
# Delete the existing authors list
self.conn.execute('''DELETE FROM books_authors_link
WHERE book=?''',(book_id,))
@ -1154,11 +1166,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# metadata. Ignore it.
pass
# Now delete the old author from the DB
bks = self.conn.get('SELECT book FROM books_authors_link WHERE author=?', (old_id,))
self.conn.execute('DELETE FROM authors WHERE id=?', (old_id,))
self.conn.commit()
# the authors are now changed, either by changing the author's name
# or replacing the author in the list. Now must fix up the books.
for (book_id,) in books:
for book_id in books:
# First, must refresh the cache to see the new authors
self.data.refresh_ids(self, [book_id])
# now fix the filesystem paths
@ -1168,14 +1181,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
SELECT authors.name
FROM authors, books_authors_link as bl
WHERE bl.book = ? and bl.author = authors.id
ORDER BY bl.id
''' , (book_id,))
# unpack the double-list structure
for i,aut in enumerate(authors):
authors[i] = aut[0]
ss = authors_to_sort_string(authors)
# Change the '|'s to ','
ss = ss.replace('|', ',')
self.conn.execute('''UPDATE books
SET author_sort=?
WHERE id=?''', (ss, old_id))
WHERE id=?''', (ss, book_id))
self.conn.commit()
# the caller will do a general refresh, so we don't need to
# do one here

View File

@ -411,7 +411,8 @@ def options(option_parser):
def opts_and_words(name, op, words):
opts = '|'.join(options(op))
words = '|'.join([w.replace("'", "\\'") for w in words])
return ('_'+name+'()'+\
fname = name.replace('-', '_')
return ('_'+fname+'()'+\
'''
{
local cur opts
@ -435,7 +436,7 @@ def opts_and_words(name, op, words):
esac
}
complete -F _'''%(opts, words) + name + ' ' + name +"\n\n").encode('utf-8')
complete -F _'''%(opts, words) + fname + ' ' + name +"\n\n").encode('utf-8')
def opts_and_exts(name, op, exts):