mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add tooltips to top level tag browser items. Move tag vategory management to db2
This commit is contained in:
commit
8a488566c1
@ -88,17 +88,28 @@ CALIBRE_METADATA_FIELDS = frozenset([
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CALIBRE_RESERVED_LABELS = frozenset([
|
||||||
|
'search', # reserved for saved searches
|
||||||
|
'date',
|
||||||
|
'all',
|
||||||
|
'ondevice',
|
||||||
|
'inlibrary',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
RESERVED_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
RESERVED_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
||||||
PUBLICATION_METADATA_FIELDS).union(
|
PUBLICATION_METADATA_FIELDS).union(
|
||||||
BOOK_STRUCTURE_FIELDS).union(
|
BOOK_STRUCTURE_FIELDS).union(
|
||||||
USER_METADATA_FIELDS).union(
|
USER_METADATA_FIELDS).union(
|
||||||
DEVICE_METADATA_FIELDS).union(
|
DEVICE_METADATA_FIELDS).union(
|
||||||
CALIBRE_METADATA_FIELDS)
|
CALIBRE_METADATA_FIELDS).union(
|
||||||
|
CALIBRE_RESERVED_LABELS)
|
||||||
|
|
||||||
assert len(RESERVED_METADATA_FIELDS) == sum(map(len, (
|
assert len(RESERVED_METADATA_FIELDS) == sum(map(len, (
|
||||||
SOCIAL_METADATA_FIELDS, PUBLICATION_METADATA_FIELDS,
|
SOCIAL_METADATA_FIELDS, PUBLICATION_METADATA_FIELDS,
|
||||||
BOOK_STRUCTURE_FIELDS, USER_METADATA_FIELDS,
|
BOOK_STRUCTURE_FIELDS, USER_METADATA_FIELDS,
|
||||||
DEVICE_METADATA_FIELDS, CALIBRE_METADATA_FIELDS,
|
DEVICE_METADATA_FIELDS, CALIBRE_METADATA_FIELDS,
|
||||||
|
CALIBRE_RESERVED_LABELS
|
||||||
)))
|
)))
|
||||||
|
|
||||||
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
||||||
|
@ -97,8 +97,6 @@ def _config():
|
|||||||
help=_('Overwrite author and title with new metadata'))
|
help=_('Overwrite author and title with new metadata'))
|
||||||
c.add_opt('enforce_cpu_limit', default=True,
|
c.add_opt('enforce_cpu_limit', default=True,
|
||||||
help=_('Limit max simultaneous jobs to number of CPUs'))
|
help=_('Limit max simultaneous jobs to number of CPUs'))
|
||||||
c.add_opt('user_categories', default={},
|
|
||||||
help=_('User-created tag browser categories'))
|
|
||||||
|
|
||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from PyQt4.QtCore import SIGNAL, Qt
|
|||||||
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
||||||
|
|
||||||
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
|
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
|
||||||
from calibre.gui2 import config
|
from calibre.utils.config import prefs
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.constants import islinux
|
from calibre.constants import islinux
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ class Item:
|
|||||||
return 'name=%s, label=%s, index=%s, exists='%(self.name, self.label, self.index, self.exists)
|
return 'name=%s, label=%s, index=%s, exists='%(self.name, self.label, self.index, self.exists)
|
||||||
|
|
||||||
class TagCategories(QDialog, Ui_TagCategories):
|
class TagCategories(QDialog, Ui_TagCategories):
|
||||||
category_labels_orig = ['', 'author', 'series', 'publisher', 'tag']
|
category_labels_orig = ['', 'authors', 'series', 'publishers', 'tags']
|
||||||
|
|
||||||
def __init__(self, window, db, index=None):
|
def __init__(self, window, db, index=None):
|
||||||
QDialog.__init__(self, window)
|
QDialog.__init__(self, window)
|
||||||
@ -64,7 +64,7 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
self.all_items.append(t)
|
self.all_items.append(t)
|
||||||
self.all_items_dict[label+':'+n] = t
|
self.all_items_dict[label+':'+n] = t
|
||||||
|
|
||||||
self.categories = dict.copy(config['user_categories'])
|
self.categories = dict.copy(prefs['user_categories'])
|
||||||
if self.categories is None:
|
if self.categories is None:
|
||||||
self.categories = {}
|
self.categories = {}
|
||||||
for cat in self.categories:
|
for cat in self.categories:
|
||||||
@ -181,7 +181,7 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
self.save_category()
|
self.save_category()
|
||||||
config['user_categories'] = self.categories
|
prefs['user_categories'] = self.categories
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
def save_category(self):
|
def save_category(self):
|
||||||
|
@ -126,7 +126,7 @@ class TagTreeItem(object): # {{{
|
|||||||
TAG = 1
|
TAG = 1
|
||||||
ROOT = 2
|
ROOT = 2
|
||||||
|
|
||||||
def __init__(self, data=None, category_icon=None, icon_map=None, parent=None):
|
def __init__(self, data=None, category_icon=None, icon_map=None, parent=None, tooltip=None):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.children = []
|
self.children = []
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
@ -144,6 +144,7 @@ class TagTreeItem(object): # {{{
|
|||||||
elif self.type == self.TAG:
|
elif self.type == self.TAG:
|
||||||
icon_map[0] = data.icon
|
icon_map[0] = data.icon
|
||||||
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
|
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
|
||||||
|
self.tooltip = tooltip
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.type == self.ROOT:
|
if self.type == self.ROOT:
|
||||||
@ -175,6 +176,8 @@ class TagTreeItem(object): # {{{
|
|||||||
return self.icon
|
return self.icon
|
||||||
if role == Qt.FontRole:
|
if role == Qt.FontRole:
|
||||||
return self.bold_font
|
return self.bold_font
|
||||||
|
if role == Qt.ToolTipRole and self.tooltip is not None:
|
||||||
|
return QVariant(self.tooltip)
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def tag_data(self, role):
|
def tag_data(self, role):
|
||||||
@ -199,31 +202,37 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
categories_orig = [_('Authors'), _('Series'), _('Formats'), _('Publishers'),
|
categories_orig = [_('Authors'), _('Series'), _('Formats'), _('Publishers'),
|
||||||
_('Ratings'), _('News'), _('Tags')]
|
_('Ratings'), _('News'), _('Tags')]
|
||||||
row_map_orig = ['author', 'series', 'format', 'publisher', 'rating',
|
row_map_orig = ['authors', 'series', 'formats', 'publishers', 'ratings',
|
||||||
'news', 'tag']
|
'news', 'tags']
|
||||||
tags_categories_start= 7
|
|
||||||
search_keys=['search', _('Searches')]
|
search_keys=['search', _('Searches')]
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, db, parent=None):
|
def __init__(self, db, parent=None):
|
||||||
QAbstractItemModel.__init__(self, parent)
|
QAbstractItemModel.__init__(self, parent)
|
||||||
self.cat_icon_map_orig = list(map(QIcon, [I('user_profile.svg'),
|
|
||||||
I('series.svg'), I('book.svg'), I('publisher.png'), I('star.png'),
|
# must do this here because 'QPixmap: Must construct a QApplication
|
||||||
I('news.svg'), I('tags.svg')]))
|
# before a QPaintDevice'
|
||||||
|
self.category_icon_map = {'authors': QIcon(I('user_profile.svg')),
|
||||||
|
'series': QIcon(I('series.svg')),
|
||||||
|
'formats':QIcon(I('book.svg')),
|
||||||
|
'publishers': QIcon(I('publisher.png')),
|
||||||
|
'ratings':QIcon(I('star.png')),
|
||||||
|
'news':QIcon(I('news.svg')),
|
||||||
|
'tags':QIcon(I('tags.svg')),
|
||||||
|
'*custom':QIcon(I('column.svg')),
|
||||||
|
'*user':QIcon(I('drawer.svg')),
|
||||||
|
'search':QIcon(I('search.svg'))}
|
||||||
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
||||||
self.custcol_icon = QIcon(I('column.svg'))
|
|
||||||
self.search_icon = QIcon(I('search.svg'))
|
|
||||||
self.usercat_icon = QIcon(I('drawer.svg'))
|
|
||||||
self.label_to_icon_map = dict(map(None, self.row_map_orig, self.cat_icon_map_orig))
|
|
||||||
self.label_to_icon_map['*custom'] = self.custcol_icon
|
|
||||||
self.db = db
|
self.db = db
|
||||||
self.search_restriction = ''
|
self.search_restriction = ''
|
||||||
self.user_categories = {}
|
|
||||||
self.ignore_next_search = 0
|
self.ignore_next_search = 0
|
||||||
data = self.get_node_tree(config['sort_by_popularity'])
|
data = self.get_node_tree(config['sort_by_popularity'])
|
||||||
self.root_item = TagTreeItem()
|
self.root_item = TagTreeItem()
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
c = TagTreeItem(parent=self.root_item,
|
c = TagTreeItem(parent=self.root_item,
|
||||||
data=self.categories[i], category_icon=self.cat_icon_map[i])
|
data=self.categories[i],
|
||||||
|
category_icon=self.category_icon_map[r],
|
||||||
|
tooltip=_('The lookup/search name is "{0}"').format(r))
|
||||||
for tag in data[r]:
|
for tag in data[r]:
|
||||||
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
||||||
|
|
||||||
@ -233,66 +242,19 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
def get_node_tree(self, sort):
|
def get_node_tree(self, sort):
|
||||||
self.row_map = []
|
self.row_map = []
|
||||||
self.categories = []
|
self.categories = []
|
||||||
# strip the icons after the 'standard' categories. We will put them back later
|
|
||||||
if self.tags_categories_start < len(self.row_map_orig):
|
|
||||||
self.cat_icon_map = self.cat_icon_map_orig[:self.tags_categories_start-len(self.row_map_orig)]
|
|
||||||
else:
|
|
||||||
self.cat_icon_map = self.cat_icon_map_orig[:]
|
|
||||||
|
|
||||||
self.user_categories = dict.copy(config['user_categories'])
|
|
||||||
column_map = config['column_map']
|
|
||||||
|
|
||||||
for i in range(0, self.tags_categories_start): # First the standard categories
|
|
||||||
self.row_map.append(self.row_map_orig[i])
|
|
||||||
self.categories.append(self.categories_orig[i])
|
|
||||||
if len(self.search_restriction):
|
if len(self.search_restriction):
|
||||||
data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map,
|
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map,
|
||||||
ids=self.db.search(self.search_restriction, return_matches=True))
|
ids=self.db.search(self.search_restriction, return_matches=True))
|
||||||
else:
|
else:
|
||||||
data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map)
|
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
|
||||||
|
|
||||||
for c in data: # now the custom columns
|
tb_categories = self.db.get_tag_browser_categories()
|
||||||
if c not in self.row_map_orig and c in column_map:
|
for category in tb_categories.iterkeys():
|
||||||
self.row_map.append(c)
|
if category in data: # They should always be there, but ...
|
||||||
self.categories.append(self.db.custom_column_label_map[c]['name'])
|
self.row_map.append(category)
|
||||||
self.cat_icon_map.append(self.custcol_icon)
|
self.categories.append(tb_categories[category]['name'])
|
||||||
|
|
||||||
# Now the rest of the normal tag categories
|
|
||||||
for i in range(self.tags_categories_start, len(self.row_map_orig)):
|
|
||||||
self.row_map.append(self.row_map_orig[i])
|
|
||||||
self.categories.append(self.categories_orig[i])
|
|
||||||
self.cat_icon_map.append(self.cat_icon_map_orig[i])
|
|
||||||
|
|
||||||
# Clean up the author's tags, getting rid of the '|' characters
|
|
||||||
if data['author'] is not None:
|
|
||||||
for t in data['author']:
|
|
||||||
t.name = t.name.replace('|', ',')
|
|
||||||
|
|
||||||
# Now do the user-defined categories. There is a time/space tradeoff here.
|
|
||||||
# By converting the tags into a map, we can do the verification in the category
|
|
||||||
# loop much faster, at the cost of duplicating the categories lists.
|
|
||||||
taglist = {}
|
|
||||||
for c in self.row_map:
|
|
||||||
taglist[c] = dict(map(lambda t:(t.name, t), data[c]))
|
|
||||||
|
|
||||||
for c in self.user_categories:
|
|
||||||
l = []
|
|
||||||
for (name,label,ign) in self.user_categories[c]:
|
|
||||||
if label in taglist and name in taglist[label]: # use same node as the complete category
|
|
||||||
l.append(taglist[label][name])
|
|
||||||
# else: do nothing, to eliminate nodes that have zero counts
|
|
||||||
if config['sort_by_popularity']:
|
|
||||||
data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.count, y.count)))
|
|
||||||
else:
|
|
||||||
data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower())))
|
|
||||||
self.row_map.append(c+'*')
|
|
||||||
self.categories.append(c)
|
|
||||||
self.cat_icon_map.append(self.usercat_icon)
|
|
||||||
|
|
||||||
data['search'] = self.get_search_nodes(self.search_icon) # Add the search category
|
|
||||||
self.row_map.append(self.search_keys[0])
|
|
||||||
self.categories.append(self.search_keys[1])
|
|
||||||
self.cat_icon_map.append(self.search_icon)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_search_nodes(self, icon):
|
def get_search_nodes(self, icon):
|
||||||
|
@ -183,7 +183,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
_('Error communicating with device'), ' ')
|
_('Error communicating with device'), ' ')
|
||||||
self.device_error_dialog.setModal(Qt.NonModal)
|
self.device_error_dialog.setModal(Qt.NonModal)
|
||||||
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
self.tb_wrapper = textwrap.TextWrapper(width=40)
|
||||||
self.device_connected = False
|
self.device_connected = None
|
||||||
self.viewers = collections.deque()
|
self.viewers = collections.deque()
|
||||||
self.content_server = None
|
self.content_server = None
|
||||||
self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self)
|
self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self)
|
||||||
@ -675,6 +675,15 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
|
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
|
||||||
self._sync_menu.connect_to_folder.connect(self.connect_to_folder)
|
self._sync_menu.connect_to_folder.connect(self.connect_to_folder)
|
||||||
self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder)
|
self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder)
|
||||||
|
if self.device_connected:
|
||||||
|
self._sync_menu.connect_to_folder_action.setEnabled(False)
|
||||||
|
if self.device_connected == 'folder':
|
||||||
|
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
|
||||||
|
else:
|
||||||
|
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
|
||||||
|
else:
|
||||||
|
self._sync_menu.connect_to_folder_action.setEnabled(True)
|
||||||
|
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
|
||||||
|
|
||||||
def add_spare_server(self, *args):
|
def add_spare_server(self, *args):
|
||||||
self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0)))
|
self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0)))
|
||||||
@ -944,7 +953,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.status_bar.showMessage(_('Device: ')+\
|
self.status_bar.showMessage(_('Device: ')+\
|
||||||
self.device_manager.device.__class__.get_gui_name()+\
|
self.device_manager.device.__class__.get_gui_name()+\
|
||||||
_(' detected.'), 3000)
|
_(' detected.'), 3000)
|
||||||
self.device_connected = True
|
self.device_connected = 'device' if not is_folder_device else 'folder'
|
||||||
self._sync_menu.enable_device_actions(True,
|
self._sync_menu.enable_device_actions(True,
|
||||||
self.device_manager.device.card_prefix(),
|
self.device_manager.device.card_prefix(),
|
||||||
self.device_manager.device)
|
self.device_manager.device)
|
||||||
@ -955,7 +964,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self._sync_menu.connect_to_folder_action.setEnabled(True)
|
self._sync_menu.connect_to_folder_action.setEnabled(True)
|
||||||
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
|
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
|
||||||
self.save_device_view_settings()
|
self.save_device_view_settings()
|
||||||
self.device_connected = False
|
self.device_connected = None
|
||||||
self._sync_menu.enable_device_actions(False)
|
self._sync_menu.enable_device_actions(False)
|
||||||
self.location_view.model().update_devices()
|
self.location_view.model().update_devices()
|
||||||
self.vanity.setText(self.vanity_template%\
|
self.vanity.setText(self.vanity_template%\
|
||||||
|
@ -141,11 +141,15 @@ class CustomColumns(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Create Tag Browser categories for custom columns
|
# Create Tag Browser categories for custom columns
|
||||||
for i, v in self.custom_column_num_map.items():
|
for k in sorted(self.custom_column_label_map.keys()):
|
||||||
|
v = self.custom_column_label_map[k]
|
||||||
if v['normalized']:
|
if v['normalized']:
|
||||||
tn = 'custom_column_{0}'.format(i)
|
tn = 'custom_column_{0}'.format(v['num'])
|
||||||
self.tag_browser_categories[tn] = [v['label'], 'value']
|
self.tag_browser_categories[v['label']] = {
|
||||||
self.tag_browser_datatype[v['label']] = v['datatype']
|
'table':tn, 'column':'value',
|
||||||
|
'type':v['datatype'], 'is_multiple':v['is_multiple'],
|
||||||
|
'kind':'custom', 'name':v['name']
|
||||||
|
}
|
||||||
|
|
||||||
def get_custom(self, idx, label=None, num=None, index_is_id=False):
|
def get_custom(self, idx, label=None, num=None, index_is_id=False):
|
||||||
if label is not None:
|
if label is not None:
|
||||||
|
@ -33,6 +33,9 @@ from calibre.customize.ui import run_plugins_on_import
|
|||||||
|
|
||||||
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.ordered_dict import OrderedDict
|
||||||
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
|
||||||
|
|
||||||
if iswindows:
|
if iswindows:
|
||||||
@ -123,24 +126,33 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if isinstance(self.dbpath, unicode):
|
if isinstance(self.dbpath, unicode):
|
||||||
self.dbpath = self.dbpath.encode(filesystem_encoding)
|
self.dbpath = self.dbpath.encode(filesystem_encoding)
|
||||||
|
|
||||||
self.tag_browser_categories = {
|
# Order as has been customary in the tags pane.
|
||||||
'tags' : ['tag', 'name'],
|
tag_browser_categories_items = [
|
||||||
'series' : ['series', 'name'],
|
('authors', {'table':'authors', 'column':'name',
|
||||||
'publishers': ['publisher', 'name'],
|
'type':'text', 'is_multiple':False,
|
||||||
'authors' : ['author', 'name'],
|
'kind':'standard', 'name':_('Authors')}),
|
||||||
'news' : ['news', 'name'],
|
('series', {'table':'series', 'column':'name',
|
||||||
'ratings' : ['rating', 'rating']
|
'type':None, 'is_multiple':False,
|
||||||
}
|
'kind':'standard', 'name':_('Series')}),
|
||||||
self.tag_browser_datatype = {
|
('formats', {'table':None, 'column':None,
|
||||||
'tag' : 'textmult',
|
'type':None, 'is_multiple':False,
|
||||||
'series' : None,
|
'kind':'standard', 'name':_('Formats')}),
|
||||||
'publisher' : 'text',
|
('publishers',{'table':'publishers', 'column':'name',
|
||||||
'author' : 'text',
|
'type':'text', 'is_multiple':False,
|
||||||
'news' : None,
|
'kind':'standard', 'name':_('Publishers')}),
|
||||||
'rating' : 'rating',
|
('ratings', {'table':'ratings', 'column':'rating',
|
||||||
}
|
'type':'rating', 'is_multiple':False,
|
||||||
|
'kind':'standard', 'name':_('Ratings')}),
|
||||||
self.tag_browser_formatters = {'rating': lambda x:u'\u2605'*int(round(x/2.))}
|
('news', {'table':'news', 'column':'name',
|
||||||
|
'type':None, 'is_multiple':False,
|
||||||
|
'kind':'standard', 'name':_('News')}),
|
||||||
|
('tags', {'table':'tags', 'column':'name',
|
||||||
|
'type':'text', 'is_multiple':True,
|
||||||
|
'kind':'standard', 'name':_('Tags')}),
|
||||||
|
]
|
||||||
|
self.tag_browser_categories = OrderedDict()
|
||||||
|
for k,v in tag_browser_categories_items:
|
||||||
|
self.tag_browser_categories[k] = v
|
||||||
|
|
||||||
self.connect()
|
self.connect()
|
||||||
self.is_case_sensitive = not iswindows and not isosx and \
|
self.is_case_sensitive = not iswindows and not isosx and \
|
||||||
@ -649,36 +661,65 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
def get_recipe(self, id):
|
def get_recipe(self, id):
|
||||||
return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False)
|
return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False)
|
||||||
|
|
||||||
|
def get_tag_browser_categories(self):
|
||||||
|
return self.tag_browser_categories
|
||||||
|
|
||||||
def get_categories(self, sort_on_count=False, ids=None, icon_map=None):
|
def get_categories(self, sort_on_count=False, ids=None, icon_map=None):
|
||||||
self.books_list_filter.change([] if not ids else ids)
|
self.books_list_filter.change([] if not ids else ids)
|
||||||
|
|
||||||
categories = {}
|
categories = {}
|
||||||
for tn, cn in self.tag_browser_categories.items():
|
|
||||||
|
#### First, build the standard and custom-column categories ####
|
||||||
|
for category in self.tag_browser_categories.keys():
|
||||||
|
tn = self.tag_browser_categories[category]['table']
|
||||||
|
categories[category] = [] #reserve the position in the ordered list
|
||||||
|
if tn is None: # Nothing to do for the moment
|
||||||
|
continue
|
||||||
|
cn = self.tag_browser_categories[category]['column']
|
||||||
if ids is None:
|
if ids is None:
|
||||||
query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn[1], tn)
|
query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn)
|
||||||
else:
|
else:
|
||||||
query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn[1], tn)
|
query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn, tn)
|
||||||
if sort_on_count:
|
if sort_on_count:
|
||||||
query += ' ORDER BY count DESC'
|
query += ' ORDER BY count DESC'
|
||||||
else:
|
else:
|
||||||
query += ' ORDER BY {0} ASC'.format(cn[1])
|
query += ' ORDER BY {0} ASC'.format(cn)
|
||||||
data = self.conn.get(query)
|
data = self.conn.get(query)
|
||||||
category = cn[0]
|
|
||||||
|
# icon_map is not None if get_categories is to store an icon and
|
||||||
|
# possibly a tooltip in the tag structure.
|
||||||
icon, tooltip = None, ''
|
icon, tooltip = None, ''
|
||||||
if icon_map:
|
if icon_map:
|
||||||
if category in icon_map:
|
if self.tag_browser_categories[category]['kind'] == 'standard':
|
||||||
icon = icon_map[category]
|
if category in icon_map:
|
||||||
else:
|
icon = icon_map[category]
|
||||||
|
elif self.tag_browser_categories[category]['kind'] == 'custom':
|
||||||
icon = icon_map['*custom']
|
icon = icon_map['*custom']
|
||||||
|
icon_map[category] = icon_map['*custom']
|
||||||
tooltip = self.custom_column_label_map[category]['name']
|
tooltip = self.custom_column_label_map[category]['name']
|
||||||
datatype = self.tag_browser_datatype[category]
|
|
||||||
formatter = self.tag_browser_formatters.get(datatype, lambda x: x)
|
datatype = self.tag_browser_categories[category]['type']
|
||||||
|
if datatype == 'rating':
|
||||||
|
item_zero_func = (lambda x: len(formatter(r[1])) > 0)
|
||||||
|
formatter = (lambda x:u'\u2605'*int(round(x/2.)))
|
||||||
|
elif category == 'authors':
|
||||||
|
item_zero_func = (lambda x: x[2] > 0)
|
||||||
|
# Clean up the authors strings to human-readable form
|
||||||
|
formatter = (lambda x: x.replace('|', ','))
|
||||||
|
else:
|
||||||
|
item_zero_func = (lambda x: x[2] > 0)
|
||||||
|
formatter = (lambda x:x)
|
||||||
|
|
||||||
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
||||||
icon=icon, tooltip = tooltip)
|
icon=icon, tooltip = tooltip)
|
||||||
for r in data
|
for r in data if item_zero_func(r)]
|
||||||
if r[2] > 0 and
|
|
||||||
(datatype != 'rating' or len(formatter(r[1])) > 0)]
|
# We delayed computing the standard formats category because it does not
|
||||||
categories['format'] = []
|
# use a view, but is computed dynamically
|
||||||
|
categories['formats'] = []
|
||||||
|
icon = None
|
||||||
|
if icon_map and 'formats' in icon_map:
|
||||||
|
icon = icon_map['formats']
|
||||||
for fmt in self.conn.get('SELECT DISTINCT format FROM data'):
|
for fmt in self.conn.get('SELECT DISTINCT format FROM data'):
|
||||||
fmt = fmt[0]
|
fmt = fmt[0]
|
||||||
if ids is not None:
|
if ids is not None:
|
||||||
@ -693,13 +734,70 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
WHERE format="%s"'''%fmt,
|
WHERE format="%s"'''%fmt,
|
||||||
all=False)
|
all=False)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
categories['format'].append(Tag(fmt, count=count))
|
categories['formats'].append(Tag(fmt, count=count, icon=icon))
|
||||||
|
|
||||||
if sort_on_count:
|
if sort_on_count:
|
||||||
categories['format'].sort(cmp=lambda x,y:cmp(x.count, y.count),
|
categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count),
|
||||||
reverse=True)
|
reverse=True)
|
||||||
else:
|
else:
|
||||||
categories['format'].sort(cmp=lambda x,y:cmp(x.name, y.name))
|
categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name))
|
||||||
|
|
||||||
|
#### Now do the user-defined categories. ####
|
||||||
|
user_categories = dict.copy(prefs['user_categories'])
|
||||||
|
|
||||||
|
# remove all user categories from tag_browser_categories. They can
|
||||||
|
# easily come and go. We will add all the existing ones in below.
|
||||||
|
for k in self.tag_browser_categories.keys():
|
||||||
|
if self.tag_browser_categories[k]['kind'] in ['user', 'search']:
|
||||||
|
del self.tag_browser_categories[k]
|
||||||
|
|
||||||
|
# We want to use same node in the user category as in the source
|
||||||
|
# category. To do that, we need to find the original Tag node. There is
|
||||||
|
# a time/space tradeoff here. By converting the tags into a map, we can
|
||||||
|
# do the verification in the category loop much faster, at the cost of
|
||||||
|
# temporarily duplicating the categories lists.
|
||||||
|
taglist = {}
|
||||||
|
for c in categories.keys():
|
||||||
|
taglist[c] = dict(map(lambda t:(t.name, t), categories[c]))
|
||||||
|
|
||||||
|
for user_cat in sorted(user_categories.keys()):
|
||||||
|
items = []
|
||||||
|
for (name,label,ign) in user_categories[user_cat]:
|
||||||
|
if label in taglist and name in taglist[label]:
|
||||||
|
items.append(taglist[label][name])
|
||||||
|
# else: do nothing, to not include nodes w zero counts
|
||||||
|
if len(items):
|
||||||
|
cat_name = user_cat+'*' # add the * to avoid name collision
|
||||||
|
self.tag_browser_categories[cat_name] = {
|
||||||
|
'table':None, 'column':None,
|
||||||
|
'type':None, 'is_multiple':False,
|
||||||
|
'kind':'user', 'name':user_cat}
|
||||||
|
# Not a problem if we accumulate entries in the icon map
|
||||||
|
if icon_map is not None:
|
||||||
|
icon_map[cat_name] = icon_map['*user']
|
||||||
|
if sort_on_count:
|
||||||
|
categories[cat_name] = \
|
||||||
|
sorted(items, cmp=(lambda x, y: cmp(y.count, x.count)))
|
||||||
|
else:
|
||||||
|
categories[cat_name] = \
|
||||||
|
sorted(items, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower())))
|
||||||
|
|
||||||
|
#### Finally, the saved searches category ####
|
||||||
|
items = []
|
||||||
|
icon = None
|
||||||
|
if icon_map and 'search' in icon_map:
|
||||||
|
icon = icon_map['search']
|
||||||
|
for srch in saved_searches.names():
|
||||||
|
items.append(Tag(srch, tooltip=saved_searches.lookup(srch), icon=icon))
|
||||||
|
if len(items):
|
||||||
|
self.tag_browser_categories['search'] = {
|
||||||
|
'table':None, 'column':None,
|
||||||
|
'type':None, 'is_multiple':False,
|
||||||
|
'kind':'search', 'name':_('Searches')}
|
||||||
|
if icon_map is not None:
|
||||||
|
icon_map['search'] = icon_map['search']
|
||||||
|
categories['search'] = items
|
||||||
|
|
||||||
return categories
|
return categories
|
||||||
|
|
||||||
def tags_older_than(self, tag, delta):
|
def tags_older_than(self, tag, delta):
|
||||||
|
@ -694,8 +694,10 @@ def _prefs():
|
|||||||
help=_('Add new formats to existing book records'))
|
help=_('Add new formats to existing book records'))
|
||||||
c.add_opt('installation_uuid', default=None, help='Installation UUID')
|
c.add_opt('installation_uuid', default=None, help='Installation UUID')
|
||||||
|
|
||||||
# this is here instead of the gui preferences because calibredb can execute searches
|
# these are here instead of the gui preferences because calibredb and
|
||||||
|
# calibre server can execute searches
|
||||||
c.add_opt('saved_searches', default={}, help=_('List of named saved searches'))
|
c.add_opt('saved_searches', default={}, help=_('List of named saved searches'))
|
||||||
|
c.add_opt('user_categories', default={}, help=_('User-created tag browser categories'))
|
||||||
|
|
||||||
c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.')
|
c.add_opt('migrated', default=False, help='For Internal use. Don\'t modify.')
|
||||||
return c
|
return c
|
||||||
|
114
src/calibre/utils/ordered_dict.py
Normal file
114
src/calibre/utils/ordered_dict.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
'''
|
||||||
|
A ordered dictionary. Use the builtin type on python >= 2.7
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections import OrderedDict
|
||||||
|
OrderedDict
|
||||||
|
except ImportError:
|
||||||
|
from UserDict import DictMixin
|
||||||
|
|
||||||
|
class OrderedDict(dict, DictMixin):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
if len(args) > 1:
|
||||||
|
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||||
|
try:
|
||||||
|
self.__end
|
||||||
|
except AttributeError:
|
||||||
|
self.clear()
|
||||||
|
self.update(*args, **kwds)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.__end = end = []
|
||||||
|
end += [None, end, end] # sentinel node for doubly linked list
|
||||||
|
self.__map = {} # key --> [key, prev, next]
|
||||||
|
dict.clear(self)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
if key not in self:
|
||||||
|
end = self.__end
|
||||||
|
curr = end[1]
|
||||||
|
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||||
|
dict.__setitem__(self, key, value)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
dict.__delitem__(self, key)
|
||||||
|
key, prev, next = self.__map.pop(key)
|
||||||
|
prev[2] = next
|
||||||
|
next[1] = prev
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
end = self.__end
|
||||||
|
curr = end[2]
|
||||||
|
while curr is not end:
|
||||||
|
yield curr[0]
|
||||||
|
curr = curr[2]
|
||||||
|
|
||||||
|
def __reversed__(self):
|
||||||
|
end = self.__end
|
||||||
|
curr = end[1]
|
||||||
|
while curr is not end:
|
||||||
|
yield curr[0]
|
||||||
|
curr = curr[1]
|
||||||
|
|
||||||
|
def popitem(self, last=True):
|
||||||
|
if not self:
|
||||||
|
raise KeyError('dictionary is empty')
|
||||||
|
if last:
|
||||||
|
key = reversed(self).next()
|
||||||
|
else:
|
||||||
|
key = iter(self).next()
|
||||||
|
value = self.pop(key)
|
||||||
|
return key, value
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
items = [[k, self[k]] for k in self]
|
||||||
|
tmp = self.__map, self.__end
|
||||||
|
del self.__map, self.__end
|
||||||
|
inst_dict = vars(self).copy()
|
||||||
|
self.__map, self.__end = tmp
|
||||||
|
if inst_dict:
|
||||||
|
return (self.__class__, (items,), inst_dict)
|
||||||
|
return self.__class__, (items,)
|
||||||
|
|
||||||
|
def keys(self):
|
||||||
|
return list(self)
|
||||||
|
|
||||||
|
setdefault = DictMixin.setdefault
|
||||||
|
update = DictMixin.update
|
||||||
|
pop = DictMixin.pop
|
||||||
|
values = DictMixin.values
|
||||||
|
items = DictMixin.items
|
||||||
|
iterkeys = DictMixin.iterkeys
|
||||||
|
itervalues = DictMixin.itervalues
|
||||||
|
iteritems = DictMixin.iteritems
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if not self:
|
||||||
|
return '%s()' % (self.__class__.__name__,)
|
||||||
|
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
return self.__class__(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fromkeys(cls, iterable, value=None):
|
||||||
|
d = cls()
|
||||||
|
for key in iterable:
|
||||||
|
d[key] = value
|
||||||
|
return d
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, OrderedDict):
|
||||||
|
return len(self)==len(other) and self.items() == other.items()
|
||||||
|
return dict.__eq__(self, other)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self == other
|
Loading…
x
Reference in New Issue
Block a user