"""Tema do openTARS 5.0: as cores, as fontes e as peças do mockup aprovado.
Fundo #151E3D, barra lateral #0F1730, cartões #1A2446 com borda #2E3C6B,
texto #E8ECF4, apoio #A0B0D8, verde #5FD97A nas ações e nos estados certos,
lavanda #A0B0D8 nos pedidos de confirmação. Texto em IBM Plex Sans, rótulos
técnicos em IBM Plex Mono (as duas vão junto, licença OFL em fontes/OFL.txt).
Cantos arredondados (botões 10 px, cartões 12 a 14 px), alvos de pelo menos 44 px.
"""
import html
from pathlib import Path
from PySide6.QtCore import QByteArray, QPointF, QRectF, QSize, Qt
from PySide6.QtGui import QColor, QFont, QFontDatabase, QIcon, QPainter, QPainterPath, QPen, QPixmap
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtSvgWidgets import QSvgWidget
from PySide6.QtWidgets import QLabel, QWidget
CORES = {
"fundo": "#151E3D",
"lateral": "#0F1730",
"cartao": "#1A2446",
"borda": "#2E3C6B",
"borda2": "#34437A",
"borda3": "#4A5A8F",
"linha": "#243158",
"texto": "#E8ECF4",
"texto2": "#C5CFEA",
"apoio": "#A0B0D8",
"mudo": "#8A9AC8",
"verde": "#5FD97A",
"escuro": "#0B1020",
"destaque": "#222F5A",
}
# Glifos que a IBM Plex NÃO tem. Quando o Qt precisa de um glifo que falta, pergunta ao fontconfig do sistema por uma
# fonte substituta; em algumas máquinas essa chamada (FcCharSetHasChar) dá segfault. Por isso a interface só usa
# caracteres que a Plex tem: estes são trocados por equivalentes que ela tem.
_TROCAS = str.maketrans({"▸": "›", "▶": "›", "►": "›", "▾": "↓", "▼": "↓", "✗": "×", "✘": "×", "✕": "×", "✔": "✓", "●": "•", "⏰": "↻"})
_FONTES_CRUAS = []
def _plex_tem(c):
if not _FONTES_CRUAS:
from PySide6.QtGui import QRawFont
_FONTES_CRUAS.extend(QRawFont(str(f), 15) for f in sorted(_PASTA_FONTES.glob("*.ttf")))
return any(f.supportsCharacter(ord(c)) for f in _FONTES_CRUAS)
def seguro(texto, externo=False):
"""O texto com os glifos que faltam na IBM Plex trocados por outros que ela tem. externo=True (texto que vem de
fora: título de feed, nome de arquivo, frase do modelo): o que ainda faltar vira "?" (no PC do Enzo, a estrela preta ou chinês
num QLabel derrubam a janela até no modo seguro)."""
texto = str(texto).translate(_TROCAS)
if not externo or texto.isascii():
return texto
return "".join(c if c.isascii() or c in "\n\t" or _plex_tem(c) else "?" for c in texto)
FONTE_TEXTO = "IBM Plex Sans"
FONTE_MONO = "IBM Plex Mono"
_PASTA_FONTES = Path(__file__).resolve().parent / "fontes"
def preparar_fontconfig(sistema="/etc/fonts/fonts.conf", casa=None):
"""Usa as fontes e as regras do sistema, mas com um cache de fontes só do openTARS (~/.cache/opentars/fontconfig).
Motivo (16/10): um cache corrompido em ~/.cache/fontconfig faz o Qt cair (segfault em FcCharSetHasChar) no primeiro
caractere que a fonte da janela não tem, até no modo seguro; o fc-cache -f conserta, mas o openTARS não deve depender
de outro programa ter deixado o cache certo. Chamar antes de criar o QApplication. Não faz nada se a pessoa já
escolheu um FONTCONFIG_FILE, com TARS_FONTCONFIG_SISTEMA=1, ou sem o fonts.conf do sistema. Devolve o arquivo ou None."""
import os
import re
if os.environ.get("FONTCONFIG_FILE") or os.environ.get("TARS_FONTCONFIG_SISTEMA") == "1":
return None
try:
texto = Path(sistema).read_text(encoding="utf-8")
except OSError:
return None
pasta = Path(casa or os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "opentars"
base = Path(sistema).parent
# as inclusões relativas passam a ser absolutas (o arquivo novo mora em outra pasta) e os caches do sistema saem
texto = re.sub(r"]*>.*?", "", texto, flags=re.S)
texto = re.sub(r'(]*>)\s*(?!/|~)([^<]+?)\s*()', lambda m: f"{m.group(1)}{base / m.group(2)}{m.group(3)}", texto)
if "" not in texto:
return None
texto = texto.replace("", f"{pasta / 'fontconfig'}\n", 1)
try:
pasta.mkdir(parents=True, exist_ok=True)
arquivo = pasta / "fonts.conf"
if not arquivo.exists() or arquivo.read_text(encoding="utf-8") != texto:
arquivo.write_text(texto, encoding="utf-8")
except OSError:
return None
os.environ["FONTCONFIG_FILE"] = str(arquivo)
return arquivo
def carregar_fontes():
"""Registra as fontes que vão junto. Devolve quantas carregaram (0 = cai na fonte do sistema)."""
carregadas = 0
for arquivo in sorted(_PASTA_FONTES.glob("*.ttf")):
if QFontDatabase.addApplicationFont(str(arquivo)) >= 0:
carregadas += 1
return carregadas
# ------------------------------------------------------------
# Marca (os mesmos SVG do mockup)
# ------------------------------------------------------------
def monolito_svg(contorno=True):
"""O monolito: quatro blocos e a barra verde, no quadrado arredondado azul."""
borda = f' stroke="{CORES["borda2"]}" stroke-width="2"' if contorno else ""
blocos = "".join(
f'' for x in (20, 36, 52, 68)
)
return (
''
)
def selo_svg():
"""O selo Tesseract: túnel de três quadrados, núcleo verde. Só em tamanho pequeno."""
return (
''
)
def svg_widget(svg, tamanho):
w = QSvgWidget()
w.load(QByteArray(svg.encode()))
w.setFixedSize(tamanho, tamanho)
return w
def icone_do_app():
renderizador = QSvgRenderer(QByteArray(monolito_svg(contorno=False).encode()))
icone = QIcon()
for lado in (16, 32, 48, 64, 128, 256):
pixmap = QPixmap(lado, lado)
pixmap.fill(Qt.transparent)
pintor = QPainter(pixmap)
renderizador.render(pintor)
pintor.end()
icone.addPixmap(pixmap)
return icone
# ------------------------------------------------------------
# Folha de estilo
# ------------------------------------------------------------
def folha_de_estilo():
c = CORES
return f"""
* {{ font-family: "{FONTE_TEXTO}"; color: {c['texto']}; }}
QWidget#raiz, QWidget#principal {{ background: {c['fundo']}; }}
QFrame#lateral {{ background: {c['lateral']}; border: 0; border-right: 1px solid {c['linha']}; }}
QLabel {{ background: transparent; border: 0; }}
QLabel[papel="h1"] {{ font-size: 34px; font-weight: 600; }}
QLabel[papel="texto"] {{ font-size: 16px; color: {c['texto2']}; }}
QLabel[papel="kicker"] {{ font-family: "{FONTE_MONO}"; font-size: 12px; color: {c['apoio']}; }}
QLabel[papel="marca"] {{ font-size: 28px; font-weight: 600; }}
QLabel[papel="slogan"] {{ font-size: 14px; color: {c['apoio']}; }}
QLabel[papel="passo"] {{ font-size: 15px; color: {c['texto2']}; }}
QLabel[papel="passo-ativo"] {{ font-size: 15px; font-weight: 600; }}
QLabel[papel="passo-futuro"] {{ font-size: 15px; color: {c['apoio']}; }}
QLabel[papel="selo"] {{ font-family: "{FONTE_MONO}"; font-size: 11px; color: {c['mudo']}; }}
QLabel[papel="mono"] {{ font-family: "{FONTE_MONO}"; font-size: 12px; color: {c['apoio']}; }}
QLabel[papel="mono-claro"] {{ font-family: "{FONTE_MONO}"; font-size: 13px; color: {c['texto2']}; }}
QLabel[papel="linha"] {{ font-size: 15px; color: {c['texto2']}; }}
QLabel[papel="nota"] {{ font-size: 13px; color: {c['apoio']}; }}
QLabel[papel="alerta"] {{ font-size: 14px; color: {c['texto']}; }}
QLabel[papel="titulo-cartao"] {{ font-size: 17px; font-weight: 600; }}
QLabel[papel="desc-cartao"] {{ font-size: 14px; color: {c['texto2']}; }}
QLabel[papel="campo"] {{ font-size: 12px; color: {c['apoio']}; }}
QLabel[papel="erro"] {{ font-size: 14px; color: {c['texto']}; }}
QPushButton {{ font-size: 14px; font-weight: 500; }}
QPushButton[papel="primario"] {{
background: {c['verde']}; color: {c['escuro']}; border: 0; border-radius: 10px;
min-height: 48px; padding: 0 28px; font-size: 15px; font-weight: 600;
}}
QPushButton[papel="primario"]:hover {{ background: #74E28E; }}
QPushButton[papel="primario"]:disabled {{ background: {c['borda']}; color: {c['apoio']}; }}
QPushButton[papel="contorno"] {{
background: transparent; color: {c['texto']}; border: 1px solid {c['borda3']}; border-radius: 10px;
min-height: 42px; padding: 0 19px;
}}
QPushButton[papel="contorno"]:hover {{ background: {c['destaque']}; }}
QPushButton[papel="contorno"]:disabled {{ color: {c['mudo']}; border-color: {c['borda2']}; }}
QPushButton[papel="chip"] {{
background: transparent; color: {c['texto2']}; border: 1px solid {c['borda2']}; border-radius: 8px;
min-height: 40px; padding: 0 14px; font-size: 13px; font-weight: 400;
}}
QPushButton[papel="chip"]:hover {{ background: {c['destaque']}; }}
QPushButton[papel="chip"]:checked {{
background: {c['fundo']}; color: {c['texto']}; border: 2px solid {c['verde']}; font-weight: 600;
}}
QFrame#cartao {{
background: transparent; border: 1px solid {c['borda2']}; border-radius: 14px;
}}
QFrame#cartao:hover {{ background: {c['destaque']}; }}
QFrame#cartao:focus {{ border: 1px solid {c['apoio']}; }}
QFrame#cartao[marcado="true"] {{ background: {c['cartao']}; border: 2px solid {c['verde']}; }}
QFrame[papel="painel"] {{ background: {c['cartao']}; border: 1px solid {c['borda']}; border-radius: 14px; }}
QFrame[papel="aviso"] {{ background: {c['destaque']}; border: 1.5px solid {c['apoio']}; border-radius: 12px; }}
QProgressBar {{ background: {c['lateral']}; border: 0; border-radius: 6px; min-height: 12px; max-height: 12px; }}
QProgressBar::chunk {{ background: {c['verde']}; border-radius: 6px; }}
QLineEdit, QSpinBox {{
min-height: 40px; padding: 0 12px; border-radius: 8px; border: 1px solid {c['borda2']};
background: {c['fundo']}; font-family: "{FONTE_MONO}"; font-size: 13px; selection-background-color: {c['borda3']};
}}
QLineEdit:focus, QSpinBox:focus {{ border: 1px solid {c['verde']}; }}
QSpinBox::up-button, QSpinBox::down-button {{ width: 0; border: 0; }}
QLineEdit:disabled {{ color: {c['mudo']}; }}
QPlainTextEdit {{
padding: 6px 10px; border-radius: 8px; border: 1px solid {c['borda2']}; background: {c['fundo']};
color: {c['texto']}; font-size: 14px; selection-background-color: {c['borda3']};
}}
QPlainTextEdit:focus {{ border: 1px solid {c['verde']}; }}
""" + folha_principal()
def folha_principal():
"""As regras da janela principal (conversa, barra lateral, compositor, configurações)."""
c = CORES
return f"""
QFrame#lateral2 {{ background: {c['lateral']}; border: 0; border-right: 1px solid {c['linha']}; }}
QFrame#cabecalho {{ background: transparent; border: 0; border-bottom: 1px solid {c['linha']}; }}
QScrollArea, QScrollArea > QWidget > QWidget {{ background: transparent; border: 0; }}
QScrollBar:vertical {{ background: transparent; width: 10px; margin: 2px; }}
QScrollBar::handle:vertical {{ background: {c['borda']}; border-radius: 4px; min-height: 30px; }}
QScrollBar::handle:vertical:hover {{ background: {c['borda3']}; }}
QScrollBar::add-line, QScrollBar::sub-line {{ height: 0; }}
QScrollBar::add-page, QScrollBar::sub-page {{ background: transparent; }}
QLabel[papel="titulo-conversa"] {{ font-size: 16px; font-weight: 600; }}
QLabel[papel="grupo"] {{ font-size: 11px; font-weight: 500; color: {c['mudo']}; padding: 0 8px; }}
QLabel[papel="rodape"] {{ font-family: "{FONTE_MONO}"; font-size: 11px; color: {c['mudo']}; }}
QLabel[papel="pilula"] {{ font-family: "{FONTE_MONO}"; font-size: 12px; color: {c['texto2']}; }}
QLabel[papel="h2"] {{ font-size: 16px; font-weight: 600; }}
QLabel[papel="h1-pagina"] {{ font-size: 30px; font-weight: 600; }}
QLabel[papel="sub-pagina"] {{ font-size: 15px; color: {c['texto2']}; }}
QLabel[papel="voz-titulo"] {{ font-size: 26px; font-weight: 600; }}
QLabel[papel="voz-fala"] {{ font-size: 17px; }}
QLabel[papel="etiqueta"] {{ font-family: "{FONTE_MONO}"; font-size: 11px; color: {c['escuro']}; background: {c['apoio']};
border-radius: 6px; padding: 3px 8px; font-weight: 500; }}
QLabel[papel="espera"] {{ font-family: "{FONTE_MONO}"; font-size: 11px; color: {c['texto']}; border: 1px solid {c['apoio']};
border-radius: 11px; padding: 2px 10px; }}
QLabel[papel="mensagem-ia"] {{ font-size: 15px; color: {c['texto']}; }}
QLabel[papel="pensamento"] {{ font-size: 13px; color: {c['mudo']}; font-style: italic; }}
QLabel[papel="ferramenta"] {{ font-family: "{FONTE_MONO}"; font-size: 12px; color: {c['apoio']}; }}
QLabel[papel="ferramenta-ok"] {{ font-family: "{FONTE_MONO}"; font-size: 12px; color: {c['verde']}; }}
QLabel[papel="aviso-linha"] {{ font-size: 13px; color: {c['apoio']}; }}
QLabel[papel="erro-linha"] {{ font-size: 13px; color: #FF9B9B; }}
QFrame[papel="bloco-codigo"] {{ background: #0E1428; border: 1px solid {c['borda']}; border-radius: 8px; }}
QFrame[papel="bloco-codigo-topo"] {{ background: {c['cartao']}; border: 0; border-bottom: 1px solid {c['borda']};
border-top-left-radius: 7px; border-top-right-radius: 7px; }}
QLabel[papel="bloco-codigo-lingua"] {{ font-size: 12px; color: {c['texto2']}; background: transparent; }}
QPlainTextEdit[papel="bloco-codigo-texto"] {{ background: #0E1428; color: {c['texto']}; border: 0; padding: 4px 8px;
border-bottom-left-radius: 7px; border-bottom-right-radius: 7px; selection-background-color: {c['borda']}; }}
QPlainTextEdit[papel="bloco-codigo-texto"] QScrollBar:horizontal {{ background: transparent; height: 10px; margin: 2px; }}
QPlainTextEdit[papel="bloco-codigo-texto"] QScrollBar::handle:horizontal {{ background: {c['borda']}; border-radius: 4px; min-width: 30px; }}
QPlainTextEdit[papel="bloco-codigo-texto"] QScrollBar::add-line:horizontal, QPlainTextEdit[papel="bloco-codigo-texto"] QScrollBar::sub-line:horizontal {{ width: 0; }}
QPushButton[papel="copiar-codigo"] {{ background: {c['fundo']}; border: 1px solid {c['borda2']}; border-radius: 6px;
padding: 2px 10px; font-size: 12px; color: {c['texto']}; }}
QPushButton[papel="copiar-codigo"]:hover {{ background: {c['destaque']}; }}
QFrame[papel="bolha"] {{ background: #25345E; border: 0; border-radius: 14px; border-bottom-right-radius: 4px; }}
QFrame[papel="cartao-etapas"] {{ background: {c['cartao']}; border: 1px solid {c['borda']}; border-radius: 12px; }}
QFrame[papel="linha-etapa"] {{ background: transparent; border: 0; }}
QFrame[papel="linha-etapa"][ativa="true"] {{ background: {c['destaque']}; }}
QFrame[papel="cartao-aprovacao"] {{ background: #1F2B52; border: 1.5px solid {c['apoio']}; border-radius: 12px; }}
QFrame[papel="comando"] {{ background: {c['fundo']}; border: 0; border-radius: 8px; }}
QFrame[papel="pilula-modelo"] {{ background: {c['cartao']}; border: 1px solid {c['borda2']}; border-radius: 16px; }}
QFrame[papel="aviso-versao"] {{ background: {c['cartao']}; border: 1px solid {c['borda2']}; border-radius: 10px; }}
QFrame[papel="compositor"] {{ background: {c['cartao']}; border: 1px solid {c['borda2']}; border-radius: 18px; }}
QFrame[papel="compositor"][foco="true"] {{ border: 1px solid {c['apoio']}; }}
QFrame[papel="segmentos"] {{ background: {c['lateral']}; border: 1px solid {c['linha']}; border-radius: 10px; }}
QFrame[papel="anexo"] {{ background: {c['cartao']}; border: 1px solid {c['borda2']}; border-radius: 16px; }}
QFrame[papel="secao"] {{ background: {c['cartao']}; border: 1px solid {c['borda']}; border-radius: 14px; }}
QFrame[papel="cartao-modelo"] {{ background: {c['fundo']}; border: 1px solid {c['borda']}; border-radius: 12px; }}
QFrame[papel="cartao-modelo"]:hover {{ border-color: {c['borda3']}; }}
QFrame[papel="cartao-modelo"][destaque="true"] {{ background: {c['cartao']}; border: 2px solid {c['verde']}; }}
QLabel[papel="selo-destaque"] {{ color: {c['escuro']}; background: {c['verde']}; border-radius: 8px; padding: 1px 8px; font-size: 11px; font-weight: 700; }}
QFrame[papel="linha-lista"] {{ background: {c['fundo']}; border: 0; border-radius: 8px; }}
QFrame[papel="faixa-voz"] {{ background: {c['cartao']}; border: 1px solid {c['borda2']}; border-radius: 14px; }}
QPushButton[papel="nova"] {{
background: {c['cartao']}; border: 1px solid {c['borda2']}; border-radius: 10px; min-height: 44px;
padding: 0 14px; text-align: left; font-size: 14px; font-weight: 500;
}}
QPushButton[papel="nova"]:hover {{ background: {c['destaque']}; }}
QPushButton[papel="item-historico"] {{
background: transparent; border: 0; border-radius: 8px; min-height: 40px; padding: 0 12px;
text-align: left; font-size: 14px; font-weight: 400; color: {c['texto2']};
}}
QPushButton[papel="item-historico"]:hover {{ background: #1A2446; }}
QPushButton[papel="item-historico"][atual="true"] {{ background: {c['destaque']}; color: {c['texto']}; font-weight: 500; }}
QPushButton[papel="rodape-lateral"] {{
background: transparent; border: 0; border-radius: 8px; min-height: 44px; padding: 0 12px;
text-align: left; font-size: 14px; font-weight: 400; color: {c['texto2']};
}}
QPushButton[papel="rodape-lateral"]:hover {{ background: #1A2446; }}
QPushButton[papel="rodape-lateral"][atual="true"] {{ background: {c['destaque']}; color: {c['texto']}; font-weight: 500; }}
QPushButton[papel="parar"] {{
background: transparent; border: 1.5px solid {c['texto']}; border-radius: 10px; min-height: 40px; padding: 0 16px;
font-size: 14px; font-weight: 600;
}}
QPushButton[papel="parar"]:hover {{ background: {c['destaque']}; }}
QPushButton[papel="atualizar"] {{
background: {c['verde']}; color: {c['escuro']}; border: 0; border-radius: 8px; min-height: 40px;
font-size: 13px; font-weight: 600;
}}
QPushButton[papel="atualizar"]:hover {{ background: #74E28E; }}
QPushButton[papel="atualizar"]:disabled {{ background: {c['borda']}; color: {c['apoio']}; }}
QPushButton[papel="icone"] {{ background: transparent; border: 0; border-radius: 12px; }}
QPushButton[papel="icone"]:hover {{ background: {c['destaque']}; }}
QPushButton[papel="icone-contorno"] {{ background: transparent; border: 1px solid {c['borda3']}; border-radius: 12px; }}
QPushButton[papel="icone-contorno"]:hover {{ background: {c['destaque']}; }}
QPushButton[papel="icone-contorno"]:disabled {{ border-color: {c['borda']}; }}
QPushButton[papel="microfone"] {{ background: {c['verde']}; border: 0; border-radius: 26px; }}
QPushButton[papel="microfone"]:hover {{ background: #74E28E; }}
QPushButton[papel="microfone"][ativo="true"] {{ background: #FFFFFF; border: 3px solid {c['verde']}; }}
QPushButton[papel="segmento"] {{
background: transparent; border: 0; border-radius: 8px; min-height: 36px; padding: 0 12px;
font-size: 12.5px; font-weight: 400; color: {c['apoio']};
}}
QPushButton[papel="segmento"]:checked {{ background: {c['borda2']}; color: {c['texto']}; font-weight: 500; }}
QPushButton[papel="segmento"]:hover:!checked {{ background: #1A2446; }}
QPushButton[papel="segmento-grande"] {{
background: transparent; border: 0; border-radius: 9px; min-height: 44px; padding: 0 20px;
font-size: 14px; font-weight: 400; color: {c['apoio']};
}}
QPushButton[papel="segmento-grande"]:checked {{ background: {c['borda2']}; color: {c['texto']}; font-weight: 500; }}
QPushButton[papel="nav-config"] {{
background: transparent; border: 0; border-radius: 8px; min-height: 44px; padding: 0 12px; text-align: left;
font-size: 14px; font-weight: 400; color: {c['texto2']};
}}
QPushButton[papel="nav-config"]:hover {{ background: #1A2446; }}
QPushButton[papel="nav-config"]:checked {{ background: {c['destaque']}; color: {c['texto']}; font-weight: 500; }}
QPushButton[papel="link"] {{ background: transparent; border: 0; color: {c['apoio']}; text-decoration: underline; min-height: 28px; }}
QPlainTextEdit#entrada {{
background: transparent; border: 0; font-size: 15px; selection-background-color: {c['borda3']};
}}
QLineEdit#busca {{
background: transparent; border: 0; min-height: 40px; padding: 0; font-family: "{FONTE_TEXTO}"; font-size: 14px;
}}
QToolButton[papel="mais"] {{ background: transparent; border: 0; color: {c['apoio']}; font-size: 12px; }}
QToolButton[papel="mais"]:hover {{ color: {c['texto']}; }}
QMenu {{ background: {c['cartao']}; border: 1px solid {c['borda2']}; border-radius: 10px; padding: 6px; }}
QMenu::item {{ padding: 8px 14px; border-radius: 6px; font-size: 13px; }}
QMenu::item:selected {{ background: {c['destaque']}; }}
QMenu::separator {{ height: 1px; background: {c['linha']}; margin: 6px 8px; }}
QComboBox {{
background: {c['fundo']}; border: 1px solid {c['borda2']}; border-radius: 8px; min-height: 40px; padding: 0 12px; font-size: 14px;
}}
QComboBox QAbstractItemView {{ background: {c['cartao']}; border: 1px solid {c['borda2']}; selection-background-color: {c['destaque']}; }}
QCheckBox {{ font-size: 14px; spacing: 10px; }}
QCheckBox::indicator {{ width: 22px; height: 22px; border-radius: 6px; border: 1.5px solid {c['borda3']}; background: {c['fundo']}; }}
QCheckBox::indicator:checked {{ background: {c['verde']}; border-color: {c['verde']}; }}
QSlider::groove:horizontal {{ height: 6px; background: {c['lateral']}; border-radius: 3px; }}
QSlider::sub-page:horizontal {{ background: {c['verde']}; border-radius: 3px; }}
QSlider::handle:horizontal {{ background: {c['texto']}; width: 20px; height: 20px; margin: -8px 0; border-radius: 10px; }}
"""
# ------------------------------------------------------------
# Peças
# ------------------------------------------------------------
# Altura de linha de cada papel, em px (o corpo do mockup usa 1,5 vezes o tamanho da fonte;
# títulos 1,15). O Qt não tem line-height em QSS, então vai por HTML no próprio rótulo.
ALTURA_DE_LINHA = {
"texto": 24, "slogan": 21, "erro": 21, "alerta": 21,
"nota": 20, "mono-claro": 20, "linha": 23,
}
class Rotulo(QLabel):
"""QLabel com altura de linha fixa (ver ALTURA_DE_LINHA)."""
def __init__(self, texto="", papel=""):
super().__init__()
self._altura = ALTURA_DE_LINHA.get(papel, 0)
self._plano = ""
self.setProperty("papel", papel)
self.setTextFormat(Qt.RichText)
self.setText(texto)
def setText(self, texto):
self._plano = str(texto)
corpo = html.escape(self._plano)
super().setText(f'
{corpo}
' if self._altura else corpo)
def plano(self):
return self._plano
def rotulo(texto, papel, quebra=False, maximo=None, html_pronto=False):
if quebra or papel in ALTURA_DE_LINHA:
r = Rotulo(texto, papel)
else:
r = QLabel(texto)
r.setProperty("papel", papel)
if quebra:
r.setWordWrap(True)
if maximo:
r.setMaximumWidth(maximo)
if papel == "kicker":
fonte = r.font()
fonte.setLetterSpacing(QFont.AbsoluteSpacing, 0.96) # 0,08 em de 12 px
r.setFont(fonte)
return r
def aplicar_papel(widget, papel):
"""Troca o papel (QSS por propriedade) de um widget que já existe."""
widget.setProperty("papel", papel)
widget.style().unpolish(widget)
widget.style().polish(widget)
class Marcador(QWidget):
"""A bolinha dos passos: feito (verde com visto), ativo (anel verde com ponto) ou pendente (anel apagado)."""
def __init__(self, estado="pendente", tamanho=28, parent=None):
super().__init__(parent)
self._estado = estado
self.setFixedSize(tamanho, tamanho)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
@property
def estado(self):
return self._estado
def definir(self, estado):
if estado != self._estado:
self._estado = estado
self.update()
def paintEvent(self, _):
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
lado = float(self.width())
if self._estado == "feito":
p.setPen(Qt.NoPen)
p.setBrush(QColor(CORES["verde"]))
p.drawEllipse(QRectF(0, 0, lado, lado))
k = lado / 2 * 0.5 / 12 # o visto ocupa metade do círculo (grade de 24 do mockup)
caminho = QPainterPath()
caminho.moveTo(QPointF(5 * k, 12.5 * k))
caminho.lineTo(QPointF(9.5 * k, 17 * k))
caminho.lineTo(QPointF(19 * k, 7 * k))
p.translate(lado / 2 - 12 * k, lado / 2 - 12 * k)
caneta = QPen(QColor(CORES["escuro"]), max(2.0, lado * 0.115))
caneta.setCapStyle(Qt.RoundCap)
caneta.setJoinStyle(Qt.RoundJoin)
p.setPen(caneta)
p.setBrush(Qt.NoBrush)
p.drawPath(caminho)
else:
cor = CORES["verde"] if self._estado == "ativo" else CORES["borda3"]
p.setPen(QPen(QColor(cor), 2))
p.setBrush(Qt.NoBrush)
p.drawEllipse(QRectF(1, 1, lado - 2, lado - 2))
if self._estado == "ativo":
miolo = lado * 0.36
p.setPen(Qt.NoPen)
p.setBrush(QColor(CORES["verde"]))
p.drawEllipse(QRectF((lado - miolo) / 2, (lado - miolo) / 2, miolo, miolo))
p.end()