Merge from trunk

This commit is contained in:
Charles Haley 2011-06-26 13:52:26 +01:00
commit 365592d066
10 changed files with 2363 additions and 2332 deletions

View File

@ -17,6 +17,8 @@ def decompress_doc(data):
return cPalmdoc.decompress(data)
def compress_doc(data):
if not data:
return u''
return cPalmdoc.compress(data)
def test():

View File

@ -5,7 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, shutil
import os
from functools import partial
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.utils.config import prefs
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.gui2.actions import InterfaceAction
@ -107,7 +107,7 @@ class ChooseLibraryAction(InterfaceAction):
self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
self.rename_menu = QMenu(_('Rename library'))
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)
ac = self.create_action(spec=(_('Pick a random book'), 'catalog.png',
@ -252,22 +252,15 @@ class ChooseLibraryAction(InterfaceAction):
def delete_requested(self, name, location):
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.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):
dirty_text = 'no'

View File

@ -16,7 +16,7 @@ from calibre.constants import isosx, __appname__, preferred_encoding, \
from calibre.gui2 import config, is_widescreen, gprefs
from calibre.gui2.library.views import BooksView, DeviceBooksView
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.notify import get_notifier

View 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'

File diff suppressed because it is too large Load Diff

View 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)
# }}}

View 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

View File

@ -39,7 +39,7 @@ from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
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): # {{{

View File

@ -72,9 +72,7 @@ class UpdateNotification(QDialog):
self.label = QLabel(('<p>'+
_('%s has been updated to version <b>%s</b>. '
'See the <a href="http://calibre-ebook.com/whats-new'
'">new features</a>.') + '<p>'+_('Update <b>only</b> if one of the '
'new features or bug fixes is important to you. '
'If the current version works well for you, there is no need to update.'))%(
'">new features</a>.'))%(
__appname__, calibre_version))
self.label.setOpenExternalLinks(True)
self.label.setWordWrap(True)