# -*- coding: utf-8 -*-

# /***************************************************************************
#  VectorToMap - QGIS Plugin
#  Automates the generation of print layouts for vector features.
#  Author: Matheus Durso Neves Caetano
# 
#  Copyright (C) 2026 Matheus Durso Neves Caetano
# 
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
# 
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#  GNU General Public License for more details.
#  ***************************************************************************/

import os
import gc
import traceback
import sys
from .layout_engine import LayoutEngine
# --- NOVO: Ajuda o Python a enxergar bibliotecas externas (como o Sentry) ---
plugin_dir = os.path.dirname(__file__)
if plugin_dir not in sys.path:
    sys.path.append(plugin_dir) # Use APPEND em vez de insert(0)
# -------------------------------------------------------------------------------
import sentry_sdk
from qgis.PyQt import sip
from qgis.PyQt.QtCore import QTranslator, QCoreApplication, Qt, QTimer, QUrl, QSize
from qgis.PyQt.QtGui import QIcon, QPixmap, QColor, QFont, QDesktopServices
from qgis.PyQt.QtWidgets import (
    QAction, QCheckBox, QTableWidgetItem, QFrame, QPushButton,
    QDialogButtonBox, QButtonGroup, QApplication, QGridLayout,
    QFileDialog, QMessageBox, QDialog, QVBoxLayout, QHBoxLayout, QLabel,
    QFontDialog, QGraphicsDropShadowEffect, QLineEdit, QWidget, QTextBrowser,
    QComboBox, QRadioButton, QSpinBox, QDoubleSpinBox, QScrollArea)
from qgis.core import (
    QgsProject, QgsPrintLayout, QgsMapLayerProxyModel, QgsFeatureRequest, QgsSettings,
    QgsLayoutExporter, QgsExpression, QgsVectorLayer, NULL, Qgis, QgsMessageLog,
    QgsExpressionContext, QgsExpressionContextUtils, QgsLayoutItemMap
)
# Resources generated by Plugin Builder
from .resources import *
from .vector_to_map_dialog import VectorToMapDialog


class BoasVindasDialog(QDialog):
    """Janela de Boas-Vindas com design moderno, fontes maiores e card de destaque."""
    def __init__(self, parent=None):
        super().__init__(parent)
        
        # --- Atalho interno para tradução ---
        def tr(texto):
            return QCoreApplication.translate('@default', texto)
        # ------------------------------------------

        self.setWindowTitle("VectorToMap")
        # Aumentado o tamanho da janela para acomodar fontes maiores com conforto
        self.resize(600, 420) 
        
        self.setWindowIcon(QIcon(':/plugins/vector_to_map/icon.png'))
        self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint)

        # --- Layout Principal ---
        layout_principal = QVBoxLayout(self)
        # Margens confortáveis para respirar: Esquerda, Topo, Direita, Baixo.
        layout_principal.setContentsMargins(35, 30, 35, 25) 
        layout_principal.setSpacing(15) 

        # --- 1. CABEÇALHO (Ícone + Título alinhados horizontalmente) ---
        layout_cabecalho = QHBoxLayout()
        layout_cabecalho.setSpacing(15)

        # Ícone do Plugin (tamanho ligeiramente maior)
        self.lbl_icone = QLabel()
        pixmap = QPixmap(':/plugins/vector_to_map/icon.png').scaled(52, 52, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
        self.lbl_icone.setPixmap(pixmap)
        layout_cabecalho.addWidget(self.lbl_icone)

        # Título
        lbl_titulo = QLabel(tr("Bem-vindo ao VectorToMap!"))
        lbl_titulo.setStyleSheet("font-size: 18pt; font-weight: bold; color: #2c3e50;")
        layout_cabecalho.addWidget(lbl_titulo)
        
        layout_cabecalho.addStretch() # Alinha tudo à esquerda
        layout_principal.addLayout(layout_cabecalho)

        # --- 2. TEXTO DE BOAS-VINDAS ---
        texto_welcome = tr("Obrigado por instalar o VectorToMap para automatizar sua cartografia técnica!")
        lbl_welcome = QLabel(texto_welcome)
        lbl_welcome.setWordWrap(True)
        lbl_welcome.setStyleSheet("font-size: 12pt; color: #34495e; margin-top: 5px;")
        layout_principal.addWidget(lbl_welcome)

        # --- 3. SEÇÃO DE DESTAQUE (CARD 100% GRATUITO) ---
        # Usamos um bloco HTML/CSS para criar um "card" interno elegante
        texto_pro_html = tr(
            "<div style='background-color: #f0f8ff; padding: 15px 20px; border-radius: 6px; border-left: 5px solid #3498db;'>"
            "<h3 style='margin: 0 0 10px 0; color: #2980b9; font-size: 14pt;'>🎉 Agora 100% Gratuito e Open-Source!</h3>"
            "<p style='margin: 0 0 12px 0; color: #2c3e50; font-size: 11pt; line-height: 1.4;'>"
            "Todos os recursos avançados da versão Pro, exportação sem limites e templates profissionais foram liberados para a comunidade.</p>"
            "<p style='margin: 0; font-size: 11pt; color: #34495e;'>"
            "☕ <b>Apoie para manter o projeto vivo:</b> <a href='https://buymeacoffee.com/matheusdurso' style='color: #e67e22; text-decoration: none; font-weight: bold;'>buymeacoffee.com/matheusdurso</a></p>"
            "</div>"
        )
        lbl_pro_texto = QLabel(texto_pro_html)
        lbl_pro_texto.setWordWrap(True)
        lbl_pro_texto.setOpenExternalLinks(True) 
        lbl_pro_texto.setAlignment(Qt.AlignmentFlag.AlignLeft) 
        layout_principal.addWidget(lbl_pro_texto)

        layout_principal.addSpacing(5)

        # --- 4. LINHA DIVISÓRIA ---
        linha = QFrame()
        linha.setFrameShape(QFrame.Shape.HLine)
        linha.setFrameShadow(QFrame.Shadow.Sunken)
        linha.setStyleSheet("border: 1px solid #e0e0e0; background-color: transparent;")
        layout_principal.addWidget(linha)

        # --- 5. TEXTO DE CONSENTIMENTO DO SENTRY ---
        texto_sentry = tr(
            "<b style='font-size: 11pt; color: #444;'>Relatórios de Erro:</b><br>"
            "<span style='font-size: 10pt; color: #666;'>Você aceita enviar relatórios automáticos e totalmente anônimos caso algum erro ocorra no sistema? "
            "Nenhuma informação pessoal ou dado geográfico será enviado.</span>"
        )
        lbl_sentry = QLabel(texto_sentry)
        lbl_sentry.setWordWrap(True)
        layout_principal.addWidget(lbl_sentry)

        # --- 6. ESPAÇADOR VERTICAL (EMPURRA BOTÕES PARA O RODAPÉ) ---
        layout_principal.addStretch()  

        # --- 7. BOTÕES DE AÇÃO ---
        layout_botoes = QHBoxLayout()
        layout_botoes.setContentsMargins(0, 10, 0, 0) # Margem superior
        
        self.btn_recusar = QPushButton(tr("Não aceito, apenas começar"))
        self.btn_recusar.setCursor(Qt.CursorShape.PointingHandCursor)
        # CSS limpo para o botão secundário
        self.btn_recusar.setStyleSheet("padding: 10px 18px; font-size: 11pt; color: #555; background-color: #f8f9fa; border: 1px solid #ccc; border-radius: 4px;")
        self.btn_recusar.clicked.connect(self.reject) 

        self.btn_aceitar = QPushButton(tr("Aceitar e Começar"))
        self.btn_aceitar.setCursor(Qt.CursorShape.PointingHandCursor)
        self.btn_aceitar.setDefault(True) 
        # CSS refinado para o botão principal
        self.btn_aceitar.setStyleSheet("""
            QPushButton {
                background-color: #3498db; 
                color: white; 
                font-weight: bold; 
                font-size: 11pt;
                padding: 10px 25px; 
                border: none; 
                border-radius: 4px;
            }
            QPushButton:hover {
                background-color: #2980b9;
            }
        """)
        self.btn_aceitar.clicked.connect(self.accept) 

        layout_botoes.addWidget(self.btn_recusar)
        layout_botoes.addStretch() # Mola entre os botões
        layout_botoes.addWidget(self.btn_aceitar)

        layout_principal.addLayout(layout_botoes)


class VectorToMap:

    ####################################################################################
    # --- 1. CICLO DE VIDA E CONFIGURAÇÕES INICIAIS ---
    ####################################################################################

    def __init__(self, iface):
        """Initialize the plugin and define global state variables."""

        self.iface = iface
        self.plugin_dir = os.path.dirname(__file__)
        self.actions = []
        self.menu = self.tr(u'&VectorToMap')
        self.first_start = True

        # "Contratamos" o motor e guardamos na variável self.engine
        self.engine = LayoutEngine(self.iface)

        self.is_rendering = False # Flag to prevent infinite rendering loops
        self.ultimo_id_camada_ativa = None

        self.colunas_atuais = []
        
        self.preview_data_cache = None
        self.ultimo_id_camada = None
        self.cache_val_atlas = None
        self.clones_preview = []
        
        # Determina a língua de exibição
        locale = QgsSettings().value('locale/userLocale')[0:2] 

        if locale != 'pt':
            # O path vai buscar vector_to_map_en.qm ou vector_to_map_es.qm
            path = os.path.join(self.plugin_dir, 'i18n', f'vector_to_map_{locale}.qm')
            
            # Se o espanhol não existir por algum motivo, ele volta pro inglês
            if not os.path.exists(path):
                path = os.path.join(self.plugin_dir, 'i18n', 'vector_to_map_en.qm')

            if os.path.exists(path):
                self.translator = QTranslator()
                self.translator.load(path)
                QCoreApplication.installTranslator(self.translator)
    

    
    
    def tr(self, message):
        """Garante que o contexto coincida com o que o pylupdate gera."""
        # Se no seu .ts aparecer <context><name>VectorToMap</name>, use o nome da classe abaixo:
        return QCoreApplication.translate('VectorToMap', message)
    

    
    
    #Determina variáveis de sistema e configuração de aparência
    def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None):
        """Helper function to add buttons and menus to the QGIS interface."""

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)
        if status_tip: action.setStatusTip(status_tip)
        if whats_this: action.setWhatsThis(whats_this)
        if add_to_toolbar: self.iface.addToolBarIcon(action)
        if add_to_menu: self.iface.addPluginToMenu(self.menu, action)
        self.actions.append(action)
        return action
    

    
    
    def initGui(self):
        """Cria as entradas de menu e a barra de ferramentas personalizada."""

        icon_path = ':/plugins/vector_to_map/icon.png'
        
        # 1. CRIAÇÃO DA BARRA DE FERRAMENTAS EXCLUSIVA
        self.toolbar = self.iface.addToolBar(u'VectorToMap')
        self.toolbar.setObjectName(u'VectorToMap')

        # 2. CRIAÇÃO DA AÇÃO PRINCIPAL
        self.action = self.add_action(
            icon_path,
            text=self.tr(u'VectorToMap'),
            callback=self.run,
            parent=self.iface.mainWindow(),
            add_to_toolbar=True,
            add_to_menu=True
        )

        # Registra o atalho no Gerenciador Nativo do QGIS (Fica visível em Configurações > Atalhos)
        self.iface.registerMainWindowAction(self.action, "Ctrl+Alt+C")

        # ==========================================================
        # --- INJETANDO O ÍCONE NO MENU PAI DA VERSÃO ---
        # ==========================================================
        menu_complementos = self.iface.pluginMenu()
        
        # 1. Defina o caminho do ícone da versão (ajuste se o nome do arquivo for diferente)
        icon_pro_path = ':/plugins/vector_to_map/icon.png' 
        
        # 2. Varre o menu em busca da pasta do plugin
        for acao_menu in menu_complementos.actions():
            # O texto deve ser idêntico ao que você definiu no self.menu (geralmente '&VectorToMap')
            if acao_menu.text() == self.tr(u'&VectorToMap'): 
                acao_menu.setIcon(QIcon(icon_pro_path))
                break


    
    
    def unload(self):
        """Limpa os recursos do plugin ao ser desativado."""

        # --- NOVO: Remove o atalho do Gerenciador do QGIS ---
        if hasattr(self, 'action') and self.action:
            self.iface.unregisterMainWindowAction(self.action)
        # ----------------------------------------------------
        
        # 1. Limpa as camadas de preview da memória
        self.limpar_clones_preview()

        # Desconecta os filtros da combo para não dar erro de runtime
        try:
            QgsProject.instance().layersAdded.disconnect(self._filtrar_camadas_da_combo)
            QgsProject.instance().layersRemoved.disconnect(self._filtrar_camadas_da_combo)
        except: pass

        # 2. Remove as ações dos menus e barras
        for action in self.actions:
            self.iface.removePluginMenu(self.menu, action)
            self.iface.removeToolBarIcon(action)
        
        # 3. Limpeza segura da Toolbar (Foco QGIS 4.0 e Plugin Reloader)
        if hasattr(self, 'toolbar') and self.toolbar:
            if not sip.isdeleted(self.toolbar):
                self.iface.mainWindow().removeToolBar(self.toolbar)
                self.toolbar.hide() # Esconde fisicamente na hora
                self.toolbar.setParent(None) # Pulo do Gato: Desvincula do QGIS instantaneamente
                self.toolbar.deleteLater() # Agenda a deleção segura na memória
            del self.toolbar

        # 4. Limpeza segura do Diálogo
        if hasattr(self, 'dlg') and self.dlg:
            if not sip.isdeleted(self.dlg):
                self.dlg.close()
                self.dlg.setParent(None) # Desvincula da memória principal
                self.dlg.deleteLater()
            del self.dlg

        # 5. Coleta de lixo final
        gc.collect()

    
    
    
    def _verificar_consentimento_sentry(self):
        """Pergunta ao usuário apenas na primeira vez que ele abre o plugin."""
        settings = QgsSettings()
        
        # Verifica se já fizemos essa pergunta no passado
        ja_perguntou = settings.value("/VectorToMap/sentry_ja_perguntou", False, type=bool)
        
        if not ja_perguntou:
            # Chama a NOSSA janela estilizada
            dialog = BoasVindasDialog(self.iface.mainWindow())
            
            # Mostra a janela e congela o QGIS até o usuário clicar em algo
            # Compatibilidade QGIS 3 (PyQt5) e QGIS 4 (PyQt6)
            if hasattr(dialog, 'exec'):
                aceitou = (dialog.exec() == 1)  # Novo padrão do QGIS 4
            else:
                aceitou = (dialog.exec_() == 1) # Padrão antigo do QGIS 3
            
            # Salva a escolha na memória do QGIS
            settings.setValue("/VectorToMap/enviar_erros_sentry", aceitou)
            settings.setValue("/VectorToMap/sentry_ja_perguntou", True)

        # Retorna a escolha salva
        return settings.value("/VectorToMap/enviar_erros_sentry", False, type=bool)


    
    
    def inicializar_sentry(self):
        """Liga o motor do Sentry se o usuário permitiu e filtra erros externos."""
        self.sentry_ativo = False
        
        def filtro_sentry(event, hint):
            """Analisa o rastro do erro. Só envia se vier do VectorToMap."""
            # 1. Verifica se a mensagem de log menciona o seu plugin
            log_msg = event.get('logentry', {}).get('message', '')
            if 'VectorToMap' in log_msg:
                return event
                
            # 2. Verifica a pilha de erros (traceback)
            try:
                frames = event.get('exception', {}).get('values', [])[0].get('stacktrace', {}).get('frames', [])
                for frame in frames:
                    arquivo = frame.get('filename', '').lower()
                    # Se o erro passou por algum arquivo com 'vector_to_map' no nome ou caminho, é nosso!
                    if 'vector_to_map' in arquivo:
                        return event
            except Exception:
                pass
                
            # Se chegou aqui, o erro é de outro plugin do QGIS. Nós descartamos.
            return None

        try:
            sentry_sdk.init(
                dsn="https://3a3fd55bd680f6cc5594929bec0c7609@o4511038786240512.ingest.de.sentry.io/4511038808457296",
                send_default_pii=False, 
                release="vectortomap@3.6.0",
                before_send=filtro_sentry  # <--- INSERIMOS O FILTRO AQUI
            )
            self.sentry_ativo = True
            
        except ImportError:
            # Falha silenciosa se a biblioteca não estiver na pasta
            pass
        except Exception:
            # Qualquer outro erro de inicialização é ignorado para não travar o plugin
            pass



    
    # ##################################################################################
    # --- 2. INTERFACE E DIÁLOGO (REFATORADO v0.4.4) ---
    # ##################################################################################

    def atualizar_textos_interface(self):
        """Força a tradução de widgets dinâmicos que o Qt não traduz automaticamente."""
        if not hasattr(self, 'dlg') or not self.dlg or sip.isdeleted(self.dlg):
            return

        # 2. Traduz a aba de Suporte e Botões da Interface
        if hasattr(self, 'btn_apoiar'):
            self.btn_apoiar.setText(self.tr("☕ Apoiar Desenvolvimento"))
        if hasattr(self, 'btn_bugreport'):
            self.btn_bugreport.setText(self.tr("🐞 Relatar Bug (GitHub)"))
        if hasattr(self, 'btn_tutorial'):
            self.btn_tutorial.setText(self.tr("▶ Assistir Tutorial"))

        # 3. Traduz os itens da Combo de Presets (Protegido contra loops)
        self.dlg.combo_presets.blockSignals(True)
        
        try:
            idx_atual = self.dlg.combo_presets.currentIndex() # Salva onde o usuário estava
            self.dlg.combo_presets.clear()
            
            # Mantém os modos matemáticos básicos
            self.dlg.combo_presets.addItem(self.tr("Mapa Quadrado (Básico)"), "quadrado")
            self.dlg.combo_presets.addItem(self.tr("Mapa Horizontal (Básico)"), "horizontal")
            self.dlg.combo_presets.addItem(self.tr("Mapa Vertical (Básico)"), "vertical")
            
            # =================================================================
            # --- LÊ A PASTA DE TEMPLATES NATIVOS DA VERSÃO POR IDIOMA ---
            # =================================================================
            locale = QgsSettings().value('locale/userLocale', 'en')[0:2] 
            pasta_base_templates = os.path.join(self.plugin_dir, 'templates')
            
            if locale == 'pt':
                pasta_alvo = pasta_base_templates
            else:
                pasta_alvo = os.path.join(pasta_base_templates, locale)
                if not os.path.exists(pasta_alvo):
                    pasta_alvo_en = os.path.join(pasta_base_templates, 'en')
                    if os.path.exists(pasta_alvo_en):
                        pasta_alvo = pasta_alvo_en
                    else:
                        pasta_alvo = pasta_base_templates

            # Agora, varremos a pasta decidida para listar os .qpt
            if os.path.exists(pasta_alvo):
                for arquivo in os.listdir(pasta_alvo):
                    if arquivo.lower().endswith('.qpt'):
                        nome_amigavel = arquivo.replace('.qpt', '').replace('_', ' ').title()
                        nome_traduzido = self.tr(nome_amigavel)
                        caminho_completo = os.path.join(pasta_alvo, arquivo)
                        self.dlg.combo_presets.addItem(f"📄 {nome_traduzido}", caminho_completo)
            # =================================================================
            
            # --- OPÇÃO PARA O USUÁRIO CARREGAR O PRÓPRIO TEMPLATE ---
            self.dlg.combo_presets.insertSeparator(self.dlg.combo_presets.count()) # Linha divisória
            self.dlg.combo_presets.addItem(self.tr("📂 Carregar Template Personalizado..."), "carregar_template")
            # --------------------------------------------------------------
            
            self.dlg.combo_presets.setCurrentIndex(idx_atual if idx_atual >= 0 else 0)
            
        finally:
            # Liberação garantida da combobox
            self.dlg.combo_presets.blockSignals(False)
        
        # 5. Traduz o Placeholder de busca de atributos (se já existir)
        if hasattr(self, 'edit_busca_atributos') and not sip.isdeleted(self.edit_busca_atributos):
            self.edit_busca_atributos.setPlaceholderText(self.tr("🔍 Filtrar colunas..."))


    
    
    def run(self):
        """Maestro da Interface: Inicializa a UI e orquestra a montagem da janela."""
        
        if self.first_start:
            self.first_start = False
            aceitou_sentry = self._verificar_consentimento_sentry()
            if aceitou_sentry:
                self.inicializar_sentry()

        # ====================================================================
        # A CURA DA JANELA DUPLICADA: Verifica se ela já existe e está viva
        # ====================================================================
        ja_estava_aberta = hasattr(self, 'dlg') and self.dlg and not sip.isdeleted(self.dlg)

        if not ja_estava_aberta:
            # Se não existe, criamos a interface do zero
            self.dlg = VectorToMapDialog()
            self.colunas_atuais = [] # Mata os fantasmas das checkboxes da janela anterior
            self.ultimo_id_camada_ativa = None

            self.dlg.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.VectorLayer)
            self._filtrar_camadas_da_combo()

            QgsProject.instance().layersAdded.connect(self._filtrar_camadas_da_combo)
            QgsProject.instance().layersRemoved.connect(self._filtrar_camadas_da_combo)

            self.dlg.setWindowFlags(self.dlg.windowFlags() | Qt.WindowType.WindowMaximizeButtonHint | Qt.WindowType.WindowMinimizeButtonHint)

            self._aplicar_estilos()
            self._configurar_widgets_iniciais() # <- Aqui nasce o btn_export
            self._conectar_sinais()
            self._configurar_janela()
            self.setup_ui_strings()

            # Força tradução de botões e combos
            self.atualizar_textos_interface()
            
            # Força a janela a abrir sempre na primeira aba (Configurações)
            if hasattr(self.dlg, 'tabWidget'):
                self.dlg.tabWidget.setCurrentIndex(0)

        # ====================================================================
        # SINCRONIZA CAMADA ATIVA (INTERFACE)
        # (Roda sempre que você clica no ícone, atualizando a janela se já estiver aberta)
        # ====================================================================
        camada_ativa = self.iface.activeLayer()
        if camada_ativa and isinstance(camada_ativa, QgsVectorLayer):
            # 1. Seta a camada no mapa principal
            self.dlg.mMapLayerComboBox.setLayer(camada_ativa)
            
            # 2. Seta a camada no mapa de localização
            if hasattr(self.dlg, 'combo_camada_loc'):
                self.dlg.combo_camada_loc.setLayer(camada_ativa)
                
            # 3. Garante que a lista de atributos seja carregada na abertura
            self.atualizar_lista_atributos()

        # ====================================================================
        # A CURA DA JANELA ESCONDIDA / MINIMIZADA
        # ====================================================================
        if self.dlg.isMinimized():
            self.dlg.showNormal()  # Desminimiza a janela
            
        self.dlg.show()            # Garante que está visível
        self.dlg.raise_()          # Traz para a frente de tudo (Z-Order)
        self.dlg.activateWindow()  # Dá o foco do mouse e teclado para ela

        # ====================================================================
        # O GATILHO DA PRIMEIRA PREVIEW
        # ====================================================================
        # Se a janela for nova, dispara a preview. 
        # Se já estava aberta, a mudança de camada acima já vai acionar a preview automaticamente pelos sinais.
        if not ja_estava_aberta:
            QTimer.singleShot(200, self.atualizar_preview)




    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE INTERFACE ---
    # ------------------------------------------------------------------------------------
    
    def _aplicar_estilos(self):
        """Injeta o CSS consolidado (Azul Profissional & Clean) e esconde barras padrão."""

        estilo_consolidado = """
            /* 1. ESTILO GERAL DAS BARRAS DE PROGRESSO */
            QProgressBar {
                border: 1px solid #dcdcdc;
                border-radius: 4px;
                text-align: center;
                background-color: #f0f0f0;
                height: 14px;
                font-size: 10px;
                color: #333;
            }
            QProgressBar::chunk {
                background-color: #3498db; /* Azul Claro */
                border-radius: 3px;
            }

            /* 2. BARRA DA PREVIEW (ULTRA-FINA ESTILO YOUTUBE) */
            QProgressBar#previewProgressBar {
                border: 1px solid #dcdcdc;
                border-radius: 4px;
                background-color: #f0f0f0;
                height: 3px;
                color: #333; 
            }
            QProgressBar#previewProgressBar::chunk {
                background-color: #3498db;
                border-radius: 3px;
            }

            /* 3. BARRAS DE ROLAGEM MODERNAS (SCROLLBARS) */
            QScrollBar:vertical, QScrollBar:horizontal {
                border: none;
                background: #f8f8f8;
                width: 8px;
                height: 8px;
                margin: 0px;
            }
            QScrollBar::handle {
                background: #cccccc;
                border-radius: 4px;
                min-height: 20px;
                min-width: 20px;
            }
            QScrollBar::handle:hover {
                background: #707070; /* cinza */
            }
            QScrollBar::add-line, QScrollBar::sub-line {
                border: none;
                background: none;
                height: 0px;
                width: 0px;
            }
            
            /* 4. PADRONIZAÇÃO DOS WIDGETS DE TEXTO */
            QCheckBox, QLabel, QRadioButton, QgsMapLayerComboBox, QComboBox {
                font-size: 9pt; 
            }
            
            /* --- 5. BOTÕES DA ABA DE SUPORTE --- */
            QPushButton#pushButton_apoiar {
                background-color: #3498db;
                color: white;
                font-weight: bold;
                font-size: 13px;
                padding: 8px 20px;
                border: none;
                border-radius: 4px;
            }
            QPushButton#pushButton_apoiar:hover {
                background-color: #2980b9;
            }

            QPushButton#pushButton_bugreport {
                background-color: transparent;
                color: palette(text); /* NOVO: Usa a cor de texto padrão do tema */
                font-size: 12px;
                padding: 8px 15px;
                border: 1px solid #cccccc;
                border-radius: 4px;
            }
            QPushButton#pushButton_bugreport:hover {
                background-color: palette(midlight); /* NOVO: Usa o hover padrão do tema */
            }

            /* =================================================== */
            /* NOVO: BOTÃO DO TUTORIAL (VERMELHO YOUTUBE)          */
            /* =================================================== */
            QPushButton#pushButton_tutorial {
                background-color: #e74c3c;
                color: white;
                font-weight: bold;
                font-size: 12px;
                padding: 8px 15px;
                border: 1px solid #e74c3c;
                border-radius: 4px;
            }
            QPushButton#pushButton_tutorial:hover {
                background-color: #c0392b;
            }

            /* =================================================== */
            /* 6. BOTÕES DE INTERFACE (EPSILON E INFO)             */
            /* =================================================== */
            
            /* Foca EXCLUSIVAMENTE nos botões Epsilon (evitando quebrar os botões de cor) */
            QgsFieldExpressionWidget QToolButton {
                background-color: #f8f9fa; /* Fundo claro fixo */
            }
            QgsFieldExpressionWidget QToolButton:hover {
                background-color: #e0e0e0;
            }
            QgsFieldExpressionWidget QToolButton:pressed {
                background-color: #dcdcdc;
            }

            /* Foca no botão de Info (Ajuda) no rodapé */
            QPushButton#btn_info_python {
                background-color: #3498db; /* Cria o círculo azul */
                color: #ffffff; /* Letra "i" branca e brilhante */
                border-radius: 11px; /* Transforma o botão 22x22 em um círculo perfeito */
                border: none;
                font-family: "Times New Roman", serif; /* Fonte clássica de Informação */
                font-weight: bold;
                font-size: 14px;
                padding-bottom: 2px; /* Ajuste fino para centralizar o 'i' */
            }
            QPushButton#btn_info_python:hover {
                background-color: #2980b9; /* Azul levemente mais escuro ao passar o mouse */
            }
        """
        self.dlg.setStyleSheet(estilo_consolidado)
        self.dlg.previewProgressBar.hide()
        self.dlg.progressBar.hide()
        self.dlg.lbl_preview.setFrameShape(QFrame.Shape.StyledPanel)

        # --- NOVO: Efeito de Sombra na Folha de Preview ---
        sombra = QGraphicsDropShadowEffect()
        sombra.setBlurRadius(20) 
        sombra.setXOffset(5)
        sombra.setYOffset(5)
        sombra.setColor(QColor(0, 0, 0, 80)) 

        if hasattr(self.dlg, 'lbl_preview'):
            self.dlg.lbl_preview.setGraphicsEffect(sombra)
        # --------------------------------------------------


    
    
    def _configurar_widgets_iniciais(self):
        """Popula as ComboBoxes, recria os botões na ordem certa e define variáveis de estado."""

        # ====================================================================
        # NOVO: ATIVA A PREVIEW AUTOMÁTICA POR PADRÃO
        # ====================================================================
        if hasattr(self.dlg, 'chk_preview_auto'):
            self.dlg.chk_preview_auto.setChecked(True)
        # ====================================================================

        # Setup de Cor da Página (Novo botão)
        if hasattr(self.dlg, 'btn_cor_pagina'):
            self.dlg.btn_cor_pagina.setColor(QColor(255, 255, 255, 255))
            self.dlg.btn_cor_pagina.setAllowOpacity(True)
            self.dlg.btn_cor_pagina.colorChanged.connect(self.disparar_preview_se_autorizado)

        # Isolamento dos Radio Buttons
        self.grupo_orientacao = QButtonGroup(self.dlg)
        self.grupo_orientacao.addButton(self.dlg.rb_retrato)
        self.grupo_orientacao.addButton(self.dlg.rb_paisagem)

        self.grupo_escala = QButtonGroup(self.dlg)
        self.grupo_escala.addButton(self.dlg.rb_escala_auto)
        self.grupo_escala.addButton(self.dlg.rb_escala_fixa)

        # ====================================================================
        # NOVO: ISOLAMENTO DOS RADIO BUTTONS DO ATLAS
        # Cria a "cerca invisível" para que só um fique marcado por vez
        # ====================================================================
        if hasattr(self.dlg, 'rb_atlas_nenhum'):
            self.grupo_atlas = QButtonGroup(self.dlg)
            self.grupo_atlas.addButton(self.dlg.rb_atlas_nenhum)
            self.grupo_atlas.addButton(self.dlg.rb_atlas_geral)
            self.grupo_atlas.addButton(self.dlg.rb_atlas_agrupar)
        # ====================================================================

        # Setup Tamanhos de Página
        self.dlg.combo_tamanho_pagina.clear()
        tamanhos_disponiveis = [
            ("A5 (148 x 210 mm)", (148.0, 210.0)),
            ("A4 (210 x 297 mm)", (210.0, 297.0)),
            ("A3 (297 x 420 mm)", (297.0, 420.0)),
            ("A2 (420 x 594 mm)", (420.0, 594.0)),
            ("A1 (594 x 841 mm)", (594.0, 841.0)),
            ("A0 (841 x 1189 mm)", (841.0, 1189.0))
        ]
        for texto, dimensoes in tamanhos_disponiveis:
            self.dlg.combo_tamanho_pagina.addItem(texto, dimensoes)
        self.dlg.combo_tamanho_pagina.setCurrentIndex(1) # Força o A4

        # Setup Escala
        self.dlg.combo_escala_fixa.clear()
        escalas = [
            ("1:1.000", 1000), ("1:5.000", 5000), ("1:10.000", 10000),
            ("1:25.000", 25000), ("1:50.000", 50000), ("1:100.000", 100000), 
            ("1:250.000", 250000), ("1:500.000", 500000), ("1:1.000.000", 1000000),
            ("1:2.500.000", 2500000), ("1:5.000.000", 5000000), ("1:7.500.000", 7500000),
            ("1:10.000.000", 10000000), ("1:50.000.000", 50000000), ("1:100.000.000", 100000000)
        ]
        for texto, valor in escalas:
            self.dlg.combo_escala_fixa.addItem(texto, valor)

        # --- NOVO: Adiciona opção de expressão na escala fixa ---
        self.dlg.combo_escala_fixa.insertSeparator(self.dlg.combo_escala_fixa.count())
        self.dlg.combo_escala_fixa.addItem(self.tr("Usar Coluna ou Expressão (ε)"), "expressao")
        # --------------------------------------------------------

        self.dlg.combo_escala_fixa.setCurrentIndex(3)

        # ====================================================================
        # NOVO: SETUP DA COMBOBOX DE ZOOM OUT (Margem do Mapa)
        # ====================================================================
        if hasattr(self.dlg, 'comboZoomOut'):
            self.dlg.comboZoomOut.clear()
            self.dlg.comboZoomOut.addItem(self.tr("0% (Justo)"), 0.0)
            self.dlg.comboZoomOut.addItem(self.tr("15%"), 15.0)
            self.dlg.comboZoomOut.addItem(self.tr("25% (Padrão)"), 25.0)
            self.dlg.comboZoomOut.addItem(self.tr("35%"), 35.0)
            self.dlg.comboZoomOut.addItem(self.tr("50%"), 50.0)
            self.dlg.comboZoomOut.addItem(self.tr("65%"), 65.0)
            self.dlg.comboZoomOut.addItem(self.tr("75%"), 75.0)
            self.dlg.comboZoomOut.addItem(self.tr("100% (Dobro)"), 100.0)
            
            # Adiciona uma linha divisória visual
            self.dlg.comboZoomOut.insertSeparator(self.dlg.comboZoomOut.count())
            self.dlg.comboZoomOut.addItem(self.tr("Usar Coluna ou Expressão (ε)"), "expressao")
            
            self.dlg.comboZoomOut.setCurrentIndex(2) # Define o 25% como padrão inicial

        # Filtro de Camadas Vectoriais
        self.dlg.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.VectorLayer)

        if hasattr(self.dlg, 'combo_camada_loc'):
            self.dlg.combo_camada_loc.setFilters(QgsMapLayerProxyModel.VectorLayer)
        
        # ====================================================================
        # NOVO: SETUP DAS OPÇÕES DO MAPA DE LOCALIZAÇÃO (Zoom vs Camada)
        # ====================================================================
        if hasattr(self.dlg, 'rb_local_zoomout') and hasattr(self.dlg, 'rb_local_camada'):
            self.grupo_localizacao = QButtonGroup(self.dlg)
            self.grupo_localizacao.addButton(self.dlg.rb_local_zoomout)
            self.grupo_localizacao.addButton(self.dlg.rb_local_camada)
            self.dlg.rb_local_camada.setChecked(True) # Padrão: usar a camada
            
        if hasattr(self.dlg, 'combo_local_zoomout'):
            self.dlg.combo_local_zoomout.clear()
            self.dlg.combo_local_zoomout.addItem(self.tr("2 x"), 2.0)
            self.dlg.combo_local_zoomout.addItem(self.tr("5 x"), 5.0)
            self.dlg.combo_local_zoomout.addItem(self.tr("10 x"), 10.0)
            self.dlg.combo_local_zoomout.addItem(self.tr("15 x"), 15.0)
            self.dlg.combo_local_zoomout.addItem(self.tr("20 x"), 20.0)
            self.dlg.combo_local_zoomout.addItem(self.tr("25 x"), 25.0)
            self.dlg.combo_local_zoomout.setCurrentIndex(1) # Define "5 x" como padrão inicial

        # Setup de Presets
        self.dlg.combo_presets.setItemText(0, self.tr("Mapa Quadrado"))
        self.dlg.combo_presets.setItemData(0, "quadrado")
        self.dlg.combo_presets.setItemText(1, self.tr("Mapa Horizontal"))
        self.dlg.combo_presets.setItemData(1, "horizontal")
        self.dlg.combo_presets.addItem(self.tr("Mapa Vertical"), "vertical")

        # Setup das Posições das Decorações
        opcoes_posicao = [
            (self.tr("Superior Esquerdo"), "SE"),
            (self.tr("Superior Direito"), "SD"),
            (self.tr("Inferior Esquerdo"), "IE"),
            (self.tr("Inferior Direito"), "ID")
        ]
        
        # Preenche os 3 combos de uma vez, se existirem
        for combo_name in ['comboLegenda', 'comboEscala', 'comboNorte', 'comboPosLoc']:
            if hasattr(self.dlg, combo_name):
                combo = getattr(self.dlg, combo_name)
                combo.clear()
                for texto, dado in opcoes_posicao:
                    combo.addItem(texto, dado)
                    
        # Define os padrões iniciais (Legenda=InfEsq, Escala=SupEsq, Norte=SupDir)
        if hasattr(self.dlg, 'comboLegenda'): self.dlg.comboLegenda.setCurrentIndex(2)
        if hasattr(self.dlg, 'comboEscala'): self.dlg.comboEscala.setCurrentIndex(0)
        if hasattr(self.dlg, 'comboNorte'): self.dlg.comboNorte.setCurrentIndex(1)
        # Define o canto padrão (Ex: Inferior Direito)
        if hasattr(self.dlg, 'comboPosLoc'): self.dlg.comboPosLoc.setCurrentIndex(3)

        # Setup do ESTILO da Seta de Norte
        if hasattr(self.dlg, 'combo_estilo_norte'):
            self.dlg.combo_estilo_norte.clear()
            # Salvamos o NOME do arquivo SVG nativo do QGIS no "Data" da combo
            self.dlg.combo_estilo_norte.addItem(self.tr("Minimalista"), "NorthArrow_02.svg")
            self.dlg.combo_estilo_norte.addItem(self.tr("Clássica (Seta N)"), "NorthArrow_01.svg")
            self.dlg.combo_estilo_norte.addItem(self.tr("Rosa dos Ventos"), "NorthArrow_04.svg")
            self.dlg.combo_estilo_norte.setCurrentIndex(0)
            
            # Gatilho: Se o usuário mudar a seta, a preview atualiza na hora!
            self.dlg.combo_estilo_norte.currentIndexChanged.connect(self.disparar_preview_se_autorizado)

        # Setup da Grade (Grid)
        if hasattr(self.dlg, 'comboGrid'):
            self.dlg.comboGrid.clear()
            self.dlg.comboGrid.addItem(self.tr("Linhas Sólidas + Zebra"), "solido")
            self.dlg.comboGrid.addItem(self.tr("Apenas Cruzes"), "cruz")

        # Setup de Cor de Fundo
        if hasattr(self.dlg, 'btn_cor_fundo'):
            self.dlg.btn_cor_fundo.setColor(QColor(255, 255, 255, 255))
            self.dlg.btn_cor_fundo.setAllowOpacity(True)
            self.dlg.btn_cor_fundo.setToolTip(self.tr("Escolha a cor de fundo do mapa. Reduza a Opacidade para 0% para exportar transparente."))

        # Reconstrução Segura dos Botões
        try:
            self.dlg.button_box.accepted.disconnect()
            self.dlg.button_box.rejected.disconnect()
        except: pass

        self.dlg.button_box.clear()

        # --- NOVO: Criação do botão de suporte/ajuda (Botão Circular Customizado) ---
        self.btn_suporte_ajuda = QPushButton("i") # Colocamos a letra 'i' de verdade!
        self.btn_suporte_ajuda.setObjectName("btn_info_python")
        self.btn_suporte_ajuda.setFixedSize(22, 22) # Mantemos os 22x22
        self.btn_suporte_ajuda.setToolTip(self.tr("Ajuda do VectorToMap"))
                
        self.dlg.button_box.clear()
        
        # Adiciona o botão de suporte no início como ActionRole para ficar do lado esquerdo
        self.dlg.button_box.addButton(self.btn_suporte_ajuda, QDialogButtonBox.ButtonRole.ActionRole)
        
        self.btn_export = QPushButton(self.tr("Exportar"))
        self.btn_render = QPushButton(self.tr("Preview"))
        self.btn_ok = QPushButton(self.tr("OK"))
        self.btn_cancel = QPushButton(self.tr("Cancelar"))

        # Adiciona os outros botões padrão
        self.dlg.button_box.addButton(self.btn_export, QDialogButtonBox.ButtonRole.ActionRole)
        self.dlg.button_box.addButton(self.btn_render, QDialogButtonBox.ButtonRole.ActionRole)
        self.dlg.button_box.addButton(self.btn_ok, QDialogButtonBox.ButtonRole.ActionRole)
        self.dlg.button_box.addButton(self.btn_cancel, QDialogButtonBox.ButtonRole.ActionRole)
        # ----------------------------------------------------------------------

        # --- NOVO: SETUP DO TÍTULO E ALINHAMENTO ---
        self.fonte_titulo = QFont("Arial", 20, QFont.Weight.Bold)

        if hasattr(self.dlg, 'lineEdit_titulo'):
            self.dlg.lineEdit_titulo.textChanged.connect(self.disparar_preview_se_autorizado)
            
        if hasattr(self.dlg, 'btn_fonte_titulo'):
            self.dlg.btn_fonte_titulo.clicked.connect(self.escolher_fonte_titulo)
            
        # Preenche a ComboBox de Alinhamento e conecta ao gerador de Preview
        if hasattr(self.dlg, 'combo_alinhamento_titulo'):
            self.dlg.combo_alinhamento_titulo.clear()
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Esquerda"), Qt.AlignmentFlag.AlignLeft)
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Centro"), Qt.AlignmentFlag.AlignHCenter)
            self.dlg.combo_alinhamento_titulo.addItem(self.tr("Direita"), Qt.AlignmentFlag.AlignRight)
            self.dlg.combo_alinhamento_titulo.setCurrentIndex(1) # Força o "Centro" como padrão
            self.dlg.combo_alinhamento_titulo.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
        # -------------------------------------------

        # Variáveis de Estado
        self.export_info = None
        self.abort_processing = False
        self.is_rendering = False
        
        # Setup Timer Preview
        self.timer_preview = QTimer()
        self.timer_preview.setSingleShot(True)

        # --- NOVO: Injeta a aba 'Sobre' dinamicamente ---
        self._adicionar_aba_sobre()
        # ------------------------------------------------

        # --- Força a janela a abrir sempre na primeira aba (Configurações) ---
        if hasattr(self.dlg, 'tabWidget'):
            self.dlg.tabWidget.setCurrentIndex(0)


    
    
    def _conectar_sinais(self):
        """Faz todas as ligações entre as ações do usuário e as funções lógicas."""
        
        # --- 1. BOTÕES PRINCIPAIS ---
        self.btn_suporte_ajuda.clicked.connect(self.mostrar_tela_sobre)
        self.btn_export.clicked.connect(self.preparar_exportacao)
        self.btn_render.clicked.connect(self.atualizar_preview)
        self.btn_ok.clicked.connect(self.processar_clique_ok)
        self.btn_cancel.clicked.connect(self.cancelar_e_fechar)

        # --- 2. NAVEGAÇÃO E SUPORTE ---
        if hasattr(self.dlg, 'pushButton_ir_suporte'):
            self.dlg.pushButton_ir_suporte.clicked.connect(self.ir_para_suporte)
        
        # OS BOTÕES DE SUPORTE AGORA SÃO CONECTADOS DENTRO DA _adicionar_aba_sobre()!

        # --- 3. TIMER E PREVIEW AUTOMÁTICA ---
        self.timer_preview.timeout.connect(self.atualizar_preview)
        if hasattr(self.dlg, 'chk_preview_auto'):
            self.dlg.chk_preview_auto.stateChanged.connect(
                lambda state: self.timer_preview.start(250) if state == Qt.CheckState.Checked else None
            )

        # --- 4. GATILHOS DE CAMADA E ESCALA ---
        self.dlg.mMapLayerComboBox.layerChanged.connect(self.configurar_escala_ao_mudar_camada)
        self.dlg.rb_escala_auto.toggled.connect(self.validar_geometria_escala)
        self.dlg.rb_escala_fixa.toggled.connect(self.validar_geometria_escala)

        if hasattr(self.dlg, 'combo_escala_fixa'):
            self.dlg.combo_escala_fixa.currentIndexChanged.connect(self.validar_geometria_escala)

        if hasattr(self.dlg, 'comboZoomOut'):
            self.dlg.comboZoomOut.currentIndexChanged.connect(self.validar_geometria_escala)
        
        # --- 5. GATILHOS DE FILTROS E HIERARQUIA (Aviso do Filtro mora aqui) ---
        self.dlg.chk_filtrar_feicoes.stateChanged.connect(self.atualizar_hierarquia_camadas)
        self.dlg.chk_exibir_so_camada_atual.stateChanged.connect(self.atualizar_hierarquia_camadas)
        self.dlg.chk_travar_camadas.stateChanged.connect(self.atualizar_estado_travar_estilos)

        # --- 6. GATILHOS DE ATRIBUTOS E LAYOUT ---
        self.dlg.chk_selecionar_todos.stateChanged.connect(self.marcar_desmarcar_todos)
        self.dlg.chk_numeracao.toggled.connect(self.disparar_preview_se_autorizado)
        
        if hasattr(self.dlg, 'chk_apenas_mapa'): 
            self.dlg.chk_apenas_mapa.stateChanged.connect(self.disparar_preview_se_autorizado)
        
        if hasattr(self.dlg, 'btn_cor_fundo'): 
            self.dlg.btn_cor_fundo.colorChanged.connect(self.disparar_preview_se_autorizado)
        
        # --- NOVO: Conecta o gatilho da cor da página ---
        if hasattr(self.dlg, 'btn_cor_pagina'): 
            self.dlg.btn_cor_pagina.colorChanged.connect(self.disparar_preview_se_autorizado)
        
        # --- NOVO: Conecta as checkboxes de transparência à Preview ---
        if hasattr(self.dlg, 'chk_map_fundo_transp'):
            self.dlg.chk_map_fundo_transp.stateChanged.connect(self.disparar_preview_se_autorizado)
            
        if hasattr(self.dlg, 'chk_pag_fundo_transp'):
            self.dlg.chk_pag_fundo_transp.stateChanged.connect(self.disparar_preview_se_autorizado)
        # --------------------------------------------------------------

        # --- Botões de Informação (Central de Ajuda Flutuante) ---
        if hasattr(self.dlg, 'btn_presets'):
            self.dlg.btn_presets.clicked.connect(
                lambda: self.mostrar_info("presets", self.dlg.btn_presets)
            )

        if hasattr(self.dlg, 'btn_info_ddo_auto'):
            self.dlg.btn_info_ddo_auto.clicked.connect(
                lambda: self.mostrar_info("ddo_auto", self.dlg.btn_info_ddo_auto)
            )
            
        if hasattr(self.dlg, 'btn_info_ddo_fixa'):
            self.dlg.btn_info_ddo_fixa.clicked.connect(
                lambda: self.mostrar_info("ddo_fixa", self.dlg.btn_info_ddo_fixa)
            )
        
        # ====================================================================
        # NOVO: GATILHO DO BOTÃO DE INFO DO AGRUPAMENTO (ATLAS)
        # ====================================================================
        if hasattr(self.dlg, 'btn_info_agrupar'):
            self.dlg.btn_info_agrupar.clicked.connect(
                lambda: self.mostrar_info("agrupar", self.dlg.btn_info_agrupar)
            )
        # ====================================================================
        
        # --- GATILHOS DE DECORAÇÕES (Preview e UI Inteligente) ---
        
        # Função auxiliar para disparar as duas ações juntas
        def _ao_mudar_decoracao_principal():
            self.atualizar_estado_decoracoes()
            self.disparar_preview_se_autorizado()

        # Conecta as checkboxes principais
        checkboxes_principais = ['chk_legenda', 'chk_escala', 'chk_norte', 'chk_grid']
        for chk_name in checkboxes_principais:
            if hasattr(self.dlg, chk_name):
                getattr(self.dlg, chk_name).stateChanged.connect(_ao_mudar_decoracao_principal)
        
        # --- NOVO: Gatilho do GroupBox de Localização ---
        if hasattr(self.dlg, 'group_mapa_loc'):
            self.dlg.group_mapa_loc.toggled.connect(_ao_mudar_decoracao_principal)

        # Conecta os sub-controles (só disparam a preview)
        sub_controles_chk = ['chk_add_raster', 'chk_incluir_raster']
        for chk_name in sub_controles_chk:
            if hasattr(self.dlg, chk_name):
                getattr(self.dlg, chk_name).stateChanged.connect(self.disparar_preview_se_autorizado)
        
        # --- NOVO: Gatilhos dos RadioButtons de Localização ---
        if hasattr(self.dlg, 'rb_local_zoomout'):
            self.dlg.rb_local_zoomout.toggled.connect(self.atualizar_estado_decoracoes)
            self.dlg.rb_local_zoomout.toggled.connect(self.disparar_preview_se_autorizado)
            
        if hasattr(self.dlg, 'combo_local_zoomout'):
            self.dlg.combo_local_zoomout.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
        # ------------------------------------------------------

        for combo_name in ['comboLegenda', 'comboEscala', 'comboNorte', 'comboPosLoc', 'comboGrid']:
            if hasattr(self.dlg, combo_name):
                getattr(self.dlg, combo_name).currentIndexChanged.connect(self.disparar_preview_se_autorizado)
                
        if hasattr(self.dlg, 'combo_camada_loc'):
            self.dlg.combo_camada_loc.layerChanged.connect(self.disparar_preview_se_autorizado)

        # --- GATILHOS DE ATRIBUTOS E LAYOUT ---
        self.dlg.combo_presets.currentIndexChanged.connect(self._verificar_selecao_template)

        # --- 7. MONITORAMENTO EM MASSA PARA DISPARO DE PREVIEW ---
        widgets_preview = [
            self.dlg.combo_tamanho_pagina,
            self.dlg.rb_retrato, self.dlg.rb_paisagem,
            self.dlg.chk_modo_formulario, self.dlg.chk_modo_individual, self.dlg.chk_exibir_atributos
        ]
        
        if hasattr(self.dlg, 'rb_atlas_nenhum'):
            widgets_preview.extend([self.dlg.rb_atlas_nenhum, self.dlg.rb_atlas_geral, self.dlg.rb_atlas_agrupar])
            self.dlg.rb_atlas_nenhum.toggled.connect(self.atualizar_estado_filtro_feicoes)
            self.dlg.rb_atlas_geral.toggled.connect(self.atualizar_estado_filtro_feicoes)
            self.dlg.rb_atlas_agrupar.toggled.connect(self.atualizar_estado_filtro_feicoes)
            
        if hasattr(self.dlg, 'exp_atlas_agrupar'):
            self.dlg.exp_atlas_agrupar.fieldChanged.connect(self.disparar_preview_se_autorizado)

        if hasattr(self.dlg, 'exp_ddo_auto'):
            self.dlg.exp_ddo_auto.fieldChanged.connect(self.disparar_preview_se_autorizado)
            
        if hasattr(self.dlg, 'exp_ddo_fixa'):
            self.dlg.exp_ddo_fixa.fieldChanged.connect(self.disparar_preview_se_autorizado)
        
        for w in widgets_preview:
            if hasattr(w, 'currentIndexChanged'): w.currentIndexChanged.connect(self.disparar_preview_se_autorizado)
            elif hasattr(w, 'stateChanged'): w.stateChanged.connect(self.disparar_preview_se_autorizado)
            elif hasattr(w, 'toggled'): w.toggled.connect(self.disparar_preview_se_autorizado)
            
        # --- 8. GERENCIAMENTO DE REDIMENSIONAMENTO ---
        original_resize = self.dlg.resizeEvent
        def novo_resize_event(event):
            self.disparar_preview_se_autorizado()
            original_resize(event)
        self.dlg.resizeEvent = novo_resize_event

        # --- 9. ESTADO INICIAL ---
        self.configurar_escala_ao_mudar_camada()
        self.atualizar_hierarquia_camadas()
        self.atualizar_estado_decoracoes()


    
    
    def abrir_tutorial_youtube(self):
        """Abre o vídeo tutorial do YouTube no navegador padrão do usuário."""
        url = QUrl("https://www.youtube.com/watch?v=AIITugHD-_o")
        QDesktopServices.openUrl(url)




    def _configurar_janela(self):
        """Define ícones, comportamento do sistema operacional, restaura tamanho e centraliza."""
        self.dlg.setWindowIcon(QIcon(':/plugins/vector_to_map/icon.png'))
        self.dlg.setWindowTitle("VectorToMap")
        
        self.dlg.setWindowFlags(
            Qt.WindowType.Window | 
            Qt.WindowType.WindowMinimizeButtonHint | 
            Qt.WindowType.WindowMaximizeButtonHint | 
            Qt.WindowType.WindowCloseButtonHint
        )

        if hasattr(self.dlg, 'lbl_aviso_feicoes'):
            self.dlg.lbl_aviso_feicoes.hide()

        # Restore window geometry
        settings = QgsSettings()
        geometria = settings.value("/VectorToMap/geometry")
        if geometria: 
            self.dlg.restoreGeometry(geometria)
        else: 
            self.dlg.resize(1100, 750) 
        
        if hasattr(self.dlg, 'splitter'): 
            self.dlg.splitter.setSizes([600, 500]) 

        # Centralizar na Tela
        screen_geometry = QApplication.primaryScreen().availableGeometry()
        centro_tela = screen_geometry.center()
        geometria_janela = self.dlg.frameGeometry()
        geometria_janela.moveCenter(centro_tela)
        self.dlg.move(geometria_janela.topLeft())
    



    def preparar_exportacao(self):
        """Abre o diálogo, sugere o último diretório e configura a exportação."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return

        # --- RECUPERA O ÚLTIMO DIRETÓRIO ---
        settings = QgsSettings()
        ultimo_dir = settings.value("/VectorToMap/ultimo_diretorio_exportacao", os.path.expanduser("~"))

        # Monta um nome base amigável já no diretório correto
        nome_sugerido = os.path.join(ultimo_dir, f"Mapas_{camada.name()}")
        
        # 1. ADICIONAMOS O SVG AQUI NO FILTRO
        filtros = "PDF (*.pdf);;SVG (*.svg);;PNG (*.png);;JPEG (*.jpg)"

        caminho_arquivo, filtro_selecionado = QFileDialog.getSaveFileName(
            self.dlg, self.tr("Exportar Mapas"), nome_sugerido, filtros
        )

        if caminho_arquivo:
            # --- SALVA O DIRETÓRIO ESCOLHIDO PARA A PRÓXIMA VEZ ---
            diretorio_escolhido = os.path.dirname(caminho_arquivo)
            settings.setValue("/VectorToMap/ultimo_diretorio_exportacao", diretorio_escolhido)

            # 2. ADICIONAMOS A IDENTIFICAÇÃO DO SVG AQUI
            ext = ".pdf"
            if "PNG" in filtro_selecionado: ext = ".png"
            elif "JPEG" in filtro_selecionado: ext = ".jpg"
            elif "SVG" in filtro_selecionado: ext = ".svg" 

            # Adiciona a extensão se o usuário esqueceu de digitar
            if not caminho_arquivo.lower().endswith(ext):
                caminho_arquivo += ext

            # 3. FORÇAMOS INDIVIDUAL PARA SVG TAMBÉM
            is_individual = self.dlg.chk_export_individual.isChecked() or ext in [".png", ".jpg", ".svg"]

            # Armazena as configurações e dispara o gerador
            self.export_info = {
                'path': caminho_arquivo,
                'ext': ext,
                'individual': is_individual
            }
            self.processar_clique_ok()




    def limpar_clones_preview(self):
        """Remove da memória os clones gerados invisivelmente para a preview (Baseado 100% em Custom Properties)."""
        
        # 1. Limpeza pelo rastreio em memória (O método original rápido)
        if hasattr(self, 'clones_preview') and self.clones_preview:
            QgsProject.instance().removeMapLayers(self.clones_preview)
            self.clones_preview.clear()
            
        if hasattr(self, 'engine') and self.engine.clones_preview:
            QgsProject.instance().removeMapLayers(self.engine.clones_preview)
            self.engine.clones_preview.clear()

        # ====================================================================
        # 2. A CURA DEFINITIVA 200% SEGURA (Varredura Ativa EXCLUSIVA pelo Carimbo)
        # ====================================================================
        camadas_para_deletar = []
        for layer_id, layer in QgsProject.instance().mapLayers().items():
            
            # Checa EXCLUSIVAMENTE se a camada tem o nosso carimbo secreto no metadado
            if layer.customProperty("vtm_is_preview_temp"):
                camadas_para_deletar.append(layer_id)
                
        if camadas_para_deletar:
            QgsProject.instance().removeMapLayers(camadas_para_deletar)




    def atualizar_hierarquia_camadas(self):
        """Gerencia travas de interface e exibe avisos sobre camadas temporárias."""
        # 1. Se o motor já estiver rodando, não mexe em nada para não causar conflitos
        if self.is_rendering:
            return

        filtrar = self.dlg.chk_filtrar_feicoes.isChecked()
        exibir_atual = self.dlg.chk_exibir_so_camada_atual.isChecked()

        # 2. Bloqueia sinais dos checkboxes de trava para que eles não disparem 
        # a preview múltiplas vezes enquanto mudamos o estado deles via código
        self.dlg.chk_travar_camadas.blockSignals(True)
        self.dlg.chk_travar_estilos.blockSignals(True)

        try:
            # 3. Lógica de Dependência: Se for filtrar ou isolar camada, 
            # a trava de camadas PRECISA estar ativa e o usuário não pode desmarcar.
            if filtrar or exibir_atual:
                self.dlg.chk_travar_camadas.setChecked(True)
                self.dlg.chk_travar_camadas.setEnabled(False)
            else:
                self.dlg.chk_travar_camadas.setEnabled(True)

            # 4. Travar Estilos só faz sentido se as camadas estiverem travadas
            travado = self.dlg.chk_travar_camadas.isChecked()
            self.dlg.chk_travar_estilos.setEnabled(travado)
            
            # Se a trava principal cair, a de estilos cai junto
            if not travado:
                self.dlg.chk_travar_estilos.setChecked(False)
                
        finally:
            # 5. O SEGURO DE VIDA: Libera os sinais novamente, MESMO SE DER ERRO ACIMA
            self.dlg.chk_travar_camadas.blockSignals(False)
            self.dlg.chk_travar_estilos.blockSignals(False)
        
        # --- 5. GESTÃO DO AVISO VISUAL ---
        # Exibe o aviso se o filtro de feições estiver ativo (criação de temps)
        if hasattr(self.dlg, 'lbl_aviso_feicoes'):
            if filtrar:
                self.dlg.lbl_aviso_feicoes.show()
                # Texto explicativo para o usuário não estranhar lentidão ou camadas extras
                self.dlg.lbl_aviso_feicoes.setText(self.tr("⚠️ O filtro de feições gera camadas temporárias internas para o layout."))
            else:
                self.dlg.lbl_aviso_feicoes.hide()
        
        # 6. Dispara a atualização da preview respeitando o timer
        self.disparar_preview_se_autorizado()
    



    def atualizar_estado_filtro_feicoes(self):
        """Bloqueia os filtros e o widget Epsilon dependendo do Radio Button escolhido."""
        if not hasattr(self.dlg, 'rb_atlas_geral'): return
        
        is_geral = self.dlg.rb_atlas_geral.isChecked()
        is_agrupar = self.dlg.rb_atlas_agrupar.isChecked()
        
        # 1. Trava do Filtro de Feições e Atributos (Se for mapa Geral, desliga tudo)
        if is_geral:
            self.dlg.chk_filtrar_feicoes.setChecked(False)
            self.dlg.chk_filtrar_feicoes.setEnabled(False)
            if hasattr(self.dlg, 'groupAtributos'): self.dlg.groupAtributos.setEnabled(False)
        else:
            self.dlg.chk_filtrar_feicoes.setEnabled(True)
            if hasattr(self.dlg, 'groupAtributos'): self.dlg.groupAtributos.setEnabled(True)
            
        # 2. Liga/Desliga o botão de Expressão (Epsilon)
        if hasattr(self.dlg, 'exp_atlas_agrupar'):
            self.dlg.exp_atlas_agrupar.setEnabled(is_agrupar)
    



    def atualizar_estado_travar_estilos(self):
        """Habilita o 'Travar Estilos' apenas se o 'Travar Camadas' estiver marcado."""
        
        travado = self.dlg.chk_travar_camadas.isChecked()
        self.dlg.chk_travar_estilos.setEnabled(travado)
        
        # Se o 'Travar Camadas' for desmarcado, garantimos que o de estilos desmarque também
        if not travado:
            self.dlg.chk_travar_estilos.blockSignals(True)
            self.dlg.chk_travar_estilos.setChecked(False)
            self.dlg.chk_travar_estilos.blockSignals(False)




    def setup_ui_strings(self):
        """Centraliza todos os Tooltips e textos da interface para tradução."""

        self.dlg.chk_modo_formulario.setToolTip(self.tr("Exibe todos os atributos em um bloco único de texto (HTML)."))
        self.dlg.chk_modo_individual.setToolTip(self.tr("Cria uma linha horizontal para cada feição (evita sobreposições)."))
        self.dlg.chk_selecionar_todos.setToolTip(self.tr("Marca ou desmarca todos os campos da lista de uma vez."))
        self.dlg.combo_presets.setToolTip(self.tr("Define o tamanho do mapa na folha (ex: 75% da página ou Quadrado)."))
        self.btn_render.setToolTip(self.tr("Gera uma prévia do layout com as configurações atuais."))
        self.btn_export.setToolTip(self.tr("Exporta os mapas diretamente como PDF, PNG ou JPG."))
        if hasattr(self.dlg, 'group_mapa_loc'):
            self.dlg.group_mapa_loc.setToolTip(self.tr("Adiciona um mapa de enquadramento geral no canto da folha."))




    def cancelar_e_fechar(self):
        """Gerencia se o clique no Cancelar deve parar o motor ou fechar a janela."""

        self.limpar_clones_preview()

        if self.is_rendering:
            # Se estiver processando, apenas levantamos a bandeira de parada
            self.engine.abort_processing = True
            self.iface.messageBar().pushMessage(
                self.tr("Aviso"), 
                self.tr("Cancelando processamento... A janela permanecerá aberta."), 
                level=Qgis.MessageLevel.Success, 
                duration=3
            )
        else:
            # Se não houver nada rodando, o comportamento de fechar é seguro
            self.dlg.reject()
    



    def abrir_gumroad(self):
        """Abre a página de doação para apoiar o projeto."""
        url_doacao = "https://buymeacoffee.com/matheusdurso" 
        QDesktopServices.openUrl(QUrl(url_doacao))




    def abrir_github(self):
        """Abre a página de Issues do GitHub para relatar bugs."""
        url_github = "https://github.com/matheusdurso/VectorToMap/issues"
        QDesktopServices.openUrl(QUrl(url_github))
    



    def ir_para_suporte(self):
        """Muda a aba ativa para a página de Desenvolvimento e Suporte."""
        if hasattr(self.dlg, 'tabWidget'):
            # setCurrentIndex(1) muda para a segunda aba. 
            # Se a aba de suporte for a terceira no seu layout, mude para 2.
            self.dlg.tabWidget.setCurrentIndex(1)
    



    def _verificar_selecao_template(self):
        """Abre o seletor de arquivos se o usuário escolher carregar um template externo e tranca a interface."""
        if not hasattr(self, 'dlg') or not self.dlg or sip.isdeleted(self.dlg) or not hasattr(self.dlg, 'combo_presets'): return
        
        # Lemos a opção clicada
        dados_atuais = self.dlg.combo_presets.currentData()
        
        if dados_atuais == "carregar_template":
            # Bloqueia os sinais para não criar um loop ao adicionar novos itens
            self.dlg.combo_presets.blockSignals(True)
            
            try:
                caminho, _ = QFileDialog.getOpenFileName(
                    self.dlg, 
                    self.tr("Selecionar Layout do QGIS"), 
                    "", 
                    "QGIS Template (*.qpt)"
                )
                
                if caminho:
                    # O usuário escolheu um arquivo! Vamos deixar o nome bonito
                    nome_arquivo = os.path.basename(caminho).replace('.qpt', '').replace('_', ' ').title()
                    nome_amigavel = f"📂 {nome_arquivo}"
                    
                    # Insere o novo arquivo logo ACIMA da linha divisória
                    idx_insercao = self.dlg.combo_presets.count() - 2
                    self.dlg.combo_presets.insertItem(idx_insercao, nome_amigavel, caminho)
                    
                    # Seleciona o item que ele acabou de carregar
                    self.dlg.combo_presets.setCurrentIndex(idx_insercao)
                    dados_atuais = caminho # Atualiza para a trava de UI
                else:
                    # Se ele cancelar a janela (clicar no X), voltamos para o primeiro mapa da lista
                    self.dlg.combo_presets.setCurrentIndex(0)
                    dados_atuais = self.dlg.combo_presets.currentData()
            finally:
                # O SEGURO: A interface sempre volta a responder, mesmo se o QFileDialog falhar
                self.dlg.combo_presets.blockSignals(False)

        # ====================================================================
        # TRAVA INTELIGENTE DA INTERFACE (A MÁGICA DA UX)
        # ====================================================================
        is_template = str(dados_atuais).endswith('.qpt') or str(dados_atuais) == "template"
        
        # Lista dos widgets visuais que queremos trancar (ignora layouts invisíveis para evitar bugs)
        widgets_visiveis = (QComboBox, QCheckBox, QRadioButton, QLineEdit, QSpinBox, QDoubleSpinBox, QPushButton, QLabel)

        # 1. Desativar itens no groupBox_2 (Papel e Fundo)
        if hasattr(self.dlg, 'groupBox_2'):
            for child in self.dlg.groupBox_2.findChildren(widgets_visiveis):
                # Mantém o label, combo_presets e btn_presets intocáveis
                nome = child.objectName()
                if 'preset' in nome or nome == 'label':
                    continue
                child.setEnabled(not is_template)

        # 2. Desativar todo o groupAtributos
        if hasattr(self.dlg, 'groupAtributos'):
            self.dlg.groupAtributos.setEnabled(not is_template)
            
            # Se voltarmos pro Modelo Matemático (is_template = False), 
            # garantimos que o grupo não fique ativado à força se a opção de "Zoom Camada" estiver ligada.
            if not is_template:
                self.atualizar_estado_filtro_feicoes()

        # 3. Desativar itens no groupBox_5 (Decorações)
        if hasattr(self.dlg, 'groupBox_5'):
            for child in self.dlg.groupBox_5.findChildren(widgets_visiveis):
                # Como o mapa de localização não mora mais aqui, bloqueamos tudo sem dó!
                child.setEnabled(not is_template)

        # Dispara a preview visual para atualizar a tela
        self.disparar_preview_se_autorizado()
    



    #################################################################################
    # --- 3. GESTÃO DE ATRIBUTOS E WIDGETS ---
    #################################################################################

    def atualizar_lista_atributos(self):
        """Reconstrói a lista de atributos com filtro e proteção de reentrada."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada:
            self.dlg.tableWidget.setRowCount(0) # Limpa se não houver camada
            return

        # PROTEÇÃO DE LOOP: Se a camada não mudou de verdade (é apenas preview), ignora
        if camada.id() == self.ultimo_id_camada_ativa:
            return
        self.ultimo_id_camada_ativa = camada.id()

        # 1. Armazena seleção atual para não desmarcar as colunas do usuário
        marcados_antes = [cb.text() for cb in self.colunas_atuais if not sip.isdeleted(cb) and cb.isChecked()]

        # 2. Gestão do campo de busca (QLineEdit)
        if not hasattr(self, 'edit_busca_atributos') or sip.isdeleted(self.edit_busca_atributos):
            self.edit_busca_atributos = QLineEdit(self.dlg)
            self.edit_busca_atributos.setPlaceholderText(self.tr("🔍 Filtrar colunas..."))
            self.edit_busca_atributos.setClearButtonEnabled(True)
            self.edit_busca_atributos.textChanged.connect(self.filtrar_widgets_atributos)
            
            # INJEÇÃO DIRETA NO NOVO NOME DO GRUPO
            if hasattr(self.dlg, 'groupAtributos') and self.dlg.groupAtributos.layout():
                self.dlg.groupAtributos.layout().insertWidget(0, self.edit_busca_atributos)

        # 3. Reconstrói as colunas fisicamente
        self.atualizar_lista_colunas(camada)

        # 4. RESTAURA A SELEÇÃO: Não deixa os filtros de feição desmarcarem as colunas
        for cb in self.colunas_atuais:
            if cb.text() in marcados_antes:
                cb.blockSignals(True)
                cb.setChecked(True)
                cb.blockSignals(False)
        
        # Força o checkbox "Selecionar Todos" a fazer a leitura real da tela 
        # logo após a restauração. Se a nova camada tiver colunas diferentes, ele desmarca.
        self.verificar_estado_selecionar_todos()

        self.atualizar_estado_modos_exibicao()
        self.atualizar_estado_filtro_feicoes()


    def atualizar_lista_colunas(self, camada):
        """Reconstrói a grade de colunas forçando o QGridLayout no container interno."""
        container = self.dlg.scrollAreaWidgetContents
        
        # Garante que o layout seja Grid (conforme o TypeError anterior)
        if container.layout() and not isinstance(container.layout(), QGridLayout):
            QWidget().setLayout(container.layout()) 
        
        if not container.layout():
            layout = QGridLayout(container)
            container.setLayout(layout)
        else:
            layout = container.layout()

        # Limpa widgets antigos
        while layout.count():
            item = layout.takeAt(0)
            if item.widget(): item.widget().deleteLater()

        layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        self.colunas_atuais = [] 
        campos = list(camada.fields())
        
        for idx, campo in enumerate(campos):
            nome = campo.name()
            cb = QCheckBox(nome)
            cb.stateChanged.connect(self.atualizar_tabela_por_selecao)                    
            cb.stateChanged.connect(self.verificar_estado_selecionar_todos)
            cb.stateChanged.connect(self.atualizar_estado_modos_exibicao)
            cb.stateChanged.connect(self.disparar_preview_se_autorizado)
            
            self.colunas_atuais.append(cb)
            layout.addWidget(cb, idx // 3, idx % 3)

    
    def _organizar_grid_atributos(self):
        """Distribui os widgets VISÍVEIS no grid interno da scrollArea."""
        # Usamos o container que se provou estável nos testes anteriores
        container = self.dlg.scrollAreaWidgetContents
        layout = container.layout()
        
        if not layout: 
            return
        
        # Remove do layout sem deletar os objetos (importante para o filtro)
        for i in reversed(range(layout.count())):
            layout.takeAt(i)

        # Filtra apenas os CheckBoxes que devem aparecer
        visiveis = [cb for cb in self.colunas_atuais if not cb.isHidden()]
        
        for idx, cb in enumerate(visiveis):
            row = idx // 3 # Mantém as 3 colunas
            col = idx % 3
            layout.addWidget(cb, row, col)


    def filtrar_widgets_atributos(self, texto):
        """Esconde os CheckBoxes que não batem com a busca e reorganiza o grid."""
        if not hasattr(self, 'colunas_atuais'): 
            return
            
        texto = texto.lower().strip()
        for cb in self.colunas_atuais:
            # Se bater com a busca ou a busca estiver vazia, fica visível
            cb.setVisible(texto in cb.text().lower() if texto else True)
        
        # Chama a função acima para "empurrar" os resultados para o topo
        self._organizar_grid_atributos()


    def marcar_desmarcar_todos(self, state):
        """Marca ou desmarca todas as colunas e garante a ativação da interface."""
        check = (state == Qt.CheckState.Checked)
        if not self.colunas_atuais: return

        self.is_rendering = True
        for cb in self.colunas_atuais:
            cb.blockSignals(True)
            cb.setChecked(check)
            cb.blockSignals(False)
        self.is_rendering = False

        # 1. Atualiza a tabela visual
        self.atualizar_tabela_por_selecao()

        # 2. O PULO DO GATO: Chama manualmente a ativação dos modos de exibição
        # Isso vai ativar os botões de Formulário/Individual e marcar o Formulário
        self.atualizar_estado_modos_exibicao()

        # 3. Reseta o cache para os rótulos aparecerem
        self.preview_data_cache = None
        self.ultimo_id_camada = None 

        # 4. Só dispara se estiver no modo automático ou manual (via botão)
        self.disparar_preview_se_autorizado()
    

    def verificar_estado_selecionar_todos(self):
        """O mestre só fica marcado se 100% dos filhos estiverem marcados."""
        if not hasattr(self, 'colunas_atuais') or not self.colunas_atuais:
            return
            
        total = len(self.colunas_atuais)
        marcados = sum(1 for cb in self.colunas_atuais if cb.isChecked())
        
        self.dlg.chk_selecionar_todos.blockSignals(True)
        # Lógica booleana: marcado se (total > 0 E marcados for igual ao total)
        self.dlg.chk_selecionar_todos.setChecked(total > 0 and marcados == total)
        self.dlg.chk_selecionar_todos.blockSignals(False)


    def atualizar_estado_modos_exibicao(self):
        """Ativa os modos de exibição e garante que um deles esteja marcado."""
        # Verifica se existe pelo menos uma coluna marcada
        tem_selecao = any(cb.isChecked() for cb in self.colunas_atuais)
        
        self.dlg.chk_modo_formulario.setEnabled(tem_selecao)
        self.dlg.chk_modo_individual.setEnabled(tem_selecao)
        
        if tem_selecao:
            # Se nada estiver marcado, força o Formulário como padrão
            if not self.dlg.chk_modo_formulario.isChecked() and not self.dlg.chk_modo_individual.isChecked():
                self.dlg.chk_modo_formulario.blockSignals(True)
                try:
                    self.dlg.chk_modo_formulario.setChecked(True)
                finally:
                    self.dlg.chk_modo_formulario.blockSignals(False)
        else:
            # Se desmarcar tudo, desmarca os modos também
            self.dlg.chk_modo_formulario.blockSignals(True)
            self.dlg.chk_modo_individual.blockSignals(True)
            try:
                self.dlg.chk_modo_formulario.setChecked(False)
                self.dlg.chk_modo_individual.setChecked(False)
            finally:
                self.dlg.chk_modo_formulario.blockSignals(False)
                self.dlg.chk_modo_individual.blockSignals(False)

    


    def atualizar_estado_decoracoes(self):
        """Habilita ou desabilita os controles das decorações com base nas checkboxes principais e trava de template."""
        if not self.dlg: return
        
        # ====================================================================
        # 1. CONTROLES DO MAPA DE LOCALIZAÇÃO (Respeitando o group_mapa_loc)
        # ====================================================================
        if hasattr(self.dlg, 'group_mapa_loc'):
            # Lemos se a "chave geral" do grupo está ligada
            is_loc_ativo = self.dlg.group_mapa_loc.isChecked()
            
            if hasattr(self.dlg, 'rb_local_zoomout'):
                is_modo_zoom = self.dlg.rb_local_zoomout.isChecked()
                
                # A MÁGICA 1: Só ativa a combo se o modo estiver marcado E o grupo todo estiver ativo
                if hasattr(self.dlg, 'combo_local_zoomout'):
                    self.dlg.combo_local_zoomout.setEnabled(is_loc_ativo and is_modo_zoom)
                if hasattr(self.dlg, 'combo_camada_loc'):
                    self.dlg.combo_camada_loc.setEnabled(is_loc_ativo and not is_modo_zoom)

        # ====================================================================
        # A MÁGICA 2: TRAVA DE TEMPLATES (O Freio)
        # Protege o groupBox_5 contra reativação indevida pelos sinais
        # ====================================================================
        if hasattr(self.dlg, 'combo_presets'):
            is_template = str(self.dlg.combo_presets.currentData()).endswith('.qpt')
            if is_template:
                return # Aborta a função aqui! Deixa a _verificar_selecao_template manter tudo trancado.

        # ====================================================================
        # 2. CONTROLES DA LEGENDA
        # ====================================================================
        if hasattr(self.dlg, 'chk_legenda'):
            is_legenda_ativa = self.dlg.chk_legenda.isChecked()
            if hasattr(self.dlg, 'chk_add_raster'):
                self.dlg.chk_add_raster.setEnabled(is_legenda_ativa)
            if hasattr(self.dlg, 'comboLegenda'):
                self.dlg.comboLegenda.setEnabled(is_legenda_ativa)

        # ====================================================================
        # 3. CONTROLES DO NORTE
        # ====================================================================
        if hasattr(self.dlg, 'chk_norte'):
            is_norte_ativo = self.dlg.chk_norte.isChecked()
            if hasattr(self.dlg, 'comboNorte'):
                self.dlg.comboNorte.setEnabled(is_norte_ativo)
            if hasattr(self.dlg, 'combo_estilo_norte'):
                self.dlg.combo_estilo_norte.setEnabled(is_norte_ativo)

        # ====================================================================
        # 4. CONTROLES DA ESCALA
        # ====================================================================
        if hasattr(self.dlg, 'chk_escala'):
            is_escala_ativa = self.dlg.chk_escala.isChecked()
            if hasattr(self.dlg, 'comboEscala'):
                self.dlg.comboEscala.setEnabled(is_escala_ativa)

        # ====================================================================
        # 5. CONTROLES DA GRADE (GRID)
        # ====================================================================
        if hasattr(self.dlg, 'chk_grid'):
            is_grid_ativo = self.dlg.chk_grid.isChecked()
            if hasattr(self.dlg, 'comboGrid'):
                self.dlg.comboGrid.setEnabled(is_grid_ativo)




    def atualizar_tabela_por_selecao(self):
        """Sincroniza a tabela de amostra com os checkboxes reais da memória."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        tabela = self.dlg.tableWidget
        if not camada: return
        
        # Pega os nomes apenas dos checkboxes que estão na lista self.colunas_atuais
        colunas = [cb.text() for cb in self.colunas_atuais if cb.isChecked()]
        
        tabela.clear()
        tabela.setColumnCount(len(colunas))
        tabela.setHorizontalHeaderLabels(colunas)
        
        if not colunas:
            tabela.setRowCount(0)
            return
            
        # Puxa 5 linhas de exemplo
        num_linhas = min(5, camada.featureCount())
        tabela.setRowCount(num_linhas)
        for i, feicao in enumerate(camada.getFeatures(QgsFeatureRequest().setLimit(num_linhas))):
            for j, col in enumerate(colunas):
                tabela.setItem(i, j, QTableWidgetItem(str(feicao[col] if feicao[col] != NULL else "")))
        tabela.resizeColumnsToContents()




    #####################################################################################
    # --- 4. LÓGICA GEOGRÁFICA E ESCALA ---
    #####################################################################################

    def configurar_escala_ao_mudar_camada(self):
        """Inicia atualização apenas se a camada mudar de verdade."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return

        # --- Diz aos widgets de expressão qual camada ler as colunas ---
        if hasattr(self.dlg, 'exp_ddo_auto'):
            self.dlg.exp_ddo_auto.setLayer(camada)
        if hasattr(self.dlg, 'exp_ddo_fixa'):
            self.dlg.exp_ddo_fixa.setLayer(camada)

        if hasattr(self.dlg, 'exp_atlas_agrupar'):
            self.dlg.exp_atlas_agrupar.setLayer(camada)
        
        # Só atualiza a interface se a camada escolhida for realmente nova
        if camada.id() != self.ultimo_id_camada_ativa:
            # ================================================================
            # NOVO: SINCRONIZAÇÃO UNIDIRECIONAL DA CAMADA DE LOCALIZAÇÃO
            # ================================================================
            if hasattr(self.dlg, 'combo_camada_loc'):
                # Bloqueia os sinais para não disparar uma preview dupla na tela
                self.dlg.combo_camada_loc.blockSignals(True)
                self.dlg.combo_camada_loc.setLayer(camada)
                self.dlg.combo_camada_loc.blockSignals(False)
            # ================================================================

            # ================================================================
            # A CURA DO WIDGET VAZIO (RESTAURADA E BLINDADA)
            # ================================================================
            if hasattr(self.dlg, 'exp_atlas_agrupar') and camada.fields().count() > 0:
                primeira_coluna = camada.fields().at(0).name()
                try:
                    self.dlg.exp_atlas_agrupar.blockSignals(True)
                    self.dlg.exp_atlas_agrupar.setField(primeira_coluna)
                finally:
                    # O 'finally' garante que os sinais SEMPRE sejam reativados!
                    self.dlg.exp_atlas_agrupar.blockSignals(False)
            # ================================================================

            self.dlg.tableWidget.setRowCount(0)
            self.dlg.tableWidget.setColumnCount(0)
            self.dlg.tableWidget.clear()

            self.dlg.rb_escala_auto.setChecked(True)
            self.atualizar_lista_atributos()
            self.validar_geometria_escala()




    def validar_geometria_escala(self):
        """Gerencia a liberação dos botões de escala e as caixas de expressão."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return

        self.dlg.combo_escala_fixa.blockSignals(True)
        
        try:
            is_auto = self.dlg.rb_escala_auto.isChecked()
            is_fixa = self.dlg.rb_escala_fixa.isChecked()

            self.dlg.rb_escala_auto.setEnabled(True)
            
            # --- 1. LÓGICA DO ZOOM AUTOMÁTICO (comboZoomOut e exp_ddo_auto) ---
            if hasattr(self.dlg, 'comboZoomOut'):
                self.dlg.comboZoomOut.setEnabled(is_auto)
                usar_exp_auto = (self.dlg.comboZoomOut.currentData() == "expressao")
            else:
                usar_exp_auto = True # Fallback de segurança

            if hasattr(self.dlg, 'exp_ddo_auto'):
                self.dlg.exp_ddo_auto.setEnabled(is_auto and usar_exp_auto)

            # --- 2. LÓGICA DA ESCALA FIXA (combo_escala_fixa e exp_ddo_fixa) ---
            usar_exp_fixa = False
            if hasattr(self.dlg, 'combo_escala_fixa'):
                self.dlg.combo_escala_fixa.setEnabled(is_fixa)
                usar_exp_fixa = (self.dlg.combo_escala_fixa.currentData() == "expressao")
                
            if hasattr(self.dlg, 'exp_ddo_fixa'):
                self.dlg.exp_ddo_fixa.setEnabled(is_fixa and usar_exp_fixa)

        finally:
            self.dlg.combo_escala_fixa.blockSignals(False)
            
        self.disparar_preview_se_autorizado()
    



    ####################################################################################
    # --- 5. RENDERIZAÇÃO E LAYOUT ---
    ####################################################################################

    def obter_dados_preview(self):
        """Atualiza o cache de feições para a preview."""
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada:
            self.preview_data_cache = None
            return None

        # Pega a regra dos Radio Buttons pelo config gerado
        config = self._gerar_config_atual()
        campo_atlas = config.get('campo_atlas')
        
        if (self.preview_data_cache is None or 
            self.ultimo_id_camada != camada.id() or 
            self.cache_val_atlas != campo_atlas):
            
            self.ultimo_id_camada = camada.id()
            self.cache_val_atlas = campo_atlas
            
            request_amostra = QgsFeatureRequest().setLimit(1)
            sample_f = next(camada.getFeatures(request_amostra), None)
            
            if sample_f:
                if campo_atlas is None:
                    self.preview_data_cache = [sample_f]
                elif campo_atlas == "__ALL_FEATURES__":
                    self.preview_data_cache = ["ALL"]
                else:
                    # MAGIA: Lê e executa a expressão matemática para a amostra
                    exp = QgsExpression(campo_atlas)
                    
                    # --- A CORREÇÃO ESTÁ NESTAS DUAS LINHAS ---
                    contexto = QgsExpressionContext()
                    contexto.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(camada))
                    # ------------------------------------------
                    
                    exp.prepare(contexto)
                    contexto.setFeature(sample_f)
                    val_amostra = exp.evaluate(contexto)
                    
                    if val_amostra is None or val_amostra == NULL:
                        filtro = f'({campo_atlas}) IS NULL'
                    else:
                        filtro = f'({campo_atlas}) = {QgsExpression.quotedValue(val_amostra)}'
                        
                    request_filtrado = QgsFeatureRequest().setFilterExpression(filtro)
                    self.preview_data_cache = list(camada.getFeatures(request_filtrado))
            else:
                self.preview_data_cache = None
        
        return self.preview_data_cache

    


    def _gerar_config_atual(self, feicoes_amostra=None):
        """Centraliza a criação do dicionário para Exportação e Preview."""
        container = self.dlg.scrollAreaWidgetContents
        colunas_selecionadas = [cb.text() for cb in container.findChildren(QCheckBox) if cb.isChecked()]
        
        # Lê a escolha do usuário baseada nos Radio Buttons
        campo_atlas = None
        if hasattr(self.dlg, 'rb_atlas_geral') and self.dlg.rb_atlas_geral.isChecked():
            campo_atlas = "__ALL_FEATURES__"
        elif hasattr(self.dlg, 'rb_atlas_agrupar') and self.dlg.rb_atlas_agrupar.isChecked():
            campo_atlas = self.dlg.exp_atlas_agrupar.asExpression()
        
        return {
            'preset': self.dlg.combo_presets.currentData(),
            'orientacao': "Retrato" if self.dlg.rb_retrato.isChecked() else "Paisagem",
            'tamanho_pg': self.dlg.combo_tamanho_pagina.currentData(),
            'campo_atlas': campo_atlas,
            'cor_pagina': self.dlg.btn_cor_pagina.color() if hasattr(self.dlg, 'btn_cor_pagina') else QColor(255, 255, 255),
            'cor_fundo': self.dlg.btn_cor_fundo.color() if hasattr(self.dlg, 'btn_cor_fundo') else QColor(255, 255, 255),
            'apenas_mapa': self.dlg.chk_apenas_mapa.isChecked() if hasattr(self.dlg, 'chk_apenas_mapa') else False,
            'texto_titulo': self.dlg.lineEdit_titulo.text() if hasattr(self.dlg, 'lineEdit_titulo') else '',
            'fonte_titulo': getattr(self, 'fonte_titulo', QFont()),
            'alinhamento_titulo': self.dlg.combo_alinhamento_titulo.currentData() if hasattr(self.dlg, 'combo_alinhamento_titulo') else Qt.AlignmentFlag.AlignHCenter,
            'escala_fixa': self.dlg.rb_escala_fixa.isChecked(),
            'escala_val': self.dlg.combo_escala_fixa.currentData(),
            'filtrar_feicoes': self.dlg.chk_filtrar_feicoes.isChecked(),
            'exibir_so_camada_atual': self.dlg.chk_exibir_so_camada_atual.isChecked(),
            'travar_camadas': self.dlg.chk_travar_camadas.isChecked(),
            'travar_estilos': self.dlg.chk_travar_estilos.isChecked() if hasattr(self.dlg, 'chk_travar_estilos') else False,
            'colunas': colunas_selecionadas,
            'exibir_atributos': self.dlg.chk_exibir_atributos.isChecked() if hasattr(self.dlg, 'chk_exibir_atributos') else True,
            'modo_formulario': self.dlg.chk_modo_formulario.isChecked(),
            'modo_individual': self.dlg.chk_modo_individual.isChecked(),
            'numeracao': self.dlg.chk_numeracao.isChecked(),
            'escala_auto_exp': self.dlg.exp_ddo_auto.asExpression() if hasattr(self.dlg, 'exp_ddo_auto') and self.dlg.rb_escala_auto.isChecked() else "",
            'escala_fixa_exp': self.dlg.exp_ddo_fixa.asExpression() if hasattr(self.dlg, 'exp_ddo_fixa') and self.dlg.rb_escala_fixa.isChecked() else "",
            'map_fundo_transp': self.dlg.chk_map_fundo_transp.isChecked() if hasattr(self.dlg, 'chk_map_fundo_transp') else False,
            'pag_fundo_transp': self.dlg.chk_pag_fundo_transp.isChecked() if hasattr(self.dlg, 'chk_pag_fundo_transp') else False,
            'inserir_escala': self.dlg.chk_escala.isChecked() if hasattr(self.dlg, 'chk_escala') else False,
            'inserir_norte': self.dlg.chk_norte.isChecked() if hasattr(self.dlg, 'chk_norte') else False,
            'inserir_legenda': self.dlg.chk_legenda.isChecked() if hasattr(self.dlg, 'chk_legenda') else False,
            'add_raster_legend': self.dlg.chk_add_raster.isChecked() if hasattr(self.dlg, 'chk_add_raster') else False,
            'pos_legenda': self.dlg.comboLegenda.currentData() if hasattr(self.dlg, 'comboLegenda') else "IE",
            'pos_escala': self.dlg.comboEscala.currentData() if hasattr(self.dlg, 'comboEscala') else "SE",
            'pos_norte': self.dlg.comboNorte.currentData() if hasattr(self.dlg, 'comboNorte') else "SD",
            'inserir_grade': self.dlg.chk_grid.isChecked() if hasattr(self.dlg, 'chk_grid') else False,
            'estilo_grade': self.dlg.comboGrid.currentData() if hasattr(self.dlg, 'comboGrid') else "solido",
            'inserir_mapa_loc': self.dlg.group_mapa_loc.isChecked() if hasattr(self.dlg, 'group_mapa_loc') else False,
            'add_raster_loc': self.dlg.chk_incluir_raster.isChecked() if hasattr(self.dlg, 'chk_incluir_raster') else False,
            'camada_loc': self.dlg.combo_camada_loc.currentLayer() if hasattr(self.dlg, 'combo_camada_loc') else None,
            'pos_mapa_loc': self.dlg.comboPosLoc.currentData() if hasattr(self.dlg, 'comboPosLoc') else "ID",
            'zoom_out_auto': self.dlg.comboZoomOut.currentData() if hasattr(self.dlg, 'comboZoomOut') else 25.0,
            'estilo_norte': self.dlg.combo_estilo_norte.currentData() if hasattr(self.dlg, 'combo_estilo_norte') else "NorthArrow_02.svg",
            'modo_local': "zoom" if (hasattr(self.dlg, 'rb_local_zoomout') and self.dlg.rb_local_zoomout.isChecked()) else "camada",
            'fator_zoom_local': self.dlg.combo_local_zoomout.currentData() if hasattr(self.dlg, 'combo_local_zoomout') else 10.0
        }


    
    
    def atualizar_preview(self):
        """Gera a preview. Chamada pelo Timer (auto) ou pelo Botão (manual)."""
        if self.is_rendering or not hasattr(self, 'dlg') or not self.dlg or sip.isdeleted(self.dlg) or not self.dlg.isVisible():
            return

        self.is_rendering = True
        
        layout = None 
        try:
            self.dlg.previewProgressBar.show() 
            self.dlg.previewProgressBar.setRange(0, 0) 
            QCoreApplication.processEvents()

            self.dlg.mMapLayerComboBox.blockSignals(True)
            self.limpar_clones_preview()
            
            feicoes = self.obter_dados_preview()
            camada = self.dlg.mMapLayerComboBox.currentLayer()

            if feicoes and camada:
                layout = QgsPrintLayout(QgsProject.instance())
                layout.initializeDefaults()
                
                # ================================================================
                # PUXA O CONFIG AQUI! 
                # Ele já sabe ler os seus novos Radio Buttons e Fórmulas!
                # ================================================================
                container = self.dlg.scrollAreaWidgetContents
                colunas_selecionadas = [cb.text() for cb in container.findChildren(QCheckBox) if cb.isChecked()]
                config = self._gerar_config_atual()
                
                campo_atlas = config.get('campo_atlas')
                nome_sufixo_preview = "Preview"

                # Define um sufixo genérico e seguro para as camadas clones na RAM
                if campo_atlas == "__ALL_FEATURES__":
                    nome_sufixo_preview = "Geral"
                elif campo_atlas is not None:
                    # Como agora o campo_atlas pode ser uma equação SQL inteira, 
                    # evitamos calcular o nome e apenas marcamos como 'Filtro'
                    nome_sufixo_preview = "Filtro"

                # --- MANDA O MOTOR DESENHAR A PREVIEW ---
                self.engine.montar_design_da_pagina(
                    layout, camada, feicoes, config['preset'], config['orientacao'], config,
                    pagina_index=0, is_preview=True, nome_sufixo=nome_sufixo_preview
                )

                layout.refresh() 

                exporter = QgsLayoutExporter(layout)
                # A MÁGICA ACONTECE AQUI: 72 DPI reduz o peso de renderização em 90%!
                image = exporter.renderPageToImage(0, QSize(), 72.0)
                if not image.isNull():
                    self.dlg.lbl_preview.setPixmap(QPixmap.fromImage(image).scaled(
                        self.dlg.lbl_preview.size(), 
                        Qt.AspectRatioMode.KeepAspectRatio, 
                        Qt.TransformationMode.SmoothTransformation
                    ))
            else:
                self.dlg.lbl_preview.clear()

        except Exception as e:
            QgsMessageLog.logMessage(f"Erro na Preview: {str(e)}", "VectorToMap", Qgis.MessageLevel.Warning)

        finally:
            self.dlg.mMapLayerComboBox.blockSignals(False)
            
            if layout:
                layout.pageCollection().clear()
                sip.delete(layout)
            
            self.limpar_clones_preview()
            QTimer.singleShot(200, self._liberar_render)




    def _liberar_render(self):
        """Libera a trava de renderização e limpa o cursor."""
        self.is_rendering = False
        # Feedback visual: garante que o QGIS entenda que acabou
        if hasattr(self, 'dlg') and self.dlg and not sip.isdeleted(self.dlg) and hasattr(self.dlg, 'previewProgressBar'):
            self.dlg.previewProgressBar.hide()
            self.dlg.previewProgressBar.setRange(0, 100) # Reseta para 100%




    def disparar_preview_se_autorizado(self):
        """Dispara o timer de preview APENAS se o checkbox auto estiver marcado."""
        if not hasattr(self, 'dlg') or not self.dlg or sip.isdeleted(self.dlg) or not self.dlg.isVisible() or self.is_rendering:
            return

        # Regra clara: Só inicia se o usuário marcou a atualização automática
        if hasattr(self.dlg, 'chk_preview_auto') and self.dlg.chk_preview_auto.isChecked():
            if hasattr(self, 'timer_preview'):
                self.timer_preview.start(300)




    def processar_clique_ok(self):
        """Processes configurations and generates the multi-page layout."""
        
        camada = self.dlg.mMapLayerComboBox.currentLayer()
        if not camada: return
        
        self.abort_processing = False
        
        try:
            # ================================================================
            # PUXA O CONFIG AQUI! 
            # O cérebro do _gerar_config_atual já leu os seus novos Radio 
            # Buttons e já sabe se tem Expressão Matemática ou não.
            # ================================================================
            config = self._gerar_config_atual()
            campo_atlas = config.get('campo_atlas')

            paginas_dados = []
            
            if campo_atlas is None:
                request = QgsFeatureRequest() 
                for f in camada.getFeatures(request): 
                    paginas_dados.append({'feicoes': [f], 'valor_grupo': None})
            
            elif campo_atlas == "__ALL_FEATURES__":
                paginas_dados.append({'feicoes': ["ALL"], 'valor_grupo': "Geral"})
            
            else:
                # O AGRUPADOR DE EXPRESSÕES DINÂMICAS! 
                exp = QgsExpression(campo_atlas)
                
                # --- A CORREÇÃO AQUI TAMBÉM ---
                contexto = QgsExpressionContext()
                contexto.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(camada))
                # ------------------------------
                
                exp.prepare(contexto)

                grupos = {}
                for f in camada.getFeatures():
                    contexto.setFeature(f)
                    val = exp.evaluate(contexto)
                    
                    # Transforma o resultado da equação em texto para podermos criar lotes
                    val_chave = "Null" if val is None or val == NULL else str(val).strip()
                    if val_chave not in grupos:
                        grupos[val_chave] = []
                    grupos[val_chave].append(f)
                
                # Ordena os lotes criados alfabeticamente
                chaves_ordenadas = sorted(grupos.keys())
                for chave in chaves_ordenadas:
                    paginas_dados.append({'feicoes': grupos[chave], 'valor_grupo': chave})

            if paginas_dados: 
                self.criar_layout_multi_paginas(
                    camada, 
                    paginas_dados,
                    export_info=getattr(self, 'export_info', None)
                )
                
        except RuntimeError as e:
            # Captura APENAS o erro de "Camada Removida" (ou outros RuntimeErrors que você lançar)
            # Mostramos o alerta para o usuário, MAS NÃO enviamos para o Sentry, pois foi o usuário quem causou.
            self.iface.messageBar().pushMessage(
                self.tr("Aviso"), 
                str(e), 
                level=Qgis.MessageLevel.Warning
            )

        except Exception as e:
            # Captura todos os outros erros inesperados (bugs reais)
            erro_detalhado = traceback.format_exc()
            
            if getattr(self, 'sentry_ativo', False):
                try:
                    sentry_sdk.capture_exception(e)
                    sentry_sdk.flush(timeout=2.0)
                except Exception:
                    pass
            
            self.iface.messageBar().pushMessage(
                self.tr("Erro"), 
                f"{str(e)}. {self.tr('Veja o console para detalhes.')}", 
                level=Qgis.MessageLevel.Critical
            )
            QgsMessageLog.logMessage(erro_detalhado, "VectorToMap", Qgis.MessageLevel.Critical)
        
        finally:
            self.export_info = None # Reseta a variável para o botão OK padrão

    


    # ====================================================================================
    # --- ORQUESTRAÇÃO DE LAYOUTS E EXPORTAÇÃO ---
    # ====================================================================================

    def criar_layout_multi_paginas(self, camada, paginas_dados, export_info=None):
        """Maestro da Interface: Coleta dados da UI e delega a geração do layout para o LayoutEngine."""

        self.is_rendering = True
        self.engine.abort_processing = False # Avisa o motor que o sinal está verde
        self.set_ui_enabled(False)
        
        try:
            project = QgsProject.instance()
            manager = project.layoutManager()
            
            # 1. Inicializa Variáveis de Exportação
            is_individual = export_info.get('individual', False) if export_info else False
            base_path = export_info.get('path', '') if export_info else ''
            ext = export_info.get('ext', '') if export_info else ''
            base_sem_ext = base_path[:-len(ext)] if ext else base_path

            # 2. Pre-flight Check: Verificação de Sobrescrita Segura
            if export_info:
                # O verificador continua aqui pois ele abre uma janela (QMessageBox)
                pode_continuar = self._verificar_sobrescrita_arquivos(base_sem_ext, ext, paginas_dados, is_individual)
                if not pode_continuar:
                    self.is_rendering = False
                    self.engine.abort_processing = True
                    return # Aborta o processo pacificamente

            # 3. Empacotamento de Configurações (DTO)
            config = self._gerar_config_atual()

            # --- CURA DO SVG: Re-injetamos a extensão no config para proteger as tabelas ---
            config['ext'] = ext 
            config['base_sem_ext'] = base_sem_ext  # <--- ADICIONE ESTA LINHA AQUI!
            # --------------------------------------------------------------------------------

            # 4. Configuração da UI
            self.dlg.progressBar.show()
            self.dlg.progressBar.setRange(0, len(paginas_dados))
            self.dlg.progressBar.setValue(0)

            # Função de retorno para o motor falar com a nossa barra de progresso
            def atualizar_progresso(valor):
                self.dlg.progressBar.setValue(valor)

            # 5. Roteamento de Trabalho para o Engine
            layout_persistente = None
            
            if is_individual:
                self.engine.exportar_paginas_individuais(
                    project, camada, paginas_dados, config, progress_callback=atualizar_progresso
                )
            else:
                layout_persistente = self.engine.montar_layout_persistente(
                    project, manager, camada, paginas_dados, config, progress_callback=atualizar_progresso
                )

            # 6. Finalização e Fechamento
            if not self.engine.abort_processing:
                if export_info:
                    # Se pediu PDF único, exportamos o layout que acumulou as páginas
                    if not is_individual and ext == ".pdf" and layout_persistente:
                        layout_persistente.refresh()
                        exporter = QgsLayoutExporter(layout_persistente)
                        settings = QgsLayoutExporter.PdfExportSettings()
                        settings.dpi = 300
                        exporter.exportToPdf(base_path, settings)
                        
                        # --- CORREÇÃO: LIMPEZA SEGURA ---
                        for item in layout_persistente.items():
                            if isinstance(item, QgsLayoutItemMap):
                                item.setKeepLayerSet(False)
                                item.setLayers([])
                                
                        layout_persistente.pageCollection().clear()
                        layout_persistente.deleteLater()
                        layout_persistente = None
                        # --------------------------------

                    # --- Mensagem de Sucesso Nativa do QGIS ---
                    dir_path = os.path.dirname(base_path)
                    url_pasta = QUrl.fromLocalFile(dir_path).toString()
                    texto_sucesso = self.tr("Layout exportado com sucesso para")
                    mensagem_html = f"{texto_sucesso} <a href='{url_pasta}' style='color: #005e00; text-decoration: underline;'>{dir_path}</a>"
                    
                    self.iface.messageBar().pushMessage(self.tr("Exportar layout"), mensagem_html, level=Qgis.MessageLevel.Success, duration=0)

                    # --- LÓGICA DE ABRIR ARQUIVO/PASTA AUTOMATICAMENTE ---
                    if hasattr(self.dlg, 'chk_abrir_arquivo') and self.dlg.chk_abrir_arquivo.isChecked():
                        url_pasta = QUrl.fromLocalFile(dir_path)
                        QDesktopServices.openUrl(url_pasta)
                        
                        if not is_individual and ext == ".pdf":
                            url_arquivo = QUrl.fromLocalFile(base_path)
                            QDesktopServices.openUrl(url_arquivo)

                else:
                    # Se clicou em "OK" apenas para visualizar no QGIS
                    if layout_persistente:
                        layout_persistente.refresh()
                        manager.addLayout(layout_persistente)
                        self.iface.openLayoutDesigner(layout_persistente)
            else:
                self.iface.messageBar().pushMessage(self.tr("Cancelado"), self.tr("O processamento foi interrompido."), level=Qgis.MessageLevel.Warning)

        except Exception as e:
            erro_detalhado = traceback.format_exc()
            
            if getattr(self, 'sentry_ativo', False):
                try:
                    sentry_sdk.capture_exception(e)
                    sentry_sdk.flush(timeout=2.0)
                except Exception:
                    pass
            
            self.iface.messageBar().pushMessage(
                self.tr("Erro"), 
                f"{str(e)}. {self.tr('Veja o console para detalhes.')}", 
                level=Qgis.MessageLevel.Critical
            )
            QgsMessageLog.logMessage(erro_detalhado, "VectorToMap", Qgis.MessageLevel.Critical)

        finally:
            self.is_rendering = False
            self.set_ui_enabled(True)
            self.dlg.progressBar.hide()
            gc.collect()
    



    # ------------------------------------------------------------------------------------
    # --- SUB-FUNÇÕES DE EXPORTAÇÃO E LAYOUT (ESPECIALISTAS) ---
    # ------------------------------------------------------------------------------------

    def _verificar_sobrescrita_arquivos(self, base_sem_ext, ext, paginas_dados, is_individual):
        """Pre-flight: Avalia o disco antes de rodar o motor pesado."""
        arquivos_existentes = False
        
        # 1. Pegamos o valor do atlas consultando o nosso Cérebro Central de Configurações
        config = self._gerar_config_atual()
        campo_atlas = config.get('campo_atlas')
        
        if is_individual:
            for i, dados in enumerate(paginas_dados):
                # 2. Chamamos a função usando o self.engine e passamos o campo_atlas e os dados pré-calculados!
                nome_teste = self.engine._gerar_nome_arquivo_pagina(dados, i, campo_atlas)
                
                if os.path.exists(f"{base_sem_ext}_{nome_teste}{ext}"):
                    arquivos_existentes = True
                    break
        else:
            if os.path.exists(f"{base_sem_ext}{ext}"):
                arquivos_existentes = True

        if arquivos_existentes:
            resposta = QMessageBox.question(
                self.dlg,
                self.tr("Confirmar Sobrescrita"),
                self.tr("Arquivos com os mesmos nomes já existem no diretório de destino.\n\nDeseja sobrescrevê-los?"),
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No
            )
            return resposta == QMessageBox.StandardButton.Yes
        return True




    def escolher_fonte_titulo(self):
        """Abre a janela nativa do Windows/Mac para escolher fonte, tamanho e estilo."""
        fonte, ok = QFontDialog.getFont(self.fonte_titulo, self.dlg, self.tr("Escolher Fonte do Título"))
        if ok:
            self.fonte_titulo = fonte
            self.disparar_preview_se_autorizado()
    



    def set_ui_enabled(self, enabled):
        """Habilita ou desabilita os botões principais de forma segura."""
        if not hasattr(self, 'dlg') or sip.isdeleted(self.dlg):
            return

        self.btn_export.setEnabled(enabled)
        self.btn_render.setEnabled(enabled)
        self.btn_ok.setEnabled(enabled)
        
        if not enabled:
            self.btn_cancel.setText(self.tr("Parar Processo"))
            QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor)
        else:
            self.btn_cancel.setText(self.tr("Cancelar"))
            QApplication.restoreOverrideCursor()
        
        self.dlg.mMapLayerComboBox.setEnabled(enabled)
    



    def _filtrar_camadas_da_combo(self):
        """Varre o projeto e remove da combo qualquer camada que não seja a principal."""
        if not hasattr(self, 'dlg') or not self.dlg or sip.isdeleted(self.dlg) or not hasattr(self.dlg, 'mMapLayerComboBox'): 
            return
        
        excluidas = []
        for layer in QgsProject.instance().mapLayers().values():
            # Critério EXCLUSIVO: Se tem o carimbo do plugin, é escondida
            if layer.customProperty("vtm_is_preview_temp"):
                excluidas.append(layer)
        
        self.dlg.mMapLayerComboBox.setExceptedLayerList(excluidas)
        
        if hasattr(self.dlg, 'combo_camada_loc'):
            self.dlg.combo_camada_loc.setExceptedLayerList(excluidas)
    



    def mostrar_info(self, topico, widget_ancora):
        """Central de Ajuda: Cria uma caixa flutuante elegante com rolagem automática."""

        texto_ajuda = ""

        # Usamos HTML básico para deixar o texto bonito dentro da caixa
        if topico == "ddo_auto":
            t_titulo = self.tr("Escala Automática Dinâmica")
            t_desc = self.tr("Use o botão 'ε' para definir a margem de zoom dinamicamente com base na sua tabela de atributos.")
            t_ex_lbl = self.tr("Exemplo:")
            t_ex_txt = self.tr("Uma coluna com o número '15' aplicará 15% de margem no enquadramento.")
            t_sql_lbl = self.tr("Expressão SQL:")
            t_sql_txt = "if(\"Tipo\" = 'Urbano', 10, 25)" 
            t_obs = self.tr("Se o cálculo falhar ou a célula estiver vazia, o plugin usará a margem padrão (25%).")

            texto_ajuda = (
                f"<b style='color: #2980b9; font-size: 14px;'>{t_titulo}</b><br><br>"
                f"{t_desc}<br><br>"
                f"<b>{t_ex_lbl}</b> {t_ex_txt}<br>"
                f"<b>{t_sql_lbl}</b> <i>{t_sql_txt}</i><br><br>"
                f"<span style='color: #555;'>{t_obs}</span>"
            )

        elif topico == "ddo_fixa":
            t_titulo = self.tr("Escala Fixa Dinâmica")
            t_desc = self.tr("Use o botão 'ε' para definir a escala absoluta dinamicamente com base na sua tabela de atributos.")
            t_ex_lbl = self.tr("Exemplo:")
            t_ex_txt = self.tr("Uma coluna com o número '5000' forçará o mapa para a escala 1:5000.")
            t_sql_lbl = self.tr("Expressão SQL:")
            t_sql_txt = "\"Minha_Coluna\" * 1000"
            t_obs = self.tr("Se o cálculo falhar ou a célula for nula, o plugin usará a escala padrão.")

            texto_ajuda = (
                f"<b style='color: #2980b9; font-size: 14px;'>{t_titulo}</b><br><br>"
                f"{t_desc}<br><br>"
                f"<b>{t_ex_lbl}</b> {t_ex_txt}<br>"
                f"<b>{t_sql_lbl}</b> <i>{t_sql_txt}</i><br><br>"
                f"<span style='color: #555;'>{t_obs}</span>"
            )

        elif topico == "presets":
            t_titulo = self.tr("Formatos e Templates")
            t_desc = self.tr("Define a estrutura visual da prancha de impressão.")
            t_mod_lbl = self.tr("Modelos Básicos (Matemáticos):")
            t_mod_txt = self.tr("O plugin constrói o layout do zero, dividindo a folha geometricamente entre o mapa, tabela de atributos e decorações nativas.")
            t_tpl_lbl = self.tr("📄 Templates:")
            t_tpl_txt = self.tr("Modelos profissionais e otimizados que acompanham a versão. Eles mantêm o design, as margens e as decorações originais, ajustando apenas o mapa dinamicamente.")
            t_cus_lbl = self.tr("📂 Template Personalizado (.qpt):")
            t_cus_txt = self.tr("Permite importar um layout desenhado por você no QGIS com a identidade visual da sua empresa (logos, selos e carimbos). O motor preserva o seu design e injeta o mapa atualizado direto no seu quadro principal.")

            texto_ajuda = (
                f"<b style='color: #2980b9; font-size: 14px;'>{t_titulo}</b><br><br>"
                f"{t_desc}<br><br>"
                f"<b>{t_mod_lbl}</b><br>{t_mod_txt}<br><br>"
                f"<b>{t_tpl_lbl}</b><br>{t_tpl_txt}<br><br>"
                f"<b>{t_cus_lbl}</b><br>{t_cus_txt}"
            )
        
        elif topico == "agrupar":
            t_titulo = self.tr("Agrupamento Avançado (Atlas)")
            t_desc = self.tr("Agrupe feições no mesmo mapa usando uma coluna da tabela ou uma expressão matemática (ε).")
            t_ex_lbl = self.tr("Exemplo Simples:")
            t_ex_txt = self.tr("Escolher a coluna 'Bairro' gerará um mapa para cada bairro, unindo todos os lotes que pertencem a ele na mesma prancha.")
            t_sql_lbl = self.tr("Expressão SQL:")
            t_sql_txt = "concat(\"Municipio\", ' - ', \"Gleba\")"
            t_obs = self.tr("O motor calculará a fórmula para todas as geometrias. Feições com o mesmo resultado exato serão desenhadas juntas.")

            texto_ajuda = (
                f"<b style='color: #2980b9; font-size: 14px;'>{t_titulo}</b><br><br>"
                f"{t_desc}<br><br>"
                f"<b>{t_ex_lbl}</b> {t_ex_txt}<br>"
                f"<b>{t_sql_lbl}</b> <i>{t_sql_txt}</i><br><br>"
                f"<span style='color: #555;'>{t_obs}</span>"
            )

        if not texto_ajuda: return

        # 1. Cria a janela popup (sem barra superior, que fecha ao perder o foco)
        self.info_popup = QWidget(self.dlg)
        self.info_popup.setWindowFlags(Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint)
        self.info_popup.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) # Fundo transparente para a sombra

        layout_principal = QVBoxLayout(self.info_popup)
        layout_principal.setContentsMargins(10, 10, 10, 10) # Margem externa para a sombra respirar

        # 2. Cria o contêiner branco (o "cartão" em si)
        container_externo = QFrame()
        container_externo.setStyleSheet("""
            QFrame {
                background-color: #ffffff;
                border: 1px solid #dcdcdc;
                border-radius: 6px;
            }
        """)
        layout_container_externo = QVBoxLayout(container_externo)
        layout_container_externo.setContentsMargins(2, 2, 2, 2) # Margem mínima para a scrollarea encaixar bem

        # 3. A MÁGICA DA ROLAGEM: Cria a ScrollArea
        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True) # O conteúdo interno se adapta à largura
        scroll_area.setFrameShape(QFrame.Shape.NoFrame) # Tira a borda feia dupla
        scroll_area.setStyleSheet("background-color: transparent;") # Deixa o fundo branco do cartão aparecer

        # 4. O Widget Interno (Onde o texto realmente vai morar)
        conteudo_interno = QWidget()
        layout_interno = QVBoxLayout(conteudo_interno)
        layout_interno.setContentsMargins(15, 15, 15, 15)

        lbl = QLabel(texto_ajuda)
        lbl.setWordWrap(True)
        lbl.setStyleSheet("""
            QLabel {
                color: #333333;
                font-size: 12px;
                border: none;
            }
        """)
        # Mantemos a largura fixa para o texto não ficar super largo e ruim de ler
        lbl.setFixedWidth(280) 
        
        layout_interno.addWidget(lbl)
        layout_interno.addStretch() # Empurra o texto para cima se a caixa for maior que ele
        
        # Junta tudo
        scroll_area.setWidget(conteudo_interno)
        layout_container_externo.addWidget(scroll_area)
        layout_principal.addWidget(container_externo)

        # 5. Sombra Flutuante (Efeito Clean)
        sombra = QGraphicsDropShadowEffect()
        sombra.setBlurRadius(15)
        sombra.setColor(QColor(0, 0, 0, 40))
        sombra.setOffset(2, 4)
        container_externo.setGraphicsEffect(sombra)

        # ====================================================================
        # 6. CÁLCULO INTELIGENTE DE ALTURA E POSIÇÃO
        # ====================================================================
        # Pega as coordenadas globais da tela com base no botão clicado
        ponto_ancora = widget_ancora.mapToGlobal(widget_ancora.rect().bottomLeft())
        
        # Pega a altura total disponível na janela principal do QGIS
        altura_janela = self.dlg.height()
        
        # Converte o Y absoluto do botão para um Y relativo à janela do QGIS
        y_relativo_botao = self.dlg.mapFromGlobal(ponto_ancora).y()
        
        # Calcula quanto espaço sobra daquele botão até o fundo da tela (deixando uma margem de segurança de 30px)
        espaco_disponivel = altura_janela - y_relativo_botao - 30
        
        # Define a altura máxima do popup. 
        # Nunca passa de 350px (para não ficar gigante) e respeita o espaço disponível até o chão.
        altura_maxima = min(350, max(150, espaco_disponivel)) # Mínimo de 150px de altura para não esmagar demais
        
        # Trava as dimensões do popup
        self.info_popup.setFixedWidth(330) # 280 do texto + margens + scrollbar
        self.info_popup.setFixedHeight(int(altura_maxima))

        # Move o popup para o exato ponto abaixo do botão
        self.info_popup.move(ponto_ancora.x(), ponto_ancora.y() + 2)

        # Exibe!
        self.info_popup.show()
    



    def mostrar_tela_sobre(self):
        """Exibe a janela de ajuda com texto corrido, no estilo algoritmo e sem ícone grande no corpo."""
        
        # Cria uma QMessageBox genérica com o diálogo principal como pai
        msg_box = QMessageBox(self.dlg)
        msg_box.setWindowTitle(self.tr("Ajuda do Algoritmo"))
        
        # Força a remoção do ícone grande dentro do corpo do texto
        msg_box.setIcon(QMessageBox.Icon.NoIcon)

        # Texto corrido e estruturado, no estilo QGIS (incluindo o aviso final)
        texto_corrido = self.tr(
            "VectorToMap<br><br>"
            "O VectorToMap automatiza a geração de layouts de impressão para cartografia técnica. "
            "Este algoritmo transforma os dados da camada vetorial em pranchas prontas, "
            "com enquadramento e escala calculados dinamicamente.<br><br>"
            
            "O parâmetro de agrupamento funciona de forma semelhante a um Atlas, permitindo gerar um mapa individual para cada feição "
            "ou agrupar várias feições que compartilham o mesmo valor em uma coluna específica da tabela.<br><br>"
            
            "O parâmetro de escala controla o nível de zoom do mapa. Pode ser definido para centralização "
            "automática ou fixado em valores absolutos. O uso de expressões (ε) permite que a margem de respiro ou "
            "a escala exata sejam lidas e aplicadas diretamente a partir dos atributos da feição atual.<br><br>"
            
            "A opção de filtrar geometrias controla a visibilidade dos elementos no layout final. "
            "Quando ativada, ela isola e exibe apenas as feições que pertencem à página que está sendo gerada, "
            "ocultando automaticamente o restante dos dados do mapa.<br><br>"
            
            "<ul><li><i>Utilize o botão de Preview para validar o enquadramento, os estilos e a renderização das "
            "informações na tela antes de executar a exportação em lote para arquivos finais.</i></li></ul>"
        )
        
        # Padrão nativo para o texto
        msg_box.setText(texto_corrido)
        msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
        
        # Exibe a janela de forma modal compatível com QGIS 3 e QGIS 4
        if hasattr(msg_box, 'exec'):
            msg_box.exec()
        else:
            msg_box.exec_()
    



    def _adicionar_aba_sobre(self):
        """Injeta dinamicamente o conteúdo na 'tab_2' (Aba Sobre) já existente na interface."""
        
        # Verifica se a tab_2 existe na interface desenhada
        if not hasattr(self.dlg, 'tab_2'): 
            return

        aba_sobre = self.dlg.tab_2
        
        # O TRUQUE DO DUMMY WIDGET (A forma que realmente funciona no PyQt!)
        # Remove o layout instantaneamente transferindo-o para um widget fantasma
        if aba_sobre.layout() is not None:
            QWidget().setLayout(aba_sobre.layout())
            
        # Cria um layout NOVO e ANCORADO DIRETAMENTE na tab_2
        layout = QVBoxLayout(aba_sobre)
        layout.setContentsMargins(20, 20, 20, 20)

        # TRADUZIMOS APENAS OS TEXTOS CURTOS E LIMPOS (TÍTULOS)
        t_title = self.tr("VectorToMap")
        t_sub = self.tr("Motor de Renderização Cartográfica Automatizada")
        
        # ====================================================================
        # 1. CABEÇALHO (TÍTULO E SUBTÍTULO NATIVOS)
        # ====================================================================
        lbl_header = QLabel(f"<span style='font-size:16pt; font-weight:bold; color:#2c3e50;'>{t_title}</span><br>"
                            f"<span style='font-size:11pt; color:#7f8c8d;'>{t_sub}</span>")
        layout.addWidget(lbl_header)
        layout.addSpacing(10) # Espaço entre o título e os botões

        # ====================================================================
        # 2. CRIAÇÃO DOS BOTÕES SUPERIORES (Logo abaixo do título)
        # ====================================================================
        layout_botoes = QHBoxLayout()
        layout_botoes.setSpacing(10) # Espaço entre os botões
        
        self.btn_apoiar = QPushButton(self.tr("☕ Apoiar Desenvolvimento"), aba_sobre)
        self.btn_bugreport = QPushButton(self.tr("🐞 Relatar Bug (GitHub)"), aba_sobre)
        self.btn_tutorial = QPushButton(self.tr("▶ Assistir Tutorial"), aba_sobre)
        
        self.btn_apoiar.setCursor(Qt.CursorShape.PointingHandCursor)
        self.btn_bugreport.setCursor(Qt.CursorShape.PointingHandCursor)
        self.btn_tutorial.setCursor(Qt.CursorShape.PointingHandCursor)
        
        self.btn_apoiar.setObjectName("pushButton_apoiar")
        self.btn_bugreport.setObjectName("pushButton_bugreport")
        self.btn_tutorial.setObjectName("pushButton_tutorial")
        
        self.btn_apoiar.clicked.connect(self.abrir_gumroad)
        self.btn_bugreport.clicked.connect(self.abrir_github)
        self.btn_tutorial.clicked.connect(self.abrir_tutorial_youtube)
        
        layout_botoes.addWidget(self.btn_apoiar)
        layout_botoes.addWidget(self.btn_bugreport)
        layout_botoes.addWidget(self.btn_tutorial)
        layout_botoes.addStretch() 
        
        layout.addLayout(layout_botoes)
        layout.addSpacing(10)
        
        # Linha divisória fina e elegante
        linha = QFrame()
        linha.setFrameShape(QFrame.Shape.HLine)
        linha.setStyleSheet("border: 1px solid #eaeaea; background-color: transparent;")
        layout.addWidget(linha)
        layout.addSpacing(5)

        # ====================================================================
        # 3. CORPO DO TEXTO
        # ====================================================================
        txt_browser = QTextBrowser(aba_sobre)
        txt_browser.setOpenExternalLinks(True)
        txt_browser.setStyleSheet("border: none; background-color: transparent;")

        t_h1 = self.tr("Visão Geral do Sistema")
        
        t_p1_pt1 = self.tr("O VectorToMap é uma solução avançada de automação em software livre para Sistemas de Informação Geográfica (SIG), criada para otimizar a sua cartografia técnica. Projetado para lidar com rotinas de alta demanda cartográfica, o complemento atua diretamente sobre a API de layouts nativa do QGIS (QgsPrintLayout), permitindo a renderização em lote de feições espaciais com parâmetros de enquadramento escaláveis.")
        
        t_p1_pt2 = self.tr("Todo esse projeto se mantém vivo e em constante evolução graças ao apoio da comunidade. Ao apoiar o nosso desenvolvimento contínuo, você garante que a ferramenta continue recebendo melhorias e se mantenha sempre funcional.")
        
        t_h2 = self.tr("Aplicações Técnicas")
        t_li1 = self.tr("<b>Engenharia e Licenciamento:</b> Geração massiva de plantas de situação, croquis de acesso e anexos cartográficos padronizados para estudos ambientais.")
        t_li2 = self.tr("<b>Gestão Fundiária:</b> Automatização de pranchas para o Cadastro Ambiental Rural (CAR), regularização fundiária e cadastros técnicos multifinalitários urbanos.")
        t_li3 = self.tr("<b>Inspeção e Laudos:</b> Padronização visual e geração rápida de encartes de mapas para relatórios de vistoria em campo.")
        
        t_h3 = self.tr("Arquitetura e Funcionalidades Core")
        t_li4 = self.tr("<b>Integração QGIS Expressions:</b> Suporte nativo a Data-Defined Overrides (DDO). O motor lê funções SQL inseridas pelo usuário para calcular dinamicamente margens e escalas com base no contexto geométrico.")
        t_li5 = self.tr("<b>Isolamento em Memória:</b> Utiliza rotinas de instanciamento de camadas transitórias (Memory Layers) e filtros de visibilidade sem afetar a Layer Tree principal do projeto.")
        t_li6 = self.tr("<b>Engine de Exportação:</b> Suporte multi-thread e multi-formato dinâmico perfeitamente ajustado para lotes de impressão.")
        
        t_dev = self.tr("Desenvolvido por:")
        t_inpi = self.tr("Registro INPI:")
        t_contato = self.tr("Contato:")
        t_linkedin = self.tr("LinkedIn:") # <--- VARIÁVEL DO LINKEDIN AQUI
        t_licenca = self.tr("Licença:")

        # INJETAMOS NO HTML COM F-STRINGS
        html_content = f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li {{ white-space: pre-wrap; }}
</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:11pt; font-weight:400; font-style:normal; color: #333333;">

<p style=" margin-top:0px; margin-bottom:8px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt; font-weight:600; color:#2c3e50;">{t_h1}</span></p>
<p style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; text-align: justify;">{t_p1_pt1}</p>
<p style=" margin-top:0px; margin-bottom:15px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; text-align: justify;">{t_p1_pt2}</p>

<p style=" margin-top:0px; margin-bottom:8px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt; font-weight:600; color:#2c3e50;">{t_h2}</span></p>
<ul style="margin-top: 0px; margin-bottom: 15px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;">
<li style=" margin-top:0px; margin-bottom:6px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li1}</li>
<li style=" margin-top:0px; margin-bottom:6px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li2}</li>
<li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li3}</li>
</ul>

<p style=" margin-top:0px; margin-bottom:8px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt; font-weight:600; color:#2c3e50;">{t_h3}</span></p>
<ul style="margin-top: 0px; margin-bottom: 15px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;">
<li style=" margin-top:0px; margin-bottom:6px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li4}</li>
<li style=" margin-top:0px; margin-bottom:6px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li5}</li>
<li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">{t_li6}</li>
</ul>

<hr style="border: 1px solid #eaeaea; margin-top:10px; margin-bottom:10px;" />

<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt; color:#555555;">
<span style=" font-weight:600;">{t_dev}</span> Matheus Durso Neves Caetano<br />
<span style=" font-weight:600;">{t_inpi}</span> BR 51 2026 001755-6<br />
<span style=" font-weight:600;">{t_contato}</span> <a href="mailto:matheusdursonc@gmail.com"><span style=" text-decoration: underline; color:#3498db;">matheusdursonc@gmail.com</span></a><br />
<span style=" font-weight:600;">{t_linkedin}</span> <a href="https://www.linkedin.com/in/matheus-durso-neves-caetano"><span style=" text-decoration: underline; color:#3498db;">Conectar no LinkedIn</span></a><br />
<span style=" font-weight:600;">{t_licenca}</span> GNU GPLv2
</span></p>

</body></html>"""

        txt_browser.setHtml(html_content)
        layout.addWidget(txt_browser)

        # Atualiza o nome da aba para garantir que vai aparecer traduzido
        if hasattr(self.dlg, 'tabWidget'):
            idx = self.dlg.tabWidget.indexOf(aba_sobre)
            if idx != -1:
                self.dlg.tabWidget.setTabText(idx, self.tr("Sobre o Plugin"))