Created
June 17, 2025 01:52
-
-
Save eros18123/1d38190ac5e560065c73311cea755277 to your computer and use it in GitHub Desktop.
Pesquisa no editor (Search in editor) 2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
from aqt import gui_hooks | |
from aqt.editor import Editor | |
from aqt.qt import ( | |
QWidget, | |
QLineEdit, | |
QShortcut, | |
QKeySequence, | |
QHBoxLayout, | |
QPushButton, | |
QFont, | |
QSplitter, | |
QTimer, | |
QLabel, ### NOVO/MODIFICADO ### - Importar QLabel | |
) | |
import re | |
# --- Constante para o Destaque --- | |
HIGHLIGHT_SPAN_START = '<span style="background-color: yellow;">' | |
HIGHLIGHT_SPAN_END = '</span>' | |
# Padrão de regex para encontrar e remover o span de destaque | |
CLEANUP_PATTERN = re.compile(re.escape(HIGHLIGHT_SPAN_START) + r'(.*?)' + re.escape(HIGHLIGHT_SPAN_END), re.DOTALL | re.IGNORECASE) | |
# --- Funções de Lógica --- | |
def _build_accent_insensitive_pattern(text: str) -> str: | |
""" | |
Cria um padrão de regex que trata vogais acentuadas e não acentuadas | |
como equivalentes e escapa outros caracteres para a busca. | |
""" | |
if not text: | |
return "" | |
char_map = { | |
'a': '[aáàâã]', 'e': '[eéê]', 'i': '[ií]', 'o': '[oóôõ]', 'u': '[uúü]', 'c': '[cç]', | |
} | |
pattern_str = "" | |
for char in text: | |
pattern_str += char_map.get(char.lower(), re.escape(char)) | |
return pattern_str | |
def _on_search_after_save(editor, text): | |
""" | |
Callback que executa DEPOIS que o editor salvou o texto. | |
Agora também conta as ocorrências. | |
""" | |
# 1. Limpa qualquer destaque amarelo antigo. | |
for i in range(len(editor.note.fields)): | |
editor.note.fields[i] = CLEANUP_PATTERN.sub(r'\1', editor.note.fields[i]) | |
if not text: | |
editor.loadNote() | |
editor._search_bar.setStyleSheet("height: 35px; background-color: lightblue;") | |
editor._results_label.setText("Resultados: 0") ### NOVO/MODIFICADO ### - Limpa o contador | |
return | |
# 2. Faz a nova busca de forma segura e conta as ocorrências. | |
total_matches = 0 ### NOVO/MODIFICADO ### - Inicializa o contador total | |
pattern_str = _build_accent_insensitive_pattern(text) | |
search_pattern = re.compile(pattern_str, re.IGNORECASE) | |
def highlight_repl(match): | |
return f"{HIGHLIGHT_SPAN_START}{match.group(0)}{HIGHLIGHT_SPAN_END}" | |
for i in range(len(editor.note.fields)): | |
original_field = editor.note.fields[i] | |
parts = re.split(r'(<[^>]+>)', original_field) | |
new_parts = [] | |
for part in parts: | |
if not re.match(r'<[^>]+>', part): | |
# Usa subn() que retorna (novo_texto, numero_de_substituicoes) | |
new_part, count = search_pattern.subn(highlight_repl, part) ### NOVO/MODIFICADO ### | |
if count > 0: | |
total_matches += count ### NOVO/MODIFICADO ### - Adiciona ao contador | |
new_parts.append(new_part) | |
else: | |
new_parts.append(part) | |
editor.note.fields[i] = "".join(new_parts) | |
# 3. Atualiza a cor da barra, o label do contador e recarrega a nota. | |
editor._results_label.setText(f"Resultados: {total_matches}") ### NOVO/MODIFICADO ### | |
if total_matches > 0: | |
editor._search_bar.setStyleSheet("height: 35px; background-color: lightblue;") | |
else: | |
editor._search_bar.setStyleSheet("height: 35px; background-color: pink;") | |
editor.loadNote() | |
def perform_search_and_highlight(editor): | |
""" | |
Função chamada pelo timer para iniciar a busca. | |
""" | |
search_text = editor._search_bar.text() | |
editor.saveNow(lambda: _on_search_after_save(editor, search_text)) | |
def _on_replace_after_save(editor): | |
"""Callback para a função de substituir.""" | |
search_text = editor._search_bar.text() | |
replace_text = editor._replace_box.text() | |
if not search_text: | |
return | |
# Limpa o destaque antes de substituir | |
for i in range(len(editor.note.fields)): | |
editor.note.fields[i] = CLEANUP_PATTERN.sub(r'\1', editor.note.fields[i]) | |
pattern_str = _build_accent_insensitive_pattern(search_text) | |
full_pattern_str = r'\b' + pattern_str + r'\b' | |
replace_pattern = re.compile(full_pattern_str, re.IGNORECASE) | |
for i in range(len(editor.note.fields)): | |
original_field = editor.note.fields[i] | |
parts = re.split(r'(<[^>]+>)', original_field) | |
new_parts = [] | |
for part in parts: | |
if not re.match(r'<[^>]+>', part): | |
new_parts.append(replace_pattern.sub(replace_text, part)) | |
else: | |
new_parts.append(part) | |
editor.note.fields[i] = "".join(new_parts) | |
editor._search_bar.clear() | |
editor._replace_box.clear() | |
editor.loadNote() | |
def replace_word(editor): | |
"""Inicia o processo de salvar-e-substituir.""" | |
editor.saveNow(lambda: _on_replace_after_save(editor)) | |
# --- Função Principal que adiciona a UI --- | |
def add_search_bar_to_editor(editor: Editor): | |
""" | |
Adiciona a barra de pesquisa/substituição na interface do editor. | |
""" | |
if hasattr(editor, "_search_replace_bar"): | |
return | |
editor._search_timer = QTimer(editor.widget) | |
editor._search_timer.setSingleShot(True) | |
editor._search_timer.setInterval(250) | |
editor._search_timer.timeout.connect(lambda e=editor: perform_search_and_highlight(e)) | |
search_widget = QWidget(editor.widget) | |
search_layout = QHBoxLayout(search_widget) | |
search_layout.setContentsMargins(0, 10, 0, 5) | |
search_layout.setSpacing(10) | |
editor._search_bar = QLineEdit() | |
editor._search_bar.setPlaceholderText("Pesquisar (Ctrl+F)") | |
editor._search_bar.textChanged.connect(editor._search_timer.start) | |
search_layout.addWidget(editor._search_bar) | |
### NOVO/MODIFICADO ### - Criação e adição do Label de resultados | |
editor._results_label = QLabel("Resultados: 0") | |
search_layout.addWidget(editor._results_label) | |
editor._replace_box = QLineEdit() | |
editor._replace_box.setPlaceholderText("Substituir por (Ctrl+H)") | |
search_layout.addWidget(editor._replace_box) | |
editor._replace_button = QPushButton("Substituir (Ctrl+J)") | |
editor._replace_button.clicked.connect(lambda _, e=editor: replace_word(e)) | |
search_layout.addWidget(editor._replace_button) | |
font = QFont("Arial", 14) | |
editor._search_bar.setFont(font) | |
editor._search_bar.setStyleSheet("height: 35px; background-color: lightblue;") | |
editor._results_label.setFont(font) ### NOVO/MODIFICADO ### - Aplica a fonte ao label | |
editor._replace_box.setFont(font) | |
editor._replace_box.setStyleSheet("height: 35px; background-color: lightgreen;") | |
editor._replace_button.setFont(font) | |
editor._replace_button.setStyleSheet("height: 35px; background-color: lightgray;") | |
QShortcut(QKeySequence("Ctrl+F"), editor.widget, editor._search_bar.setFocus) | |
QShortcut(QKeySequence("Ctrl+H"), editor.widget, editor._replace_box.setFocus) | |
QShortcut(QKeySequence("Ctrl+J"), editor.widget, editor._replace_button.click) | |
splitter = editor.widget.findChild(QSplitter) | |
if splitter: | |
fields_widget = splitter.widget(0) | |
if fields_widget and fields_widget.layout(): | |
fields_widget.layout().addWidget(search_widget) | |
else: | |
editor.widget.layout().addWidget(search_widget) | |
editor._search_replace_bar = search_widget | |
# *** FUNÇÃO DE LIMPEZA COM A ASSINATURA CORRIGIDA *** | |
def cleanup_html_before_save(html: str, editor: Editor) -> str: | |
""" | |
Esta função é chamada para CADA campo individualmente antes de salvar. | |
Ela limpa o destaque amarelo e retorna o HTML limpo. | |
""" | |
cleaned_html = CLEANUP_PATTERN.sub(r'\1', html) | |
return cleaned_html | |
# --- Hooks do Anki --- | |
gui_hooks.editor_did_load_note.append(add_search_bar_to_editor) | |
gui_hooks.editor_will_munge_html.append(cleanup_html_before_save) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment