Skip to content

Instantly share code, notes, and snippets.

@eros18123
Created February 21, 2026 20:22
Show Gist options
  • Select an option

  • Save eros18123/ff4dd2a42f91b5e87d659adebff1de9f to your computer and use it in GitHub Desktop.

Select an option

Save eros18123/ff4dd2a42f91b5e87d659adebff1de9f to your computer and use it in GitHub Desktop.
Oclusao de tabela aleatoria
# -*- coding: utf-8 -*-
import os
import json
import time
from aqt.qt import *
from aqt import gui_hooks
from aqt import mw
# Caminho para salvar as configurações
ADDON_DIR = os.path.dirname(__file__)
CONFIG_FILE = os.path.join(ADDON_DIR, "config_shuffle.json")
class ShuffleTableDialog(QDialog):
def __init__(self, editor):
super().__init__(editor.widget)
self.editor = editor
self.setWindowTitle("Oclusão de Tabela Aleatória")
self.resize(700, 500)
self.cells = []
# Carrega a última configuração salva
self.config_data = self.load_config()
# Layouts principais
self.main_layout = QVBoxLayout(self)
self.controls_layout = QHBoxLayout()
self.grid_layout = QGridLayout()
self.buttons_layout = QHBoxLayout()
# Controles de Linhas e Colunas
self.rows_spin = QSpinBox()
self.rows_spin.setRange(1, 100)
self.rows_spin.setValue(self.config_data.get("rows", 5))
self.cols_spin = QSpinBox()
self.cols_spin.setRange(1, 20)
self.cols_spin.setValue(self.config_data.get("cols", 2))
# Checkbox para decidir se embaralha as colunas também
self.shuffle_cols_cb = QCheckBox("Embaralhar itens na linha")
self.shuffle_cols_cb.setChecked(self.config_data.get("shuffle_cols", True))
self.controls_layout.addWidget(QLabel("Linhas:"))
self.controls_layout.addWidget(self.rows_spin)
self.controls_layout.addWidget(QLabel("Colunas:"))
self.controls_layout.addWidget(self.cols_spin)
self.controls_layout.addStretch()
# Instrução para o usuário
instruction = QLabel("<i>Dica: Clique no botão <b>⬜</b> ao lado do texto para aplicar oclusão naquele item.</i>")
self.main_layout.addLayout(self.controls_layout)
self.main_layout.addWidget(instruction)
# Área de rolagem para a grade de inputs
self.grid_widget = QWidget()
self.grid_widget.setLayout(self.grid_layout)
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setWidget(self.grid_widget)
# Modo de oclusão
mode_group = QGroupBox("Modo de Oclusão")
mode_layout = QVBoxLayout(mode_group)
self.mode_interactive = QRadioButton("Interativo — uma tabela, revelar célula por célula (atalho W ou clique)")
self.mode_cards = QRadioButton("Um card por oclusão — gera um card separado para cada célula oculta")
saved_mode = self.config_data.get("occlusion_mode", "interactive")
if saved_mode == "cards":
self.mode_cards.setChecked(True)
else:
self.mode_interactive.setChecked(True)
mode_layout.addWidget(self.mode_interactive)
mode_layout.addWidget(self.mode_cards)
# Botões de ação
self.btn_generate = QPushButton("Inserir Tabela no Card")
self.btn_generate.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; padding: 5px;")
self.buttons_layout.addWidget(self.shuffle_cols_cb)
self.buttons_layout.addStretch()
self.buttons_layout.addWidget(self.btn_generate)
# Adicionando ao layout principal
self.main_layout.addWidget(self.scroll_area)
self.main_layout.addWidget(mode_group)
self.main_layout.addLayout(self.buttons_layout)
# Sinais (Eventos)
self.rows_spin.valueChanged.connect(lambda: self.update_grid(initial_load=False))
self.cols_spin.valueChanged.connect(lambda: self.update_grid(initial_load=False))
self.btn_generate.clicked.connect(self.generate_and_insert)
# Inicializa a grade com os dados salvos
self.update_grid(initial_load=True)
def load_config(self):
default_config = {"rows": 5, "cols": 2, "shuffle_cols": True, "occlusion_mode": "interactive", "data": []}
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
loaded = json.load(f)
default_config.update(loaded)
except Exception:
pass
return default_config
def save_config(self, current_data):
config_to_save = {
"rows": self.rows_spin.value(),
"cols": self.cols_spin.value(),
"shuffle_cols": self.shuffle_cols_cb.isChecked(),
"occlusion_mode": "cards" if self.mode_cards.isChecked() else "interactive",
"data": current_data
}
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config_to_save, f, indent=4)
except Exception as e:
pass
def update_grid(self, initial_load=False):
rows = self.rows_spin.value()
cols = self.cols_spin.value()
old_values = []
if initial_load:
old_values = self.config_data.get("data", [])
else:
for r in range(len(self.cells)):
row_vals = []
for c in range(len(self.cells[r])):
row_vals.append({
"text": self.cells[r][c]["le"].text(),
"occluded": self.cells[r][c]["btn"].isChecked()
})
old_values.append(row_vals)
while self.grid_layout.count():
child = self.grid_layout.takeAt(0)
if child.widget():
child.widget().deleteLater()
self.cells = []
for r in range(rows):
row_widgets = []
for c in range(cols):
cell_widget = QWidget()
cell_layout = QHBoxLayout(cell_widget)
cell_layout.setContentsMargins(0, 0, 0, 0)
cell_layout.setSpacing(2)
le = QLineEdit()
le.setPlaceholderText(f"Linha {r+1}, Col {c+1}")
btn_occ = QPushButton("⬜")
btn_occ.setCheckable(True)
btn_occ.setToolTip("Marcar para Oclusão")
btn_occ.setFixedWidth(35)
btn_occ.setStyleSheet("""
QPushButton { background-color: #ddd; border: 1px solid #aaa; border-radius: 3px; }
QPushButton:checked { background-color: #FFD700; border: 1px solid #B8860B; }
""")
if r < len(old_values) and c < len(old_values[r]):
val = old_values[r][c]
if isinstance(val, dict):
le.setText(val.get("text", ""))
btn_occ.setChecked(val.get("occluded", False))
else:
le.setText(str(val))
btn_occ.setChecked(False)
cell_layout.addWidget(le)
cell_layout.addWidget(btn_occ)
self.grid_layout.addWidget(cell_widget, r, c)
row_widgets.append({"le": le, "btn": btn_occ})
self.cells.append(row_widgets)
def generate_and_insert(self):
# 1. Coletar dados
current_data = []
valid_rows = []
for r in range(self.rows_spin.value()):
row_data = []
is_row_valid = False
for c in range(self.cols_spin.value()):
text = self.cells[r][c]["le"].text().strip()
is_occ = self.cells[r][c]["btn"].isChecked()
row_data.append({"text": text, "occluded": is_occ})
if text: is_row_valid = True
current_data.append(row_data)
if is_row_valid:
valid_rows.append(row_data)
self.save_config(current_data)
if not valid_rows:
self.accept()
return
if self.mode_cards.isChecked():
self._generate_multiple_cards(valid_rows)
else:
self._generate_interactive(valid_rows)
self.accept()
def _build_table_html(self, valid_rows, highlight_occ_index=None, table_id=None, shuffle_cols="false"):
"""
Constrói o HTML da tabela.
highlight_occ_index: se None, mostra todas as oclusões marcadas com fundo.
se inteiro, oculta APENAS a oclusão de índice highlight_occ_index (0-based entre todas as células occluded),
deixando as demais visíveis normalmente.
"""
if table_id is None:
table_id = f"shuffle-table-{int(time.time() * 1000)}"
html = f'<table id="{table_id}" class="anki-shuffle-table" style="border-collapse: collapse; margin: 15px auto; min-width: 50%; max-width: 80%; border: 2px solid #888;"><tbody>'
occ_counter = 0
for row_data in valid_rows:
html += '<tr class="anki-shuffle-row">'
for cell in row_data:
text = cell["text"] if cell["text"] else "&nbsp;"
is_occ = cell["occluded"]
if is_occ:
if highlight_occ_index is None:
# Modo interativo: marca todas com fundo amarelo claro
occ_class = " target-occlusion"
bg_style = "background-color: rgba(255, 215, 0, 0.3);"
else:
# Modo cards: só oculta a célula do índice especificado
if occ_counter == highlight_occ_index:
occ_class = " target-occlusion"
bg_style = "background-color: rgba(255, 215, 0, 0.3);"
else:
occ_class = ""
bg_style = ""
occ_counter += 1
else:
occ_class = ""
bg_style = ""
html += f'<td class="anki-shuffle-cell{occ_class}" style="border: 2px solid #888; padding: 10px; text-align: center; font-size: 1.1em; {bg_style}"><div class="cell-content">{text}</div></td>'
html += '</tr>'
html += '</tbody></table>'
return html, table_id
def _generate_interactive(self, valid_rows):
table_id = f"shuffle-table-{int(time.time() * 1000)}"
shuffle_cols = "true" if self.shuffle_cols_cb.isChecked() else "false"
html = f'<table id="{table_id}" class="anki-shuffle-table" style="border-collapse: collapse; margin: 15px auto; min-width: 50%; max-width: 80%; border: 2px solid #888;"><tbody>'
for row_data in valid_rows:
html += '<tr class="anki-shuffle-row">'
for cell in row_data:
text = cell["text"] if cell["text"] else "&nbsp;"
occ_class = " target-occlusion" if cell["occluded"] else ""
bg_style = "background-color: rgba(255, 215, 0, 0.3);" if cell["occluded"] else ""
html += f'<td class="anki-shuffle-cell{occ_class}" style="border: 2px solid #888; padding: 10px; text-align: center; font-size: 1.1em; {bg_style}"><div class="cell-content">{text}</div></td>'
html += '</tr>'
html += '</tbody></table>'
js_code = f"""
this.style.display='none';
if (document.body.isContentEditable || document.designMode === 'on') return;
var table = document.getElementById('{table_id}');
if (table && !table.dataset.shuffled) {{
table.dataset.shuffled = 'true';
var tbody = table.querySelector('tbody');
var rows = Array.from(tbody.querySelectorAll('.anki-shuffle-row'));
var shouldShuffleCols = {shuffle_cols};
for (var i = rows.length - 1; i > 0; i--) {{
var j = Math.floor(Math.random() * (i + 1));
var temp = rows[i];
rows[i] = rows[j];
rows[j] = temp;
}}
rows.forEach(function(row) {{
var cells = Array.from(row.querySelectorAll('.anki-shuffle-cell'));
if (shouldShuffleCols) {{
for (var i = cells.length - 1; i > 0; i--) {{
var j = Math.floor(Math.random() * (i + 1));
var temp = cells[i];
cells[i] = cells[j];
cells[j] = temp;
}}
cells.forEach(function(cell) {{ row.appendChild(cell); }});
}}
var targets = Array.from(row.querySelectorAll('.target-occlusion'));
if (targets.length > 1) {{
var revealIndex = Math.floor(Math.random() * targets.length);
targets[revealIndex].style.backgroundColor = '';
targets.splice(revealIndex, 1);
}}
targets.forEach(function(cell) {{
cell.dataset.occludable = 'true';
cell.dataset.isOccluded = 'true';
cell.style.backgroundColor = '#FFD700';
cell.style.color = 'transparent';
cell.style.cursor = 'pointer';
var content = cell.querySelector('.cell-content');
if(content) content.style.visibility = 'hidden';
cell.onclick = function() {{
if (this.dataset.isOccluded === 'true') {{
this.dataset.isOccluded = 'false';
this.style.backgroundColor = '';
this.style.color = '';
var c = this.querySelector('.cell-content');
if(c) c.style.visibility = 'visible';
}} else {{
this.dataset.isOccluded = 'true';
this.style.backgroundColor = '#FFD700';
this.style.color = 'transparent';
var c = this.querySelector('.cell-content');
if(c) c.style.visibility = 'hidden';
}}
}};
}});
tbody.appendChild(row);
}});
if (!window.shuffleTableShortcutAdded) {{
window.shuffleTableShortcutAdded = true;
document.addEventListener('keydown', function(e) {{
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
if (e.key.toLowerCase() === 'w') {{
var allTables = document.querySelectorAll('.anki-shuffle-table');
var totalOccluded = 0;
var totalOccludable = 0;
for (var t = 0; t < allTables.length; t++) {{
var allCells = Array.from(allTables[t].querySelectorAll('.anki-shuffle-cell'));
allCells.forEach(function(c) {{
if (c.dataset.occludable === 'true') {{
totalOccludable++;
if (c.dataset.isOccluded === 'true') totalOccluded++;
}}
}});
}}
if (totalOccluded === 0 && totalOccludable > 0) {{
for (var t = 0; t < allTables.length; t++) {{
var allCells = Array.from(allTables[t].querySelectorAll('.anki-shuffle-cell'));
allCells.forEach(function(c) {{
if (c.dataset.occludable === 'true') {{
c.dataset.isOccluded = 'true';
c.style.backgroundColor = '#FFD700';
c.style.color = 'transparent';
var content = c.querySelector('.cell-content');
if(content) content.style.visibility = 'hidden';
}}
}});
}}
return;
}}
var revealedSomething = false;
for (var t = 0; t < allTables.length; t++) {{
var tRows = Array.from(allTables[t].querySelectorAll('.anki-shuffle-row'));
for (var r = 0; r < tRows.length; r++) {{
var cellsInRow = Array.from(tRows[r].querySelectorAll('.anki-shuffle-cell'));
var occludedCells = cellsInRow.filter(function(c) {{ return c.dataset.isOccluded === 'true'; }});
if (occludedCells.length > 0) {{
occludedCells.forEach(function(c) {{
c.dataset.isOccluded = 'false';
c.style.backgroundColor = '';
c.style.color = '';
var content = c.querySelector('.cell-content');
if(content) content.style.visibility = 'visible';
}});
revealedSomething = true;
break;
}}
}}
if (revealedSomething) break;
}}
}}
}});
}}
}}
"""
js_code_escaped = js_code.replace('\n', ' ').replace(' ', ' ').replace('"', '&quot;')
html += f'<img src="x" style="display:none;" onerror="{js_code_escaped}">'
self.editor.web.eval(f"document.execCommand('insertHTML', false, {json.dumps(html)});")
def _generate_multiple_cards(self, valid_rows):
"""
Gera um card separado para cada célula marcada como oclusão.
Cada card mostra a tabela completa, mas com apenas UMA célula oculta.
"""
from aqt.utils import showInfo
# Coleta todas as células com oclusão
occ_cells = []
for r, row_data in enumerate(valid_rows):
for c, cell in enumerate(row_data):
if cell["occluded"]:
occ_cells.append((r, c))
if not occ_cells:
# Nenhuma célula marcada: insere tabela normal sem oclusão
self._generate_interactive(valid_rows)
return
# Obtém o note atual aberto no editor
note = self.editor.note
if note is None:
from aqt.utils import showWarning
showWarning("Nenhum card aberto no editor.")
return
# Descobre o campo atual focado no editor
current_field = self.editor.currentField if self.editor.currentField is not None else 0
shuffle_cols = "true" if self.shuffle_cols_cb.isChecked() else "false"
def build_card_html(occ_idx):
tid = f"shuffle-table-{int(time.time() * 1000)}-{occ_idx}"
html = f'<table id="{tid}" class="anki-shuffle-table" style="border-collapse: collapse; margin: 15px auto; min-width: 50%; max-width: 80%; border: 2px solid #888;"><tbody>'
global_occ_counter = 0
for row_data in valid_rows:
html += '<tr class="anki-shuffle-row">'
for cell in row_data:
text = cell["text"] if cell["text"] else "&nbsp;"
is_this_occ = cell["occluded"]
if is_this_occ and global_occ_counter == occ_idx:
# Esta é a célula a ser ocluída neste card
occ_class = " target-occlusion"
bg_style = "background-color: rgba(255, 215, 0, 0.3);"
else:
occ_class = ""
bg_style = ""
if is_this_occ:
global_occ_counter += 1
html += f'<td class="anki-shuffle-cell{occ_class}" style="border: 2px solid #888; padding: 10px; text-align: center; font-size: 1.1em; {bg_style}"><div class="cell-content">{text}</div></td>'
html += '</tr>'
html += '</tbody></table>'
js_code = f"""
this.style.display='none';
if (document.body.isContentEditable || document.designMode === 'on') return;
var table = document.getElementById('{tid}');
if (table && !table.dataset.shuffled) {{
table.dataset.shuffled = 'true';
var tbody = table.querySelector('tbody');
var rows = Array.from(tbody.querySelectorAll('.anki-shuffle-row'));
var shouldShuffleCols = {shuffle_cols};
for (var i = rows.length - 1; i > 0; i--) {{
var j = Math.floor(Math.random() * (i + 1));
var temp = rows[i]; rows[i] = rows[j]; rows[j] = temp;
}}
rows.forEach(function(row) {{
var cells = Array.from(row.querySelectorAll('.anki-shuffle-cell'));
if (shouldShuffleCols) {{
for (var i = cells.length - 1; i > 0; i--) {{
var j = Math.floor(Math.random() * (i + 1));
var temp = cells[i]; cells[i] = cells[j]; cells[j] = temp;
}}
cells.forEach(function(cell) {{ row.appendChild(cell); }});
}}
var targets = Array.from(row.querySelectorAll('.target-occlusion'));
targets.forEach(function(cell) {{
cell.dataset.occludable = 'true';
cell.dataset.isOccluded = 'true';
cell.style.backgroundColor = '#FFD700';
cell.style.color = 'transparent';
cell.style.cursor = 'pointer';
var content = cell.querySelector('.cell-content');
if(content) content.style.visibility = 'hidden';
cell.onclick = function() {{
if (this.dataset.isOccluded === 'true') {{
this.dataset.isOccluded = 'false';
this.style.backgroundColor = '';
this.style.color = '';
var c = this.querySelector('.cell-content');
if(c) c.style.visibility = 'visible';
}} else {{
this.dataset.isOccluded = 'true';
this.style.backgroundColor = '#FFD700';
this.style.color = 'transparent';
var c = this.querySelector('.cell-content');
if(c) c.style.visibility = 'hidden';
}}
}};
}});
tbody.appendChild(row);
}});
if (!window.shuffleTableShortcutAdded) {{
window.shuffleTableShortcutAdded = true;
document.addEventListener('keydown', function(e) {{
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
if (e.key.toLowerCase() === 'w') {{
var allTables = document.querySelectorAll('.anki-shuffle-table');
var totalOccluded = 0;
var totalOccludable = 0;
for (var t = 0; t < allTables.length; t++) {{
var allCells = Array.from(allTables[t].querySelectorAll('.anki-shuffle-cell'));
allCells.forEach(function(c) {{
if (c.dataset.occludable === 'true') {{
totalOccludable++;
if (c.dataset.isOccluded === 'true') totalOccluded++;
}}
}});
}}
if (totalOccluded === 0 && totalOccludable > 0) {{
for (var t = 0; t < allTables.length; t++) {{
var allCells = Array.from(allTables[t].querySelectorAll('.anki-shuffle-cell'));
allCells.forEach(function(c) {{
if (c.dataset.occludable === 'true') {{
c.dataset.isOccluded = 'true';
c.style.backgroundColor = '#FFD700';
c.style.color = 'transparent';
var content = c.querySelector('.cell-content');
if(content) content.style.visibility = 'hidden';
}}
}});
}}
return;
}}
var revealedSomething = false;
for (var t = 0; t < allTables.length; t++) {{
var tRows = Array.from(allTables[t].querySelectorAll('.anki-shuffle-row'));
for (var r = 0; r < tRows.length; r++) {{
var cellsInRow = Array.from(tRows[r].querySelectorAll('.anki-shuffle-cell'));
var occludedCells = cellsInRow.filter(function(c) {{ return c.dataset.isOccluded === 'true'; }});
if (occludedCells.length > 0) {{
occludedCells.forEach(function(c) {{
c.dataset.isOccluded = 'false';
c.style.backgroundColor = '';
c.style.color = '';
var content = c.querySelector('.cell-content');
if(content) content.style.visibility = 'visible';
}});
revealedSomething = true;
break;
}}
}}
if (revealedSomething) break;
}}
}}
}});
}}
}}
"""
js_escaped = js_code.replace('\n', ' ').replace(' ', ' ').replace('"', '&quot;')
html += f'<img src="x" style="display:none;" onerror="{js_escaped}">'
return html
# Primeiro card: insere no campo atual do card aberto
first_html = build_card_html(0)
self.editor.web.eval(f"document.execCommand('insertHTML', false, {json.dumps(first_html)});")
if len(occ_cells) == 1:
return
# Cards adicionais: duplica o note e muda o campo
col = mw.col
first_note_id = note.id
for occ_idx in range(1, len(occ_cells)):
# Cria uma cópia do note
new_note = col.new_note(note.note_type())
# Copia todos os campos
for i in range(len(note.fields)):
new_note.fields[i] = note.fields[i]
# Copia as tags
new_note.tags = list(note.tags)
# Substitui o campo alvo pelo HTML desta variação
card_html = build_card_html(occ_idx)
# Limpa o campo e coloca o novo HTML
new_note.fields[current_field] = card_html
try:
did = note.cards()[0].did
except Exception:
did = col.decks.get_current_id()
col.add_note(new_note, did)
showInfo(f"{len(occ_cells)} cards criados, um para cada célula oculta.")
def setup_shuffle_table_button(buttons, editor):
btn = editor.addButton(
icon=None,
cmd="shuffleTableButton",
func=lambda ed=editor: ShuffleTableDialog(ed).exec(),
tip="Oclusão de Tabela Aleatória",
label="🔀Tab"
)
buttons.append(btn)
return buttons
gui_hooks.editor_did_init_buttons.append(setup_shuffle_table_button)
@eros18123
Copy link
Author

eros18123 commented Feb 21, 2026

image
image
aperte w ou clique para mostrar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment