mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Allow saving/loading of search and replace expressions in the bulk metadata edit dialog. Add right click search for books in a category to the Tag Browser
This commit is contained in:
commit
2fad5774e7
@ -6,7 +6,8 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import re, os
|
import re, os
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
|
from PyQt4.Qt import Qt, QDialog, QGridLayout, QVBoxLayout, QFont, QLabel, \
|
||||||
pyqtSignal, QDialogButtonBox, QDate, QLineEdit
|
pyqtSignal, QDialogButtonBox, QInputDialog, QLineEdit, \
|
||||||
|
QMessageBox, QDate
|
||||||
|
|
||||||
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||||
@ -16,7 +17,7 @@ from calibre.ebooks.metadata.meta import get_metadata
|
|||||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||||
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE
|
from calibre.gui2 import error_dialog, ResizableDialog, UNDEFINED_QDATE
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.utils.config import dynamic
|
from calibre.utils.config import dynamic, JSONConfig
|
||||||
from calibre.utils.titlecase import titlecase
|
from calibre.utils.titlecase import titlecase
|
||||||
from calibre.utils.icu import sort_key, capitalize
|
from calibre.utils.icu import sort_key, capitalize
|
||||||
from calibre.utils.config import prefs, tweaks
|
from calibre.utils.config import prefs, tweaks
|
||||||
@ -451,6 +452,15 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed)
|
self.results_count.valueChanged[int].connect(self.s_r_display_bounds_changed)
|
||||||
self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed)
|
self.starting_from.valueChanged[int].connect(self.s_r_display_bounds_changed)
|
||||||
|
|
||||||
|
self.save_button.clicked.connect(self.s_r_save_query)
|
||||||
|
self.remove_button.clicked.connect(self.s_r_remove_query)
|
||||||
|
|
||||||
|
self.queries = JSONConfig("search_replace_queries")
|
||||||
|
self.query_field.addItem("")
|
||||||
|
self.query_field.addItems(sorted([q for q in self.queries], key=sort_key))
|
||||||
|
self.query_field.currentIndexChanged[str].connect(self.s_r_query_change)
|
||||||
|
self.query_field.setCurrentIndex(0)
|
||||||
|
|
||||||
def s_r_get_field(self, mi, field):
|
def s_r_get_field(self, mi, field):
|
||||||
if field:
|
if field:
|
||||||
if field == '{template}':
|
if field == '{template}':
|
||||||
@ -862,3 +872,117 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
|||||||
def series_changed(self, *args):
|
def series_changed(self, *args):
|
||||||
self.write_series = True
|
self.write_series = True
|
||||||
|
|
||||||
|
def s_r_remove_query(self, *args):
|
||||||
|
if self.query_field.currentIndex() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
ret = QMessageBox.question(self, _("Delete saved search/replace"),
|
||||||
|
_("The selected saved search/replace will be deleted. "
|
||||||
|
"Are you sure?"),
|
||||||
|
QMessageBox.Ok, QMessageBox.Cancel)
|
||||||
|
|
||||||
|
if ret == QMessageBox.Cancel:
|
||||||
|
return
|
||||||
|
|
||||||
|
item_id = self.query_field.currentIndex()
|
||||||
|
item_name = unicode(self.query_field.currentText())
|
||||||
|
|
||||||
|
self.query_field.blockSignals(True)
|
||||||
|
self.query_field.removeItem(item_id)
|
||||||
|
self.query_field.blockSignals(False)
|
||||||
|
self.query_field.setCurrentIndex(0)
|
||||||
|
|
||||||
|
if item_name in self.queries.keys():
|
||||||
|
del(self.queries[item_name])
|
||||||
|
self.queries.commit()
|
||||||
|
|
||||||
|
def s_r_save_query(self, *args):
|
||||||
|
name, ok = QInputDialog.getText(self, _('Save search/replace'),
|
||||||
|
_('Search/replace name:'))
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
new = True
|
||||||
|
name = unicode(name)
|
||||||
|
if name in self.queries.keys():
|
||||||
|
ret = QMessageBox.question(self, _("Save search/replace"),
|
||||||
|
_("That saved search/replace already exists and will be overwritten. "
|
||||||
|
"Are you sure?"),
|
||||||
|
QMessageBox.Ok, QMessageBox.Cancel)
|
||||||
|
if ret == QMessageBox.Cancel:
|
||||||
|
return
|
||||||
|
new = False
|
||||||
|
|
||||||
|
query = {}
|
||||||
|
query['name'] = name
|
||||||
|
query['search_field'] = unicode(self.search_field.currentText())
|
||||||
|
query['search_mode'] = unicode(self.search_mode.currentText())
|
||||||
|
query['s_r_template'] = unicode(self.s_r_template.text())
|
||||||
|
query['search_for'] = unicode(self.search_for.text())
|
||||||
|
query['case_sensitive'] = self.case_sensitive.isChecked()
|
||||||
|
query['replace_with'] = unicode(self.replace_with.text())
|
||||||
|
query['replace_func'] = unicode(self.replace_func.currentText())
|
||||||
|
query['destination_field'] = unicode(self.destination_field.currentText())
|
||||||
|
query['replace_mode'] = unicode(self.replace_mode.currentText())
|
||||||
|
query['comma_separated'] = self.comma_separated.isChecked()
|
||||||
|
query['results_count'] = self.results_count.value()
|
||||||
|
query['starting_from'] = self.starting_from.value()
|
||||||
|
query['multiple_separator'] = unicode(self.multiple_separator.text())
|
||||||
|
|
||||||
|
self.queries[name] = query
|
||||||
|
self.queries.commit()
|
||||||
|
|
||||||
|
if new:
|
||||||
|
self.query_field.blockSignals(True)
|
||||||
|
self.query_field.clear()
|
||||||
|
self.query_field.addItem('')
|
||||||
|
self.query_field.addItems(sorted([q for q in self.queries], key=sort_key))
|
||||||
|
self.query_field.blockSignals(False)
|
||||||
|
self.query_field.setCurrentIndex(self.query_field.findText(name))
|
||||||
|
|
||||||
|
def s_r_query_change(self, item_name):
|
||||||
|
if not item_name:
|
||||||
|
self.s_r_reset_query_fields()
|
||||||
|
return
|
||||||
|
item = self.queries.get(unicode(item_name), None)
|
||||||
|
if item is None:
|
||||||
|
self.s_r_reset_query_fields()
|
||||||
|
return
|
||||||
|
|
||||||
|
def set_index(attr, txt):
|
||||||
|
try:
|
||||||
|
attr.setCurrentIndex(attr.findText(txt))
|
||||||
|
except:
|
||||||
|
attr.setCurrentIndex(0)
|
||||||
|
|
||||||
|
set_index(self.search_mode, item['search_mode'])
|
||||||
|
set_index(self.search_field, item['search_field'])
|
||||||
|
self.s_r_template.setText(item['s_r_template'])
|
||||||
|
self.s_r_template_changed() #simulate gain/loss of focus
|
||||||
|
self.search_for.setText(item['search_for'])
|
||||||
|
self.case_sensitive.setChecked(item['case_sensitive'])
|
||||||
|
self.replace_with.setText(item['replace_with'])
|
||||||
|
set_index(self.replace_func, item['replace_func'])
|
||||||
|
set_index(self.destination_field, item['destination_field'])
|
||||||
|
set_index(self.replace_mode, item['replace_mode'])
|
||||||
|
self.comma_separated.setChecked(item['comma_separated'])
|
||||||
|
self.results_count.setValue(int(item['results_count']))
|
||||||
|
self.starting_from.setValue(int(item['starting_from']))
|
||||||
|
self.multiple_separator.setText(item['multiple_separator'])
|
||||||
|
|
||||||
|
def s_r_reset_query_fields(self):
|
||||||
|
# Don't reset the search mode. The user will probably want to use it
|
||||||
|
# as it was
|
||||||
|
self.search_field.setCurrentIndex(0)
|
||||||
|
self.s_r_template.setText("")
|
||||||
|
self.search_for.setText("")
|
||||||
|
self.case_sensitive.setChecked(False)
|
||||||
|
self.replace_with.setText("")
|
||||||
|
self.replace_func.setCurrentIndex(0)
|
||||||
|
self.destination_field.setCurrentIndex(0)
|
||||||
|
self.replace_mode.setCurrentIndex(0)
|
||||||
|
self.comma_separated.setChecked(True)
|
||||||
|
self.results_count.setValue(999)
|
||||||
|
self.starting_from.setValue(1)
|
||||||
|
self.multiple_separator.setText(" ::: ")
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>850</width>
|
<width>850</width>
|
||||||
<height>650</height>
|
<height>700</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>842</width>
|
<width>842</width>
|
||||||
<height>589</height>
|
<height>639</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
@ -574,7 +574,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
<property name="sizeConstraint">
|
<property name="sizeConstraint">
|
||||||
<enum>QLayout::SetMinimumSize</enum>
|
<enum>QLayout::SetMinimumSize</enum>
|
||||||
</property>
|
</property>
|
||||||
<item row="1" column="0" colspan="3">
|
<item row="0" column="0" colspan="4">
|
||||||
<widget class="QLabel" name="s_r_heading">
|
<widget class="QLabel" name="s_r_heading">
|
||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@ -584,14 +584,91 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="filler">
|
<widget class="QLabel" name="filler">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string/>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="0" colspan="3">
|
||||||
|
<widget class="Line" name="line">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="xlabel_22">
|
||||||
|
<property name="text">
|
||||||
|
<string>Load searc&h/replace:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>search_field</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QComboBox" name="query_field">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Select saved search/replace to load.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="2">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeType">
|
||||||
|
<enum>QSizePolicy::Fixed</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="save_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Save current search/replace</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Sa&ve</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="remove_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Delete saved search/replace</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Delete</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="xlabel_21">
|
<widget class="QLabel" name="xlabel_21">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Search &field:</string>
|
<string>Search &field:</string>
|
||||||
@ -601,14 +678,14 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="QComboBox" name="search_field">
|
<widget class="QComboBox" name="search_field">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>The name of the field that you want to search</string>
|
<string>The name of the field that you want to search</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="2">
|
<item row="4" column="2">
|
||||||
<layout class="QHBoxLayout" name="HLayout_3">
|
<layout class="QHBoxLayout" name="HLayout_3">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="xlabel_24">
|
<widget class="QLabel" name="xlabel_24">
|
||||||
@ -642,7 +719,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLabel" name="template_label">
|
<widget class="QLabel" name="template_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Te&mplate:</string>
|
<string>Te&mplate:</string>
|
||||||
@ -652,7 +729,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="HistoryLineEdit" name="s_r_template">
|
<widget class="HistoryLineEdit" name="s_r_template">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
@ -665,7 +742,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QLabel" name="xlabel_2">
|
<widget class="QLabel" name="xlabel_2">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Search for:</string>
|
<string>&Search for:</string>
|
||||||
@ -675,7 +752,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="HistoryLineEdit" name="search_for">
|
<widget class="HistoryLineEdit" name="search_for">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
@ -688,7 +765,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="2">
|
<item row="6" column="2">
|
||||||
<widget class="QCheckBox" name="case_sensitive">
|
<widget class="QCheckBox" name="case_sensitive">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string>
|
<string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string>
|
||||||
@ -701,7 +778,7 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" column="0">
|
||||||
<widget class="QLabel" name="xlabel_4">
|
<widget class="QLabel" name="xlabel_4">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Replace with:</string>
|
<string>&Replace with:</string>
|
||||||
@ -711,14 +788,14 @@ Future conversion of these books will use the default settings.</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="7" column="1">
|
||||||
<widget class="HistoryLineEdit" name="replace_with">
|
<widget class="HistoryLineEdit" name="replace_with">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>The replacement text. The matched search text will be replaced with this string</string>
|
<string>The replacement text. The matched search text will be replaced with this string</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="2">
|
<item row="7" column="2">
|
||||||
<layout class="QHBoxLayout" name="verticalLayout">
|
<layout class="QHBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_41">
|
<widget class="QLabel" name="label_41">
|
||||||
@ -753,7 +830,7 @@ field is processed. In regular expression mode, only the matched text is process
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0">
|
<item row="8" column="0">
|
||||||
<widget class="QLabel" name="destination_field_label">
|
<widget class="QLabel" name="destination_field_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Destination field:</string>
|
<string>&Destination field:</string>
|
||||||
@ -763,7 +840,7 @@ field is processed. In regular expression mode, only the matched text is process
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="8" column="1">
|
||||||
<widget class="QComboBox" name="destination_field">
|
<widget class="QComboBox" name="destination_field">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>The field that the text will be put into after all replacements.
|
<string>The field that the text will be put into after all replacements.
|
||||||
@ -771,7 +848,7 @@ If blank, the source field is used if the field is modifiable</string>
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="2">
|
<item row="8" column="2">
|
||||||
<layout class="QHBoxLayout" name="verticalLayout">
|
<layout class="QHBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="replace_mode_label">
|
<widget class="QLabel" name="replace_mode_label">
|
||||||
@ -820,7 +897,7 @@ not multiple and the destination field is multiple</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1" colspan="2">
|
<item row="9" column="1" colspan="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_21">
|
<layout class="QHBoxLayout" name="horizontalLayout_21">
|
||||||
<item>
|
<item>
|
||||||
<spacer name="HSpacer_347">
|
<spacer name="HSpacer_347">
|
||||||
@ -906,7 +983,7 @@ not multiple and the destination field is multiple</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0" colspan="4">
|
<item row="10" column="0" colspan="4">
|
||||||
<widget class="QScrollArea" name="scrollArea11">
|
<widget class="QScrollArea" name="scrollArea11">
|
||||||
<property name="frameShape">
|
<property name="frameShape">
|
||||||
<enum>QFrame::NoFrame</enum>
|
<enum>QFrame::NoFrame</enum>
|
||||||
@ -919,8 +996,8 @@ not multiple and the destination field is multiple</string>
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>197</width>
|
<width>826</width>
|
||||||
<height>60</height>
|
<height>323</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="testgrid">
|
<layout class="QGridLayout" name="testgrid">
|
||||||
@ -1030,6 +1107,9 @@ not multiple and the destination field is multiple</string>
|
|||||||
<tabstop>series_numbering_restarts</tabstop>
|
<tabstop>series_numbering_restarts</tabstop>
|
||||||
<tabstop>series_start_number</tabstop>
|
<tabstop>series_start_number</tabstop>
|
||||||
<tabstop>button_box</tabstop>
|
<tabstop>button_box</tabstop>
|
||||||
|
<tabstop>query_field</tabstop>
|
||||||
|
<tabstop>save_button</tabstop>
|
||||||
|
<tabstop>remove_button</tabstop>
|
||||||
<tabstop>search_field</tabstop>
|
<tabstop>search_field</tabstop>
|
||||||
<tabstop>search_mode</tabstop>
|
<tabstop>search_mode</tabstop>
|
||||||
<tabstop>s_r_template</tabstop>
|
<tabstop>s_r_template</tabstop>
|
||||||
@ -1045,6 +1125,23 @@ not multiple and the destination field is multiple</string>
|
|||||||
<tabstop>multiple_separator</tabstop>
|
<tabstop>multiple_separator</tabstop>
|
||||||
<tabstop>test_text</tabstop>
|
<tabstop>test_text</tabstop>
|
||||||
<tabstop>test_result</tabstop>
|
<tabstop>test_result</tabstop>
|
||||||
|
<tabstop>scrollArea</tabstop>
|
||||||
|
<tabstop>central_widget</tabstop>
|
||||||
|
<tabstop>swap_title_and_author</tabstop>
|
||||||
|
<tabstop>clear_series</tabstop>
|
||||||
|
<tabstop>adddate</tabstop>
|
||||||
|
<tabstop>clear_adddate_button</tabstop>
|
||||||
|
<tabstop>apply_adddate</tabstop>
|
||||||
|
<tabstop>pubdate</tabstop>
|
||||||
|
<tabstop>clear_pubdate_button</tabstop>
|
||||||
|
<tabstop>apply_pubdate</tabstop>
|
||||||
|
<tabstop>remove_format</tabstop>
|
||||||
|
<tabstop>change_title_to_title_case</tabstop>
|
||||||
|
<tabstop>remove_conversion_settings</tabstop>
|
||||||
|
<tabstop>cover_generate</tabstop>
|
||||||
|
<tabstop>cover_remove</tabstop>
|
||||||
|
<tabstop>cover_from_fmt</tabstop>
|
||||||
|
<tabstop>scrollArea11</tabstop>
|
||||||
</tabstops>
|
</tabstops>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../../resources/images.qrc"/>
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
@ -186,7 +186,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
def context_menu_handler(self, action=None, category=None,
|
def context_menu_handler(self, action=None, category=None,
|
||||||
key=None, index=None):
|
key=None, index=None, negate=None):
|
||||||
if not action:
|
if not action:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@ -199,6 +199,9 @@ class TagsView(QTreeView): # {{{
|
|||||||
if action == 'manage_categories':
|
if action == 'manage_categories':
|
||||||
self.user_category_edit.emit(category)
|
self.user_category_edit.emit(category)
|
||||||
return
|
return
|
||||||
|
if action == 'search_category':
|
||||||
|
self.tags_marked.emit(category + ':' + str(not negate))
|
||||||
|
return
|
||||||
if action == 'manage_searches':
|
if action == 'manage_searches':
|
||||||
self.saved_search_edit.emit(category)
|
self.saved_search_edit.emit(category)
|
||||||
return
|
return
|
||||||
@ -268,6 +271,15 @@ class TagsView(QTreeView): # {{{
|
|||||||
m.addAction(col,
|
m.addAction(col,
|
||||||
partial(self.context_menu_handler, action='show', category=col))
|
partial(self.context_menu_handler, action='show', category=col))
|
||||||
|
|
||||||
|
# search by category
|
||||||
|
self.context_menu.addAction(
|
||||||
|
_('Search for books in category %s')%category,
|
||||||
|
partial(self.context_menu_handler, action='search_category',
|
||||||
|
category=key, negate=False))
|
||||||
|
self.context_menu.addAction(
|
||||||
|
_('Search for books not in category %s')%category,
|
||||||
|
partial(self.context_menu_handler, action='search_category',
|
||||||
|
category=key, negate=True))
|
||||||
# Offer specific editors for tags/series/publishers/saved searches
|
# Offer specific editors for tags/series/publishers/saved searches
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
if key in ['tags', 'publisher', 'series'] or \
|
if key in ['tags', 'publisher', 'series'] or \
|
||||||
|
@ -135,7 +135,7 @@ def _match(query, value, matchkind):
|
|||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class CacheRow(list):
|
class CacheRow(list): # {{{
|
||||||
|
|
||||||
def __init__(self, db, composites, val):
|
def __init__(self, db, composites, val):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -166,14 +166,16 @@ class CacheRow(list):
|
|||||||
def __getslice__(self, i, j):
|
def __getslice__(self, i, j):
|
||||||
return self.__getitem__(slice(i, j))
|
return self.__getitem__(slice(i, j))
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class ResultCache(SearchQueryParser): # {{{
|
class ResultCache(SearchQueryParser): # {{{
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Stores sorted and filtered metadata in memory.
|
Stores sorted and filtered metadata in memory.
|
||||||
'''
|
'''
|
||||||
def __init__(self, FIELD_MAP, field_metadata):
|
def __init__(self, FIELD_MAP, field_metadata, db_prefs=None):
|
||||||
self.FIELD_MAP = FIELD_MAP
|
self.FIELD_MAP = FIELD_MAP
|
||||||
|
self.db_prefs = db_prefs
|
||||||
self.composites = {}
|
self.composites = {}
|
||||||
for key in field_metadata:
|
for key in field_metadata:
|
||||||
if field_metadata[key]['datatype'] == 'composite':
|
if field_metadata[key]['datatype'] == 'composite':
|
||||||
@ -191,7 +193,7 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
self._data = self.field_metadata = self.FIELD_MAP = \
|
self._data = self.field_metadata = self.FIELD_MAP = \
|
||||||
self.numeric_search_relops = self.date_search_relops = \
|
self.numeric_search_relops = self.date_search_relops = \
|
||||||
self.all_search_locations = None
|
self.all_search_locations = self.db_prefs = None
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, row):
|
def __getitem__(self, row):
|
||||||
@ -405,6 +407,22 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
matches.add(item[0])
|
matches.add(item[0])
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
def get_user_category_matches(self, location, query, candidates):
|
||||||
|
res = set([])
|
||||||
|
if self.db_prefs is None:
|
||||||
|
return res
|
||||||
|
user_cats = self.db_prefs.get('user_categories', [])
|
||||||
|
if location not in user_cats:
|
||||||
|
return res
|
||||||
|
c = set(candidates)
|
||||||
|
for (item, category, ign) in user_cats[location]:
|
||||||
|
s = self.get_matches(category, '=' + item, candidates=c)
|
||||||
|
c -= s
|
||||||
|
res |= s
|
||||||
|
if query == 'false':
|
||||||
|
return candidates - res
|
||||||
|
return res
|
||||||
|
|
||||||
def get_matches(self, location, query, allow_recursion=True, candidates=None):
|
def get_matches(self, location, query, allow_recursion=True, candidates=None):
|
||||||
matches = set([])
|
matches = set([])
|
||||||
if candidates is None:
|
if candidates is None:
|
||||||
@ -443,6 +461,10 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
return self.get_numeric_matches(location, query[1:],
|
return self.get_numeric_matches(location, query[1:],
|
||||||
candidates, val_func=vf)
|
candidates, val_func=vf)
|
||||||
|
|
||||||
|
# check for user categories
|
||||||
|
if len(location) >= 2 and location.startswith('@'):
|
||||||
|
return self.get_user_category_matches(location[1:], query.lower(),
|
||||||
|
candidates)
|
||||||
# everything else, or 'all' matches
|
# everything else, or 'all' matches
|
||||||
matchkind = CONTAINS_MATCH
|
matchkind = CONTAINS_MATCH
|
||||||
if (len(query) > 1):
|
if (len(query) > 1):
|
||||||
@ -468,6 +490,8 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
for x in range(len(self.FIELD_MAP)):
|
for x in range(len(self.FIELD_MAP)):
|
||||||
col_datatype.append('')
|
col_datatype.append('')
|
||||||
for x in self.field_metadata:
|
for x in self.field_metadata:
|
||||||
|
if x.startswith('@'):
|
||||||
|
continue
|
||||||
if len(self.field_metadata[x]['search_terms']):
|
if len(self.field_metadata[x]['search_terms']):
|
||||||
db_col[x] = self.field_metadata[x]['rec_index']
|
db_col[x] = self.field_metadata[x]['rec_index']
|
||||||
if self.field_metadata[x]['datatype'] not in \
|
if self.field_metadata[x]['datatype'] not in \
|
||||||
|
@ -332,7 +332,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
self.book_on_device_func = None
|
self.book_on_device_func = None
|
||||||
self.data = ResultCache(self.FIELD_MAP, self.field_metadata)
|
self.data = ResultCache(self.FIELD_MAP, self.field_metadata, db_prefs=self.prefs)
|
||||||
self.search = self.data.search
|
self.search = self.data.search
|
||||||
self.search_getting_ids = self.data.search_getting_ids
|
self.search_getting_ids = self.data.search_getting_ids
|
||||||
self.refresh = functools.partial(self.data.refresh, self)
|
self.refresh = functools.partial(self.data.refresh, self)
|
||||||
|
@ -475,6 +475,8 @@ class FieldMetadata(dict):
|
|||||||
val = self._tb_cats[key]
|
val = self._tb_cats[key]
|
||||||
if val['is_category'] and val['kind'] in ('user', 'search'):
|
if val['is_category'] and val['kind'] in ('user', 'search'):
|
||||||
del self._tb_cats[key]
|
del self._tb_cats[key]
|
||||||
|
if key in self._search_term_map:
|
||||||
|
del self._search_term_map[key]
|
||||||
|
|
||||||
def cc_series_index_column_for(self, key):
|
def cc_series_index_column_for(self, key):
|
||||||
return self._tb_cats[key]['rec_index'] + 1
|
return self._tb_cats[key]['rec_index'] + 1
|
||||||
@ -482,11 +484,12 @@ class FieldMetadata(dict):
|
|||||||
def add_user_category(self, label, name):
|
def add_user_category(self, label, name):
|
||||||
if label in self._tb_cats:
|
if label in self._tb_cats:
|
||||||
raise ValueError('Duplicate user field [%s]'%(label))
|
raise ValueError('Duplicate user field [%s]'%(label))
|
||||||
self._tb_cats[label] = {'table':None, 'column':None,
|
self._tb_cats[label] = {'table':None, 'column':None,
|
||||||
'datatype':None, 'is_multiple':None,
|
'datatype':None, 'is_multiple':None,
|
||||||
'kind':'user', 'name':name,
|
'kind':'user', 'name':name,
|
||||||
'search_terms':[], 'is_custom':False,
|
'search_terms':[label],'is_custom':False,
|
||||||
'is_category':True}
|
'is_category':True}
|
||||||
|
self._add_search_terms_to_map(label, [label])
|
||||||
|
|
||||||
def add_search_category(self, label, name):
|
def add_search_category(self, label, name):
|
||||||
if label in self._tb_cats:
|
if label in self._tb_cats:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user