KG updates pre 0.7.15

This commit is contained in:
GRiker 2010-08-17 04:38:05 -07:00
commit 5db52d70af
25 changed files with 417 additions and 106 deletions

View File

@ -24,6 +24,7 @@ series_index_auto_increment = 'next'
# invert: use "fn ln" -> "ln, fn" (the original algorithm) # invert: use "fn ln" -> "ln, fn" (the original algorithm)
# copy : copy author to author_sort without modification # copy : copy author to author_sort without modification
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert' # comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
# nocomma : "fn ln" -> "ln fn" (without the comma)
author_sort_copy_method = 'invert' author_sort_copy_method = 'invert'

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

View File

@ -0,0 +1,38 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
futurismic.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Futurismic(BasicNewsRecipe):
title = 'Futurismic'
__author__ = 'Darko Miletic'
description = 'Near-future science fiction and fact since 2001'
oldest_article = 15
max_articles_per_feed = 100
language = 'en'
encoding = 'utf-8'
no_stylesheets = True
use_embedded_content = False
publication_type = 'blog'
extra_css = ' body{font-family: Arial,Verdana,sans-serif} '
conversion_options = {
'comment' : description
, 'tags' : 'blog, sf'
, 'publisher': 'Futurismic'
, 'language' : language
}
remove_attributes = ['width','height']
keep_only_tags = [dict(attrs={'class':['post','commentlist']})]
remove_tags = [dict(attrs={'class':['sociable','feedback','tagwords']})]
feeds = [(u'Posts', u'http://feeds2.feedburner.com/futurismic_feed')]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -657,9 +657,14 @@ class ActionEditCollections(InterfaceActionBase):
name = 'Edit Collections' name = 'Edit Collections'
actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction' actual_plugin = 'calibre.gui2.actions.edit_collections:EditCollectionsAction'
class ActionCopyToLibrary(InterfaceActionBase):
name = 'Copy To Library'
actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction'
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
ActionRestart, ActionOpenFolder, ActionConnectShare, ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary] ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
ActionCopyToLibrary]

View File

@ -55,9 +55,9 @@ class ANDROID(USBMS):
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX'] 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID'] 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID'] 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID']
OSX_MAIN_MEM = 'HTC Android Phone Media' OSX_MAIN_MEM = 'HTC Android Phone Media'

View File

@ -80,6 +80,13 @@ class HANLINV3(USBMS):
drives['carda'] = main drives['carda'] = main
return drives return drives
class SPECTRA(HANLINV3):
name = 'Spectra'
gui_name = 'Spectra'
PRODUCT_ID = [0xa4a5]
FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'djvu', 'pdf', 'rtf', 'txt']
class HANLINV5(HANLINV3): class HANLINV5(HANLINV3):
name = 'Hanlin V5 driver' name = 'Hanlin V5 driver'

View File

@ -38,7 +38,7 @@ def author_to_author_sort(author):
author = _bracket_pat.sub('', author).strip() author = _bracket_pat.sub('', author).strip()
tokens = author.split() tokens = author.split()
tokens = tokens[-1:] + tokens[:-1] tokens = tokens[-1:] + tokens[:-1]
if len(tokens) > 1: if len(tokens) > 1 and method != 'nocomma':
tokens[0] += ',' tokens[0] += ','
return ' '.join(tokens) return ' '.join(tokens)

View File

@ -26,8 +26,9 @@ class InterfaceAction(QObject):
If two :class:`InterfaceAction` objects have the same name, the one with higher If two :class:`InterfaceAction` objects have the same name, the one with higher
priority takes precedence. priority takes precedence.
Sub-classes should implement the :meth:`genesis` and Sub-classes should implement the :meth:`genesis`, :meth:`library_moved`,
:meth:`location_selected` methods. :meth:`location_selected` :meth:`shutting_down`
and :meth:`initialization_complete` methods.
Once initialized, this plugin has access to the main calibre GUI via the Once initialized, this plugin has access to the main calibre GUI via the
:attr:`gui` member. You can access other plugins by name, for example:: :attr:`gui` member. You can access other plugins by name, for example::
@ -108,3 +109,28 @@ class InterfaceAction(QObject):
''' '''
pass pass
def library_changed(self, db):
'''
Called whenever the current library is changed.
:param db: The LibraryDatabase corresponding to the current library.
'''
pass
def initialization_complete(self):
'''
Called once per action when the initialization of the main GUI is
completed.
'''
pass
def shutting_down(self):
'''
Called once per plugin when the main GUI is in the process of shutting
down. Release any used resources, but try not to block the shutdown for
long periods of time.
:return: False to halt the shutdown. You are responsible for telling
the user why the shutdown was halted.
'''
return True

View File

@ -97,10 +97,21 @@ class ChooseLibraryAction(InterfaceAction):
ac.triggered.connect(partial(self.qs_requested, i)) ac.triggered.connect(partial(self.qs_requested, i))
self.choose_menu.addAction(ac) self.choose_menu.addAction(ac)
def library_used(self, db): def library_name(self):
db = self.gui.library_view.model().db
path = db.library_path
if isbytestring(path):
path = path.decode(filesystem_encoding)
path = path.replace(os.sep, '/')
return self.stats.pretty(path)
def library_changed(self, db):
self.stats.library_used(db) self.stats.library_used(db)
self.build_menus() self.build_menus()
def initialization_complete(self):
self.library_changed(self.gui.library_view.model().db)
def build_menus(self): def build_menus(self):
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
locations = list(self.stats.locations(db)) locations = list(self.stats.locations(db))

View File

@ -0,0 +1,21 @@
#!/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'
from PyQt4.Qt import QMenu
from calibre.gui2.actions import InterfaceAction
class CopyToLibraryAction(InterfaceAction):
name = 'Copy To Library'
action_spec = (_('Copy to library'), 'lt.png',
_('Copy selected books to the specified library'), None)
def genesis(self):
self.menu = QMenu(self.gui)

View File

@ -174,8 +174,14 @@ class EditMetadataAction(InterfaceAction):
_('No books selected')) _('No books selected'))
d.exec_() d.exec_()
return return
if MetadataBulkDialog(self.gui, rows, # Prevent the TagView from updating due to signals from the database
self.gui.library_view.model().db).changed: self.gui.tags_view.blockSignals(True)
try:
changed = MetadataBulkDialog(self.gui, rows,
self.gui.library_view.model().db).changed
finally:
self.gui.tags_view.blockSignals(False)
if changed:
self.gui.library_view.model().resort(reset=False) self.gui.library_view.model().resort(reset=False)
self.gui.library_view.model().research() self.gui.library_view.model().research()
self.gui.tags_view.recount() self.gui.tags_view.recount()

View File

@ -31,7 +31,12 @@ class FetchNewsAction(InterfaceAction):
self.qaction.setMenu(self.scheduler.news_menu) self.qaction.setMenu(self.scheduler.news_menu)
self.qaction.triggered.connect( self.qaction.triggered.connect(
self.scheduler.show_dialog) self.scheduler.show_dialog)
self.database_changed = self.scheduler.database_changed
def library_changed(self, db):
self.scheduler.database_changed(db)
def initialization_complete(self):
self.connect_scheduler()
def connect_scheduler(self): def connect_scheduler(self):
self.scheduler.delete_old_news.connect( self.scheduler.delete_old_news.connect(

View File

@ -196,6 +196,7 @@ class CoverFlowMixin(object):
def show_cover_browser(self): def show_cover_browser(self):
d = CBDialog(self, self.cover_flow) d = CBDialog(self, self.cover_flow)
d.addAction(self.cb_splitter.action_toggle)
self.cover_flow.setVisible(True) self.cover_flow.setVisible(True)
self.cover_flow.setFocus(Qt.OtherFocusReason) self.cover_flow.setFocus(Qt.OtherFocusReason)
d.show() d.show()

View File

@ -11,7 +11,7 @@ from functools import partial
from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \ QDate, QGroupBox, QVBoxLayout, QPlainTextEdit, QSizePolicy, \
QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \ QSpacerItem, QIcon, QCheckBox, QWidget, QHBoxLayout, SIGNAL, \
QPushButton QPushButton, QCoreApplication
from calibre.utils.date import qt_to_dt, now from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox from calibre.gui2.widgets import TagsLineEdit, EnComboBox
@ -406,13 +406,17 @@ class BulkBase(Base):
def commit(self, book_ids, notify=False): def commit(self, book_ids, notify=False):
if self.process_each_book(): if self.process_each_book():
for book_id in book_ids: for book_id in book_ids:
QCoreApplication.processEvents()
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
self.db.set_custom(book_id, self.getter(val), num=self.col_id, notify=notify) new_val = self.getter(val)
if set(val) != new_val:
self.db.set_custom(book_id, new_val, num=self.col_id, notify=notify)
else: else:
val = self.getter() val = self.getter()
val = self.normalize_ui_val(val) val = self.normalize_ui_val(val)
if val != self.initial_val: if val != self.initial_val:
for book_id in book_ids: for book_id in book_ids:
QCoreApplication.processEvents()
self.db.set_custom(book_id, val, num=self.col_id, notify=notify) self.db.set_custom(book_id, val, num=self.col_id, notify=notify)
class BulkBool(BulkBase, Bool): class BulkBool(BulkBase, Bool):
@ -431,6 +435,7 @@ class BulkDateTime(BulkBase, DateTime):
pass pass
class BulkSeries(BulkBase): class BulkSeries(BulkBase):
def setup_ui(self, parent): def setup_ui(self, parent):
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower())) values.sort(cmp = lambda x,y: cmp(x.lower(), y.lower()))
@ -456,6 +461,7 @@ class BulkSeries(BulkBase):
update_indices = self.idx_widget.checkState() update_indices = self.idx_widget.checkState()
if val != '': if val != '':
for book_id in book_ids: for book_id in book_ids:
QCoreApplication.processEvents()
if update_indices: if update_indices:
if tweaks['series_index_auto_increment'] == 'next': if tweaks['series_index_auto_increment'] == 'next':
s_index = self.db.get_next_cc_series_num_for\ s_index = self.db.get_next_cc_series_num_for\
@ -544,8 +550,9 @@ class BulkText(BulkBase):
ans = set(original_value) ans = set(original_value)
ans -= set([v.strip() for v in ans -= set([v.strip() for v in
unicode(self.removing_widget.tags_box.text()).split(',')]) unicode(self.removing_widget.tags_box.text()).split(',')])
ans |= set([v.strip() for v in txt = unicode(self.adding_widget.text())
unicode(self.adding_widget.text()).split(',')]) if txt:
ans |= set([v.strip() for v in txt.split(',')])
return ans # returning a set instead of a list works, for now at least. return ans # returning a set instead of a list works, for now at least.
val = unicode(self.widgets[1].currentText()).strip() val = unicode(self.widgets[1].currentText()).strip()
if not val: if not val:

View File

@ -48,11 +48,11 @@ class BookInfo(QDialog, Ui_BookInfo):
self.refresh(row) self.refresh(row)
def open_book_path(self, path): def open_book_path(self, path):
if os.sep in unicode(path): path = unicode(path)
if os.sep in path:
open_local_file(path) open_local_file(path)
else: else:
format = unicode(path) path = self.view.model().db.format_abspath(self.current_row, path)
path = self.view.model().db.format_abspath(self.current_row, format)
if path is not None: if path is not None:
open_local_file(path) open_local_file(path)

View File

@ -57,7 +57,7 @@
<item> <item>
<widget class="QCheckBox" name="fit_cover"> <widget class="QCheckBox" name="fit_cover">
<property name="text"> <property name="text">
<string>Fit &amp;cover to view</string> <string>Fit &amp;cover within view</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -3,14 +3,15 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Dialog to edit metadata in bulk''' '''Dialog to edit metadata in bulk'''
from PyQt4.QtCore import SIGNAL, QObject from PyQt4.Qt import SIGNAL, QObject, QDialog, QGridLayout, \
from PyQt4.QtGui import QDialog, QGridLayout QCoreApplication
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.ebooks.metadata import string_to_authors, \ from calibre.ebooks.metadata import string_to_authors, \
authors_to_string authors_to_string
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2.dialogs.progress import ProgressDialog
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
@ -25,10 +26,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
len(rows)) len(rows))
self.write_series = False self.write_series = False
self.changed = False self.changed = False
QObject.connect(self.button_box, SIGNAL("accepted()"), self.sync)
self.tags.update_tags_cache(self.db.all_tags()) all_tags = self.db.all_tags()
self.remove_tags.update_tags_cache(self.db.all_tags()) self.tags.update_tags_cache(all_tags)
self.remove_tags.update_tags_cache(all_tags)
self.initialize_combos() self.initialize_combos()
@ -102,43 +103,40 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.tags.update_tags_cache(self.db.all_tags()) self.tags.update_tags_cache(self.db.all_tags())
self.remove_tags.update_tags_cache(self.db.all_tags()) self.remove_tags.update_tags_cache(self.db.all_tags())
def sync(self): def accept(self):
for id in self.ids: if len(self.ids) < 1:
return QDialog.accept(self)
pd = ProgressDialog(_('Working'),
_('Applying changes to %d books. This may take a while.')%len(self.ids),
0, 0, self, cancelable=False)
pd.setModal(True)
pd.show()
def upd():
QCoreApplication.processEvents()
try:
remove = unicode(self.remove_tags.text()).strip().split(',')
add = unicode(self.tags.text()).strip().split(',')
au = unicode(self.authors.text()) au = unicode(self.authors.text())
if au:
au = string_to_authors(au)
self.db.set_authors(id, au, notify=False)
if self.auto_author_sort.isChecked():
x = self.db.author_sort_from_book(id, index_is_id=True)
if x:
self.db.set_author_sort(id, x, notify=False)
aus = unicode(self.author_sort.text()) aus = unicode(self.author_sort.text())
if aus and self.author_sort.isEnabled(): do_aus = self.author_sort.isEnabled()
self.db.set_author_sort(id, aus, notify=False) rating = self.rating.value()
if self.rating.value() != -1:
self.db.set_rating(id, 2*self.rating.value(), notify=False)
pub = unicode(self.publisher.text()) pub = unicode(self.publisher.text())
if pub: do_series = self.write_series
self.db.set_publisher(id, pub, notify=False)
remove_tags = unicode(self.remove_tags.text()).strip()
if remove_tags:
remove_tags = [i.strip() for i in remove_tags.split(',')]
self.db.unapply_tags(id, remove_tags, notify=False)
tags = unicode(self.tags.text()).strip()
if tags:
tags = map(lambda x: x.strip(), tags.split(','))
self.db.set_tags(id, tags, append=True, notify=False)
if self.write_series:
series = unicode(self.series.currentText()).strip() series = unicode(self.series.currentText()).strip()
next = self.db.get_next_series_num_for(series) do_autonumber = self.autonumber_series.isChecked()
self.db.set_series(id, series, notify=False) do_remove_format = self.remove_format.currentIndex() > -1
num = next if self.autonumber_series.isChecked() and series else 1.0 remove_format = unicode(self.remove_format.currentText())
self.db.set_series_index(id, num, notify=False) do_swap_ta = self.swap_title_and_author.isChecked()
do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked()
if self.remove_format.currentIndex() > -1: upd()
self.db.remove_format(id, unicode(self.remove_format.currentText()), index_is_id=True, notify=False) self.changed = bool(self.ids)
for id in self.ids:
if self.swap_title_and_author.isChecked(): upd()
if do_swap_ta:
title = self.db.title(id, index_is_id=True) title = self.db.title(id, index_is_id=True)
aum = self.db.authors(id, index_is_id=True) aum = self.db.authors(id, index_is_id=True)
if aum: if aum:
@ -148,13 +146,55 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
if title: if title:
new_authors = string_to_authors(title) new_authors = string_to_authors(title)
self.db.set_authors(id, new_authors, notify=False) self.db.set_authors(id, new_authors, notify=False)
upd()
if self.remove_conversion_settings.isChecked(): if au:
self.db.set_authors(id, string_to_authors(au), notify=False)
upd()
if do_auto_author:
x = self.db.author_sort_from_book(id, index_is_id=True)
if x:
self.db.set_author_sort(id, x, notify=False)
upd()
if aus and do_aus:
self.db.set_author_sort(id, aus, notify=False)
upd()
if rating != -1:
self.db.set_rating(id, 2*rating, notify=False)
if pub:
self.db.set_publisher(id, pub, notify=False)
upd()
if do_series:
next = self.db.get_next_series_num_for(series)
self.db.set_series(id, series, notify=False)
num = next if do_autonumber and series else 1.0
self.db.set_series_index(id, num, notify=False)
upd()
if do_remove_format:
self.db.remove_format(id, remove_format, index_is_id=True, notify=False)
upd()
if do_remove_conv:
self.db.delete_conversion_options(id, 'PIPE') self.db.delete_conversion_options(id, 'PIPE')
self.changed = True upd()
for w in getattr(self, 'custom_column_widgets', []): for w in getattr(self, 'custom_column_widgets', []):
w.commit(self.ids) w.commit(self.ids)
self.db.bulk_modify_tags(self.ids, add=add, remove=remove,
notify=False)
upd()
finally:
pd.hide()
return QDialog.accept(self)
def series_changed(self): def series_changed(self):

View File

@ -13,7 +13,8 @@ class ProgressDialog(QDialog, Ui_Dialog):
canceled_signal = pyqtSignal() canceled_signal = pyqtSignal()
def __init__(self, title, msg='', min=0, max=99, parent=None): def __init__(self, title, msg='', min=0, max=99, parent=None,
cancelable=True):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setupUi(self) self.setupUi(self)
self.setWindowTitle(title) self.setWindowTitle(title)
@ -26,6 +27,9 @@ class ProgressDialog(QDialog, Ui_Dialog):
self.canceled = False self.canceled = False
self.button_box.rejected.connect(self._canceled) self.button_box.rejected.connect(self._canceled)
if not cancelable:
self.button_box.setVisible(False)
self.cancelable = cancelable
def set_msg(self, msg=''): def set_msg(self, msg=''):
self.message.setText(msg) self.message.setText(msg)
@ -54,8 +58,14 @@ class ProgressDialog(QDialog, Ui_Dialog):
self.title.setText(_('Aborting...')) self.title.setText(_('Aborting...'))
self.canceled_signal.emit() self.canceled_signal.emit()
def reject(self):
if not self.cancelable:
return
QDialog.reject(self)
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Escape: if ev.key() == Qt.Key_Escape:
if self.cancelable:
self._canceled() self._canceled()
else: else:
QDialog.keyPressEvent(self, ev) QDialog.keyPressEvent(self, ev)

View File

@ -14,7 +14,8 @@ from Queue import Empty, Queue
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \ from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \ QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \ QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \
QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication, QAction, \
QByteArray
from calibre.utils.ipc.server import Server from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob from calibre.utils.ipc.job import ParallelJob
@ -281,6 +282,7 @@ class JobsButton(QFrame):
self.pi = ProgressIndicator(self, size) self.pi = ProgressIndicator(self, size)
self._jobs = QLabel('<b>'+_('Jobs:')+' 0') self._jobs = QLabel('<b>'+_('Jobs:')+' 0')
self._jobs.mouseReleaseEvent = self.mouseReleaseEvent self._jobs.mouseReleaseEvent = self.mouseReleaseEvent
self.shortcut = _('Shift+Alt+J')
if horizontal: if horizontal:
self.setLayout(QHBoxLayout()) self.setLayout(QHBoxLayout())
@ -297,15 +299,24 @@ class JobsButton(QFrame):
self.layout().setMargin(0) self.layout().setMargin(0)
self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self._jobs.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.setCursor(Qt.PointingHandCursor) self.setCursor(Qt.PointingHandCursor)
self.setToolTip(_('Click to see list of active jobs.')) b = _('Click to see list of jobs')
self.setToolTip(b + u' (%s)'%self.shortcut)
self.action_toggle = QAction(b, parent)
parent.addAction(self.action_toggle)
self.action_toggle.setShortcut(self.shortcut)
self.action_toggle.triggered.connect(self.toggle)
def initialize(self, jobs_dialog, job_manager): def initialize(self, jobs_dialog, job_manager):
self.jobs_dialog = jobs_dialog self.jobs_dialog = jobs_dialog
job_manager.job_added.connect(self.job_added) job_manager.job_added.connect(self.job_added)
job_manager.job_done.connect(self.job_done) job_manager.job_done.connect(self.job_done)
self.jobs_dialog.addAction(self.action_toggle)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
self.toggle()
def toggle(self, *args):
if self.jobs_dialog.isVisible(): if self.jobs_dialog.isVisible():
self.jobs_dialog.hide() self.jobs_dialog.hide()
else: else:
@ -365,13 +376,27 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate) self.jobs_view.setItemDelegateForColumn(2, self.pb_delegate)
self.jobs_view.doubleClicked.connect(self.show_job_details) self.jobs_view.doubleClicked.connect(self.show_job_details)
self.jobs_view.horizontalHeader().setMovable(True) self.jobs_view.horizontalHeader().setMovable(True)
state = gprefs.get('jobs view column layout', None) self.restore_state()
if state is not None:
def restore_state(self):
try: try:
self.jobs_view.horizontalHeader().restoreState(bytes(state)) geom = gprefs.get('jobs_dialog_geometry', bytearray(''))
self.restoreGeometry(QByteArray(geom))
state = gprefs.get('jobs view column layout', bytearray(''))
self.jobs_view.horizontalHeader().restoreState(QByteArray(state))
except: except:
pass pass
def save_state(self):
try:
state = bytearray(self.jobs_view.horizontalHeader().saveState())
gprefs['jobs view column layout'] = state
geom = bytearray(self.saveGeometry())
gprefs['jobs_dialog_geometry'] = geom
except:
pass
def show_job_details(self, index): def show_job_details(self, index):
row = index.row() row = index.row()
job = self.jobs_view.model().row_to_job(row) job = self.jobs_view.model().row_to_job(row)
@ -394,9 +419,13 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.model.kill_all_jobs() self.model.kill_all_jobs()
def closeEvent(self, e): def closeEvent(self, e):
try: self.save_state()
state = bytearray(self.jobs_view.horizontalHeader().saveState()) return QDialog.closeEvent(self, e)
gprefs['jobs view column layout'] = state
except: def show(self, *args):
pass self.restore_state()
e.accept() return QDialog.show(self, *args)
def hide(self, *args):
self.save_state()
return QDialog.hide(self, *args)

View File

@ -78,6 +78,7 @@ class TagsView(QTreeView): # {{{
self.setAnimated(True) self.setAnimated(True)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.setItemDelegate(TagDelegate(self)) self.setItemDelegate(TagDelegate(self))
self.made_connections = False
def set_database(self, db, tag_match, sort_by): def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories'] self.hidden_categories = config['tag_browser_hidden_categories']
@ -90,12 +91,14 @@ class TagsView(QTreeView): # {{{
self.search_restriction = None self.search_restriction = None
self.setModel(self._model) self.setModel(self._model)
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
pop = config['sort_tags_by'] pop = config['sort_tags_by']
self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop)) self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop))
self.sort_by.currentIndexChanged.connect(self.sort_changed) if not self.made_connections:
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
self.sort_by.currentIndexChanged.connect(self.sort_changed)
self.made_connections = True
db.add_listener(self.database_changed) db.add_listener(self.database_changed)
def database_changed(self, event, ids): def database_changed(self, event, ids):

View File

@ -132,7 +132,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
# Jobs Button {{{ # Jobs Button {{{
self.job_manager = JobManager() self.job_manager = JobManager()
self.jobs_dialog = JobsDialog(self, self.job_manager) self.jobs_dialog = JobsDialog(self, self.job_manager)
self.jobs_button = JobsButton(horizontal=True) self.jobs_button = JobsButton(horizontal=True, parent=self)
self.jobs_button.initialize(self.jobs_dialog, self.job_manager) self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
# }}} # }}}
@ -249,9 +249,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.read_settings() self.read_settings()
self.finalize_layout() self.finalize_layout()
self.donate_button.start_animation() self.donate_button.start_animation()
self.set_window_title()
self.iactions['Fetch News'].connect_scheduler() for ac in self.iactions.values():
self.iactions['Choose Library'].library_used(self.library_view.model().db) ac.initialization_complete()
def start_content_server(self): def start_content_server(self):
from calibre.library.server.main import start_threaded_server from calibre.library.server.main import start_threaded_server
@ -351,8 +352,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
def booklists(self): def booklists(self):
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
def library_moved(self, newloc): def library_moved(self, newloc):
if newloc is None: return if newloc is None: return
db = LibraryDatabase2(newloc) db = LibraryDatabase2(newloc)
@ -367,10 +366,14 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.saved_search.clear_to_help() self.saved_search.clear_to_help()
self.book_details.reset_info() self.book_details.reset_info()
self.library_view.model().count_changed() self.library_view.model().count_changed()
self.iactions['Fetch News'].database_changed(db)
prefs['library_path'] = self.library_path prefs['library_path'] = self.library_path
self.iactions['Choose Library'].library_used(self.library_view.model().db) db = self.library_view.model().db
for action in self.iactions.values():
action.library_changed(db)
self.set_window_title()
def set_window_title(self):
self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())
def location_selected(self, location): def location_selected(self, location):
''' '''
@ -511,6 +514,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
def shutdown(self, write_settings=True): def shutdown(self, write_settings=True):
for action in self.iactions.values():
if not action.shutting_down():
return
if write_settings: if write_settings:
self.write_settings() self.write_settings()
self.check_messages_timer.stop() self.check_messages_timer.stop()

View File

@ -871,14 +871,14 @@ class LayoutButton(QToolButton):
def set_state_to_show(self, *args): def set_state_to_show(self, *args):
self.setChecked(False) self.setChecked(False)
label =_('Show') label =_('Show')
self.setText(label + ' ' + self.label + ' ' + self.shortcut) self.setText(label + ' ' + self.label + u' (%s)'%self.shortcut)
self.setToolTip(self.text()) self.setToolTip(self.text())
self.setStatusTip(self.text()) self.setStatusTip(self.text())
def set_state_to_hide(self, *args): def set_state_to_hide(self, *args):
self.setChecked(True) self.setChecked(True)
label = _('Hide') label = _('Hide')
self.setText(label + ' ' + self.label+ ' ' + self.shortcut) self.setText(label + ' ' + self.label+ u' (%s)'%self.shortcut)
self.setToolTip(self.text()) self.setToolTip(self.text())
self.setStatusTip(self.text()) self.setStatusTip(self.text())
@ -941,7 +941,10 @@ class Splitter(QSplitter):
@property @property
def is_side_index_hidden(self): def is_side_index_hidden(self):
sizes = list(self.sizes()) sizes = list(self.sizes())
try:
return sizes[self.side_index] == 0 return sizes[self.side_index] == 0
except IndexError:
return True
@property @property
def save_name(self): def save_name(self):

View File

@ -26,7 +26,7 @@ from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
from calibre import isbytestring
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
@ -116,6 +116,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# so that various code taht connects directly will not complain about # so that various code taht connects directly will not complain about
# missing functions # missing functions
self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter') self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter')
# Store temporary tables in memory
self.conn.execute('pragma temp_store=2')
self.conn.commit()
@classmethod @classmethod
def exists_at(cls, path): def exists_at(cls, path):
@ -1369,6 +1372,80 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return set([]) return set([])
return set([r[0] for r in result]) return set([r[0] for r in result])
@classmethod
def cleanup_tags(cls, tags):
tags = [x.strip() for x in tags if x.strip()]
tags = [x.decode(preferred_encoding, 'replace') \
if isbytestring(x) else x for x in tags]
tags = [u' '.join(x.split()) for x in tags]
ans, seen = [], set([])
for tag in tags:
if tag.lower() not in seen:
seen.add(tag.lower())
ans.append(tag)
return ans
def bulk_modify_tags(self, ids, add=[], remove=[], notify=False):
add = self.cleanup_tags(add)
remove = self.cleanup_tags(remove)
remove = set(remove) - set(add)
if not ids or (not add and not remove):
return
# Add tags that do not already exist into the tag table
all_tags = self.all_tags()
lt = [t.lower() for t in all_tags]
new_tags = [t for t in add if t.lower() not in lt]
if new_tags:
self.conn.executemany('INSERT INTO tags(name) VALUES (?)', [(x,) for x in
new_tags])
# Create the temporary tables to store the ids for books and tags
# to be operated on
tables = ('temp_bulk_tag_edit_books', 'temp_bulk_tag_edit_add',
'temp_bulk_tag_edit_remove')
drops = '\n'.join(['DROP TABLE IF EXISTS %s;'%t for t in tables])
creates = '\n'.join(['CREATE TEMP TABLE %s(id INTEGER PRIMARY KEY);'%t
for t in tables])
self.conn.executescript(drops + creates)
# Populate the books temp table
self.conn.executemany(
'INSERT INTO temp_bulk_tag_edit_books VALUES (?)',
[(x,) for x in ids])
# Populate the add/remove tags temp tables
for table, tags in enumerate([add, remove]):
if not tags:
continue
table = tables[table+1]
insert = ('INSERT INTO %s(id) SELECT tags.id FROM tags WHERE name=?'
' COLLATE PYNOCASE LIMIT 1')
self.conn.executemany(insert%table, [(x,) for x in tags])
if remove:
self.conn.execute(
'''DELETE FROM books_tags_link WHERE
book IN (SELECT id FROM %s) AND
tag IN (SELECT id FROM %s)'''
% (tables[0], tables[2]))
if add:
self.conn.execute(
'''
INSERT INTO books_tags_link(book, tag) SELECT {0}.id, {1}.id FROM
{0}, {1}
'''.format(tables[0], tables[1])
)
self.conn.executescript(drops)
self.conn.commit()
for x in ids:
tags = u','.join(self.get_tags(x))
self.data.set(x, self.FIELD_MAP['tags'], tags, row_is_id=True)
if notify:
self.notify('metadata', ids)
def set_tags(self, id, tags, append=False, notify=True): def set_tags(self, id, tags, append=False, notify=True):
''' '''
@param tags: list of strings @param tags: list of strings
@ -1378,10 +1455,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,)) self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
self.conn.execute('DELETE FROM tags WHERE (SELECT COUNT(id) FROM books_tags_link WHERE tag=tags.id) < 1') self.conn.execute('DELETE FROM tags WHERE (SELECT COUNT(id) FROM books_tags_link WHERE tag=tags.id) < 1')
otags = self.get_tags(id) otags = self.get_tags(id)
tags = [x.strip() for x in tags if x.strip()] tags = self.cleanup_tags(tags)
tags = [x.decode(preferred_encoding, 'replace') if not isinstance(x,
unicode) else x for x in tags]
tags = [u' '.join(x.split()) for x in tags]
for tag in (set(tags)-otags): for tag in (set(tags)-otags):
tag = tag.strip() tag = tag.strip()
if not tag: if not tag:
@ -1407,7 +1481,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)', self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)',
(id, tid)) (id, tid))
self.conn.commit() self.conn.commit()
tags = ','.join(self.get_tags(id)) tags = u','.join(self.get_tags(id))
self.data.set(id, self.FIELD_MAP['tags'], tags, row_is_id=True) self.data.set(id, self.FIELD_MAP['tags'], tags, row_is_id=True)
if notify: if notify:
self.notify('metadata', [id]) self.notify('metadata', [id])

View File

@ -13,10 +13,12 @@ from threading import Thread
from Queue import Queue from Queue import Queue
from threading import RLock from threading import RLock
from datetime import datetime from datetime import datetime
from functools import partial
from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat from calibre.utils.date import parse_date, isoformat
from calibre import isbytestring
global_lock = RLock() global_lock = RLock()
@ -98,6 +100,19 @@ def _author_to_author_sort(x):
if not x: return '' if not x: return ''
return author_to_author_sort(x.replace('|', ',')) return author_to_author_sort(x.replace('|', ','))
def pynocase(one, two, encoding='utf-8'):
if isbytestring(one):
try:
one = one.decode(encoding, 'replace')
except:
pass
if isbytestring(two):
try:
two = two.decode(encoding, 'replace')
except:
pass
return cmp(one.lower(), two.lower())
class DBThread(Thread): class DBThread(Thread):
CLOSE = '-------close---------' CLOSE = '-------close---------'
@ -115,10 +130,13 @@ class DBThread(Thread):
def connect(self): def connect(self):
self.conn = sqlite.connect(self.path, factory=Connection, self.conn = sqlite.connect(self.path, factory=Connection,
detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES)
encoding = self.conn.execute('pragma encoding').fetchone()[0]
self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row) self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row)
self.conn.create_aggregate('concat', 1, Concatenate) self.conn.create_aggregate('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
self.conn.create_collation('PYNOCASE', partial(pynocase,
encoding=encoding))
if tweaks['title_series_sorting'] == 'strictly_alphabetic': if tweaks['title_series_sorting'] == 'strictly_alphabetic':
self.conn.create_function('title_sort', 1, lambda x:x) self.conn.create_function('title_sort', 1, lambda x:x)
else: else: