diff --git a/documentation/implementacoes.md b/documentation/implementacoes.md index f041571..1779347 100644 --- a/documentation/implementacoes.md +++ b/documentation/implementacoes.md @@ -41,6 +41,7 @@ diretório debug/ |---|---|---| | `main.py` | Janela principal: menus, botões, tabelas, ligação com a simulação | `MainFrame.java` | | `simulation.py` | Controle da simulação e janelas de checkpoint | `MainFrame.java` (parte de simulação), `CheckpointController.java` | +| `theme.py` | Paletas dos temas claro/escuro, cores da grade e preferência salva | — | | `router_matrix.py` | Desenho da grade e de cada roteador (imagens, setas, %) | `Roteador.java`, `UJPanelImagem.java`, `MainFrame.createNoCPanel` | | `util/MPSoCConfig.py` | Leitura de `platform.cfg` e `services.cfg`, constantes e endereçamento | `util/MPSoCConfig.java` | | `information/packet_information.py` | Um pacote do `traffic_router.txt` | `PacketInformation.java` | @@ -239,6 +240,10 @@ Reaproveita o layout do `Roteador.py` (gerado de `Roteador.ui`) e acrescenta: As imagens (pasta `images/`) são desenhadas pelo `_ImagePainter`, um *event filter* que pinta a imagem esticada sobre os `QWidget` vazios do `.ui`, substituindo o `UJPanelImagem` do Java. As imagens ficam em cache (`_pixmap_cache`). +Os PNGs das setas não têm canal alfa (fundo branco sólido). Ao carregar, `_white_to_alpha` converte o fundo e o serrilhado da borda em transparência, mantendo a cor da seta, para que elas funcionem sobre o fundo escuro. As imagens do corpo do roteador (`Router*.png`) são usadas como estão. + +As cores da grade vêm de uma única folha de estilo aplicada no `RouterMatrixWidget` (`apply_theme(dark)`): fundo da área, texto do % da porta local e borda dos clusters seguem o tema; os rótulos dentro do corpo do roteador são sempre pretos, porque o corpo tem cor clara nos dois temas. + --- ## 8. Simulação — `simulation.py` @@ -334,7 +339,16 @@ Divide o tempo em janelas de **0,5 ms** (`window_size_ms`). `set_time(ticks)` co 5. Limpa o campo **Back To**. 6. Mostra *"The project has been successfully loaded!"*. -### 9.2 Mensagens +### 9.2 Tema claro/escuro + +**Menu:** `Edit → Dark Mode` (Ctrl+T), item marcável. + +- `theme.py` define as paletas do Fusion para os dois temas, incluindo as cores de itens **desabilitados** e do texto de *placeholder* (sem elas, botões desabilitados ficavam ilegíveis no modo escuro). +- `MinhaJanela.set_dark_mode(dark)` aplica a paleta na aplicação inteira, atualiza a marcação do menu e recolore a grade (`RouterMatrixWidget.apply_theme`). Uma grade criada depois (novo debug ou reset) já nasce com o tema atual. +- A escolha é salva com `QSettings` (organização `GAPH`, aplicação `MemphisGraphicalDebugger`) e restaurada ao abrir o programa. +- As janelas Communication Overview e Task Mapping mantêm suas cores fixas. + +### 9.3 Mensagens | Situação | Mensagem | |---|---| diff --git a/main.py b/main.py index 7d6671f..fb349f0 100644 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ QVBoxLayout, QHBoxLayout, QScrollArea, QTableWidgetItem, QTableWidget, QLabel, QHeaderView) +import theme from ui_mainwindow import MainWindow from commsUi import Ui_Form from taskmap import Ui_TaskMap @@ -40,16 +41,15 @@ def __init__(self): # --------------------------------------------------------- # Limpa a paleta fixa gerada pelo arquivo .ui para herdar a do App self.setPalette(QPalette()) - self.apply_light_theme(app) - self.is_dark_mode = False - # Cria uma ação para alternar o tema e adiciona no menu "Edit" - self.actionToggle_Theme = QAction("Alternar Tema (Dark/Light)", self) + + # Ação marcável no menu "Edit"; o último tema escolhido é restaurado + self.actionToggle_Theme = QAction("Dark Mode", self) + self.actionToggle_Theme.setCheckable(True) self.actionToggle_Theme.setShortcut(QKeySequence("Ctrl+T")) self.menuEdit.addAction(self.actionToggle_Theme) - self.actionToggle_Theme.triggered.connect(self.toggle_theme) + self.actionToggle_Theme.toggled.connect(self.set_dark_mode) - # Garante que o aplicativo inicie no modo claro padrão do estilo Fusion - self.apply_light_theme(QApplication.instance()) + self.set_dark_mode(theme.load_dark_mode()) # --------------------------------------------------------- self.actionCommunication_Overview.setEnabled(False) @@ -102,56 +102,21 @@ def __init__(self): # LÓGICA DE MUDANÇA DE TEMA # ========================================== def toggle_theme(self): - app = QApplication.instance() - if not self.is_dark_mode: - self.apply_dark_theme(app) - self.is_dark_mode = True - else: - self.apply_light_theme(app) - self.is_dark_mode = False - - def apply_dark_theme(self, app): - app.setStyle("Fusion") - dark_palette = QPalette() - - # Configurando as cores do modo escuro - dark_palette.setColor(QPalette.ColorRole.Window, QColor(53, 53, 53)) - dark_palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white) - dark_palette.setColor(QPalette.ColorRole.Base, QColor(35, 35, 35)) - dark_palette.setColor(QPalette.ColorRole.AlternateBase, QColor(53, 53, 53)) - dark_palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(25, 25, 25)) - dark_palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.white) - dark_palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white) - dark_palette.setColor(QPalette.ColorRole.Button, QColor(53, 53, 53)) - dark_palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white) - dark_palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) - dark_palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218)) - dark_palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218)) - dark_palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black) - - # Aplica na aplicação inteira (afeta abas secundárias também) - app.setPalette(dark_palette) - - def apply_light_theme(self, app): - app.setStyle("Fusion") - light_palette = QPalette() - - # Força as cores do modo claro para sobrescrever o tema do Sistema Operacional - light_palette.setColor(QPalette.ColorRole.Window, QColor(240, 240, 240)) - light_palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.black) - light_palette.setColor(QPalette.ColorRole.Base, Qt.GlobalColor.white) - light_palette.setColor(QPalette.ColorRole.AlternateBase, QColor(225, 225, 225)) - light_palette.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white) - light_palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.black) - light_palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.black) - light_palette.setColor(QPalette.ColorRole.Button, QColor(240, 240, 240)) - light_palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.black) - light_palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red) - light_palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218)) - light_palette.setColor(QPalette.ColorRole.Highlight, QColor(42, 130, 218)) - light_palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.white) - - app.setPalette(light_palette) + self.set_dark_mode(not self.is_dark_mode) + + def set_dark_mode(self, dark): + """Aplica o tema na aplicação inteira e na grade de roteadores.""" + self.is_dark_mode = dark + theme.apply_theme(dark) + theme.save_dark_mode(dark) + + # Mantém a marcação do menu em sincronia sem disparar o sinal de novo + self.actionToggle_Theme.blockSignals(True) + self.actionToggle_Theme.setChecked(dark) + self.actionToggle_Theme.blockSignals(False) + + if self.router_matrix_widget is not None: + self.router_matrix_widget.apply_theme(dark) # ========================================== @@ -226,6 +191,7 @@ def build_router_matrix(self): self.mpconfig.cluster_y, mpsoc_config=self.mpconfig, ) + self.router_matrix_widget.apply_theme(self.is_dark_mode) # Primeira vez: cria um QScrollArea dentro do self.frame para # comportar malhas maiores que a área visível da janela. diff --git a/pe_matrix.py b/pe_matrix.py index 0d1cd2a..012dd65 100644 --- a/pe_matrix.py +++ b/pe_matrix.py @@ -21,9 +21,8 @@ # Cores fixas, independentes do tema (claro/escuro) do MainWindow — mesmo -# motivo do _ROUTER_STYLESHEET em router_matrix.py e do _SLAVE_STYLESHEET -# em slave_matrix.py: sem isso, o toggle Dark/Light recoloriria essas -# células via QApplication.setPalette(). +# motivo do _SLAVE_STYLESHEET em slave_matrix.py: sem isso, o toggle +# Dark/Light recoloriria essas células via QApplication.setPalette(). _PE_STYLESHEET = """ QWidget#peWidget { background-color: #ffffff; diff --git a/router_matrix.py b/router_matrix.py index 8d6f332..750a698 100644 --- a/router_matrix.py +++ b/router_matrix.py @@ -32,9 +32,10 @@ import os from PySide6.QtCore import QEvent, QObject, QRect, Qt, Signal -from PySide6.QtGui import QFont, QPainter, QPixmap +from PySide6.QtGui import QColor, QFont, QImage, QPainter, QPixmap from PySide6.QtWidgets import QFrame, QGridLayout, QWidget +import theme from Roteador import Ui_Form from util.MPSoCConfig import MPSoCConfig @@ -45,9 +46,43 @@ _pixmap_cache: dict[str, QPixmap] = {} +def _white_to_alpha(image: QImage) -> QImage: + """ + Remove o fundo branco das imagens das setas (os PNGs não têm canal + alfa), para que fiquem legíveis sobre o fundo escuro do modo escuro. + + A cor mais distante do branco é tomada como a cor da seta; os pixels + mais claros (fundo e serrilhado da borda) viram essa cor com opacidade + proporcional à distância do branco. + """ + image = image.convertToFormat(QImage.Format.Format_ARGB32) + width, height = image.width(), image.height() + + def distance(color: QColor) -> int: + return 255 - min(color.red(), color.green(), color.blue()) + + pixels = [[image.pixelColor(x, y) for x in range(width)] for y in range(height)] + base = max((color for row in pixels for color in row), key=distance) + base_distance = distance(base) or 1 + + for y, row in enumerate(pixels): + for x, color in enumerate(row): + alpha = distance(color) / base_distance + if alpha < 1: + faded = QColor(base) + faded.setAlphaF(alpha) + image.setPixelColor(x, y, faded) + + return image + + def _pixmap(name: str) -> QPixmap: if name not in _pixmap_cache: - _pixmap_cache[name] = QPixmap(os.path.join(IMAGES_DIR, name)) + image = QImage(os.path.join(IMAGES_DIR, name)) + # As imagens do corpo do roteador são opacas e não têm fundo branco + if not name.startswith("Router"): + image = _white_to_alpha(image) + _pixmap_cache[name] = QPixmap.fromImage(image) return _pixmap_cache[name] @@ -136,22 +171,30 @@ def eventFilter(self, obj, event): return super().eventFilter(obj, event) -# Cores fixas do roteador, independentes do tema (claro/escuro) que o -# MainWindow aplica globalmente via QApplication.setPalette(). Sem isso, -# alternar o tema (Ctrl+T) recolore os widgets internos do roteador (portas, -# rótulos) porque eles não têm paleta própria e herdam a paleta do app, -# quebrando a visualização (fundo/texto ficam com a mesma cor, sumindo, ou -# os pequenos retângulos de porta ficam visíveis onde antes eram -# transparentes ao fundo). -_ROUTER_STYLESHEET = """ -QWidget { - background-color: #ffffff; - color: #000000; -} -QLabel { - background-color: transparent; -} -""" +def _matrix_stylesheet(dark: bool) -> str: + """ + Folha de estilo da grade inteira (aplicada no RouterMatrixWidget e + herdada pelos roteadores). Os rótulos dentro do corpo do roteador são + sempre pretos, pois o corpo tem cor clara nos dois temas; o % da porta + local fica fora do corpo e acompanha o tema. + """ + colors = theme.matrix_colors(dark) + return f""" + #routerMatrix {{ + background-color: {colors.canvas}; + }} + #routerMatrix QLabel {{ + background-color: transparent; + color: #000000; + }} + #routerMatrix QLabel#local_Label {{ + color: {colors.text}; + }} + #routerMatrix QFrame[clusterFrame="true"] {{ + background-color: transparent; + border: 1px solid {colors.cluster_border}; + }} + """ class RouterWidget(QWidget, Ui_Form): @@ -200,13 +243,12 @@ def __init__(self, pos_x: int, pos_y: int, parent=None, for label_name in set(_PORT_LABELS.values()): getattr(self, label_name).setFont(percent_font) - # Isola este roteador do tema global do app (ver comentário acima - # de _ROUTER_STYLESHEET): fixa cores próprias em toda a subárvore, - # que não são afetadas por um app.setPalette() posterior. - self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) - for child in self.findChildren(QWidget): - child.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) - self.setStyleSheet(_ROUTER_STYLESHEET) + # O Roteador.ui fixa fundo branco no Form inteiro; as cores passam a + # vir da folha de estilo da matriz (_matrix_stylesheet), que segue o tema + self.setStyleSheet("") + + # Mesma posição do Java: acima do corpo do roteador, sem cruzar a borda + self.local_Label.setGeometry(QRect(21, 16, 40, 13)) self.reset_arrows() @@ -291,6 +333,10 @@ def __init__(self, mpsoc_x: int, mpsoc_y: int, self.mpsoc_config = mpsoc_config + # Fundo e cores da grade seguem o tema (ver apply_theme) + self.setObjectName("routerMatrix") + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + # mpsoc_x/mpsoc_y é o tamanho TOTAL da malha. self.total_cols = max(1, mpsoc_x) self.total_rows = max(1, mpsoc_y) @@ -349,13 +395,8 @@ def _build_cluster(self, cluster_x: int, cluster_y: int, cluster_frame.setFrameShape(QFrame.Shape.Box) cluster_frame.setFrameShadow(QFrame.Shadow.Plain) cluster_frame.setLineWidth(1) - # Borda do cluster com cor fixa, isolada do tema global (mesmo - # motivo do _ROUTER_STYLESHEET: sem isso, o toggle Dark/Light - # do MainWindow também recolore essa borda de forma inconsistente). - cluster_frame.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) - cluster_frame.setStyleSheet( - "QFrame { background-color: #ffffff; border: 1px solid #808080; }" - ) + # A cor da borda vem da folha de estilo da matriz (_matrix_stylesheet) + cluster_frame.setProperty("clusterFrame", True) cluster_layout = QGridLayout(cluster_frame) cluster_layout.setSpacing(0) @@ -391,6 +432,10 @@ def get_router(self, x: int, y: int) -> RouterWidget | None: """Retorna o RouterWidget na posição global (x, y), se existir.""" return self.routers.get((x, y)) + def apply_theme(self, dark: bool) -> None: + """Aplica as cores do tema claro ou escuro na grade.""" + self.setStyleSheet(_matrix_stylesheet(dark)) + def get_router_by_address(self, router_address: int) -> RouterWidget | None: """Retorna o RouterWidget pelo endereço hamiltoniano, se existir.""" return self.routers_by_address.get(router_address) diff --git a/slave_matrix.py b/slave_matrix.py index d9fdf79..be63f6e 100644 --- a/slave_matrix.py +++ b/slave_matrix.py @@ -21,9 +21,9 @@ from PySide6.QtWidgets import QGridLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget -# Cores fixas, independentes do tema (claro/escuro) do MainWindow — mesmo -# motivo do _ROUTER_STYLESHEET em router_matrix.py: sem isso, o toggle -# Dark/Light recoloriaria essas células via QApplication.setPalette(). +# Cores fixas, independentes do tema (claro/escuro) do MainWindow: sem +# isso, o toggle Dark/Light recoloriria essas células via +# QApplication.setPalette(). _SLAVE_STYLESHEET = """ QWidget#slaveWidget { background-color: #0000cc;