mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
365592d066
@ -17,6 +17,8 @@ def decompress_doc(data):
|
|||||||
return cPalmdoc.decompress(data)
|
return cPalmdoc.decompress(data)
|
||||||
|
|
||||||
def compress_doc(data):
|
def compress_doc(data):
|
||||||
|
if not data:
|
||||||
|
return u''
|
||||||
return cPalmdoc.compress(data)
|
return cPalmdoc.compress(data)
|
||||||
|
|
||||||
def test():
|
def test():
|
||||||
|
@ -5,7 +5,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, shutil
|
import os
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
|
from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
|
||||||
@ -14,7 +14,7 @@ from calibre import isbytestring
|
|||||||
from calibre.constants import filesystem_encoding, iswindows
|
from calibre.constants import filesystem_encoding, iswindows
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.gui2 import (gprefs, warning_dialog, Dispatcher, error_dialog,
|
from calibre.gui2 import (gprefs, warning_dialog, Dispatcher, error_dialog,
|
||||||
question_dialog, info_dialog)
|
question_dialog, info_dialog, open_local_file)
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
|
self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
|
||||||
self.rename_menu = QMenu(_('Rename library'))
|
self.rename_menu = QMenu(_('Rename library'))
|
||||||
self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu)
|
self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu)
|
||||||
self.delete_menu = QMenu(_('Delete library'))
|
self.delete_menu = QMenu(_('Remove library'))
|
||||||
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
|
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
|
||||||
|
|
||||||
ac = self.create_action(spec=(_('Pick a random book'), 'catalog.png',
|
ac = self.create_action(spec=(_('Pick a random book'), 'catalog.png',
|
||||||
@ -252,22 +252,15 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
|
|
||||||
def delete_requested(self, name, location):
|
def delete_requested(self, name, location):
|
||||||
loc = location.replace('/', os.sep)
|
loc = location.replace('/', os.sep)
|
||||||
if not question_dialog(self.gui, _('Are you sure?'),
|
|
||||||
_('<h1 style="color:red">WARNING</h1>')+
|
|
||||||
_('<b style="color: red">All files</b> (not just ebooks) '
|
|
||||||
'from <br><br><b>%s</b><br><br> will be '
|
|
||||||
'<b>permanently deleted</b>. Are you sure?') % loc,
|
|
||||||
show_copy_button=False, default_yes=False):
|
|
||||||
return
|
|
||||||
exists = self.gui.library_view.model().db.exists_at(loc)
|
|
||||||
if exists:
|
|
||||||
try:
|
|
||||||
shutil.rmtree(loc, ignore_errors=True)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
self.stats.remove(location)
|
self.stats.remove(location)
|
||||||
self.build_menus()
|
self.build_menus()
|
||||||
self.gui.iactions['Copy To Library'].build_menus()
|
self.gui.iactions['Copy To Library'].build_menus()
|
||||||
|
info_dialog(self.gui, _('Library removed'),
|
||||||
|
_('The library %s has been removed from calibre. '
|
||||||
|
'The files remain on your computer, if you want '
|
||||||
|
'to delete them, you will have to do so manually.') % loc,
|
||||||
|
show=True)
|
||||||
|
open_local_file(loc)
|
||||||
|
|
||||||
def backup_status(self, location):
|
def backup_status(self, location):
|
||||||
dirty_text = 'no'
|
dirty_text = 'no'
|
||||||
|
@ -16,7 +16,7 @@ from calibre.constants import isosx, __appname__, preferred_encoding, \
|
|||||||
from calibre.gui2 import config, is_widescreen, gprefs
|
from calibre.gui2 import config, is_widescreen, gprefs
|
||||||
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
||||||
from calibre.gui2.widgets import Splitter
|
from calibre.gui2.widgets import Splitter
|
||||||
from calibre.gui2.tag_view import TagBrowserWidget
|
from calibre.gui2.tag_browser.ui import TagBrowserWidget
|
||||||
from calibre.gui2.book_details import BookDetails
|
from calibre.gui2.book_details import BookDetails
|
||||||
from calibre.gui2.notify import get_notifier
|
from calibre.gui2.notify import get_notifier
|
||||||
|
|
||||||
|
11
src/calibre/gui2/tag_browser/__init__.py
Normal file
11
src/calibre/gui2/tag_browser/__init__.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
|
1292
src/calibre/gui2/tag_browser/model.py
Normal file
1292
src/calibre/gui2/tag_browser/model.py
Normal file
File diff suppressed because it is too large
Load Diff
458
src/calibre/gui2/tag_browser/ui.py
Normal file
458
src/calibre/gui2/tag_browser/ui.py
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from PyQt4.Qt import (Qt, QIcon, QWidget, QHBoxLayout, QVBoxLayout, QShortcut,
|
||||||
|
QKeySequence, QToolButton, QString, QLabel, QFrame, QTimer, QComboBox,
|
||||||
|
QMenu, QPushButton)
|
||||||
|
|
||||||
|
from calibre.gui2 import error_dialog, question_dialog
|
||||||
|
from calibre.gui2.widgets import HistoryLineEdit
|
||||||
|
from calibre.library.field_metadata import category_icon_map
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
from calibre.gui2.tag_browser.view import TagsView
|
||||||
|
from calibre.ebooks.metadata import title_sort
|
||||||
|
from calibre.gui2.dialogs.tag_categories import TagCategories
|
||||||
|
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
||||||
|
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
|
||||||
|
|
||||||
|
class TagBrowserMixin(object): # {{{
|
||||||
|
|
||||||
|
def __init__(self, db):
|
||||||
|
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
|
||||||
|
self.tags_view.set_database(db, self.tag_match, self.sort_by)
|
||||||
|
self.tags_view.tags_marked.connect(self.search.set_search_string)
|
||||||
|
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
||||||
|
self.tags_view.edit_user_category.connect(self.do_edit_user_categories)
|
||||||
|
self.tags_view.delete_user_category.connect(self.do_delete_user_category)
|
||||||
|
self.tags_view.del_item_from_user_cat.connect(self.do_del_item_from_user_cat)
|
||||||
|
self.tags_view.add_subcategory.connect(self.do_add_subcategory)
|
||||||
|
self.tags_view.add_item_to_user_cat.connect(self.do_add_item_to_user_cat)
|
||||||
|
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
||||||
|
self.tags_view.rebuild_saved_searches.connect(self.do_rebuild_saved_searches)
|
||||||
|
self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
|
||||||
|
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
|
||||||
|
self.tags_view.search_item_renamed.connect(self.saved_searches_changed)
|
||||||
|
self.tags_view.drag_drop_finished.connect(self.drag_drop_finished)
|
||||||
|
self.tags_view.restriction_error.connect(self.do_restriction_error,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
for text, func, args, cat_name in (
|
||||||
|
(_('Manage Authors'),
|
||||||
|
self.do_author_sort_edit, (self, None), 'authors'),
|
||||||
|
(_('Manage Series'),
|
||||||
|
self.do_tags_list_edit, (None, 'series'), 'series'),
|
||||||
|
(_('Manage Publishers'),
|
||||||
|
self.do_tags_list_edit, (None, 'publisher'), 'publisher'),
|
||||||
|
(_('Manage Tags'),
|
||||||
|
self.do_tags_list_edit, (None, 'tags'), 'tags'),
|
||||||
|
(_('Manage User Categories'),
|
||||||
|
self.do_edit_user_categories, (None,), 'user:'),
|
||||||
|
(_('Manage Saved Searches'),
|
||||||
|
self.do_saved_search_edit, (None,), 'search')
|
||||||
|
):
|
||||||
|
self.manage_items_button.menu().addAction(
|
||||||
|
QIcon(I(category_icon_map[cat_name])),
|
||||||
|
text, partial(func, *args))
|
||||||
|
|
||||||
|
def do_restriction_error(self):
|
||||||
|
error_dialog(self.tags_view, _('Invalid search restriction'),
|
||||||
|
_('The current search restriction is invalid'), show=True)
|
||||||
|
|
||||||
|
def do_add_subcategory(self, on_category_key, new_category_name=None):
|
||||||
|
'''
|
||||||
|
Add a subcategory to the category 'on_category'. If new_category_name is
|
||||||
|
None, then a default name is shown and the user is offered the
|
||||||
|
opportunity to edit the name.
|
||||||
|
'''
|
||||||
|
db = self.library_view.model().db
|
||||||
|
user_cats = db.prefs.get('user_categories', {})
|
||||||
|
|
||||||
|
# Ensure that the temporary name we will use is not already there
|
||||||
|
i = 0
|
||||||
|
if new_category_name is not None:
|
||||||
|
new_name = new_category_name.replace('.', '')
|
||||||
|
else:
|
||||||
|
new_name = _('New Category').replace('.', '')
|
||||||
|
n = new_name
|
||||||
|
while True:
|
||||||
|
new_cat = on_category_key[1:] + '.' + n
|
||||||
|
if new_cat not in user_cats:
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
n = new_name + unicode(i)
|
||||||
|
# Add the new category
|
||||||
|
user_cats[new_cat] = []
|
||||||
|
db.prefs.set('user_categories', user_cats)
|
||||||
|
self.tags_view.set_new_model()
|
||||||
|
m = self.tags_view.model()
|
||||||
|
idx = m.index_for_path(m.find_category_node('@' + new_cat))
|
||||||
|
m.show_item_at_index(idx)
|
||||||
|
# Open the editor on the new item to rename it
|
||||||
|
if new_category_name is None:
|
||||||
|
self.tags_view.edit(idx)
|
||||||
|
|
||||||
|
def do_edit_user_categories(self, on_category=None):
|
||||||
|
'''
|
||||||
|
Open the user categories editor.
|
||||||
|
'''
|
||||||
|
db = self.library_view.model().db
|
||||||
|
d = TagCategories(self, db, on_category)
|
||||||
|
if d.exec_() == d.Accepted:
|
||||||
|
db.prefs.set('user_categories', d.categories)
|
||||||
|
db.field_metadata.remove_user_categories()
|
||||||
|
for k in d.categories:
|
||||||
|
db.field_metadata.add_user_category('@' + k, k)
|
||||||
|
db.data.change_search_locations(db.field_metadata.get_search_terms())
|
||||||
|
self.tags_view.set_new_model()
|
||||||
|
|
||||||
|
def do_delete_user_category(self, category_name):
|
||||||
|
'''
|
||||||
|
Delete the user category named category_name. Any leading '@' is removed
|
||||||
|
'''
|
||||||
|
if category_name.startswith('@'):
|
||||||
|
category_name = category_name[1:]
|
||||||
|
db = self.library_view.model().db
|
||||||
|
user_cats = db.prefs.get('user_categories', {})
|
||||||
|
cat_keys = sorted(user_cats.keys(), key=sort_key)
|
||||||
|
has_children = False
|
||||||
|
found = False
|
||||||
|
for k in cat_keys:
|
||||||
|
if k == category_name:
|
||||||
|
found = True
|
||||||
|
has_children = len(user_cats[k])
|
||||||
|
elif k.startswith(category_name + '.'):
|
||||||
|
has_children = True
|
||||||
|
if not found:
|
||||||
|
return error_dialog(self.tags_view, _('Delete user category'),
|
||||||
|
_('%s is not a user category')%category_name, show=True)
|
||||||
|
if has_children:
|
||||||
|
if not question_dialog(self.tags_view, _('Delete user category'),
|
||||||
|
_('%s contains items. Do you really '
|
||||||
|
'want to delete it?')%category_name):
|
||||||
|
return
|
||||||
|
for k in cat_keys:
|
||||||
|
if k == category_name:
|
||||||
|
del user_cats[k]
|
||||||
|
elif k.startswith(category_name + '.'):
|
||||||
|
del user_cats[k]
|
||||||
|
db.prefs.set('user_categories', user_cats)
|
||||||
|
self.tags_view.set_new_model()
|
||||||
|
|
||||||
|
def do_del_item_from_user_cat(self, user_cat, item_name, item_category):
|
||||||
|
'''
|
||||||
|
Delete the item (item_name, item_category) from the user category with
|
||||||
|
key user_cat. Any leading '@' characters are removed
|
||||||
|
'''
|
||||||
|
if user_cat.startswith('@'):
|
||||||
|
user_cat = user_cat[1:]
|
||||||
|
db = self.library_view.model().db
|
||||||
|
user_cats = db.prefs.get('user_categories', {})
|
||||||
|
if user_cat not in user_cats:
|
||||||
|
error_dialog(self.tags_view, _('Remove category'),
|
||||||
|
_('User category %s does not exist')%user_cat,
|
||||||
|
show=True)
|
||||||
|
return
|
||||||
|
self.tags_view.model().delete_item_from_user_category(user_cat,
|
||||||
|
item_name, item_category)
|
||||||
|
self.tags_view.recount()
|
||||||
|
|
||||||
|
def do_add_item_to_user_cat(self, dest_category, src_name, src_category):
|
||||||
|
'''
|
||||||
|
Add the item src_name in src_category to the user category
|
||||||
|
dest_category. Any leading '@' is removed
|
||||||
|
'''
|
||||||
|
db = self.library_view.model().db
|
||||||
|
user_cats = db.prefs.get('user_categories', {})
|
||||||
|
|
||||||
|
if dest_category and dest_category.startswith('@'):
|
||||||
|
dest_category = dest_category[1:]
|
||||||
|
|
||||||
|
if dest_category not in user_cats:
|
||||||
|
return error_dialog(self.tags_view, _('Add to user category'),
|
||||||
|
_('A user category %s does not exist')%dest_category, show=True)
|
||||||
|
|
||||||
|
# Now add the item to the destination user category
|
||||||
|
add_it = True
|
||||||
|
if src_category == 'news':
|
||||||
|
src_category = 'tags'
|
||||||
|
for tup in user_cats[dest_category]:
|
||||||
|
if src_name == tup[0] and src_category == tup[1]:
|
||||||
|
add_it = False
|
||||||
|
if add_it:
|
||||||
|
user_cats[dest_category].append([src_name, src_category, 0])
|
||||||
|
db.prefs.set('user_categories', user_cats)
|
||||||
|
self.tags_view.recount()
|
||||||
|
|
||||||
|
def do_tags_list_edit(self, tag, category):
|
||||||
|
'''
|
||||||
|
Open the 'manage_X' dialog where X == category. If tag is not None, the
|
||||||
|
dialog will position the editor on that item.
|
||||||
|
'''
|
||||||
|
db=self.library_view.model().db
|
||||||
|
if category == 'tags':
|
||||||
|
result = db.get_tags_with_ids()
|
||||||
|
key = sort_key
|
||||||
|
elif category == 'series':
|
||||||
|
result = db.get_series_with_ids()
|
||||||
|
key = lambda x:sort_key(title_sort(x))
|
||||||
|
elif category == 'publisher':
|
||||||
|
result = db.get_publishers_with_ids()
|
||||||
|
key = sort_key
|
||||||
|
else: # should be a custom field
|
||||||
|
cc_label = None
|
||||||
|
if category in db.field_metadata:
|
||||||
|
cc_label = db.field_metadata[category]['label']
|
||||||
|
result = db.get_custom_items_with_ids(label=cc_label)
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
key = sort_key
|
||||||
|
|
||||||
|
d = TagListEditor(self, tag_to_match=tag, data=result, key=key)
|
||||||
|
d.exec_()
|
||||||
|
if d.result() == d.Accepted:
|
||||||
|
to_rename = d.to_rename # dict of new text to old id
|
||||||
|
to_delete = d.to_delete # list of ids
|
||||||
|
orig_name = d.original_names # dict of id: name
|
||||||
|
|
||||||
|
rename_func = None
|
||||||
|
if category == 'tags':
|
||||||
|
rename_func = db.rename_tag
|
||||||
|
delete_func = db.delete_tag_using_id
|
||||||
|
elif category == 'series':
|
||||||
|
rename_func = db.rename_series
|
||||||
|
delete_func = db.delete_series_using_id
|
||||||
|
elif category == 'publisher':
|
||||||
|
rename_func = db.rename_publisher
|
||||||
|
delete_func = db.delete_publisher_using_id
|
||||||
|
else:
|
||||||
|
rename_func = partial(db.rename_custom_item, label=cc_label)
|
||||||
|
delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
|
||||||
|
m = self.tags_view.model()
|
||||||
|
if rename_func:
|
||||||
|
for item in to_delete:
|
||||||
|
delete_func(item)
|
||||||
|
m.delete_item_from_all_user_categories(orig_name[item], category)
|
||||||
|
for old_id in to_rename:
|
||||||
|
rename_func(old_id, new_name=unicode(to_rename[old_id]))
|
||||||
|
m.rename_item_in_all_user_categories(orig_name[old_id],
|
||||||
|
category, unicode(to_rename[old_id]))
|
||||||
|
|
||||||
|
# Clean up the library view
|
||||||
|
self.do_tag_item_renamed()
|
||||||
|
self.tags_view.recount()
|
||||||
|
|
||||||
|
def do_tag_item_renamed(self):
|
||||||
|
# Clean up library view and search
|
||||||
|
# get information to redo the selection
|
||||||
|
rows = [r.row() for r in \
|
||||||
|
self.library_view.selectionModel().selectedRows()]
|
||||||
|
m = self.library_view.model()
|
||||||
|
ids = [m.id(r) for r in rows]
|
||||||
|
|
||||||
|
m.refresh(reset=False)
|
||||||
|
m.research()
|
||||||
|
self.library_view.select_rows(ids)
|
||||||
|
# refreshing the tags view happens at the emit()/call() site
|
||||||
|
|
||||||
|
def do_author_sort_edit(self, parent, id, select_sort=True):
|
||||||
|
'''
|
||||||
|
Open the manage authors dialog
|
||||||
|
'''
|
||||||
|
db = self.library_view.model().db
|
||||||
|
editor = EditAuthorsDialog(parent, db, id, select_sort)
|
||||||
|
d = editor.exec_()
|
||||||
|
if d:
|
||||||
|
for (id, old_author, new_author, new_sort) in editor.result:
|
||||||
|
if old_author != new_author:
|
||||||
|
# The id might change if the new author already exists
|
||||||
|
id = db.rename_author(id, new_author)
|
||||||
|
db.set_sort_field_for_author(id, unicode(new_sort),
|
||||||
|
commit=False, notify=False)
|
||||||
|
db.commit()
|
||||||
|
self.library_view.model().refresh()
|
||||||
|
self.tags_view.recount()
|
||||||
|
|
||||||
|
def drag_drop_finished(self, ids):
|
||||||
|
self.library_view.model().refresh_ids(ids)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class TagBrowserWidget(QWidget): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.parent = parent
|
||||||
|
self._layout = QVBoxLayout()
|
||||||
|
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'))
|
||||||
|
except:
|
||||||
|
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'
|
||||||
|
'to particular categories using syntax similar to search. For example,\n'
|
||||||
|
'tags:foo will find foo in any tag, but not in authors etc. Entering\n'
|
||||||
|
'*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.activated.connect(self.set_focus_to_find_box)
|
||||||
|
|
||||||
|
self.search_button = QToolButton()
|
||||||
|
self.search_button.setText(_('F&ind'))
|
||||||
|
self.search_button.setToolTip(_('Find the first/next matching item'))
|
||||||
|
search_layout.addWidget(self.search_button)
|
||||||
|
|
||||||
|
self.expand_button = QToolButton()
|
||||||
|
self.expand_button.setText('-')
|
||||||
|
self.expand_button.setToolTip(_('Collapse all categories'))
|
||||||
|
search_layout.addWidget(self.expand_button)
|
||||||
|
search_layout.setStretch(0, 10)
|
||||||
|
search_layout.setStretch(1, 1)
|
||||||
|
search_layout.setStretch(2, 1)
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.item_search.lineEdit().textEdited.connect(self.find_text_changed)
|
||||||
|
self.item_search.activated[QString].connect(self.do_find)
|
||||||
|
self.item_search.completer().setCaseSensitivity(Qt.CaseSensitive)
|
||||||
|
|
||||||
|
parent.tags_view = TagsView(parent)
|
||||||
|
self.tags_view = parent.tags_view
|
||||||
|
self.expand_button.clicked.connect(self.tags_view.collapseAll)
|
||||||
|
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'),
|
||||||
|
_('Sort by average rating')):
|
||||||
|
parent.sort_by.addItem(x)
|
||||||
|
parent.sort_by.setToolTip(
|
||||||
|
_('Set the sort order for entries in the Tag Browser'))
|
||||||
|
parent.sort_by.setStatusTip(parent.sort_by.toolTip())
|
||||||
|
parent.sort_by.setCurrentIndex(0)
|
||||||
|
self._layout.addWidget(parent.sort_by)
|
||||||
|
|
||||||
|
# Must be in the same order as db2.MATCH_TYPE
|
||||||
|
parent.tag_match = QComboBox(parent)
|
||||||
|
for x in (_('Match any'), _('Match all')):
|
||||||
|
parent.tag_match.addItem(x)
|
||||||
|
parent.tag_match.setCurrentIndex(0)
|
||||||
|
self._layout.addWidget(parent.tag_match)
|
||||||
|
parent.tag_match.setToolTip(
|
||||||
|
_('When selecting multiple entries in the Tag Browser '
|
||||||
|
'match any or all of them'))
|
||||||
|
parent.tag_match.setStatusTip(parent.tag_match.toolTip())
|
||||||
|
|
||||||
|
|
||||||
|
l = parent.manage_items_button = QPushButton(self)
|
||||||
|
l.setStyleSheet('QPushButton {text-align: left; }')
|
||||||
|
l.setText(_('Manage authors, tags, etc'))
|
||||||
|
l.setToolTip(_('All of these category_managers are available by right-clicking '
|
||||||
|
'on items in the tag browser above'))
|
||||||
|
l.m = QMenu()
|
||||||
|
l.setMenu(l.m)
|
||||||
|
self._layout.addWidget(l)
|
||||||
|
|
||||||
|
# self.leak_test_timer = QTimer(self)
|
||||||
|
# self.leak_test_timer.timeout.connect(self.test_for_leak)
|
||||||
|
# self.leak_test_timer.start(5000)
|
||||||
|
|
||||||
|
def set_pane_is_visible(self, to_what):
|
||||||
|
self.tags_view.set_pane_is_visible(to_what)
|
||||||
|
|
||||||
|
def find_text_changed(self, str):
|
||||||
|
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_find_position = None
|
||||||
|
self.find()
|
||||||
|
|
||||||
|
def find(self):
|
||||||
|
model = self.tags_view.model()
|
||||||
|
model.clear_boxed()
|
||||||
|
txt = unicode(self.item_search.currentText()).strip()
|
||||||
|
|
||||||
|
if txt.startswith('*'):
|
||||||
|
self.tags_view.set_new_model(filter_categories_by=txt[1:])
|
||||||
|
self.current_find_position = None
|
||||||
|
return
|
||||||
|
if model.get_filter_categories_by():
|
||||||
|
self.tags_view.set_new_model(filter_categories_by=None)
|
||||||
|
self.current_find_position = None
|
||||||
|
model = self.tags_view.model()
|
||||||
|
|
||||||
|
if not txt:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.item_search.lineEdit().blockSignals(True)
|
||||||
|
self.search_button.setFocus(True)
|
||||||
|
self.item_search.lineEdit().blockSignals(False)
|
||||||
|
|
||||||
|
key = None
|
||||||
|
colon = txt.rfind(':') if len(txt) > 2 else 0
|
||||||
|
if colon > 0:
|
||||||
|
key = self.parent.library_view.model().db.\
|
||||||
|
field_metadata.search_term_to_field_key(txt[:colon])
|
||||||
|
txt = txt[colon+1:]
|
||||||
|
|
||||||
|
self.current_find_position = \
|
||||||
|
model.find_item_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():
|
||||||
|
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)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
588
src/calibre/gui2/tag_browser/view.py
Normal file
588
src/calibre/gui2/tag_browser/view.py
Normal file
@ -0,0 +1,588 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import cPickle
|
||||||
|
from functools import partial
|
||||||
|
from itertools import izip
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QItemDelegate, Qt, QTreeView, pyqtSignal, QSize, QIcon,
|
||||||
|
QApplication, QMenu, QPoint, QModelIndex)
|
||||||
|
|
||||||
|
from calibre.gui2.tag_browser.model import (TagTreeItem, TAG_SEARCH_STATES,
|
||||||
|
TagsModel)
|
||||||
|
from calibre.gui2 import config, gprefs
|
||||||
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
|
class TagDelegate(QItemDelegate): # {{{
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
item = index.data(Qt.UserRole).toPyObject()
|
||||||
|
if item.type != TagTreeItem.TAG:
|
||||||
|
QItemDelegate.paint(self, painter, option, index)
|
||||||
|
return
|
||||||
|
r = option.rect
|
||||||
|
model = self.parent().model()
|
||||||
|
icon = model.data(index, Qt.DecorationRole).toPyObject()
|
||||||
|
painter.save()
|
||||||
|
if item.tag.state != 0 or not config['show_avg_rating'] or \
|
||||||
|
item.tag.avg_rating is None:
|
||||||
|
icon.paint(painter, r, Qt.AlignLeft)
|
||||||
|
else:
|
||||||
|
painter.setOpacity(0.3)
|
||||||
|
icon.paint(painter, r, Qt.AlignLeft)
|
||||||
|
painter.setOpacity(1)
|
||||||
|
rating = item.tag.avg_rating
|
||||||
|
painter.setClipRect(r.left(), r.bottom()-int(r.height()*(rating/5.0)),
|
||||||
|
r.width(), r.height())
|
||||||
|
icon.paint(painter, r, Qt.AlignLeft)
|
||||||
|
painter.setClipRect(r)
|
||||||
|
|
||||||
|
# Paint the text
|
||||||
|
if item.boxed:
|
||||||
|
painter.drawRoundedRect(r.adjusted(1,1,-1,-1), 5, 5)
|
||||||
|
r.setLeft(r.left()+r.height()+3)
|
||||||
|
painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
|
||||||
|
model.data(index, Qt.DisplayRole).toString())
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class TagsView(QTreeView): # {{{
|
||||||
|
|
||||||
|
refresh_required = pyqtSignal()
|
||||||
|
tags_marked = pyqtSignal(object)
|
||||||
|
edit_user_category = pyqtSignal(object)
|
||||||
|
delete_user_category = pyqtSignal(object)
|
||||||
|
del_item_from_user_cat = pyqtSignal(object, object, object)
|
||||||
|
add_item_to_user_cat = pyqtSignal(object, object, object)
|
||||||
|
add_subcategory = pyqtSignal(object)
|
||||||
|
tag_list_edit = pyqtSignal(object, object)
|
||||||
|
saved_search_edit = pyqtSignal(object)
|
||||||
|
rebuild_saved_searches = pyqtSignal()
|
||||||
|
author_sort_edit = pyqtSignal(object, object)
|
||||||
|
tag_item_renamed = pyqtSignal()
|
||||||
|
search_item_renamed = pyqtSignal()
|
||||||
|
drag_drop_finished = pyqtSignal(object)
|
||||||
|
restriction_error = pyqtSignal()
|
||||||
|
show_at_path = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QTreeView.__init__(self, parent=None)
|
||||||
|
self.tag_match = None
|
||||||
|
self.disable_recounting = False
|
||||||
|
self.setUniformRowHeights(True)
|
||||||
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.setIconSize(QSize(30, 30))
|
||||||
|
self.setTabKeyNavigation(True)
|
||||||
|
self.setAlternatingRowColors(True)
|
||||||
|
self.setAnimated(True)
|
||||||
|
self.setHeaderHidden(True)
|
||||||
|
self.setItemDelegate(TagDelegate(self))
|
||||||
|
self.made_connections = False
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.setDragEnabled(True)
|
||||||
|
self.setDragDropMode(self.DragDrop)
|
||||||
|
self.setDropIndicatorShown(True)
|
||||||
|
self.setAutoExpandDelay(500)
|
||||||
|
self.pane_is_visible = False
|
||||||
|
self.search_icon = QIcon(I('search.png'))
|
||||||
|
self.user_category_icon = QIcon(I('tb_folder.png'))
|
||||||
|
self.delete_icon = QIcon(I('list_remove.png'))
|
||||||
|
self.rename_icon = QIcon(I('edit-undo.png'))
|
||||||
|
self.show_at_path.connect(self.show_item_at_path,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
|
||||||
|
self._model = TagsModel(self)
|
||||||
|
self._model.search_item_renamed.connect(self.search_item_renamed)
|
||||||
|
self._model.refresh_required.connect(self.refresh_required,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
self._model.tag_item_renamed.connect(self.tag_item_renamed)
|
||||||
|
self._model.restriction_error.connect(self.restriction_error)
|
||||||
|
self._model.user_categories_edited.connect(self.user_categories_edited,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
self._model.drag_drop_finished.connect(self.drag_drop_finished)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hidden_categories(self):
|
||||||
|
return self._model.hidden_categories
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db(self):
|
||||||
|
return self._model.db
|
||||||
|
|
||||||
|
@property
|
||||||
|
def collapse_model(self):
|
||||||
|
return self._model.collapse_model
|
||||||
|
|
||||||
|
def set_pane_is_visible(self, to_what):
|
||||||
|
pv = self.pane_is_visible
|
||||||
|
self.pane_is_visible = to_what
|
||||||
|
if to_what and not pv:
|
||||||
|
self.recount()
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
state_map = {}
|
||||||
|
expanded_categories = []
|
||||||
|
for row, category in enumerate(self._model.category_nodes):
|
||||||
|
if self.isExpanded(self._model.index(row, 0, QModelIndex())):
|
||||||
|
expanded_categories.append(category.py_name)
|
||||||
|
states = [c.tag.state for c in category.child_tags()]
|
||||||
|
names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
|
||||||
|
state_map[category.py_name] = dict(izip(names, states))
|
||||||
|
return expanded_categories, state_map
|
||||||
|
|
||||||
|
def reread_collapse_parameters(self):
|
||||||
|
self._model.reread_collapse_parameters(self.get_state()[1])
|
||||||
|
|
||||||
|
def set_database(self, db, tag_match, sort_by):
|
||||||
|
self._model.set_database(db)
|
||||||
|
|
||||||
|
self.pane_is_visible = True # because TagsModel.set_database did a recount
|
||||||
|
self.sort_by = sort_by
|
||||||
|
self.tag_match = tag_match
|
||||||
|
self.setModel(self._model)
|
||||||
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
pop = config['sort_tags_by']
|
||||||
|
self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop))
|
||||||
|
try:
|
||||||
|
match_pop = self.db.MATCH_TYPE.index(config['match_tags_type'])
|
||||||
|
except ValueError:
|
||||||
|
match_pop = 0
|
||||||
|
self.tag_match.setCurrentIndex(match_pop)
|
||||||
|
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.sort_by.currentIndexChanged.connect(self.sort_changed)
|
||||||
|
self.tag_match.currentIndexChanged.connect(self.match_changed)
|
||||||
|
self.made_connections = True
|
||||||
|
self.refresh_signal_processed = True
|
||||||
|
db.add_listener(self.database_changed)
|
||||||
|
self.expanded.connect(self.item_expanded)
|
||||||
|
|
||||||
|
def database_changed(self, event, ids):
|
||||||
|
if self.refresh_signal_processed:
|
||||||
|
self.refresh_signal_processed = False
|
||||||
|
self.refresh_required.emit()
|
||||||
|
|
||||||
|
def user_categories_edited(self, user_cats, nkey):
|
||||||
|
state_map = self.get_state()[1]
|
||||||
|
self.db.prefs.set('user_categories', user_cats)
|
||||||
|
self._model.rebuild_node_tree(state_map=state_map)
|
||||||
|
self.show_at_path.emit('@'+nkey)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def match_all(self):
|
||||||
|
return self.tag_match and self.tag_match.currentIndex() > 0
|
||||||
|
|
||||||
|
def sort_changed(self, pop):
|
||||||
|
config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop])
|
||||||
|
self.recount()
|
||||||
|
|
||||||
|
def match_changed(self, pop):
|
||||||
|
try:
|
||||||
|
config.set('match_tags_type', self.db.MATCH_TYPE[pop])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_search_restriction(self, s):
|
||||||
|
s = s if s else None
|
||||||
|
self._model.set_search_restriction(s)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
# Swallow everything except leftButton so context menus work correctly
|
||||||
|
if event.button() == Qt.LeftButton:
|
||||||
|
QTreeView.mouseReleaseEvent(self, event)
|
||||||
|
|
||||||
|
def mouseDoubleClickEvent(self, event):
|
||||||
|
# swallow these to avoid toggling and editing at the same time
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def search_string(self):
|
||||||
|
tokens = self._model.tokens()
|
||||||
|
joiner = ' and ' if self.match_all else ' or '
|
||||||
|
return joiner.join(tokens)
|
||||||
|
|
||||||
|
def toggle(self, index):
|
||||||
|
self._toggle(index, None)
|
||||||
|
|
||||||
|
def _toggle(self, index, set_to):
|
||||||
|
'''
|
||||||
|
set_to: if None, advance the state. Otherwise must be one of the values
|
||||||
|
in TAG_SEARCH_STATES
|
||||||
|
'''
|
||||||
|
modifiers = int(QApplication.keyboardModifiers())
|
||||||
|
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
|
||||||
|
if self._model.toggle(index, exclusive, set_to=set_to):
|
||||||
|
self.tags_marked.emit(self.search_string)
|
||||||
|
|
||||||
|
def conditional_clear(self, search_string):
|
||||||
|
if search_string != self.search_string:
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
def context_menu_handler(self, action=None, category=None,
|
||||||
|
key=None, index=None, search_state=None):
|
||||||
|
if not action:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if action == 'edit_item':
|
||||||
|
self.edit(index)
|
||||||
|
return
|
||||||
|
if action == 'open_editor':
|
||||||
|
self.tag_list_edit.emit(category, key)
|
||||||
|
return
|
||||||
|
if action == 'manage_categories':
|
||||||
|
self.edit_user_category.emit(category)
|
||||||
|
return
|
||||||
|
if action == 'search':
|
||||||
|
self._toggle(index, set_to=search_state)
|
||||||
|
return
|
||||||
|
if action == 'add_to_category':
|
||||||
|
tag = index.tag
|
||||||
|
if len(index.children) > 0:
|
||||||
|
for c in index.children:
|
||||||
|
self.add_item_to_user_cat.emit(category, c.tag.original_name,
|
||||||
|
c.tag.category)
|
||||||
|
self.add_item_to_user_cat.emit(category, tag.original_name,
|
||||||
|
tag.category)
|
||||||
|
return
|
||||||
|
if action == 'add_subcategory':
|
||||||
|
self.add_subcategory.emit(key)
|
||||||
|
return
|
||||||
|
if action == 'search_category':
|
||||||
|
self._toggle(index, set_to=search_state)
|
||||||
|
return
|
||||||
|
if action == 'delete_user_category':
|
||||||
|
self.delete_user_category.emit(key)
|
||||||
|
return
|
||||||
|
if action == 'delete_search':
|
||||||
|
saved_searches().delete(key)
|
||||||
|
self.rebuild_saved_searches.emit()
|
||||||
|
return
|
||||||
|
if action == 'delete_item_from_user_category':
|
||||||
|
tag = index.tag
|
||||||
|
if len(index.children) > 0:
|
||||||
|
for c in index.children:
|
||||||
|
self.del_item_from_user_cat.emit(key, c.tag.original_name,
|
||||||
|
c.tag.category)
|
||||||
|
self.del_item_from_user_cat.emit(key, tag.original_name, tag.category)
|
||||||
|
return
|
||||||
|
if action == 'manage_searches':
|
||||||
|
self.saved_search_edit.emit(category)
|
||||||
|
return
|
||||||
|
if action == 'edit_author_sort':
|
||||||
|
self.author_sort_edit.emit(self, index)
|
||||||
|
return
|
||||||
|
|
||||||
|
reset_filter_categories = True
|
||||||
|
if action == 'hide':
|
||||||
|
self.hidden_categories.add(category)
|
||||||
|
elif action == 'show':
|
||||||
|
self.hidden_categories.discard(category)
|
||||||
|
elif action == 'categorization':
|
||||||
|
changed = self.collapse_model != category
|
||||||
|
self._model.collapse_model = category
|
||||||
|
if changed:
|
||||||
|
reset_filter_categories = False
|
||||||
|
gprefs['tags_browser_partition_method'] = category
|
||||||
|
elif action == 'defaults':
|
||||||
|
self.hidden_categories.clear()
|
||||||
|
self.db.prefs.set('tag_browser_hidden_categories', list(self.hidden_categories))
|
||||||
|
if reset_filter_categories:
|
||||||
|
self._model.filter_categories_by = None
|
||||||
|
self._model.rebuild_node_tree()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
def show_context_menu(self, point):
|
||||||
|
def display_name( tag):
|
||||||
|
if tag.category == 'search':
|
||||||
|
n = tag.name
|
||||||
|
if len(n) > 45:
|
||||||
|
n = n[:45] + '...'
|
||||||
|
return "'" + n + "'"
|
||||||
|
return tag.name
|
||||||
|
|
||||||
|
index = self.indexAt(point)
|
||||||
|
self.context_menu = QMenu(self)
|
||||||
|
|
||||||
|
if index.isValid():
|
||||||
|
item = index.data(Qt.UserRole).toPyObject()
|
||||||
|
tag = None
|
||||||
|
|
||||||
|
if item.type == TagTreeItem.TAG:
|
||||||
|
tag_item = item
|
||||||
|
tag = item.tag
|
||||||
|
while item.type != TagTreeItem.CATEGORY:
|
||||||
|
item = item.parent
|
||||||
|
|
||||||
|
if item.type == TagTreeItem.CATEGORY:
|
||||||
|
if not item.category_key.startswith('@'):
|
||||||
|
while item.parent != self._model.root_item:
|
||||||
|
item = item.parent
|
||||||
|
category = unicode(item.name.toString())
|
||||||
|
key = item.category_key
|
||||||
|
# Verify that we are working with a field that we know something about
|
||||||
|
if key not in self.db.field_metadata:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Did the user click on a leaf node?
|
||||||
|
if tag:
|
||||||
|
# If the user right-clicked on an editable item, then offer
|
||||||
|
# the possibility of renaming that item.
|
||||||
|
if tag.is_editable:
|
||||||
|
# Add the 'rename' items
|
||||||
|
self.context_menu.addAction(self.rename_icon,
|
||||||
|
_('Rename %s')%display_name(tag),
|
||||||
|
partial(self.context_menu_handler, action='edit_item',
|
||||||
|
index=index))
|
||||||
|
if key == 'authors':
|
||||||
|
self.context_menu.addAction(_('Edit sort for %s')%display_name(tag),
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
action='edit_author_sort', index=tag.id))
|
||||||
|
|
||||||
|
# is_editable is also overloaded to mean 'can be added
|
||||||
|
# to a user category'
|
||||||
|
m = self.context_menu.addMenu(self.user_category_icon,
|
||||||
|
_('Add %s to user category')%display_name(tag))
|
||||||
|
nt = self.model().category_node_tree
|
||||||
|
def add_node_tree(tree_dict, m, path):
|
||||||
|
p = path[:]
|
||||||
|
for k in sorted(tree_dict.keys(), key=sort_key):
|
||||||
|
p.append(k)
|
||||||
|
n = k[1:] if k.startswith('@') else k
|
||||||
|
m.addAction(self.user_category_icon, n,
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
'add_to_category',
|
||||||
|
category='.'.join(p), index=tag_item))
|
||||||
|
if len(tree_dict[k]):
|
||||||
|
tm = m.addMenu(self.user_category_icon,
|
||||||
|
_('Children of %s')%n)
|
||||||
|
add_node_tree(tree_dict[k], tm, p)
|
||||||
|
p.pop()
|
||||||
|
add_node_tree(nt, m, [])
|
||||||
|
elif key == 'search':
|
||||||
|
self.context_menu.addAction(self.rename_icon,
|
||||||
|
_('Rename %s')%display_name(tag),
|
||||||
|
partial(self.context_menu_handler, action='edit_item',
|
||||||
|
index=index))
|
||||||
|
self.context_menu.addAction(self.delete_icon,
|
||||||
|
_('Delete search %s')%display_name(tag),
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
action='delete_search', key=tag.name))
|
||||||
|
if key.startswith('@') and not item.is_gst:
|
||||||
|
self.context_menu.addAction(self.user_category_icon,
|
||||||
|
_('Remove %s from category %s')%
|
||||||
|
(display_name(tag), item.py_name),
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
action='delete_item_from_user_category',
|
||||||
|
key = key, index = tag_item))
|
||||||
|
# Add the search for value items. All leaf nodes are searchable
|
||||||
|
self.context_menu.addAction(self.search_icon,
|
||||||
|
_('Search for %s')%display_name(tag),
|
||||||
|
partial(self.context_menu_handler, action='search',
|
||||||
|
search_state=TAG_SEARCH_STATES['mark_plus'],
|
||||||
|
index=index))
|
||||||
|
self.context_menu.addAction(self.search_icon,
|
||||||
|
_('Search for everything but %s')%display_name(tag),
|
||||||
|
partial(self.context_menu_handler, action='search',
|
||||||
|
search_state=TAG_SEARCH_STATES['mark_minus'],
|
||||||
|
index=index))
|
||||||
|
self.context_menu.addSeparator()
|
||||||
|
elif key.startswith('@') and not item.is_gst:
|
||||||
|
if item.can_be_edited:
|
||||||
|
self.context_menu.addAction(self.rename_icon,
|
||||||
|
_('Rename %s')%item.py_name,
|
||||||
|
partial(self.context_menu_handler, action='edit_item',
|
||||||
|
index=index))
|
||||||
|
self.context_menu.addAction(self.user_category_icon,
|
||||||
|
_('Add sub-category to %s')%item.py_name,
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
action='add_subcategory', key=key))
|
||||||
|
self.context_menu.addAction(self.delete_icon,
|
||||||
|
_('Delete user category %s')%item.py_name,
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
action='delete_user_category', key=key))
|
||||||
|
self.context_menu.addSeparator()
|
||||||
|
# Hide/Show/Restore categories
|
||||||
|
self.context_menu.addAction(_('Hide category %s') % category,
|
||||||
|
partial(self.context_menu_handler, action='hide',
|
||||||
|
category=key))
|
||||||
|
if self.hidden_categories:
|
||||||
|
m = self.context_menu.addMenu(_('Show category'))
|
||||||
|
for col in sorted(self.hidden_categories,
|
||||||
|
key=lambda x: sort_key(self.db.field_metadata[x]['name'])):
|
||||||
|
m.addAction(self.db.field_metadata[col]['name'],
|
||||||
|
partial(self.context_menu_handler, action='show', category=col))
|
||||||
|
|
||||||
|
# search by category. Some categories are not searchable, such
|
||||||
|
# as search and news
|
||||||
|
if item.tag.is_searchable:
|
||||||
|
self.context_menu.addAction(self.search_icon,
|
||||||
|
_('Search for books in category %s')%category,
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
action='search_category',
|
||||||
|
index=self._model.createIndex(item.row(), 0, item),
|
||||||
|
search_state=TAG_SEARCH_STATES['mark_plus']))
|
||||||
|
self.context_menu.addAction(self.search_icon,
|
||||||
|
_('Search for books not in category %s')%category,
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
action='search_category',
|
||||||
|
index=self._model.createIndex(item.row(), 0, item),
|
||||||
|
search_state=TAG_SEARCH_STATES['mark_minus']))
|
||||||
|
# Offer specific editors for tags/series/publishers/saved searches
|
||||||
|
self.context_menu.addSeparator()
|
||||||
|
if key in ['tags', 'publisher', 'series'] or \
|
||||||
|
self.db.field_metadata[key]['is_custom']:
|
||||||
|
self.context_menu.addAction(_('Manage %s')%category,
|
||||||
|
partial(self.context_menu_handler, action='open_editor',
|
||||||
|
category=tag.original_name if tag else None,
|
||||||
|
key=key))
|
||||||
|
elif key == 'authors':
|
||||||
|
self.context_menu.addAction(_('Manage %s')%category,
|
||||||
|
partial(self.context_menu_handler, action='edit_author_sort'))
|
||||||
|
elif key == 'search':
|
||||||
|
self.context_menu.addAction(_('Manage Saved Searches'),
|
||||||
|
partial(self.context_menu_handler, action='manage_searches',
|
||||||
|
category=tag.name if tag else None))
|
||||||
|
|
||||||
|
# Always show the user categories editor
|
||||||
|
self.context_menu.addSeparator()
|
||||||
|
if key.startswith('@') and \
|
||||||
|
key[1:] in self.db.prefs.get('user_categories', {}).keys():
|
||||||
|
self.context_menu.addAction(_('Manage User Categories'),
|
||||||
|
partial(self.context_menu_handler, action='manage_categories',
|
||||||
|
category=key[1:]))
|
||||||
|
else:
|
||||||
|
self.context_menu.addAction(_('Manage User Categories'),
|
||||||
|
partial(self.context_menu_handler, action='manage_categories',
|
||||||
|
category=None))
|
||||||
|
|
||||||
|
if self.hidden_categories:
|
||||||
|
if not self.context_menu.isEmpty():
|
||||||
|
self.context_menu.addSeparator()
|
||||||
|
self.context_menu.addAction(_('Show all categories'),
|
||||||
|
partial(self.context_menu_handler, action='defaults'))
|
||||||
|
|
||||||
|
m = self.context_menu.addMenu(_('Change sub-categorization scheme'))
|
||||||
|
da = m.addAction('Disable',
|
||||||
|
partial(self.context_menu_handler, action='categorization', category='disable'))
|
||||||
|
fla = m.addAction('By first letter',
|
||||||
|
partial(self.context_menu_handler, action='categorization', category='first letter'))
|
||||||
|
pa = m.addAction('Partition',
|
||||||
|
partial(self.context_menu_handler, action='categorization', category='partition'))
|
||||||
|
if self.collapse_model == 'disable':
|
||||||
|
da.setCheckable(True)
|
||||||
|
da.setChecked(True)
|
||||||
|
elif self.collapse_model == 'first letter':
|
||||||
|
fla.setCheckable(True)
|
||||||
|
fla.setChecked(True)
|
||||||
|
else:
|
||||||
|
pa.setCheckable(True)
|
||||||
|
pa.setChecked(True)
|
||||||
|
|
||||||
|
if not self.context_menu.isEmpty():
|
||||||
|
self.context_menu.popup(self.mapToGlobal(point))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def dragMoveEvent(self, event):
|
||||||
|
QTreeView.dragMoveEvent(self, event)
|
||||||
|
self.setDropIndicatorShown(False)
|
||||||
|
index = self.indexAt(event.pos())
|
||||||
|
if not index.isValid():
|
||||||
|
return
|
||||||
|
src_is_tb = event.mimeData().hasFormat('application/calibre+from_tag_browser')
|
||||||
|
item = index.data(Qt.UserRole).toPyObject()
|
||||||
|
flags = self._model.flags(index)
|
||||||
|
if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled:
|
||||||
|
self.setDropIndicatorShown(not src_is_tb)
|
||||||
|
return
|
||||||
|
if item.type == TagTreeItem.CATEGORY and not item.is_gst:
|
||||||
|
fm_dest = self.db.metadata_for_field(item.category_key)
|
||||||
|
if fm_dest['kind'] == 'user':
|
||||||
|
if src_is_tb:
|
||||||
|
if event.dropAction() == Qt.MoveAction:
|
||||||
|
data = str(event.mimeData().data('application/calibre+from_tag_browser'))
|
||||||
|
src = cPickle.loads(data)
|
||||||
|
for s in src:
|
||||||
|
if s[0] == TagTreeItem.TAG and \
|
||||||
|
(not s[1].startswith('@') or s[2]):
|
||||||
|
return
|
||||||
|
self.setDropIndicatorShown(True)
|
||||||
|
return
|
||||||
|
md = event.mimeData()
|
||||||
|
if hasattr(md, 'column_name'):
|
||||||
|
fm_src = self.db.metadata_for_field(md.column_name)
|
||||||
|
if md.column_name in ['authors', 'publisher', 'series'] or \
|
||||||
|
(fm_src['is_custom'] and (
|
||||||
|
(fm_src['datatype'] in ['series', 'text', 'enumeration'] and
|
||||||
|
not fm_src['is_multiple']) or
|
||||||
|
(fm_src['datatype'] == 'composite' and
|
||||||
|
fm_src['display'].get('make_category', False)))):
|
||||||
|
self.setDropIndicatorShown(True)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
if self.model():
|
||||||
|
self.model().clear_state()
|
||||||
|
|
||||||
|
def is_visible(self, idx):
|
||||||
|
item = idx.data(Qt.UserRole).toPyObject()
|
||||||
|
if getattr(item, 'type', None) == TagTreeItem.TAG:
|
||||||
|
idx = idx.parent()
|
||||||
|
return self.isExpanded(idx)
|
||||||
|
|
||||||
|
def recount(self, *args):
|
||||||
|
'''
|
||||||
|
Rebuild the category tree, expand any categories that were expanded,
|
||||||
|
reset the search states, and reselect the current node.
|
||||||
|
'''
|
||||||
|
if self.disable_recounting or not self.pane_is_visible:
|
||||||
|
return
|
||||||
|
self.refresh_signal_processed = True
|
||||||
|
ci = self.currentIndex()
|
||||||
|
if not ci.isValid():
|
||||||
|
ci = self.indexAt(QPoint(10, 10))
|
||||||
|
path = self.model().path_for_index(ci) if self.is_visible(ci) else None
|
||||||
|
expanded_categories, state_map = self.get_state()
|
||||||
|
self._model.rebuild_node_tree(state_map=state_map)
|
||||||
|
for category in expanded_categories:
|
||||||
|
self.expand(self._model.index_for_category(category))
|
||||||
|
self.show_item_at_path(path)
|
||||||
|
|
||||||
|
def show_item_at_path(self, path, box=False,
|
||||||
|
position=QTreeView.PositionAtCenter):
|
||||||
|
'''
|
||||||
|
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._model.index_for_path(path), box=box,
|
||||||
|
position=position)
|
||||||
|
|
||||||
|
def show_item_at_index(self, idx, box=False,
|
||||||
|
position=QTreeView.PositionAtCenter):
|
||||||
|
if idx.isValid():
|
||||||
|
self.setCurrentIndex(idx)
|
||||||
|
self.scrollTo(idx, position)
|
||||||
|
self.setCurrentIndex(idx)
|
||||||
|
if box:
|
||||||
|
self._model.set_boxed(idx)
|
||||||
|
|
||||||
|
def item_expanded(self, idx):
|
||||||
|
'''
|
||||||
|
Called by the expanded signal
|
||||||
|
'''
|
||||||
|
self.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
@ -39,7 +39,7 @@ from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
|||||||
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
||||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||||
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||||
from calibre.gui2.tag_view import TagBrowserMixin
|
from calibre.gui2.tag_browser.ui import TagBrowserMixin
|
||||||
|
|
||||||
|
|
||||||
class Listener(Thread): # {{{
|
class Listener(Thread): # {{{
|
||||||
|
@ -72,9 +72,7 @@ class UpdateNotification(QDialog):
|
|||||||
self.label = QLabel(('<p>'+
|
self.label = QLabel(('<p>'+
|
||||||
_('%s has been updated to version <b>%s</b>. '
|
_('%s has been updated to version <b>%s</b>. '
|
||||||
'See the <a href="http://calibre-ebook.com/whats-new'
|
'See the <a href="http://calibre-ebook.com/whats-new'
|
||||||
'">new features</a>.') + '<p>'+_('Update <b>only</b> if one of the '
|
'">new features</a>.'))%(
|
||||||
'new features or bug fixes is important to you. '
|
|
||||||
'If the current version works well for you, there is no need to update.'))%(
|
|
||||||
__appname__, calibre_version))
|
__appname__, calibre_version))
|
||||||
self.label.setOpenExternalLinks(True)
|
self.label.setOpenExternalLinks(True)
|
||||||
self.label.setWordWrap(True)
|
self.label.setWordWrap(True)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user