%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/gui2/dialogs/ |
| Current File : //usr/lib/calibre/calibre/gui2/dialogs/metadata_bulk.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2008, Kovid Goyal <kovid at kovidgoyal.net>
import numbers
import regex
from collections import defaultdict, namedtuple
from io import BytesIO
from qt.core import (
QApplication, QComboBox, QCompleter, QDateTime, QDialog,
QDialogButtonBox, QFont, QGridLayout, QInputDialog, QLabel, QLineEdit,
QProgressBar, QSize, Qt, QVBoxLayout, pyqtSignal
)
from threading import Thread
from calibre import human_readable, prints
from calibre.constants import DEBUG
from calibre.db import _get_next_series_num_for_list
from calibre.ebooks.metadata import authors_to_string, string_to_authors, title_sort
from calibre.ebooks.metadata.book.formatter import SafeFormat
from calibre.ebooks.metadata.opf2 import OPF
from calibre.gui2 import (
UNDEFINED_QDATETIME, FunctionDispatcher, error_dialog, gprefs, info_dialog,
question_dialog
)
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from calibre.gui2.widgets import LineEditECM
from calibre.utils.config import JSONConfig, dynamic, prefs, tweaks
from calibre.utils.date import internal_iso_format_string, qt_to_dt
from calibre.utils.icu import capitalize, sort_key
from calibre.utils.titlecase import titlecase
from polyglot.builtins import error_message, iteritems, itervalues, native_string_type
Settings = namedtuple('Settings',
'remove_all remove add au aus do_aus rating pub do_series do_autonumber '
'do_swap_ta do_remove_conv do_auto_author series do_series_restart series_start_value series_increment '
'do_title_case cover_action clear_series clear_pub pubdate adddate do_title_sort languages clear_languages '
'restore_original comments generate_cover_settings read_file_metadata casing_algorithm do_compress_cover compress_cover_quality')
null = object()
class Caser(LineEditECM):
def __init__(self, title):
self.title = title
def text(self):
return self.title
def setText(self, text):
self.title = text
class MyBlockingBusy(QDialog): # {{{
all_done = pyqtSignal()
progress_update = pyqtSignal(int)
progress_finished_cur_step = pyqtSignal()
progress_next_step_range = pyqtSignal(int)
def __init__(self, args, ids, db, refresh_books, cc_widgets, s_r_func, do_sr, sr_calls, parent=None, window_title=_('Working')):
QDialog.__init__(self, parent)
self._layout = l = QVBoxLayout()
self.setLayout(l)
self.cover_sizes = {'old': 0, 'new': 0}
# Every Path that will be taken in do_all
options = [
args.cover_action == 'fromfmt' or args.read_file_metadata,
args.do_swap_ta, args.do_title_case and not
args.do_swap_ta, args.do_title_sort, bool(args.au),
args.do_auto_author, bool(args.aus) and args.do_aus,
args.cover_action == 'remove' or args.cover_action ==
'generate' or args.cover_action == 'trim' or
args.cover_action == 'clone', args.restore_original,
args.rating != -1, args.clear_pub, bool(args.pub),
args.clear_series, args.pubdate is not None, args.adddate
is not None, args.do_series, bool(args.series) and
args.do_autonumber, args.comments is not null,
args.do_remove_conv, args.clear_languages, args.remove_all,
bool(do_sr), args.do_compress_cover
]
self.selected_options = sum(options)
if DEBUG:
print("Number of steps for bulk metadata: %d" % self.selected_options)
print("Optionslist: ")
print(options)
self.msg = QLabel(_('Processing %d books, please wait...') % len(ids))
self.font = QFont()
self.font.setPointSize(self.font.pointSize() + 8)
self.msg.setFont(self.font)
self.current_step_pb = QProgressBar(self)
self.current_step_pb.setFormat(_("Current step progress: %p %"))
if self.selected_options > 1:
# More than one Option needs to be done! Add Overall ProgressBar
self.overall_pb = QProgressBar(self)
self.overall_pb.setRange(0, self.selected_options)
self.overall_pb.setValue(0)
self.overall_pb.setFormat(_("Step %v/%m"))
self._layout.addWidget(self.overall_pb)
self._layout.addSpacing(15)
self.current_option = 0
self.current_step_value = 0
self._layout.addWidget(self.current_step_pb)
self._layout.addSpacing(15)
self._layout.addWidget(self.msg, 0, Qt.AlignmentFlag.AlignHCenter)
self.setWindowTitle(window_title + '...')
self.setMinimumWidth(200)
self.resize(self.sizeHint())
self.error = None
self.all_done.connect(self.on_all_done, type=Qt.ConnectionType.QueuedConnection)
self.progress_update.connect(self.on_progress_update, type=Qt.ConnectionType.QueuedConnection)
self.progress_finished_cur_step.connect(self.on_progress_finished_cur_step, type=Qt.ConnectionType.QueuedConnection)
self.progress_next_step_range.connect(self.on_progress_next_step_range, type=Qt.ConnectionType.QueuedConnection)
self.args, self.ids = args, ids
self.db, self.cc_widgets = db, cc_widgets
self.s_r_func = FunctionDispatcher(s_r_func)
self.do_sr = do_sr
self.sr_calls = sr_calls
self.refresh_books = refresh_books
def accept(self):
pass
def reject(self):
pass
def on_progress_update(self, processed_steps):
"""
This signal should be emitted if a step can be traced with numbers.
"""
self.current_step_value += processed_steps
self.current_step_pb.setValue(self.current_step_value)
def on_progress_finished_cur_step(self):
if self.selected_options > 1:
self.current_option += 1
self.overall_pb.setValue(self.current_option)
def on_progress_next_step_range(self, steps_of_progress):
"""
If steps_of_progress equals 0 results this in a indetermined ProgressBar
Otherwise the range is from 0..steps_of_progress
"""
self.current_step_value = 0
self.current_step_pb.setRange(0, steps_of_progress)
def on_all_done(self):
if not self.error:
# The cc widgets can only be accessed in the GUI thread
try:
for w in self.cc_widgets:
w.commit(self.ids)
except Exception as err:
import traceback
self.error = (err, traceback.format_exc())
QDialog.accept(self)
def exec(self):
self.thread = Thread(target=self.do_it)
self.thread.start()
return QDialog.exec(self)
exec_ = exec
def do_it(self):
try:
self.do_all()
except Exception as err:
import traceback
try:
err = str(err)
except:
err = repr(err)
self.error = (err, traceback.format_exc())
self.all_done.emit()
def read_file_metadata(self, args):
from calibre.utils.ipc.simple_worker import offload_worker
db = self.db.new_api
worker = offload_worker()
try:
self.progress_next_step_range.emit(len(self.ids))
for book_id in self.ids:
fmts = db.formats(book_id, verify_formats=False)
paths = list(filter(None, [db.format_abspath(book_id, fmt) for fmt in fmts]))
if paths:
ret = worker(
'calibre.ebooks.metadata.worker', 'read_metadata_bulk',
args.read_file_metadata, args.cover_action == 'fromfmt', paths)
if ret['tb'] is not None:
prints(ret['tb'])
else:
ans = ret['result']
opf, cdata = ans['opf'], ans['cdata']
if opf is not None:
try:
mi = OPF(BytesIO(opf), populate_spine=False, try_to_guess_cover=False).to_book_metadata()
except Exception:
import traceback
traceback.print_exc()
else:
db.set_metadata(book_id, mi, allow_case_change=True)
if cdata is not None:
db.set_cover({book_id: cdata})
self.progress_update.emit(1)
self.progress_finished_cur_step.emit()
finally:
worker.shutdown()
def do_all(self):
cache = self.db.new_api
args = self.args
from_file = args.cover_action == 'fromfmt' or args.read_file_metadata
if from_file:
old = prefs['read_file_metadata']
if not old:
prefs['read_file_metadata'] = True
try:
self.read_file_metadata(args)
finally:
if old != prefs['read_file_metadata']:
prefs['read_file_metadata'] = old
def change_title_casing(val):
caser = Caser(val)
getattr(caser, args.casing_algorithm)()
return caser.title
# Title and authors
if args.do_swap_ta:
self.progress_next_step_range.emit(3)
title_map = cache.all_field_for('title', self.ids)
authors_map = cache.all_field_for('authors', self.ids)
self.progress_update.emit(1)
def new_title(authors):
ans = authors_to_string(authors)
return change_title_casing(ans) if args.do_title_case else ans
new_title_map = {bid:new_title(authors) for bid, authors in iteritems(authors_map)}
new_authors_map = {bid:string_to_authors(title) for bid, title in iteritems(title_map)}
self.progress_update.emit(1)
cache.set_field('authors', new_authors_map)
cache.set_field('title', new_title_map)
self.progress_update.emit(1)
self.progress_finished_cur_step.emit()
if args.do_title_case and not args.do_swap_ta:
self.progress_next_step_range.emit(0)
title_map = cache.all_field_for('title', self.ids)
cache.set_field('title', {bid:change_title_casing(title) for bid, title in iteritems(title_map)})
self.progress_finished_cur_step.emit()
if args.do_title_sort:
self.progress_next_step_range.emit(2)
lang_map = cache.all_field_for('languages', self.ids)
title_map = cache.all_field_for('title', self.ids)
self.progress_update.emit(1)
def get_sort(book_id):
if args.languages:
lang = args.languages[0]
else:
try:
lang = lang_map[book_id][0]
except (KeyError, IndexError, TypeError, AttributeError):
lang = 'eng'
return title_sort(title_map[book_id], lang=lang)
cache.set_field('sort', {bid:get_sort(bid) for bid in self.ids})
self.progress_update.emit(1)
self.progress_finished_cur_step.emit()
if args.au:
self.progress_next_step_range.emit(0)
self.processed_books = 0
authors = string_to_authors(args.au)
cache.set_field('authors', {bid: authors for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.do_auto_author:
self.progress_next_step_range.emit(0)
aus_map = cache.author_sort_strings_for_books(self.ids)
cache.set_field('author_sort', {book_id: ' & '.join(aus_map[book_id]) for book_id in aus_map})
self.progress_finished_cur_step.emit()
if args.aus and args.do_aus:
self.progress_next_step_range.emit(0)
cache.set_field('author_sort', {bid:args.aus for bid in self.ids})
self.progress_finished_cur_step.emit()
# Covers
if args.cover_action == 'remove':
self.progress_next_step_range.emit(0)
cache.set_cover({bid: None for bid in self.ids})
self.progress_finished_cur_step.emit()
elif args.cover_action == 'generate':
self.progress_next_step_range.emit(len(self.ids))
from calibre.ebooks.covers import generate_cover
for book_id in self.ids:
mi = self.db.get_metadata(book_id, index_is_id=True)
cdata = generate_cover(mi, prefs=args.generate_cover_settings)
cache.set_cover({book_id:cdata})
self.progress_update.emit(1)
self.progress_finished_cur_step.emit()
elif args.cover_action == 'trim':
self.progress_next_step_range.emit(len(self.ids))
from calibre.utils.img import (
image_from_data, image_to_data, remove_borders_from_image
)
for book_id in self.ids:
cdata = cache.cover(book_id)
if cdata:
img = image_from_data(cdata)
nimg = remove_borders_from_image(img)
if nimg is not img:
cdata = image_to_data(nimg)
cache.set_cover({book_id:cdata})
self.progress_update.emit(1)
self.progress_finished_cur_step.emit()
elif args.cover_action == 'clone':
self.progress_next_step_range.emit(len(self.ids))
cdata = None
for book_id in self.ids:
cdata = cache.cover(book_id)
if cdata:
break
self.progress_update.emit(1)
self.progress_finished_cur_step.emit()
if cdata:
self.progress_next_step_range.emit(0)
cache.set_cover({bid:cdata for bid in self.ids if bid != book_id})
self.progress_finished_cur_step.emit()
if args.restore_original:
self.progress_next_step_range.emit(len(self.ids))
for book_id in self.ids:
formats = cache.formats(book_id)
originals = tuple(x.upper() for x in formats if x.upper().startswith('ORIGINAL_'))
for ofmt in originals:
cache.restore_original_format(book_id, ofmt)
self.progress_update.emit(1)
self.progress_finished_cur_step.emit()
# Various fields
if args.rating != -1:
self.progress_next_step_range.emit(0)
cache.set_field('rating', {bid: args.rating for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.clear_pub:
self.progress_next_step_range.emit(0)
cache.set_field('publisher', {bid: '' for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.pub:
self.progress_next_step_range.emit(0)
cache.set_field('publisher', {bid: args.pub for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.clear_series:
self.progress_next_step_range.emit(0)
cache.set_field('series', {bid: '' for bid in self.ids})
cache.set_field('series_index', {bid:1.0 for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.pubdate is not None:
self.progress_next_step_range.emit(0)
cache.set_field('pubdate', {bid: args.pubdate for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.adddate is not None:
self.progress_next_step_range.emit(0)
cache.set_field('timestamp', {bid: args.adddate for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.do_series:
self.progress_next_step_range.emit(0)
sval = args.series_start_value if args.do_series_restart else cache.get_next_series_num_for(args.series, current_indices=True)
cache.set_field('series', {bid:args.series for bid in self.ids})
self.progress_finished_cur_step.emit()
if not args.series:
self.progress_next_step_range.emit(0)
cache.set_field('series_index', {bid:1.0 for bid in self.ids})
self.progress_finished_cur_step.emit()
else:
def next_series_num(bid, i):
if args.do_series_restart:
return sval + (i * args.series_increment)
next_num = _get_next_series_num_for_list(sorted(itervalues(sval)), unwrap=False)
sval[bid] = next_num
return next_num
smap = {bid:next_series_num(bid, i) for i, bid in enumerate(self.ids)}
if args.do_autonumber:
self.progress_next_step_range.emit(0)
cache.set_field('series_index', smap)
self.progress_finished_cur_step.emit()
elif tweaks['series_index_auto_increment'] != 'no_change':
self.progress_next_step_range.emit(0)
cache.set_field('series_index', {bid:1.0 for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.comments is not null:
self.progress_next_step_range.emit(0)
cache.set_field('comments', {bid: args.comments for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.do_remove_conv:
self.progress_next_step_range.emit(0)
cache.delete_conversion_options(self.ids)
self.progress_finished_cur_step.emit()
if args.clear_languages:
self.progress_next_step_range.emit(0)
cache.set_field('languages', {bid: () for bid in self.ids})
self.progress_finished_cur_step.emit()
elif args.languages:
self.progress_next_step_range.emit(0)
cache.set_field('languages', {bid: args.languages for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.remove_all:
self.progress_next_step_range.emit(0)
cache.set_field('tags', {bid: () for bid in self.ids})
self.progress_finished_cur_step.emit()
if args.add or args.remove:
self.progress_next_step_range.emit(0)
self.db.bulk_modify_tags(self.ids, add=args.add, remove=args.remove)
self.progress_finished_cur_step.emit()
if args.do_compress_cover:
self.progress_next_step_range.emit(len(self.ids))
def pc(book_id, old_sz, new_sz):
if isinstance(new_sz, int):
self.cover_sizes['old'] += old_sz
self.cover_sizes['new'] += new_sz
self.progress_update.emit(1)
self.db.new_api.compress_covers(self.ids, args.compress_cover_quality, pc)
self.progress_finished_cur_step.emit()
if self.do_sr:
self.progress_next_step_range.emit(len(self.ids))
for book_id in self.ids:
ans = self.s_r_func(book_id)
if isinstance(ans, bool) and not ans:
break
self.progress_update.emit(1)
if self.sr_calls:
self.progress_next_step_range.emit(len(self.sr_calls))
self.progress_update.emit(0)
for field, book_id_val_map in iteritems(self.sr_calls):
self.refresh_books.update(self.db.new_api.set_field(field, book_id_val_map))
self.progress_update.emit(1)
self.progress_finished_cur_step.emit()
self.progress_finished_cur_step.emit()
# }}}
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
s_r_functions = {'' : lambda x: x,
_('Lower Case') : lambda x: icu_lower(x),
_('Upper Case') : lambda x: icu_upper(x),
_('Title Case') : lambda x: titlecase(x),
_('Capitalize') : lambda x: capitalize(x),
}
s_r_match_modes = [_('Character match'),
_('Regular expression'),
]
s_r_replace_modes = [_('Replace field'),
_('Prepend to field'),
_('Append to field'),
]
def __init__(self, window, rows, model, tab, refresh_books):
QDialog.__init__(self, window)
self.setupUi(self)
self.model = model
self.db = model.db
self.refresh_book_list.setChecked(gprefs['refresh_book_list_on_bulk_edit'])
self.refresh_book_list.toggled.connect(self.save_refresh_booklist)
self.ids = [self.db.id(r) for r in rows]
self.first_title = self.db.title(self.ids[0], index_is_id=True)
self.cover_clone.setToolTip(str(self.cover_clone.toolTip()) + ' (%s)' % self.first_title)
self.setWindowTitle(ngettext(
'Editing metadata for one book',
'Editing metadata for {} books', len(rows)).format(len(rows)))
self.write_series = False
self.changed = False
self.refresh_books = refresh_books
self.comments = null
self.comments_button.clicked.connect(self.set_comments)
all_tags = self.db.new_api.all_field_names('tags')
self.tags.update_items_cache(all_tags)
self.tags.set_elide_mode(Qt.TextElideMode.ElideMiddle)
self.remove_tags.update_items_cache(all_tags)
self.remove_tags.set_elide_mode(Qt.TextElideMode.ElideMiddle)
self.initialize_combos()
self.series.currentIndexChanged[int].connect(self.series_changed)
connect_lambda(self.rating.currentIndexChanged, self, lambda self:self.apply_rating.setChecked(True))
self.series.editTextChanged.connect(self.series_changed)
self.tag_editor_button.clicked.connect(self.tag_editor)
self.autonumber_series.stateChanged[int].connect(self.auto_number_changed)
pubdate_format = tweaks['gui_pubdate_display_format']
if pubdate_format == 'iso':
pubdate_format = internal_iso_format_string()
if pubdate_format is not None:
self.pubdate.setDisplayFormat(pubdate_format)
self.pubdate.setSpecialValueText(_('Undefined'))
self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
self.pubdate.dateTimeChanged.connect(self.do_apply_pubdate)
self.adddate.setDateTime(QDateTime.currentDateTime())
adddate_format = tweaks['gui_timestamp_display_format']
if adddate_format == 'iso':
adddate_format = internal_iso_format_string()
if adddate_format is not None:
self.adddate.setDisplayFormat(adddate_format)
self.adddate.setSpecialValueText(_('Undefined'))
self.clear_adddate_button.clicked.connect(self.clear_adddate)
self.adddate.dateTimeChanged.connect(self.do_apply_adddate)
self.casing_algorithm.addItems([
_('Title case'), _('Capitalize'), _('Upper case'), _('Lower case'), _('Swap case')
])
self.casing_map = ['title_case', 'capitalize', 'upper_case', 'lower_case', 'swap_case']
prevca = gprefs.get('bulk-mde-casing-algorithm', 'title_case')
idx = max(0, self.casing_map.index(prevca))
self.casing_algorithm.setCurrentIndex(idx)
self.casing_algorithm.setEnabled(False)
connect_lambda(
self.change_title_to_title_case.toggled, self,
lambda self: self.casing_algorithm.setEnabled(self.change_title_to_title_case.isChecked()))
if len(self.db.custom_field_keys(include_composites=False)) == 0:
self.central_widget.removeTab(1)
else:
self.create_custom_column_editors()
self.prepare_search_and_replace()
self.button_box.clicked.connect(self.button_clicked)
self.button_box.button(QDialogButtonBox.StandardButton.Apply).setToolTip(_(
'Immediately make all changes without closing the dialog. '
'This operation cannot be canceled or undone'))
self.do_again = False
self.central_widget.setCurrentIndex(tab)
geom = gprefs.get('bulk_metadata_window_geometry', None)
if geom is not None:
QApplication.instance().safe_restore_geometry(self, bytes(geom))
else:
self.resize(self.sizeHint())
ct = gprefs.get('bulk_metadata_window_tab', 0)
self.central_widget.setCurrentIndex(ct)
self.languages.init_langs(self.db)
self.languages.setEditText('')
self.authors.setFocus(Qt.FocusReason.OtherFocusReason)
self.generate_cover_settings = None
self.button_config_cover_gen.clicked.connect(self.customize_cover_generation)
self.exec()
def sizeHint(self):
geom = self.screen().availableSize()
nh, nw = max(300, geom.height()-50), max(400, geom.width()-70)
return QSize(nw, nh)
def customize_cover_generation(self):
from calibre.gui2.covers import CoverSettingsDialog
d = CoverSettingsDialog(parent=self)
if d.exec() == QDialog.DialogCode.Accepted:
self.generate_cover_settings = d.prefs_for_rendering
def set_comments(self):
from calibre.gui2.dialogs.comments_dialog import CommentsDialog
d = CommentsDialog(self, '' if self.comments is null else (self.comments or ''), _('Comments'))
if d.exec() == QDialog.DialogCode.Accepted:
self.comments = d.textbox.html
b = self.comments_button
b.setStyleSheet('QPushButton { font-weight: bold }')
if str(b.text())[-1] != '*':
b.setText(str(b.text()) + ' *')
def save_refresh_booklist(self, *args):
gprefs['refresh_book_list_on_bulk_edit'] = bool(self.refresh_book_list.isChecked())
def save_state(self, *args):
gprefs['bulk_metadata_window_geometry'] = \
bytearray(self.saveGeometry())
gprefs['bulk_metadata_window_tab'] = self.central_widget.currentIndex()
def do_apply_pubdate(self, *args):
self.apply_pubdate.setChecked(True)
def clear_pubdate(self, *args):
self.pubdate.setDateTime(UNDEFINED_QDATETIME)
def do_apply_adddate(self, *args):
self.apply_adddate.setChecked(True)
def clear_adddate(self, *args):
self.adddate.setDateTime(UNDEFINED_QDATETIME)
def button_clicked(self, which):
if which == self.button_box.button(QDialogButtonBox.StandardButton.Apply):
self.do_again = True
self.accept()
# S&R {{{
def prepare_search_and_replace(self):
self.search_for.initialize('bulk_edit_search_for')
self.replace_with.initialize('bulk_edit_replace_with')
self.s_r_template.setLineEdit(TemplateLineEditor(self.s_r_template))
self.s_r_template.initialize('bulk_edit_template')
self.test_text.initialize('bulk_edit_test_test')
self.all_fields = ['']
self.writable_fields = ['']
fm = self.db.field_metadata
for f in fm:
if (f in ['author_sort'] or
(fm[f]['datatype'] in ['text', 'series', 'enumeration', 'comments', 'rating'] and
fm[f].get('search_terms', None) and
f not in ['formats', 'ondevice', 'series_sort']) or
(fm[f]['datatype'] in ['int', 'float', 'bool', 'datetime'] and
f not in ['id', 'timestamp'])):
self.all_fields.append(f)
self.writable_fields.append(f)
if fm[f]['datatype'] == 'composite':
self.all_fields.append(f)
self.all_fields.sort()
self.all_fields.insert(1, '{template}')
self.writable_fields.sort()
self.search_field.setMaxVisibleItems(25)
self.destination_field.setMaxVisibleItems(25)
self.testgrid.setColumnStretch(1, 1)
self.testgrid.setColumnStretch(2, 1)
offset = 10
self.s_r_number_of_books = min(10, len(self.ids))
for i in range(1,self.s_r_number_of_books+1):
w = QLabel(self.tabWidgetPage3)
w.setText(_('Book %d:')%i)
self.testgrid.addWidget(w, i+offset, 0, 1, 1)
w = QLineEdit(self.tabWidgetPage3)
w.setReadOnly(True)
name = 'book_%d_text'%i
setattr(self, name, w)
self.book_1_text.setObjectName(name)
self.testgrid.addWidget(w, i+offset, 1, 1, 1)
w = QLineEdit(self.tabWidgetPage3)
w.setReadOnly(True)
name = 'book_%d_result'%i
setattr(self, name, w)
self.book_1_text.setObjectName(name)
self.testgrid.addWidget(w, i+offset, 2, 1, 1)
ident_types = sorted(self.db.get_all_identifier_types(), key=sort_key)
self.s_r_dst_ident.setCompleter(QCompleter(ident_types))
try:
self.s_r_dst_ident.setPlaceholderText(_('Enter an identifier type'))
except:
pass
self.s_r_src_ident.addItems(ident_types)
self.main_heading = _(
'<b>You can destroy your library using this feature.</b> '
'Changes are permanent. There is no undo function. '
'You are strongly encouraged to back up your library '
'before proceeding.<p>'
'Search and replace in text fields using character matching '
'or regular expressions. ')
self.character_heading = _(
'In character mode, the field is searched for the entered '
'search text. The text is replaced by the specified replacement '
'text everywhere it is found in the specified field. After '
'replacement is finished, the text can be changed to '
'upper-case, lower-case, or title-case. If the Case-sensitive '
'check box is checked, the search text must match exactly. If '
'it is unchecked, the search text will match both upper- and '
'lower-case letters'
)
self.regexp_heading = _(
'In regular expression mode, the search text is an '
'arbitrary Python-compatible regular expression. The '
'replacement text can contain backreferences to parenthesized '
'expressions in the pattern. The search is not anchored, '
'and can match and replace multiple times on the same string. '
'The modification functions (lower-case etc) are applied to the '
'matched text, not to the field as a whole. '
'The destination box specifies the field where the result after '
'matching and replacement is to be assigned. You can replace '
'the text in the field, or prepend or append the matched text. '
'See <a href="https://docs.python.org/library/re.html">'
'this reference</a> for more information on Python\'s regular '
'expressions, and in particular the \'sub\' function.'
)
self.search_mode.addItems(self.s_r_match_modes)
self.search_mode.setCurrentIndex(dynamic.get('s_r_search_mode', 0))
self.replace_mode.addItems(self.s_r_replace_modes)
self.replace_mode.setCurrentIndex(0)
self.s_r_search_mode = 0
self.s_r_error = None
self.s_r_obj = None
self.replace_func.addItems(sorted(self.s_r_functions.keys()))
self.search_mode.currentIndexChanged[int].connect(self.s_r_search_mode_changed)
self.search_field.currentIndexChanged[int].connect(self.s_r_search_field_changed)
self.destination_field.currentIndexChanged[int].connect(self.s_r_destination_field_changed)
self.replace_mode.currentIndexChanged[int].connect(self.s_r_paint_results)
self.replace_func.currentIndexChanged[native_string_type].connect(self.s_r_paint_results)
self.search_for.editTextChanged[native_string_type].connect(self.s_r_paint_results)
self.replace_with.editTextChanged[native_string_type].connect(self.s_r_paint_results)
self.test_text.editTextChanged[native_string_type].connect(self.s_r_paint_results)
self.comma_separated.stateChanged.connect(self.s_r_paint_results)
self.case_sensitive.stateChanged.connect(self.s_r_paint_results)
self.s_r_src_ident.currentIndexChanged[int].connect(self.s_r_identifier_type_changed)
self.s_r_dst_ident.textChanged.connect(self.s_r_paint_results)
self.s_r_template.lost_focus.connect(self.s_r_template_changed)
self.central_widget.setCurrentIndex(0)
self.search_for.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitivity.CaseSensitive)
self.s_r_search_mode_changed(self.search_mode.currentIndex())
self.multiple_separator.setFixedWidth(30)
self.multiple_separator.setText(' ::: ')
self.multiple_separator.textChanged.connect(self.s_r_separator_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.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.saved_search_name = ''
self.query_field.addItem("")
self.query_field_values = sorted(self.queries, key=sort_key)
self.query_field.addItems(self.query_field_values)
self.query_field.currentIndexChanged[native_string_type].connect(self.s_r_query_change)
self.query_field.setCurrentIndex(0)
self.search_field.setCurrentIndex(0)
self.s_r_search_field_changed(0)
def s_r_sf_itemdata(self, idx):
if idx is None:
idx = self.search_field.currentIndex()
return str(self.search_field.itemData(idx) or '')
def s_r_df_itemdata(self, idx):
if idx is None:
idx = self.destination_field.currentIndex()
return str(self.destination_field.itemData(idx) or '')
def s_r_get_field(self, mi, field):
if field:
if field == '{template}':
v = SafeFormat().safe_format(
str(self.s_r_template.text()), mi, _('S/R TEMPLATE ERROR'), mi)
return [v]
fm = self.db.metadata_for_field(field)
if field == 'sort':
val = mi.get('title_sort', None)
elif fm['datatype'] == 'datetime':
val = mi.format_field(field)[1]
else:
val = mi.get(field, None)
if isinstance(val, (numbers.Number, bool)):
val = str(val)
elif fm['is_csp']:
# convert the csp dict into a list
id_type = str(self.s_r_src_ident.currentText())
if id_type:
val = [val.get(id_type, '')]
else:
val = ['%s:%s'%(t[0], t[1]) for t in iteritems(val)]
if val is None:
val = [] if fm['is_multiple'] else ['']
elif not fm['is_multiple']:
val = [val]
elif fm['datatype'] == 'composite':
val = [v2.strip() for v2 in val.split(fm['is_multiple']['ui_to_list'])]
elif field == 'authors':
val = [v2.replace('|', ',') for v2 in val]
else:
val = []
if not val:
val = ['']
return val
def s_r_display_bounds_changed(self, i):
self.s_r_search_field_changed(self.search_field.currentIndex())
def s_r_template_changed(self):
self.s_r_search_field_changed(self.search_field.currentIndex())
def s_r_identifier_type_changed(self, idx):
self.s_r_search_field_changed(self.search_field.currentIndex())
self.s_r_paint_results(idx)
def s_r_search_field_changed(self, idx):
self.s_r_template.setVisible(False)
self.template_label.setVisible(False)
self.s_r_src_ident_label.setVisible(False)
self.s_r_src_ident.setVisible(False)
if idx == 1: # Template
self.s_r_template.setVisible(True)
self.template_label.setVisible(True)
elif self.s_r_sf_itemdata(idx) == 'identifiers':
self.s_r_src_ident_label.setVisible(True)
self.s_r_src_ident.setVisible(True)
for i in range(0, self.s_r_number_of_books):
w = getattr(self, 'book_%d_text'%(i+1))
mi = self.db.get_metadata(self.ids[i], index_is_id=True)
src = self.s_r_sf_itemdata(idx)
t = self.s_r_get_field(mi, src)
if len(t) > 1:
t = t[self.starting_from.value()-1:
self.starting_from.value()-1 + self.results_count.value()]
w.setText(str(self.multiple_separator.text()).join(t))
if self.search_mode.currentIndex() == 0:
self.destination_field.setCurrentIndex(idx)
else:
self.s_r_destination_field_changed(self.destination_field.currentIndex())
self.s_r_paint_results(None)
def s_r_destination_field_changed(self, idx):
self.s_r_dst_ident_label.setVisible(False)
self.s_r_dst_ident.setVisible(False)
txt = self.s_r_df_itemdata(idx)
if not txt:
txt = self.s_r_sf_itemdata(None)
if txt and txt in self.writable_fields:
if txt == 'identifiers':
self.s_r_dst_ident_label.setVisible(True)
self.s_r_dst_ident.setVisible(True)
self.destination_field_fm = self.db.metadata_for_field(txt)
self.s_r_paint_results(None)
def s_r_search_mode_changed(self, val):
self.search_field.clear()
self.destination_field.clear()
if val == 0:
for f in self.writable_fields:
self.search_field.addItem(f if f != 'sort' else 'title_sort', f)
self.destination_field.addItem(f if f != 'sort' else 'title_sort', f)
self.destination_field.setCurrentIndex(0)
self.destination_field.setVisible(False)
self.destination_field_label.setVisible(False)
self.replace_mode.setCurrentIndex(0)
self.replace_mode.setVisible(False)
self.replace_mode_label.setVisible(False)
self.comma_separated.setVisible(False)
self.s_r_heading.setText('<p>'+self.main_heading + self.character_heading)
else:
self.search_field.blockSignals(True)
self.destination_field.blockSignals(True)
for f in self.all_fields:
self.search_field.addItem(f if f != 'sort' else 'title_sort', f)
for f in self.writable_fields:
self.destination_field.addItem(f if f != 'sort' else 'title_sort', f)
self.search_field.blockSignals(False)
self.destination_field.blockSignals(False)
self.destination_field.setVisible(True)
self.destination_field_label.setVisible(True)
self.replace_mode.setVisible(True)
self.replace_mode_label.setVisible(True)
self.comma_separated.setVisible(True)
self.s_r_heading.setText('<p>'+self.main_heading + self.regexp_heading)
self.s_r_paint_results(None)
def s_r_separator_changed(self, txt):
self.s_r_search_field_changed(self.search_field.currentIndex())
def s_r_set_colors(self):
if self.s_r_error is not None:
self.test_result.setText(error_message(self.s_r_error))
self.test_result.setStyleSheet(
QApplication.instance().stylesheet_for_line_edit(self.s_r_error is not None))
for i in range(0,self.s_r_number_of_books):
getattr(self, 'book_%d_result'%(i+1)).setText('')
def s_r_func(self, match):
rfunc = self.s_r_functions[str(self.replace_func.currentText())]
rtext = str(self.replace_with.text())
rtext = match.expand(rtext)
return rfunc(rtext)
def s_r_do_regexp(self, mi):
src_field = self.s_r_sf_itemdata(None)
src = self.s_r_get_field(mi, src_field)
result = []
rfunc = self.s_r_functions[str(self.replace_func.currentText())]
for s in src:
t = self.s_r_obj.sub(self.s_r_func, s)
if self.search_mode.currentIndex() == 0:
t = rfunc(t)
result.append(t)
return result
def s_r_do_destination(self, mi, val):
src = self.s_r_sf_itemdata(None)
if src == '':
return ''
dest = self.s_r_df_itemdata(None)
if dest == '':
if (src == '{template}' or
self.db.metadata_for_field(src)['datatype'] == 'composite'):
raise Exception(_('You must specify a destination when source is '
'a composite field or a template'))
dest = src
if self.destination_field_fm['datatype'] == 'rating' and val[0]:
ok = True
try:
v = int(val[0])
if v < 0 or v > 10:
ok = False
except:
ok = False
if not ok:
raise Exception(_('The replacement value for a rating column must '
'be empty or an integer between 0 and 10'))
dest_mode = self.replace_mode.currentIndex()
if self.destination_field_fm['is_csp']:
dest_ident = str(self.s_r_dst_ident.text())
if not dest_ident or (src == 'identifiers' and dest_ident == '*'):
raise Exception(_('You must specify a destination identifier type'))
if self.destination_field_fm['is_multiple']:
if self.comma_separated.isChecked():
splitter = self.destination_field_fm['is_multiple']['ui_to_list']
res = []
for v in val:
res.extend([x.strip() for x in v.split(splitter) if x.strip()])
val = res
else:
val = [v.replace(',', '') for v in val]
if dest_mode != 0:
dest_val = mi.get(dest, '')
if self.db.metadata_for_field(dest)['is_csp']:
dst_id_type = str(self.s_r_dst_ident.text())
if dst_id_type:
dest_val = [dest_val.get(dst_id_type, '')]
else:
# convert the csp dict into a list
dest_val = ['%s:%s'%(t[0], t[1]) for t in iteritems(dest_val)]
if dest_val is None:
dest_val = []
elif not isinstance(dest_val, list):
dest_val = [dest_val]
else:
dest_val = []
if dest_mode == 1:
val.extend(dest_val)
elif dest_mode == 2:
val[0:0] = dest_val
return val
def s_r_replace_mode_separator(self):
if self.comma_separated.isChecked():
return ','
return ''
def s_r_paint_results(self, txt):
self.s_r_error = None
self.s_r_set_colors()
flags = regex.FULLCASE | regex.UNICODE
if not self.case_sensitive.isChecked():
flags |= regex.IGNORECASE
try:
stext = str(self.search_for.text())
if not stext:
raise Exception(_('You must specify a search expression in the "Search for" field'))
if self.search_mode.currentIndex() == 0:
self.s_r_obj = regex.compile(regex.escape(stext), flags | regex.V1)
else:
try:
self.s_r_obj = regex.compile(stext, flags | regex.V1)
except regex.error:
self.s_r_obj = regex.compile(stext, flags)
except Exception as e:
self.s_r_obj = None
self.s_r_error = e
self.s_r_set_colors()
return
try:
test_result = self.s_r_obj.sub(self.s_r_func, self.test_text.text())
if self.search_mode.currentIndex() == 0:
rfunc = self.s_r_functions[self.replace_func.currentText()]
test_result = rfunc(test_result)
self.test_result.setText(test_result)
except Exception as e:
self.s_r_error = e
self.s_r_set_colors()
return
for i in range(0,self.s_r_number_of_books):
mi = self.db.get_metadata(self.ids[i], index_is_id=True)
wr = getattr(self, 'book_%d_result'%(i+1))
try:
result = self.s_r_do_regexp(mi)
t = self.s_r_do_destination(mi, result)
if len(t) > 1 and self.destination_field_fm['is_multiple']:
t = t[self.starting_from.value()-1:
self.starting_from.value()-1 + self.results_count.value()]
t = str(self.multiple_separator.text()).join(t)
else:
t = self.s_r_replace_mode_separator().join(t)
wr.setText(t)
except Exception as e:
self.s_r_error = e
self.s_r_set_colors()
break
def do_search_replace(self, book_id):
source = self.s_r_sf_itemdata(None)
if not source or not self.s_r_obj:
return
dest = self.s_r_df_itemdata(None)
if not dest:
dest = source
dfm = self.db.field_metadata[dest]
mi = self.db.new_api.get_proxy_metadata(book_id)
val = self.s_r_do_regexp(mi)
val = self.s_r_do_destination(mi, val)
if dfm['is_multiple']:
if dfm['is_csp']:
# convert the colon-separated pair strings back into a dict,
# which is what set_identifiers wants
dst_id_type = str(self.s_r_dst_ident.text())
if dst_id_type and dst_id_type != '*':
v = ''.join(val)
ids = mi.get(dest)
ids[dst_id_type] = v
val = ids
else:
try:
val = dict([(t.split(':', maxsplit=1)) for t in val])
except:
import traceback
ans = question_dialog(self, _('Invalid identifier string'),
_('The identifier string for book "{0}" (id {1}) is '
'invalid. It must be a comma-separated list of '
'pairs of strings separated by a colon.\n\n'
'Do you want to continue processing books?').format(mi.title, mi.id),
det_msg='\n'.join([_('Result identifier string: '),
', '.join(val), '-----', traceback.format_exc()]),
show_copy_button=True)
return ans
else:
val = self.s_r_replace_mode_separator().join(val)
if dest == 'title' and len(val) == 0:
val = _('Unknown')
if not val and dfm['datatype'] == 'datetime':
val = None
if dfm['datatype'] == 'rating':
if (not val or int(val) == 0):
val = None
if dest == 'rating' and val:
val = (int(val) // 2) * 2
self.set_field_calls[dest][book_id] = val
# }}}
def create_custom_column_editors(self):
w = self.tab
layout = QGridLayout()
self.custom_column_widgets, self.__cc_spacers = \
populate_metadata_page(layout, self.db, self.ids, parent=w,
two_column=False, bulk=True)
w.setLayout(layout)
self.__custom_col_layouts = [layout]
ans = self.custom_column_widgets
for i in range(len(ans)-1):
w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
for c in range(2, len(ans[i].widgets), 2):
w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
def initialize_combos(self):
self.initalize_authors()
self.initialize_series()
self.initialize_publisher()
for x in ('authors', 'publisher', 'series'):
x = getattr(self, x)
x.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
x.setMinimumContentsLength(25)
def initalize_authors(self):
all_authors = self.db.all_authors()
all_authors.sort(key=lambda x : sort_key(x[1]))
self.authors.set_separator('&')
self.authors.set_space_before_sep(True)
self.authors.set_add_separator(tweaks['authors_completer_append_separator'])
self.authors.update_items_cache(self.db.new_api.all_field_names('authors'))
self.authors.show_initial_value('')
def initialize_series(self):
self.series.set_separator(None)
self.series.update_items_cache(self.db.new_api.all_field_names('series'))
self.series.show_initial_value('')
self.publisher.set_add_separator(False)
def initialize_publisher(self):
self.publisher.update_items_cache(self.db.new_api.all_field_names('publisher'))
self.publisher.set_add_separator(False)
self.publisher.show_initial_value('')
def tag_editor(self, *args):
d = TagEditor(self, self.db, None)
d.exec()
if d.result() == QDialog.DialogCode.Accepted:
tag_string = ', '.join(d.tags)
self.tags.setText(tag_string)
all_tags = self.db.new_api.all_field_names('tags')
self.tags.update_items_cache(all_tags)
self.remove_tags.update_items_cache(all_tags)
def auto_number_changed(self, state):
self.series_start_number.setEnabled(bool(state))
self.series_increment.setEnabled(bool(state))
if state:
self.series_numbering_restarts.setEnabled(True)
else:
self.series_numbering_restarts.setEnabled(False)
self.series_numbering_restarts.setChecked(False)
self.series_start_number.setValue(1.0)
self.series_increment.setValue(1.0)
def reject(self):
self.save_state()
QDialog.reject(self)
def accept(self):
self.save_state()
if len(self.ids) < 1:
return QDialog.accept(self)
try:
source = self.s_r_sf_itemdata(None)
except:
source = ''
do_sr = source and self.s_r_obj
if self.s_r_error is not None and do_sr:
error_dialog(self, _('Search/replace invalid'),
_('Search/replace is invalid: %s')%error_message(self.s_r_error),
show=True)
return False
self.changed = bool(self.ids)
# Cache values from GUI so that Qt widgets are not used in
# non GUI thread
for w in getattr(self, 'custom_column_widgets', []):
w.gui_val
remove_all = self.remove_all_tags.isChecked()
remove = []
if not remove_all:
remove = str(self.remove_tags.text()).strip().split(',')
add = str(self.tags.text()).strip().split(',')
au = str(self.authors.text())
aus = str(self.author_sort.text())
do_aus = self.author_sort.isEnabled()
rating = self.rating.rating_value
if not self.apply_rating.isChecked():
rating = -1
pub = str(self.publisher.text())
do_series = self.write_series
clear_series = self.clear_series.isChecked()
clear_pub = self.clear_pub.isChecked()
series = str(self.series.currentText()).strip()
do_autonumber = self.autonumber_series.isChecked()
do_series_restart = self.series_numbering_restarts.isChecked()
series_start_value = self.series_start_number.value()
series_increment = self.series_increment.value()
do_swap_ta = self.swap_title_and_author.isChecked()
do_remove_conv = self.remove_conversion_settings.isChecked()
do_auto_author = self.auto_author_sort.isChecked()
do_title_case = self.change_title_to_title_case.isChecked()
do_title_sort = self.update_title_sort.isChecked()
do_compress_cover = self.compress_cover_images.isChecked()
compress_cover_quality = self.compress_quality.value()
read_file_metadata = self.read_file_metadata.isChecked()
clear_languages = self.clear_languages.isChecked()
restore_original = self.restore_original.isChecked()
languages = self.languages.lang_codes
pubdate = adddate = None
if self.apply_pubdate.isChecked():
pubdate = qt_to_dt(self.pubdate.dateTime())
if self.apply_adddate.isChecked():
adddate = qt_to_dt(self.adddate.dateTime())
cover_action = None
if self.cover_remove.isChecked():
cover_action = 'remove'
elif self.cover_generate.isChecked():
cover_action = 'generate'
elif self.cover_from_fmt.isChecked():
cover_action = 'fromfmt'
elif self.cover_trim.isChecked():
cover_action = 'trim'
elif self.cover_clone.isChecked():
cover_action = 'clone'
args = Settings(
remove_all, remove, add, au, aus, do_aus, rating, pub, do_series,
do_autonumber, do_swap_ta, do_remove_conv, do_auto_author, series,
do_series_restart, series_start_value, series_increment,
do_title_case, cover_action, clear_series, clear_pub, pubdate,
adddate, do_title_sort, languages, clear_languages,
restore_original, self.comments, self.generate_cover_settings,
read_file_metadata, self.casing_map[self.casing_algorithm.currentIndex()], do_compress_cover, compress_cover_quality)
if DEBUG:
print('Running bulk metadata operation with settings:')
print(args)
self.set_field_calls = defaultdict(dict)
bb = MyBlockingBusy(args, self.ids, self.db, self.refresh_books,
getattr(self, 'custom_column_widgets', []),
self.do_search_replace, do_sr, self.set_field_calls, parent=self)
# The metadata backup thread causes database commits
# which can slow down bulk editing of large numbers of books
self.model.stop_metadata_backup()
try:
bb.exec()
finally:
self.model.start_metadata_backup()
bb.thread = bb.db = bb.cc_widgets = None
if bb.error is not None:
return error_dialog(self, _('Failed'),
bb.error[0], det_msg=bb.error[1],
show=True)
dynamic['s_r_search_mode'] = self.search_mode.currentIndex()
gprefs.set('bulk-mde-casing-algorithm', args.casing_algorithm)
self.db.clean()
if args.do_compress_cover:
total_old, total_new = bb.cover_sizes['old'], bb.cover_sizes['new']
percent = (total_old - total_new) / total_old
info_dialog(self, _('Covers compressed'), _(
'Covers were compressed by {percent:.1%} from a total size of'
' {old} to {new}.').format(
percent=percent, old=human_readable(total_old), new=human_readable(total_new))
).exec()
return QDialog.accept(self)
def series_changed(self, *args):
self.write_series = bool(str(self.series.currentText()).strip())
self.autonumber_series.setEnabled(True)
def s_r_remove_query(self, *args):
if self.query_field.currentIndex() == 0:
return
if not question_dialog(self, _("Delete saved search/replace"),
_("The selected saved search/replace will be deleted. "
"Are you sure?")):
return
item_id = self.query_field.currentIndex()
item_name = str(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 list(self.queries.keys()):
del(self.queries[item_name])
self.queries.commit()
def s_r_save_query(self, *args):
names = ['']
names.extend(self.query_field_values)
try:
dex = names.index(self.saved_search_name)
except:
dex = 0
name = ''
while not name:
name, ok = QInputDialog.getItem(self, _('Save search/replace'),
_('Search/replace name:'), names, dex, True)
if not ok:
return
if not name:
error_dialog(self, _("Save search/replace"),
_("You must provide a name."), show=True)
new = True
name = str(name)
if name in list(self.queries.keys()):
if not question_dialog(self, _("Save search/replace"),
_("That saved search/replace already exists and will be overwritten. "
"Are you sure?")):
return
new = False
query = {}
query['name'] = name
query['search_field'] = str(self.search_field.currentText())
query['search_mode'] = str(self.search_mode.currentText())
query['s_r_template'] = str(self.s_r_template.text())
query['s_r_src_ident'] = str(self.s_r_src_ident.currentText())
query['search_for'] = str(self.search_for.text())
query['case_sensitive'] = self.case_sensitive.isChecked()
query['replace_with'] = str(self.replace_with.text())
query['replace_func'] = str(self.replace_func.currentText())
query['destination_field'] = str(self.destination_field.currentText())
query['s_r_dst_ident'] = str(self.s_r_dst_ident.text())
query['replace_mode'] = str(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'] = str(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_values = sorted(self.queries, key=sort_key)
self.query_field.addItems(self.query_field_values)
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()
self.saved_search_name = ''
return
item = self.queries.get(str(item_name), None)
if item is None:
self.s_r_reset_query_fields()
return
self.saved_search_name = item_name
def set_text(attr, key):
try:
attr.setText(item[key])
except:
pass
def set_checked(attr, key):
try:
attr.setChecked(item[key])
except:
attr.setChecked(False)
def set_value(attr, key):
try:
attr.setValue(int(item[key]))
except:
attr.setValue(0)
def set_index(attr, key):
try:
attr.setCurrentIndex(attr.findText(item[key]))
except:
attr.setCurrentIndex(0)
set_index(self.search_mode, 'search_mode')
set_index(self.search_field, 'search_field')
set_text(self.s_r_template, 's_r_template')
self.s_r_template_changed() # simulate gain/loss of focus
set_index(self.s_r_src_ident, 's_r_src_ident')
set_text(self.s_r_dst_ident, 's_r_dst_ident')
set_text(self.search_for, 'search_for')
set_checked(self.case_sensitive, 'case_sensitive')
set_text(self.replace_with, 'replace_with')
set_index(self.replace_func, 'replace_func')
set_index(self.destination_field, 'destination_field')
set_index(self.replace_mode, 'replace_mode')
set_checked(self.comma_separated, 'comma_separated')
set_value(self.results_count, 'results_count')
set_value(self.starting_from, 'starting_from')
set_text(self.multiple_separator, '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_src_ident.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.s_r_dst_ident.setText('')
self.replace_mode.setCurrentIndex(0)
self.comma_separated.setChecked(True)
self.results_count.setValue(999)
self.starting_from.setValue(1)
self.multiple_separator.setText(" ::: ")