KG updates pre 0.7.36

This commit is contained in:
GRiker 2010-12-31 13:16:33 -07:00
commit f6159ab767
13 changed files with 305 additions and 205 deletions

View File

@ -77,9 +77,9 @@ categories_use_field_for_author_name = 'author'
# sort: the sort value. For authors, this is the author_sort for that author
# category: the category (e.g., authors, series) that the item is in.
categories_collapse_more_than = 50
categories_collapsed_name_template = '{first.name:shorten(4,'',0)}{last.name::shorten(4,'',0)| - |}'
categories_collapsed_rating_template = '{first.avg_rating:4.2f}{last.avg_rating:4.2f| - |}'
categories_collapsed_popularity_template = '{first.count:d}{last.count:d| - |}'
categories_collapsed_name_template = '{first.name:shorten(4,'',0)} - {last.name::shorten(4,'',0)}'
categories_collapsed_rating_template = '{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = '{first.count:d} - {last.count:d}'
categories_collapse_model = 'first letter'
# Set whether boolean custom columns are two- or three-valued.

View File

@ -140,11 +140,19 @@ class CollectionsBookList(BookList):
all_by_author = ''
all_by_title = ''
ca = []
all_by_something = []
for c in collection_attributes:
if c.startswith('aba:') and c[4:]:
if c.startswith('aba:') and c[4:].strip():
all_by_author = c[4:].strip()
elif c.startswith('abt:') and c[4:]:
elif c.startswith('abt:') and c[4:].strip():
all_by_title = c[4:].strip()
elif c.startswith('abs:') and c[4:].strip():
name = c[4:].strip()
sby = self.in_category_sort_rules(name)
if sby is None:
sby = name
if name and sby:
all_by_something.append((name, sby))
else:
ca.append(c.lower())
collection_attributes = ca
@ -251,6 +259,10 @@ class CollectionsBookList(BookList):
if all_by_title not in collections:
collections[all_by_title] = {}
collections[all_by_title][lpath] = (book, tsval, asval)
for (n, sb) in all_by_something:
if n not in collections:
collections[n] = {}
collections[n][lpath] = (book, book.get(sb, ''), tsval)
# Sort collections
result = {}

View File

@ -6,11 +6,118 @@ __docformat__ = 'restructuredtext en'
import re
class TCRCompressor(object):
'''
TCR compression takes the form header+code_dict+coded_text.
The header is always "!!8-Bit!!". The code dict is a list of 256 strings.
The list takes the form 1 byte length and then a string. Each position in
The list corresponds to a code found in the file. The coded text is
string of characters values. for instance the character Q represents the
value 81 which corresponds to the string in the code list at position 81.
'''
def _reset(self):
# List of indexes in the codes list that are empty and can hold new codes
self.unused_codes = set()
self.coded_txt = ''
# Generate initial codes from text.
# The index of the list will be the code that represents the characters at that location
# in the list
self.codes = []
def _combine_codes(self):
'''
Combine two codes that always appear in pair into a single code.
The intent is to create more unused codes.
'''
possible_codes = []
a_code = set(re.findall('(?msu).', self.coded_txt))
for code in a_code:
single_code = set(re.findall('(?msu)%s.' % re.escape(code), self.coded_txt))
if len(single_code) == 1:
possible_codes.append(single_code.pop())
for code in possible_codes:
self.coded_txt = self.coded_txt.replace(code, code[0])
self.codes[ord(code[0])] = '%s%s' % (self.codes[ord(code[0])], self.codes[ord(code[1])])
def _free_unused_codes(self):
'''
Look for codes that do no not appear in the coded text and add them to
the list of free codes.
'''
for i in xrange(256):
if i not in self.unused_codes:
if chr(i) not in self.coded_txt:
self.unused_codes.add(i)
def _new_codes(self):
'''
Create new codes from codes that occur in pairs often.
'''
possible_new_codes = list(set(re.findall('(?msu)..', self.coded_txt)))
new_codes_count = []
for c in possible_new_codes:
count = self.coded_txt.count(c)
# Less than 3 occurrences will not produce any size reduction.
if count > 2:
new_codes_count.append((c, count))
# Arrange the codes in order of least to most occurring.
possible_new_codes = [x[0] for x in sorted(new_codes_count, key=lambda c: c[1])]
return possible_new_codes
def compress(self, txt):
self._reset()
self.codes = list(set(re.findall('(?msu).', txt)))
# Replace the text with their corresponding code
for c in txt:
self.coded_txt += chr(self.codes.index(c))
# Zero the unused codes and record which are unused.
for i in range(len(self.codes), 256):
self.codes.append('')
self.unused_codes.add(i)
self._combine_codes()
possible_codes = self._new_codes()
while possible_codes and self.unused_codes:
while possible_codes and self.unused_codes:
unused_code = self.unused_codes.pop()
# Take the last possible codes and split it into individual
# codes. The last possible code is the most often occurring.
code1, code2 = possible_codes.pop()
self.codes[unused_code] = '%s%s' % (self.codes[ord(code1)], self.codes[ord(code2)])
self.coded_txt = self.coded_txt.replace('%s%s' % (code1, code2), chr(unused_code))
self._combine_codes()
self._free_unused_codes()
possible_codes = self._new_codes()
self._free_unused_codes()
# Generate the code dictionary.
code_dict = []
for i in xrange(0, 256):
if i in self.unused_codes:
code_dict.append(chr(0))
else:
code_dict.append(chr(len(self.codes[i])) + self.codes[i])
# Join the identifier with the dictionary and coded text.
return '!!8-Bit!!'+''.join(code_dict)+self.coded_txt
def decompress(stream):
txt = []
stream.seek(0)
if stream.read(9) != '!!8-Bit!!':
raise ValueError('File %s contaions an invalid TCR header.' % stream.name)
raise ValueError('File %s contains an invalid TCR header.' % stream.name)
# Codes that the file contents are broken down into.
entries = []
@ -26,101 +133,6 @@ def decompress(stream):
return ''.join(txt)
def compress(txt, level=5):
'''
TCR compression takes the form header+code_list+coded_text.
The header is always "!!8-Bit!!". The code list is a list of 256 strings.
The list takes the form 1 byte length and then a string. Each position in
The list corresponds to a code found in the file. The coded text is
string of characters vaules. for instance the character Q represents the
value 81 which corresponds to the string in the code list at position 81.
'''
# Turn each unique character into a coded value.
# The code of the string at a given position are represented by the position
# they occupy in the list.
codes = list(set(re.findall('(?msu).', txt)))
for i in range(len(codes), 256):
codes.append('')
# Set the compression level.
if level <= 1:
new_length = 256
if level >= 10:
new_length = 1
else:
new_length = int(256 * (10 - level) * .1)
new_length = 1 if new_length < 1 else new_length
# Replace txt with codes.
coded_txt = ''
for c in txt:
coded_txt += chr(codes.index(c))
txt = coded_txt
# Start compressing the text.
new = True
merged = True
while new or merged:
# Merge codes that always follow another code
merge = []
merged = False
for i in xrange(256):
if codes[i] != '':
# Find all codes that are next to i.
fall = list(set(re.findall('(?msu)%s.' % re.escape(chr(i)), txt)))
# 1 if only one code comes after i.
if len(fall) == 1:
# We are searching codes and each code is always 1 character.
j = ord(fall[0][1:2])
# Only merge if the total length of the string represented by
# code is less than 256.
if len(codes[i]) + len(codes[j]) < 256:
merge.append((i, j))
if merge:
merged = True
for i, j in merge:
# Merge the string for j into the string for i.
if i == j:
# Don't use += here just in case something goes wrong. This
# will prevent out of control memory consumption. This is
# unecessary but when creating this routine it happened due
# to an error.
codes[i] = codes[i] + codes[i]
else:
codes[i] = codes[i] + codes[j]
txt = txt.replace(chr(i)+chr(j), chr(i))
if chr(j) not in txt:
codes[j] = ''
new = False
if '' in codes:
# Create a list of codes based on combinations of codes that are next
# to each other. The amount of savings for the new code is calculated.
new_codes = []
for c in list(set(re.findall('(?msu)..', txt))):
i = ord(c[0:1])
j = ord(c[1:2])
if codes[i]+codes[j] in codes:
continue
savings = txt.count(chr(i)+chr(j)) - len(codes[i]) - len(codes[j])
if savings > 2 and len(codes[i]) + len(codes[j]) < 256:
new_codes.append((savings, i, j, codes[i], codes[j]))
if new_codes:
new = True
# Sort the codes from highest savings to lowest.
new_codes.sort(lambda x, y: -1 if x[0] > y[0] else 1 if x[0] < y[0] else 0)
# The shorter new_length the more chances time merging will happen
# giving more changes for better codes to be created. However,
# the shorter new_lengh the longer it will take to compress.
new_codes = new_codes[:new_length]
for code in new_codes:
if '' not in codes:
break
c = codes.index('')
codes[c] = code[3]+code[4]
txt = txt.replace(chr(code[1])+chr(code[2]), chr(c))
# Generate the code dictionary.
header = []
for code in codes:
header.append(chr(len(code))+code)
for i in xrange(len(header), 256):
header.append(chr(0))
# Join the identifier with the dictionary and coded text.
return '!!8-Bit!!'+''.join(header)+txt
def compress(txt):
t = TCRCompressor()
return t.compress(txt)

View File

@ -22,11 +22,6 @@ class TCROutput(OutputFormatPlugin):
level=OptionRecommendation.LOW,
help=_('Specify the character encoding of the output document. ' \
'The default is utf-8.')),
OptionRecommendation(name='compression_level', recommended_value=5,
level=OptionRecommendation.LOW,
help=_('Specify the compression level to use. Scale 1 - 10. 1 ' \
'being the lowest compression but the fastest and 10 being the ' \
'highest compression but the slowest.')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
@ -48,7 +43,7 @@ class TCROutput(OutputFormatPlugin):
txt = writer.extract_content(oeb_book, opts).encode(opts.output_encoding, 'replace')
log.info('Compressing text...')
txt = compress(txt, opts.compression_level)
txt = compress(txt)
out_stream.seek(0)
out_stream.truncate()

View File

@ -6,11 +6,9 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from calibre.gui2 import gprefs
from calibre.gui2.catalog.catalog_csv_xml_ui import Ui_Form
from calibre.library.database2 import LibraryDatabase2
from calibre.utils.config import prefs
from calibre.library import db as db_
from PyQt4.Qt import QWidget, QListWidgetItem
class PluginWidget(QWidget, Ui_Form):
@ -30,8 +28,7 @@ class PluginWidget(QWidget, Ui_Form):
self.all_fields.append(x)
QListWidgetItem(x, self.db_fields)
dbpath = os.path.abspath(prefs['library_path'])
db = LibraryDatabase2(dbpath)
db = db_()
for x in sorted(db.custom_field_keys()):
self.all_fields.append(x)
QListWidgetItem(x, self.db_fields)

View File

@ -97,7 +97,7 @@ class PluginWidget(QWidget,Ui_Form):
#self.read_source_field.setCurrentIndex(index)
getattr(self,c_name).setCurrentIndex(index)
elif c_type in ['line_edit']:
getattr(self, c_name).setText(opt_value)
getattr(self, c_name).setText(opt_value if opt_value else '')
elif c_type in ['radio_button'] and opt_value is not None:
getattr(self, c_name).setChecked(opt_value)
elif c_type in ['spin_box']:
@ -145,11 +145,11 @@ class PluginWidget(QWidget,Ui_Form):
if c_type in ['check_box', 'radio_button']:
opt_value = getattr(self, c_name).isChecked()
elif c_type in ['combo_box']:
opt_value = unicode(getattr(self,c_name).currentText())
opt_value = unicode(getattr(self,c_name).currentText()).strip()
elif c_type in ['line_edit']:
opt_value = unicode(getattr(self, c_name).text())
opt_value = unicode(getattr(self, c_name).text()).strip()
elif c_type in ['spin_box']:
opt_value = unicode(getattr(self, c_name).cleanText())
opt_value = unicode(getattr(self, c_name).value())
gprefs.set(self.name + '_' + c_name, opt_value)
# Construct opts object

View File

@ -44,35 +44,35 @@
<item row="0" column="0">
<widget class="QCheckBox" name="generate_titles">
<property name="text">
<string>Books by Title</string>
<string>Books by &amp;Title</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="generate_series">
<property name="text">
<string>Books by Series</string>
<string>Books by &amp;Series</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="generate_recently_added">
<property name="text">
<string>Recently Added</string>
<string>Recently &amp;Added</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="generate_genres">
<property name="text">
<string>Books by Genre</string>
<string>Books by &amp;Genre</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="generate_descriptions">
<property name="text">
<string>Descriptions</string>
<string>&amp;Descriptions</string>
</property>
</widget>
</item>
@ -133,7 +133,7 @@ p, li { white-space: pre-wrap; }
</size>
</property>
<property name="text">
<string>Tags to exclude</string>
<string>Tags to &amp;exclude</string>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>
@ -144,6 +144,9 @@ p, li { white-space: pre-wrap; }
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_genre</cstring>
</property>
</widget>
</item>
<item>
@ -214,7 +217,7 @@ p, li { white-space: pre-wrap; }
</size>
</property>
<property name="text">
<string>Tags to exclude</string>
<string>Tags to &amp;exclude</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -222,6 +225,9 @@ p, li { white-space: pre-wrap; }
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_tags</cstring>
</property>
</widget>
</item>
<item>
@ -261,7 +267,7 @@ p, li { white-space: pre-wrap; }
</size>
</property>
<property name="text">
<string>Column/value</string>
<string>&amp;Column/value</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -269,6 +275,9 @@ p, li { white-space: pre-wrap; }
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_source_field</cstring>
</property>
</widget>
</item>
<item>
@ -349,7 +358,7 @@ p, li { white-space: pre-wrap; }
</size>
</property>
<property name="text">
<string>Column/value</string>
<string>&amp;Column/value</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -357,6 +366,9 @@ p, li { white-space: pre-wrap; }
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>read_source_field</cstring>
</property>
</widget>
</item>
<item>
@ -443,7 +455,7 @@ p, li { white-space: pre-wrap; }
<string/>
</property>
<property name="text">
<string>Wishlist tag</string>
<string>&amp;Wishlist tag</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -451,6 +463,9 @@ p, li { white-space: pre-wrap; }
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>wishlist_tag</cstring>
</property>
</widget>
</item>
<item>
@ -479,7 +494,7 @@ p, li { white-space: pre-wrap; }
</size>
</property>
<property name="text">
<string>Thumbnail width</string>
<string>&amp;Thumbnail width</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@ -487,6 +502,9 @@ p, li { white-space: pre-wrap; }
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>thumb_width</cstring>
</property>
</widget>
</item>
<item>
@ -501,7 +519,7 @@ p, li { white-space: pre-wrap; }
<string>Size hint for Description cover thumbnails</string>
</property>
<property name="suffix">
<string>&quot;</string>
<string> inch</string>
</property>
<property name="decimals">
<number>2</number>
@ -545,11 +563,14 @@ p, li { white-space: pre-wrap; }
<string/>
</property>
<property name="text">
<string>Description note</string>
<string>&amp;Description note</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>header_note_source_field</cstring>
</property>
</widget>
</item>
<item>
@ -590,11 +611,14 @@ p, li { white-space: pre-wrap; }
</size>
</property>
<property name="text">
<string>Merge with Comments</string>
<string>&amp;Merge with Comments</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>merge_source_field</cstring>
</property>
</widget>
</item>
<item>
@ -623,7 +647,7 @@ p, li { white-space: pre-wrap; }
<string>Merge additional content before Comments</string>
</property>
<property name="text">
<string>Before</string>
<string>&amp;Before</string>
</property>
</widget>
</item>
@ -633,7 +657,7 @@ p, li { white-space: pre-wrap; }
<string>Merge additional content after Comments</string>
</property>
<property name="text">
<string>After</string>
<string>&amp;After</string>
</property>
</widget>
</item>
@ -650,7 +674,7 @@ p, li { white-space: pre-wrap; }
<string>Separate Comments and additional content with horizontal rule</string>
</property>
<property name="text">
<string>&lt;hr /&gt;</string>
<string>&amp;Separator</string>
</property>
</widget>
</item>

View File

@ -11,19 +11,19 @@ from itertools import izip
from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox,\
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
QPushButton, QWidget, QItemDelegate, QString
QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,\
QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,\
QPushButton, QWidget, QItemDelegate, QString, QLabel, \
QShortcut, QKeySequence, SIGNAL
from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE
from calibre.library.field_metadata import TagsIcons, category_icon_map
from calibre.library.database2 import Tag
from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key, upper, lower
from calibre.utils.icu import sort_key, upper, lower, strcmp
from calibre.utils.search_query_parser import saved_searches
from calibre.utils.formatter import eval_formatter
from calibre.gui2 import error_dialog, warning_dialog
from calibre.gui2 import error_dialog
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
@ -327,11 +327,7 @@ class TagsView(QTreeView): # {{{
path = None
except: #Database connection could be closed if an integrity check is happening
pass
if path:
idx = self.model().index_for_path(path)
if idx.isValid():
self.setCurrentIndex(idx)
self.scrollTo(idx, QTreeView.PositionAtCenter)
self._model.show_item_at_path(path)
# If the number of user categories changed, if custom columns have come or
# gone, or if columns have been hidden or restored, we must rebuild the
@ -674,7 +670,6 @@ class TagsModel(QAbstractItemModel): # {{{
if data is None:
return False
row_index = -1
empty_tag = Tag('')
collapse = tweaks['categories_collapse_more_than']
collapse_model = tweaks['categories_collapse_model']
if sort_by == 'name':
@ -726,7 +721,7 @@ class TagsModel(QAbstractItemModel): # {{{
if cat_len > idx + collapse:
d['last'] = data[r][idx+collapse-1]
else:
d['last'] = empty_tag
d['last'] = data[r][cat_len-1]
name = eval_formatter.safe_format(collapse_template,
d, 'TAG_VIEW', None)
sub_cat = TagTreeItem(parent=category,
@ -802,11 +797,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.tags_view.tag_item_renamed.emit()
item.tag.name = val
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)
self.show_item_at_path(path)
return True
def headerData(self, *args):
@ -934,7 +925,8 @@ class TagsModel(QAbstractItemModel): # {{{
if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue
row_index += 1
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
if key.endswith(':'):
# User category, so skip it. The tag will be marked in its real category
continue
category_item = self.root_item.children[row_index]
for tag_item in category_item.child_tags():
@ -952,13 +944,22 @@ class TagsModel(QAbstractItemModel): # {{{
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
return ans
def find_node(self, key, txt, start_index):
def find_node(self, key, txt, start_path):
'''
Search for an item (a node) in the tags browser list that matches both
the key (exact case-insensitive match) and txt (contains case-
insensitive match). Returns the path to the node. Note that paths are to
a location (second item, fourth item, 25 item), not to a node. If
start_path is None, the search starts with the topmost node. If the tree
is changed subsequent to calling this method, the path can easily refer
to a different node or no node at all.
'''
if not txt:
return None
txt = lower(txt)
if start_index is None or not start_index.isValid():
start_index = QModelIndex()
self.node_found = None
self.path_found = None
if start_path is None:
start_path = []
def process_tag(depth, tag_index, tag_item, start_path):
path = self.path_for_index(tag_index)
@ -968,7 +969,7 @@ class TagsModel(QAbstractItemModel): # {{{
if tag is None:
return False
if lower(tag.name).find(txt) >= 0:
self.node_found = tag_index
self.path_found = path
return True
return False
@ -979,7 +980,7 @@ class TagsModel(QAbstractItemModel): # {{{
return False
if path[depth] > start_path[depth]:
start_path = path
if key and category_index.internalPointer().category_key != key:
if key and strcmp(category_index.internalPointer().category_key, key) != 0:
return False
for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index)
@ -993,21 +994,32 @@ class TagsModel(QAbstractItemModel): # {{{
return False
for i in xrange(self.rowCount(QModelIndex())):
if process_level(0, self.index(i, 0, QModelIndex()),
self.path_for_index(start_index)):
if process_level(0, self.index(i, 0, QModelIndex()), start_path):
break
return self.node_found
return self.path_found
def show_item_at_path(self, path, box=False):
'''
Scroll the browser and open categories to show the item referenced by
path. If possible, the item is placed in the center. If box=True, a
box is drawn around the item.
'''
if path:
self.show_item_at_index(self.index_for_path(path), box)
def show_item_at_index(self, idx, box=False):
if idx.isValid():
tag_item = idx.internalPointer()
self.tags_view.setCurrentIndex(idx)
self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter)
if box:
tag_item = idx.internalPointer()
tag_item.boxed = True
self.dataChanged.emit(idx, idx)
def clear_boxed(self):
'''
Clear all boxes around items.
'''
def process_tag(tag_index, tag_item):
if tag_item.boxed:
tag_item.boxed = False
@ -1148,14 +1160,15 @@ class TagBrowserWidget(QWidget): # {{{
self.setLayout(self._layout)
self._layout.setContentsMargins(0,0,0,0)
# Set up the find box & button
search_layout = QHBoxLayout()
self._layout.addLayout(search_layout)
self.item_search = HistoryLineEdit(parent)
try:
self.item_search.lineEdit().setPlaceholderText(_('Find item in tag browser'))
self.item_search.lineEdit().setPlaceholderText(
_('Find item in tag browser'))
except:
# Using Qt < 4.7
pass
pass # Using Qt < 4.7
self.item_search.setToolTip(_(
'Search for items. This is a "contains" search; items containing the\n'
'text anywhere in the name will be found. You can limit the search\n'
@ -1164,12 +1177,16 @@ class TagBrowserWidget(QWidget): # {{{
'*foo will filter all categories at once, showing only those items\n'
'containing the text "foo"'))
search_layout.addWidget(self.item_search)
# Not sure if the shortcut should be translatable ...
sc = QShortcut(QKeySequence(_('ALT+f')), parent)
sc.connect(sc, SIGNAL('activated()'), self.set_focus_to_find_box)
self.search_button = QPushButton()
self.search_button.setText(_('&Find'))
self.search_button.setText(_('F&ind'))
self.search_button.setToolTip(_('Find the first/next matching item'))
self.search_button.setFixedWidth(40)
search_layout.addWidget(self.search_button)
self.current_position = None
self.current_find_position = None
self.search_button.clicked.connect(self.find)
self.item_search.initialize('tag_browser_search')
self.item_search.lineEdit().returnPressed.connect(self.do_find)
@ -1181,6 +1198,22 @@ class TagBrowserWidget(QWidget): # {{{
self.tags_view = parent.tags_view
self._layout.addWidget(parent.tags_view)
# Now the floating 'not found' box
l = QLabel(self.tags_view)
self.not_found_label = l
l.setFrameStyle(QFrame.StyledPanel)
l.setAutoFillBackground(True)
l.setText('<p><b>'+_('No More Matches.</b><p> Click Find again to go to first match'))
l.setAlignment(Qt.AlignVCenter)
l.setWordWrap(True)
l.resize(l.sizeHint())
l.move(10,20)
l.setVisible(False)
self.not_found_label_timer = QTimer()
self.not_found_label_timer.setSingleShot(True)
self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event,
type=Qt.QueuedConnection)
parent.sort_by = QComboBox(parent)
# Must be in the same order as db2.CATEGORY_SORTS
for x in (_('Sort by name'), _('Sort by popularity'),
@ -1212,10 +1245,14 @@ class TagBrowserWidget(QWidget): # {{{
self.tags_view.set_pane_is_visible(to_what)
def find_text_changed(self, str):
self.current_position = None
self.current_find_position = None
def set_focus_to_find_box(self):
self.item_search.setFocus()
self.item_search.lineEdit().selectAll()
def do_find(self, str=None):
self.current_position = None
self.current_find_position = None
self.find()
def find(self):
@ -1225,28 +1262,18 @@ class TagBrowserWidget(QWidget): # {{{
if txt.startswith('*'):
self.tags_view.set_new_model(filter_categories_by=txt[1:])
self.current_position = None
self.current_find_position = None
return
if model.get_filter_categories_by():
self.tags_view.set_new_model(filter_categories_by=None)
self.current_position = None
self.current_find_position = None
model = self.tags_view.model()
if not txt:
return
self.item_search.blockSignals(True)
self.item_search.lineEdit().blockSignals(True)
self.search_button.setFocus(True)
idx = self.item_search.findText(txt, Qt.MatchFixedString)
if idx < 0:
self.item_search.insertItem(0, txt)
else:
t = self.item_search.itemText(idx)
self.item_search.removeItem(idx)
self.item_search.insertItem(0, t)
self.item_search.setCurrentIndex(0)
self.item_search.blockSignals(False)
self.item_search.lineEdit().blockSignals(False)
colon = txt.find(':')
@ -1256,14 +1283,24 @@ class TagBrowserWidget(QWidget): # {{{
field_metadata.search_term_to_field_key(txt[:colon])
txt = txt[colon+1:]
self.current_position = model.find_node(key, txt, self.current_position)
if self.current_position:
model.show_item_at_index(self.current_position, box=True)
self.current_find_position = model.find_node(key, txt,
self.current_find_position)
if self.current_find_position:
model.show_item_at_path(self.current_find_position, box=True)
elif self.item_search.text():
warning_dialog(self.tags_view, _('No item found'),
_('No (more) matches for that search')).exec_()
self.not_found_label.setVisible(True)
if self.tags_view.verticalScrollBar().isVisible():
sbw = self.tags_view.verticalScrollBar().width()
else:
sbw = 0
width = self.width() - 8 - sbw
height = self.not_found_label.heightForWidth(width) + 20
self.not_found_label.resize(width, height)
self.not_found_label.move(4, 10)
self.not_found_label_timer.start(2000)
def not_found_label_timer_event(self):
self.not_found_label.setVisible(False)
# }}}

View File

@ -551,7 +551,11 @@ class HistoryLineEdit(QComboBox):
item = unicode(self.itemText(i))
if item not in items:
items.append(item)
self.blockSignals(True)
self.clear()
self.addItems(items)
self.setEditText(ct)
self.blockSignals(False)
history.set(self.store_name, items)
def setText(self, t):

View File

@ -144,8 +144,10 @@ class SendEmail(QWidget, Ui_Form):
bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject)
d.tl = QLabel('<p>'+_('You can sign up for a free {name} email '
'account at <a href="http://{url}">http://{url}</a>. {extra}').format(
d.tl = QLabel(('<p>'+_('Setup sending email using') +
' <b>{name}</b><p>' +
_('If you don\'t have an account, you can sign up for a free {name} email '
'account at <a href="http://{url}">http://{url}</a>. {extra}')).format(
**service))
l.addWidget(d.tl, 0, 0, 3, 0)
d.tl.setWordWrap(True)

View File

@ -148,7 +148,6 @@ class CSV_XML(CatalogPlugin):
outfile.close()
elif self.fmt == 'xml':
#from lxml import etree
from lxml.builder import E
root = E.calibredb()
@ -1809,10 +1808,11 @@ class EPUB_MOBI(CatalogPlugin):
if not self.useSeriesPrefixInTitlesSection:
title_list = self.booksByTitle_noSeriesPrefix
drtc = 0
divRunningTag = None
for book in title_list:
if self.letter_or_symbol(book['title_sort'][0]) != current_letter :
# Start a new letter
if drtc:
if drtc and divRunningTag is not None:
divTag.insert(dtc, divRunningTag)
dtc += 1
divRunningTag = Tag(soup, 'div')
@ -1874,12 +1874,14 @@ class EPUB_MOBI(CatalogPlugin):
pBookTag.insert(ptc, emTag)
ptc += 1
divRunningTag.insert(drtc, pBookTag)
if divRunningTag is not None:
divRunningTag.insert(drtc, pBookTag)
drtc += 1
# Add the last divRunningTag to divTag
divTag.insert(dtc, divRunningTag)
dtc += 1
if divRunningTag is not None:
divTag.insert(dtc, divRunningTag)
dtc += 1
# Add the divTag to the body
body.insert(btc, divTag)
@ -1938,6 +1940,7 @@ class EPUB_MOBI(CatalogPlugin):
# Loop through booksByAuthor
book_count = 0
divRunningTag = None
for book in self.booksByAuthor:
book_count += 1
if self.letter_or_symbol(book['author_sort'][0].upper()) != current_letter :
@ -1964,7 +1967,7 @@ class EPUB_MOBI(CatalogPlugin):
# Add divOpeningTag to divTag
divTag.insert(dtc, divOpeningTag)
dtc += 1
elif author_count > 2:
elif author_count > 2 and divRunningTag is not None:
divTag.insert(dtc, divRunningTag)
dtc += 1
@ -1983,7 +1986,7 @@ class EPUB_MOBI(CatalogPlugin):
if author_count == 1:
divOpeningTag.insert(dotc, pAuthorTag)
dotc += 1
else:
elif divRunningTag is not None:
divRunningTag.insert(drtc,pAuthorTag)
drtc += 1
@ -2008,7 +2011,7 @@ class EPUB_MOBI(CatalogPlugin):
if author_count == 1:
divOpeningTag.insert(dotc, pSeriesTag)
dotc += 1
else:
elif divRunningTag is not None:
divRunningTag.insert(drtc,pSeriesTag)
drtc += 1
if current_series and not book['series']:
@ -2056,7 +2059,7 @@ class EPUB_MOBI(CatalogPlugin):
if author_count == 1:
divOpeningTag.insert(dotc, pBookTag)
dotc += 1
else:
elif divRunningTag is not None:
divRunningTag.insert(drtc,pBookTag)
drtc += 1
@ -4048,9 +4051,13 @@ class EPUB_MOBI(CatalogPlugin):
field,
index_is_id=True)
if field_contents:
if re.search(pat, unicode(field_contents),
re.IGNORECASE) is not None:
return True
try:
if re.search(pat, unicode(field_contents),
re.IGNORECASE) is not None:
return True
except:
# Compiling of pat failed, ignore it
pass
return False

View File

@ -2728,6 +2728,10 @@ books_series_link feeds
(book_id, name))
self.commit()
def get_ids_for_custom_book_data(self, name):
s = self.conn.get('''SELECT book FROM books_plugin_data WHERE name=?''', (name,))
return [x[0] for x in s]
def get_custom_recipes(self):
for id, title, script in self.conn.get('SELECT id,title,script FROM feeds'):
yield id, title, script

View File

@ -371,6 +371,12 @@ class TemplateFormatter(string.Formatter):
raise Exception('get_value must be implemented in the subclass')
def format_field(self, val, fmt):
# ensure we are dealing with a string.
if isinstance(val, (int, float)):
if val:
val = unicode(val)
else:
val = ''
# Handle conditional text
fmt, prefix, suffix = self._explode_format_string(fmt)