From a5193339e7ab35ac9a59063c42626f0da4733fd4 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 13 Jul 2021 16:33:45 +0200 Subject: [PATCH] Rework Find & Replace dialog: - Add option to affect whole collection - Allow to open without selection - Add parameter for presetting field --- ftl/core/browsing.ftl | 1 + qt/aqt/browser/browser.py | 1 - qt/aqt/browser/find_and_replace.py | 69 ++++++++++++++++++++------ qt/aqt/forms/findreplace.ui | 80 +++++++++++++++++------------- rslib/src/backend/search/mod.rs | 7 ++- rslib/src/backend/tags.rs | 7 ++- 6 files changed, 111 insertions(+), 54 deletions(-) diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl index 190ba2739..c2e69950f 100644 --- a/ftl/core/browsing.ftl +++ b/ftl/core/browsing.ftl @@ -92,6 +92,7 @@ browsing-reschedule = Reschedule browsing-search-bar-hint = Search cards/notes (type text, then press Enter) browsing-search-in = Search in: browsing-search-within-formatting-slow = Search within formatting (slow) +browsing-selected-notes-only = Selected notes only browsing-shift-position-of-existing-cards = Shift position of existing cards browsing-sidebar = Sidebar browsing-sidebar-filter = Sidebar filter diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index e5eb6c912..44cc740aa 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -812,7 +812,6 @@ class Browser(QMainWindow): ###################################################################### @no_arg_trigger - @skip_if_selection_is_empty @ensure_editor_saved def onFindReplace(self) -> None: FindAndReplaceDialog(self, mw=self.mw, note_ids=self.selected_notes()) diff --git a/qt/aqt/browser/find_and_replace.py b/qt/aqt/browser/find_and_replace.py index 2abb814d4..254c18be1 100644 --- a/qt/aqt/browser/find_and_replace.py +++ b/qt/aqt/browser/find_and_replace.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import List, Sequence +from typing import List, Optional, Sequence import aqt from anki.notes import NoteId @@ -25,6 +25,7 @@ from aqt.utils import ( save_combo_index_for_session, save_is_checked, saveGeom, + tooltip, tr, ) @@ -33,19 +34,34 @@ class FindAndReplaceDialog(QDialog): COMBO_NAME = "BrowserFindAndReplace" def __init__( - self, parent: QWidget, *, mw: AnkiQt, note_ids: Sequence[NoteId] + self, + parent: QWidget, + *, + mw: AnkiQt, + note_ids: Sequence[NoteId], + field: Optional[str] = None, ) -> None: + """ + If 'field' is passed, only this is added to the field selector. + Otherwise, the fields belonging to the 'note_ids' are added. + """ super().__init__(parent) self.mw = mw self.note_ids = note_ids self.field_names: List[str] = [] + self._field = field - # fetch field names and then show - QueryOp( - parent=mw, - op=lambda col: col.field_names_for_note_ids(note_ids), - success=self._show, - ).run_in_background() + if field: + self._show([field]) + elif note_ids: + # fetch field names and then show + QueryOp( + parent=mw, + op=lambda col: col.field_names_for_note_ids(note_ids), + success=self._show, + ).run_in_background() + else: + self._show([]) def _show(self, field_names: Sequence[str]) -> None: # add "all fields" and "tags" to the top of the list @@ -68,13 +84,23 @@ class FindAndReplaceDialog(QDialog): ) self.form.replace.completer().setCaseSensitivity(Qt.CaseSensitive) + if not self.note_ids: + # no selected notes to affect + self.form.selected_notes.setChecked(False) + self.form.selected_notes.setEnabled(False) + elif self._field: + self.form.selected_notes.setChecked(False) + restore_is_checked(self.form.re, self.COMBO_NAME + "Regex") restore_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase") self.form.field.addItems(self.field_names) - restore_combo_index_for_session( - self.form.field, self.field_names, self.COMBO_NAME + "Field" - ) + if self._field: + self.form.field.setCurrentIndex(self.field_names.index(self._field)) + else: + restore_combo_index_for_session( + self.form.field, self.field_names, self.COMBO_NAME + "Field" + ) qconnect(self.form.buttonBox.helpRequested, self.show_help) @@ -97,16 +123,20 @@ class FindAndReplaceDialog(QDialog): save_is_checked(self.form.re, self.COMBO_NAME + "Regex") save_is_checked(self.form.ignoreCase, self.COMBO_NAME + "ignoreCase") + if not self.form.selected_notes.isChecked(): + # an empty list means *all* notes + self.note_ids = [] + # tags? if self.form.field.currentIndex() == 1: - find_and_replace_tag( + op = find_and_replace_tag( parent=self.parentWidget(), note_ids=self.note_ids, search=search, replacement=replace, regex=regex, match_case=match_case, - ).run_in_background() + ) else: # fields if self.form.field.currentIndex() == 0: @@ -114,7 +144,7 @@ class FindAndReplaceDialog(QDialog): else: field = self.field_names[self.form.field.currentIndex()] - find_and_replace( + op = find_and_replace( parent=self.parentWidget(), note_ids=self.note_ids, search=search, @@ -122,7 +152,16 @@ class FindAndReplaceDialog(QDialog): regex=regex, field_name=field, match_case=match_case, - ).run_in_background() + ) + + if not self.note_ids: + op.success( + lambda out: tooltip( + tr.browsing_notes_updated(count=out.count), + parent=self.parentWidget(), + ) + ) + op.run_in_background() super().accept() diff --git a/qt/aqt/forms/findreplace.ui b/qt/aqt/forms/findreplace.ui index c763fc4ac..8d08770ab 100644 --- a/qt/aqt/forms/findreplace.ui +++ b/qt/aqt/forms/findreplace.ui @@ -6,7 +6,7 @@ 0 0 - 367 + 377 224 @@ -16,15 +16,8 @@ - - - - browsing_find - - - - - + + 9 @@ -49,8 +42,46 @@ - - + + + + browsing_in + + + + + + + browsing_find + + + + + + + QComboBox::AdjustToMinimumContentsLength + + + + + + + browsing_treat_input_as_regular_expression + + + + + + + browsing_ignore_case + + + true + + + + + 9 @@ -68,31 +99,10 @@ - - - - browsing_in - - - - - - - QComboBox::AdjustToMinimumContentsLength - - - - - - - browsing_treat_input_as_regular_expression - - - - + - browsing_ignore_case + browsing_selected_notes_only true diff --git a/rslib/src/backend/search/mod.rs b/rslib/src/backend/search/mod.rs index 7201b4cf0..22fc04274 100644 --- a/rslib/src/backend/search/mod.rs +++ b/rslib/src/backend/search/mod.rs @@ -6,7 +6,7 @@ mod search_node; use std::{convert::TryInto, str::FromStr, sync::Arc}; -use super::Backend; +use super::{notes::to_note_ids, Backend}; pub(super) use crate::backend_proto::search_service::Service as SearchService; use crate::{ backend_proto as pb, @@ -74,7 +74,7 @@ impl SearchService for Backend { if !input.match_case { search = format!("(?i){}", search); } - let nids = input.nids.into_iter().map(NoteId).collect(); + let mut nids = to_note_ids(input.nids); let field_name = if input.field_name.is_empty() { None } else { @@ -82,6 +82,9 @@ impl SearchService for Backend { }; let repl = input.replacement; self.with_col(|col| { + if nids.is_empty() { + nids = col.search_notes_unordered("")? + }; col.find_and_replace(nids, &search, &repl, field_name) .map(Into::into) }) diff --git a/rslib/src/backend/tags.rs b/rslib/src/backend/tags.rs index 75201e618..5f91c248d 100644 --- a/rslib/src/backend/tags.rs +++ b/rslib/src/backend/tags.rs @@ -73,8 +73,13 @@ impl TagsService for Backend { input: pb::FindAndReplaceTagRequest, ) -> Result { self.with_col(|col| { + let note_ids = if input.note_ids.is_empty() { + col.search_notes_unordered("")? + } else { + to_note_ids(input.note_ids) + }; col.find_and_replace_tag( - &to_note_ids(input.note_ids), + ¬e_ids, &input.search, &input.replacement, input.regex,