SONY driver: Allow the creation of an All by Something category via the tweaks. Tweaks to Tab Browser search and subcategories

This commit is contained in:
Kovid Goyal 2010-12-31 10:36:51 -07:00
commit 5c30dd001e
5 changed files with 115 additions and 60 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 # 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. # category: the category (e.g., authors, series) that the item is in.
categories_collapse_more_than = 50 categories_collapse_more_than = 50
categories_collapsed_name_template = '{first.name:shorten(4,'',0)}{last.name::shorten(4,'',0)| - |}' 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_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_collapsed_popularity_template = '{first.count:d} - {last.count:d}'
categories_collapse_model = 'first letter' categories_collapse_model = 'first letter'
# Set whether boolean custom columns are two- or three-valued. # Set whether boolean custom columns are two- or three-valued.

View File

@ -140,11 +140,19 @@ class CollectionsBookList(BookList):
all_by_author = '' all_by_author = ''
all_by_title = '' all_by_title = ''
ca = [] ca = []
all_by_something = []
for c in collection_attributes: 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() 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() 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: else:
ca.append(c.lower()) ca.append(c.lower())
collection_attributes = ca collection_attributes = ca
@ -251,6 +259,10 @@ class CollectionsBookList(BookList):
if all_by_title not in collections: if all_by_title not in collections:
collections[all_by_title] = {} collections[all_by_title] = {}
collections[all_by_title][lpath] = (book, tsval, asval) 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 # Sort collections
result = {} result = {}

View File

@ -11,19 +11,19 @@ from itertools import izip
from functools import partial from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox,\ QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,\
QAbstractItemModel, QVariant, QModelIndex, QMenu, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,\
QPushButton, QWidget, QItemDelegate, QString QPushButton, QWidget, QItemDelegate, QString, QLabel, \
QShortcut, QKeySequence, SIGNAL
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE from calibre.gui2 import config, NONE
from calibre.library.field_metadata import TagsIcons, category_icon_map 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.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.search_query_parser import saved_searches
from calibre.utils.formatter import eval_formatter 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.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.tag_list_editor import TagListEditor
@ -327,11 +327,7 @@ class TagsView(QTreeView): # {{{
path = None path = None
except: #Database connection could be closed if an integrity check is happening except: #Database connection could be closed if an integrity check is happening
pass pass
if path: self._model.show_item_at_path(path)
idx = self.model().index_for_path(path)
if idx.isValid():
self.setCurrentIndex(idx)
self.scrollTo(idx, QTreeView.PositionAtCenter)
# If the number of user categories changed, if custom columns have come or # 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 # gone, or if columns have been hidden or restored, we must rebuild the
@ -674,7 +670,6 @@ class TagsModel(QAbstractItemModel): # {{{
if data is None: if data is None:
return False return False
row_index = -1 row_index = -1
empty_tag = Tag('')
collapse = tweaks['categories_collapse_more_than'] collapse = tweaks['categories_collapse_more_than']
collapse_model = tweaks['categories_collapse_model'] collapse_model = tweaks['categories_collapse_model']
if sort_by == 'name': if sort_by == 'name':
@ -726,7 +721,7 @@ class TagsModel(QAbstractItemModel): # {{{
if cat_len > idx + collapse: if cat_len > idx + collapse:
d['last'] = data[r][idx+collapse-1] d['last'] = data[r][idx+collapse-1]
else: else:
d['last'] = empty_tag d['last'] = data[r][cat_len-1]
name = eval_formatter.safe_format(collapse_template, name = eval_formatter.safe_format(collapse_template,
d, 'TAG_VIEW', None) d, 'TAG_VIEW', None)
sub_cat = TagTreeItem(parent=category, sub_cat = TagTreeItem(parent=category,
@ -802,11 +797,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.tags_view.tag_item_renamed.emit() self.tags_view.tag_item_renamed.emit()
item.tag.name = val item.tag.name = val
self.refresh() # Should work, because no categories can have disappeared self.refresh() # Should work, because no categories can have disappeared
if path: self.show_item_at_path(path)
idx = self.index_for_path(path)
if idx.isValid():
self.tags_view.setCurrentIndex(idx)
self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter)
return True return True
def headerData(self, *args): def headerData(self, *args):
@ -934,7 +925,8 @@ class TagsModel(QAbstractItemModel): # {{{
if self.hidden_categories and self.categories[i] in self.hidden_categories: if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue continue
row_index += 1 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 continue
category_item = self.root_item.children[row_index] category_item = self.root_item.children[row_index]
for tag_item in category_item.child_tags(): for tag_item in category_item.child_tags():
@ -952,13 +944,22 @@ class TagsModel(QAbstractItemModel): # {{{
ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
return ans 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: if not txt:
return None return None
txt = lower(txt) txt = lower(txt)
if start_index is None or not start_index.isValid(): self.path_found = None
start_index = QModelIndex() if start_path is None:
self.node_found = None start_path = []
def process_tag(depth, tag_index, tag_item, start_path): def process_tag(depth, tag_index, tag_item, start_path):
path = self.path_for_index(tag_index) path = self.path_for_index(tag_index)
@ -968,7 +969,7 @@ class TagsModel(QAbstractItemModel): # {{{
if tag is None: if tag is None:
return False return False
if lower(tag.name).find(txt) >= 0: if lower(tag.name).find(txt) >= 0:
self.node_found = tag_index self.path_found = path
return True return True
return False return False
@ -979,7 +980,7 @@ class TagsModel(QAbstractItemModel): # {{{
return False return False
if path[depth] > start_path[depth]: if path[depth] > start_path[depth]:
start_path = path 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 return False
for j in xrange(self.rowCount(category_index)): for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index) tag_index = self.index(j, 0, category_index)
@ -993,21 +994,32 @@ class TagsModel(QAbstractItemModel): # {{{
return False return False
for i in xrange(self.rowCount(QModelIndex())): for i in xrange(self.rowCount(QModelIndex())):
if process_level(0, self.index(i, 0, QModelIndex()), if process_level(0, self.index(i, 0, QModelIndex()), start_path):
self.path_for_index(start_index)):
break 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): def show_item_at_index(self, idx, box=False):
if idx.isValid(): if idx.isValid():
tag_item = idx.internalPointer()
self.tags_view.setCurrentIndex(idx) self.tags_view.setCurrentIndex(idx)
self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter) self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter)
if box: if box:
tag_item = idx.internalPointer()
tag_item.boxed = True tag_item.boxed = True
self.dataChanged.emit(idx, idx) self.dataChanged.emit(idx, idx)
def clear_boxed(self): def clear_boxed(self):
'''
Clear all boxes around items.
'''
def process_tag(tag_index, tag_item): def process_tag(tag_index, tag_item):
if tag_item.boxed: if tag_item.boxed:
tag_item.boxed = False tag_item.boxed = False
@ -1148,14 +1160,15 @@ class TagBrowserWidget(QWidget): # {{{
self.setLayout(self._layout) self.setLayout(self._layout)
self._layout.setContentsMargins(0,0,0,0) self._layout.setContentsMargins(0,0,0,0)
# Set up the find box & button
search_layout = QHBoxLayout() search_layout = QHBoxLayout()
self._layout.addLayout(search_layout) self._layout.addLayout(search_layout)
self.item_search = HistoryLineEdit(parent) self.item_search = HistoryLineEdit(parent)
try: try:
self.item_search.lineEdit().setPlaceholderText(_('Find item in tag browser')) self.item_search.lineEdit().setPlaceholderText(
_('Find item in tag browser'))
except: except:
# Using Qt < 4.7 pass # Using Qt < 4.7
pass
self.item_search.setToolTip(_( self.item_search.setToolTip(_(
'Search for items. This is a "contains" search; items containing the\n' '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' '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' '*foo will filter all categories at once, showing only those items\n'
'containing the text "foo"')) 'containing the text "foo"'))
search_layout.addWidget(self.item_search) 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 = 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.setToolTip(_('Find the first/next matching item'))
self.search_button.setFixedWidth(40) self.search_button.setFixedWidth(40)
search_layout.addWidget(self.search_button) search_layout.addWidget(self.search_button)
self.current_position = None self.current_find_position = None
self.search_button.clicked.connect(self.find) self.search_button.clicked.connect(self.find)
self.item_search.initialize('tag_browser_search') self.item_search.initialize('tag_browser_search')
self.item_search.lineEdit().returnPressed.connect(self.do_find) self.item_search.lineEdit().returnPressed.connect(self.do_find)
@ -1181,6 +1198,22 @@ class TagBrowserWidget(QWidget): # {{{
self.tags_view = parent.tags_view self.tags_view = parent.tags_view
self._layout.addWidget(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) parent.sort_by = QComboBox(parent)
# Must be in the same order as db2.CATEGORY_SORTS # Must be in the same order as db2.CATEGORY_SORTS
for x in (_('Sort by name'), _('Sort by popularity'), 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) self.tags_view.set_pane_is_visible(to_what)
def find_text_changed(self, str): 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): def do_find(self, str=None):
self.current_position = None self.current_find_position = None
self.find() self.find()
def find(self): def find(self):
@ -1225,28 +1262,18 @@ class TagBrowserWidget(QWidget): # {{{
if txt.startswith('*'): if txt.startswith('*'):
self.tags_view.set_new_model(filter_categories_by=txt[1:]) self.tags_view.set_new_model(filter_categories_by=txt[1:])
self.current_position = None self.current_find_position = None
return return
if model.get_filter_categories_by(): if model.get_filter_categories_by():
self.tags_view.set_new_model(filter_categories_by=None) self.tags_view.set_new_model(filter_categories_by=None)
self.current_position = None self.current_find_position = None
model = self.tags_view.model() model = self.tags_view.model()
if not txt: if not txt:
return return
self.item_search.blockSignals(True)
self.item_search.lineEdit().blockSignals(True) self.item_search.lineEdit().blockSignals(True)
self.search_button.setFocus(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) self.item_search.lineEdit().blockSignals(False)
colon = txt.find(':') colon = txt.find(':')
@ -1256,14 +1283,20 @@ class TagBrowserWidget(QWidget): # {{{
field_metadata.search_term_to_field_key(txt[:colon]) field_metadata.search_term_to_field_key(txt[:colon])
txt = txt[colon+1:] txt = txt[colon+1:]
self.current_position = model.find_node(key, txt, self.current_position) self.current_find_position = model.find_node(key, txt,
if self.current_position: self.current_find_position)
model.show_item_at_index(self.current_position, box=True) if self.current_find_position:
model.show_item_at_path(self.current_find_position, box=True)
elif self.item_search.text(): elif self.item_search.text():
warning_dialog(self.tags_view, _('No item found'), self.not_found_label.setVisible(True)
_('No (more) matches for that search')).exec_() width = self.not_found_label.parent().width()-8
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)) item = unicode(self.itemText(i))
if item not in items: if item not in items:
items.append(item) items.append(item)
self.blockSignals(True)
self.clear()
self.addItems(items)
self.setEditText(ct)
self.blockSignals(False)
history.set(self.store_name, items) history.set(self.store_name, items)
def setText(self, t): def setText(self, t):

View File

@ -371,6 +371,12 @@ class TemplateFormatter(string.Formatter):
raise Exception('get_value must be implemented in the subclass') raise Exception('get_value must be implemented in the subclass')
def format_field(self, val, fmt): 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 # Handle conditional text
fmt, prefix, suffix = self._explode_format_string(fmt) fmt, prefix, suffix = self._explode_format_string(fmt)