responds-forms 0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cartas/__init__.py +2060 -0
- cartas/origin.py +1196 -0
- pyflutter_ui/__init__.py +30 -0
- pyflutter_ui/app.py +289 -0
- pyflutter_ui/file_picker.py +274 -0
- pyflutter_ui/navigator.py +93 -0
- pyflutter_ui/scheduler.py +154 -0
- pyflutter_ui/theme.py +1058 -0
- pyflutter_ui/ui/__init__.py +27 -0
- pyflutter_ui/ui/buttons.py +219 -0
- pyflutter_ui/ui/container.py +119 -0
- pyflutter_ui/ui/core.py +820 -0
- pyflutter_ui/ui/list_builder.py +396 -0
- pyflutter_ui/ui/progress.py +80 -0
- pyflutter_ui/ui/tables.py +109 -0
- pyflutter_ui/ui/widgets.py +1418 -0
- responds_forms-0.4.dist-info/METADATA +15 -0
- responds_forms-0.4.dist-info/RECORD +38 -0
- responds_forms-0.4.dist-info/WHEEL +4 -0
- responds_ui/__init__.py +1 -0
- responds_ui/__main__.py +25 -0
- responds_ui/assets/forms_config.json +57 -0
- responds_ui/config_app.py +109 -0
- responds_ui/lib_toi.py +864 -0
- responds_ui/page_forms/Diagnorstico.txt +108 -0
- responds_ui/page_forms/__init__.py +5 -0
- responds_ui/page_forms/common.py +192 -0
- responds_ui/page_forms/config.py +68 -0
- responds_ui/page_forms/controller_forms.py +390 -0
- responds_ui/page_forms/diagnose_forms.py +64 -0
- responds_ui/page_forms/master_forms.py +113 -0
- responds_ui/page_forms/model_forms.py +329 -0
- responds_ui/page_forms/service.py +347 -0
- responds_ui/page_forms/view_forms.py +164 -0
- responds_ui/table_widget.py +140 -0
- responds_ui/util.py +74 -0
- responds_ui/version.py +2 -0
- whatsapp_files/__init__.py +1092 -0
pyflutter_ui/theme.py
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
# theme.py
|
|
2
|
+
"""
|
|
3
|
+
Sistema de temas para pyflutter — inspirado na ThemeData do Flutter.
|
|
4
|
+
|
|
5
|
+
Funcionalidades:
|
|
6
|
+
- ThemeMode: light | dark | system (detecta o SO automaticamente)
|
|
7
|
+
- ColorScheme: paleta completa com primary, secondary, surface, error, etc.
|
|
8
|
+
- TextTheme: estilos tipográficos (displayLarge → labelSmall)
|
|
9
|
+
- ButtonThemeData: estilo para ElevatedButton, OutlinedButton, TextButton
|
|
10
|
+
- ThemeData: objeto raiz que agrega tudo e aplica no ttk.Style
|
|
11
|
+
- ThemeProvider(ChangeNotifier): permite trocar o tema em runtime
|
|
12
|
+
|
|
13
|
+
Uso básico:
|
|
14
|
+
from pyflutter.theme import ThemeData, ThemeMode, ThemeProvider
|
|
15
|
+
|
|
16
|
+
provider = ThemeProvider(ThemeData.light())
|
|
17
|
+
app.register_provider(provider)
|
|
18
|
+
|
|
19
|
+
# Para mudar o tema em runtime:
|
|
20
|
+
provider.set_mode(ThemeMode.DARK)
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import sys
|
|
24
|
+
import tkinter as tk
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from enum import Enum
|
|
27
|
+
from tkinter import ttk
|
|
28
|
+
from typing import Optional, TypeVar
|
|
29
|
+
from pyflutter_ui.ui.core import ChangeNotifier, BaseMaterialApp, Navigator
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
T = TypeVar('T')
|
|
33
|
+
|
|
34
|
+
# ============================================================
|
|
35
|
+
# THEME MODE
|
|
36
|
+
# ============================================================
|
|
37
|
+
|
|
38
|
+
class ThemeMode(Enum):
|
|
39
|
+
"""
|
|
40
|
+
Equivalente ao ThemeMode do Flutter.
|
|
41
|
+
SYSTEM detecta a preferência do sistema operacional.
|
|
42
|
+
"""
|
|
43
|
+
LIGHT = "light"
|
|
44
|
+
DARK = "dark"
|
|
45
|
+
SYSTEM = "system"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _detect_system_dark_mode() -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Tenta detectar se o SO está em modo escuro.
|
|
51
|
+
Suporta Windows, macOS e Linux (via tk).
|
|
52
|
+
Retorna True se dark mode estiver ativo.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
if sys.platform == "darwin":
|
|
56
|
+
# macOS: consulta via AppleScript
|
|
57
|
+
import subprocess
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
["defaults", "read", "-g", "AppleInterfaceStyle"],
|
|
60
|
+
capture_output=True, text=True
|
|
61
|
+
)
|
|
62
|
+
return result.stdout.strip().lower() == "dark"
|
|
63
|
+
|
|
64
|
+
elif sys.platform == "win32":
|
|
65
|
+
# Windows: consulta o registro
|
|
66
|
+
import winreg
|
|
67
|
+
key = winreg.OpenKey(
|
|
68
|
+
winreg.HKEY_CURRENT_USER,
|
|
69
|
+
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
|
|
70
|
+
)
|
|
71
|
+
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
|
|
72
|
+
return value == 0 # 0 = dark, 1 = light
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
# Linux: heurística via tema GTK (fallback: light)
|
|
76
|
+
root = tk.Tk()
|
|
77
|
+
root.withdraw()
|
|
78
|
+
style = ttk.Style(root)
|
|
79
|
+
bg = style.lookup("TFrame", "background") or "#ffffff"
|
|
80
|
+
root.destroy()
|
|
81
|
+
# Se o background for escuro, assume dark mode
|
|
82
|
+
r, g, b = int(bg[1:3], 16), int(bg[3:5], 16), int(bg[5:7], 16)
|
|
83
|
+
luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
|
84
|
+
return luminance < 0.5
|
|
85
|
+
|
|
86
|
+
except Exception:
|
|
87
|
+
return False # fallback seguro: light mode
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ============================================================
|
|
91
|
+
# COLOR SCHEME
|
|
92
|
+
# ============================================================
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ColorScheme:
|
|
96
|
+
"""
|
|
97
|
+
Paleta de cores completa, espelhando o ColorScheme do Flutter (Material 3).
|
|
98
|
+
|
|
99
|
+
Convenção:
|
|
100
|
+
- `primary` : cor principal da marca
|
|
101
|
+
- `on_primary` : texto/ícone sobre primary
|
|
102
|
+
- `primary_container` : variante de fundo para primary
|
|
103
|
+
- `on_primary_container` : texto sobre primary_container
|
|
104
|
+
- `secondary` : cor de destaque secundária
|
|
105
|
+
- `on_secondary` : texto sobre secondary
|
|
106
|
+
- `secondary_container`
|
|
107
|
+
- `on_secondary_container`
|
|
108
|
+
- `tertiary` / `on_tertiary` / `tertiary_container` / `on_tertiary_container`
|
|
109
|
+
- `error` / `on_error` / `error_container` / `on_error_container`
|
|
110
|
+
- `surface` : fundo dos cards/dialogs
|
|
111
|
+
- `on_surface` : texto sobre surface
|
|
112
|
+
- `surface_variant` : variante de surface
|
|
113
|
+
- `on_surface_variant`
|
|
114
|
+
- `background` : cor de fundo da janela
|
|
115
|
+
- `on_background` : texto sobre background
|
|
116
|
+
- `outline` : bordas e divisores
|
|
117
|
+
- `shadow` : cor de sombra
|
|
118
|
+
- `inverse_surface` / `on_inverse_surface` / `inverse_primary`
|
|
119
|
+
- `scrim` : overlay semi-transparente (Drawer, Modal)
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
# ── Primary ──────────────────────────────────────────────
|
|
123
|
+
primary: str = "#6750A4"
|
|
124
|
+
on_primary: str = "#FFFFFF"
|
|
125
|
+
primary_container: str = "#EADDFF"
|
|
126
|
+
on_primary_container: str = "#21005D"
|
|
127
|
+
|
|
128
|
+
# ── Secondary ────────────────────────────────────────────
|
|
129
|
+
secondary: str = "#625B71"
|
|
130
|
+
on_secondary: str = "#FFFFFF"
|
|
131
|
+
secondary_container: str = "#E8DEF8"
|
|
132
|
+
on_secondary_container: str = "#1D192B"
|
|
133
|
+
|
|
134
|
+
# ── Tertiary ─────────────────────────────────────────────
|
|
135
|
+
tertiary: str = "#7D5260"
|
|
136
|
+
on_tertiary: str = "#FFFFFF"
|
|
137
|
+
tertiary_container: str = "#FFD8E4"
|
|
138
|
+
on_tertiary_container: str = "#31111D"
|
|
139
|
+
|
|
140
|
+
# ── Error ────────────────────────────────────────────────
|
|
141
|
+
error: str = "#B3261E"
|
|
142
|
+
on_error: str = "#FFFFFF"
|
|
143
|
+
error_container: str = "#F9DEDC"
|
|
144
|
+
on_error_container: str = "#410E0B"
|
|
145
|
+
|
|
146
|
+
# ── Surface / Background ─────────────────────────────────
|
|
147
|
+
surface: str = "#FFFBFE"
|
|
148
|
+
on_surface: str = "#1C1B1F"
|
|
149
|
+
surface_variant: str = "#E7E0EC"
|
|
150
|
+
on_surface_variant: str = "#49454F"
|
|
151
|
+
|
|
152
|
+
background: str = "#FFFBFE"
|
|
153
|
+
on_background: str = "#1C1B1F"
|
|
154
|
+
|
|
155
|
+
# ── Utilities ────────────────────────────────────────────
|
|
156
|
+
outline: str = "#79747E"
|
|
157
|
+
outline_variant: str = "#CAC4D0"
|
|
158
|
+
shadow: str = "#000000"
|
|
159
|
+
scrim: str = "#000000"
|
|
160
|
+
|
|
161
|
+
# ── Inverse ──────────────────────────────────────────────
|
|
162
|
+
inverse_surface: str = "#313033"
|
|
163
|
+
on_inverse_surface: str = "#F4EFF4"
|
|
164
|
+
inverse_primary: str = "#D0BCFF"
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def light(cls) -> "ColorScheme":
|
|
168
|
+
"""Paleta Material 3 Light padrão."""
|
|
169
|
+
return cls()
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def dark(cls) -> "ColorScheme":
|
|
173
|
+
"""Paleta Material 3 Dark padrão."""
|
|
174
|
+
return cls(
|
|
175
|
+
primary="#D0BCFF",
|
|
176
|
+
on_primary="#381E72",
|
|
177
|
+
primary_container="#4F378B",
|
|
178
|
+
on_primary_container="#EADDFF",
|
|
179
|
+
|
|
180
|
+
secondary="#CCC2DC",
|
|
181
|
+
on_secondary="#332D41",
|
|
182
|
+
secondary_container="#4A4458",
|
|
183
|
+
on_secondary_container="#E8DEF8",
|
|
184
|
+
|
|
185
|
+
tertiary="#EFB8C8",
|
|
186
|
+
on_tertiary="#492532",
|
|
187
|
+
tertiary_container="#633B48",
|
|
188
|
+
on_tertiary_container="#FFD8E4",
|
|
189
|
+
|
|
190
|
+
error="#F2B8B5",
|
|
191
|
+
on_error="#601410",
|
|
192
|
+
error_container="#8C1D18",
|
|
193
|
+
on_error_container="#F9DEDC",
|
|
194
|
+
|
|
195
|
+
surface="#1C1B1F",
|
|
196
|
+
on_surface="#E6E1E5",
|
|
197
|
+
surface_variant="#49454F",
|
|
198
|
+
on_surface_variant="#CAC4D0",
|
|
199
|
+
|
|
200
|
+
background="#1C1B1F",
|
|
201
|
+
on_background="#E6E1E5",
|
|
202
|
+
|
|
203
|
+
outline="#938F99",
|
|
204
|
+
outline_variant="#49454F",
|
|
205
|
+
shadow="#000000",
|
|
206
|
+
scrim="#000000",
|
|
207
|
+
|
|
208
|
+
inverse_surface="#E6E1E5",
|
|
209
|
+
on_inverse_surface="#313033",
|
|
210
|
+
inverse_primary="#6750A4",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ============================================================
|
|
215
|
+
# TEXT STYLE & TEXT THEME
|
|
216
|
+
# ============================================================
|
|
217
|
+
|
|
218
|
+
@dataclass
|
|
219
|
+
class TextStyle:
|
|
220
|
+
"""
|
|
221
|
+
Estilo tipográfico individual.
|
|
222
|
+
Mapeia para os parâmetros de fonte do ttk.Label / tk.font.Font.
|
|
223
|
+
"""
|
|
224
|
+
font_family: str = "Helvetica"
|
|
225
|
+
font_size: int = 10
|
|
226
|
+
font_weight: str = "normal" # "normal" | "bold"
|
|
227
|
+
font_slant: str = "roman" # "roman" | "italic"
|
|
228
|
+
color: Optional[str] = None # None = herda do ColorScheme
|
|
229
|
+
|
|
230
|
+
def to_tk_font(self) -> tuple:
|
|
231
|
+
"""Retorna tupla compatível com o parâmetro `font=` do Tk."""
|
|
232
|
+
style = []
|
|
233
|
+
if self.font_weight == "bold":
|
|
234
|
+
style.append("bold")
|
|
235
|
+
if self.font_slant == "italic":
|
|
236
|
+
style.append("italic")
|
|
237
|
+
return (self.font_family, self.font_size) + (tuple(style) if style else ())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass
|
|
241
|
+
class TextTheme:
|
|
242
|
+
"""
|
|
243
|
+
Hierarquia tipográfica completa — espelha o TextTheme do Flutter (Material 3).
|
|
244
|
+
|
|
245
|
+
Display → headings grandes (splash screens, hero)
|
|
246
|
+
Headline → títulos de seção
|
|
247
|
+
Title → AppBar, dialogs
|
|
248
|
+
Body → texto corrido
|
|
249
|
+
Label → botões, captions, chips
|
|
250
|
+
"""
|
|
251
|
+
display_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=57, font_weight="normal"))
|
|
252
|
+
display_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=45, font_weight="normal"))
|
|
253
|
+
display_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=36, font_weight="normal"))
|
|
254
|
+
|
|
255
|
+
headline_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=32, font_weight="normal"))
|
|
256
|
+
headline_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=28, font_weight="normal"))
|
|
257
|
+
headline_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=24, font_weight="normal"))
|
|
258
|
+
|
|
259
|
+
title_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=22, font_weight="normal"))
|
|
260
|
+
title_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=14, font_weight="bold"))
|
|
261
|
+
title_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=11, font_weight="bold"))
|
|
262
|
+
|
|
263
|
+
body_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=16, font_weight="normal"))
|
|
264
|
+
body_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=14, font_weight="normal"))
|
|
265
|
+
body_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=11, font_weight="normal"))
|
|
266
|
+
|
|
267
|
+
label_large: TextStyle = field(default_factory=lambda: TextStyle(font_size=14, font_weight="bold"))
|
|
268
|
+
label_medium: TextStyle = field(default_factory=lambda: TextStyle(font_size=12, font_weight="bold"))
|
|
269
|
+
label_small: TextStyle = field(default_factory=lambda: TextStyle(font_size=10, font_weight="bold"))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ============================================================
|
|
273
|
+
# BUTTON THEME DATA
|
|
274
|
+
# ============================================================
|
|
275
|
+
|
|
276
|
+
@dataclass
|
|
277
|
+
class ButtonThemeData:
|
|
278
|
+
"""
|
|
279
|
+
Configuração visual dos botões.
|
|
280
|
+
Espelha ElevatedButtonThemeData / OutlinedButtonThemeData / TextButtonThemeData.
|
|
281
|
+
"""
|
|
282
|
+
# Padding interno (px)
|
|
283
|
+
padding_x: int = 16
|
|
284
|
+
padding_y: int = 8
|
|
285
|
+
|
|
286
|
+
# Raio da borda (aproximado via relief no Tk)
|
|
287
|
+
border_radius: int = 12
|
|
288
|
+
|
|
289
|
+
# Altura mínima (px)
|
|
290
|
+
min_height: int = 20
|
|
291
|
+
|
|
292
|
+
# Estilos para cada variante
|
|
293
|
+
elevated_relief: str = "raised"
|
|
294
|
+
outlined_relief: str = "groove"
|
|
295
|
+
text_relief: str = "flat"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# ============================================================
|
|
299
|
+
# INPUT DECORATION THEME
|
|
300
|
+
# ============================================================
|
|
301
|
+
|
|
302
|
+
@dataclass
|
|
303
|
+
class InputDecorationTheme:
|
|
304
|
+
"""
|
|
305
|
+
Estilo dos campos de texto (TextField).
|
|
306
|
+
"""
|
|
307
|
+
border_color: Optional[str] = None # None = usa outline do ColorScheme
|
|
308
|
+
focused_border_color: Optional[str] = None # None = usa primary
|
|
309
|
+
label_color: Optional[str] = None
|
|
310
|
+
hint_color: Optional[str] = None
|
|
311
|
+
fill_color: Optional[str] = None # None = usa surface_variant
|
|
312
|
+
filled: bool = True
|
|
313
|
+
border_width: int = 1
|
|
314
|
+
padding_x: int = 12
|
|
315
|
+
padding_y: int = 8
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ============================================================
|
|
319
|
+
# APP BAR THEME
|
|
320
|
+
# ============================================================
|
|
321
|
+
|
|
322
|
+
@dataclass
|
|
323
|
+
class AppBarTheme:
|
|
324
|
+
"""
|
|
325
|
+
Estilo da AppBar.
|
|
326
|
+
"""
|
|
327
|
+
background_color: Optional[str] = None # None = usa surface do ColorScheme
|
|
328
|
+
foreground_color: Optional[str] = None # None = usa on_surface
|
|
329
|
+
elevation: int = 0
|
|
330
|
+
center_title: bool = False
|
|
331
|
+
title_text_style: Optional[TextStyle] = None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ============================================================
|
|
335
|
+
# CARD THEME
|
|
336
|
+
# ============================================================
|
|
337
|
+
|
|
338
|
+
@dataclass
|
|
339
|
+
class CardTheme:
|
|
340
|
+
"""
|
|
341
|
+
Estilo dos Cards.
|
|
342
|
+
"""
|
|
343
|
+
color: Optional[str] = None # None = usa surface
|
|
344
|
+
shadow_color: Optional[str] = None # None = usa shadow
|
|
345
|
+
elevation: int = 1
|
|
346
|
+
margin: int = 4
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# ============================================================
|
|
350
|
+
# DIVIDER THEME
|
|
351
|
+
# ============================================================
|
|
352
|
+
|
|
353
|
+
@dataclass
|
|
354
|
+
class DividerTheme:
|
|
355
|
+
color: Optional[str] = None # None = usa outline_variant
|
|
356
|
+
thickness: int = 1
|
|
357
|
+
indent: int = 0
|
|
358
|
+
end_indent: int = 0
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ============================================================
|
|
362
|
+
# THEME DATA
|
|
363
|
+
# ============================================================
|
|
364
|
+
|
|
365
|
+
class ThemeData:
|
|
366
|
+
"""
|
|
367
|
+
Objeto raiz do tema — equivalente ao ThemeData do Flutter.
|
|
368
|
+
|
|
369
|
+
Agrega ColorScheme, TextTheme, ButtonThemeData e sub-temas,
|
|
370
|
+
e aplica tudo no ttk.Style quando `apply()` é chamado.
|
|
371
|
+
|
|
372
|
+
Exemplo:
|
|
373
|
+
theme = ThemeData.light()
|
|
374
|
+
theme.apply(root)
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
def __init__(
|
|
378
|
+
self,
|
|
379
|
+
color_scheme: ColorScheme,
|
|
380
|
+
text_theme: Optional[TextTheme] = None,
|
|
381
|
+
button_theme: Optional[ButtonThemeData] = None,
|
|
382
|
+
input_decoration_theme: Optional[InputDecorationTheme] = None,
|
|
383
|
+
app_bar_theme: Optional[AppBarTheme] = None,
|
|
384
|
+
card_theme: Optional[CardTheme] = None,
|
|
385
|
+
divider_theme: Optional[DividerTheme] = None,
|
|
386
|
+
font_family: str = "Helvetica",
|
|
387
|
+
) -> None:
|
|
388
|
+
self.color_scheme = color_scheme
|
|
389
|
+
self.text_theme = text_theme or TextTheme()
|
|
390
|
+
self.button_theme = button_theme or ButtonThemeData()
|
|
391
|
+
self.input_decoration_theme = input_decoration_theme or InputDecorationTheme()
|
|
392
|
+
self.app_bar_theme = app_bar_theme or AppBarTheme()
|
|
393
|
+
self.card_theme = card_theme or CardTheme()
|
|
394
|
+
self.divider_theme = divider_theme or DividerTheme()
|
|
395
|
+
self.font_family = font_family
|
|
396
|
+
|
|
397
|
+
# Propaga font_family padrão para todos os TextStyle sem família definida
|
|
398
|
+
self._propagate_font_family()
|
|
399
|
+
|
|
400
|
+
def _propagate_font_family(self) -> None:
|
|
401
|
+
for attr in vars(self.text_theme).values():
|
|
402
|
+
if isinstance(attr, TextStyle) and attr.font_family == "Helvetica":
|
|
403
|
+
attr.font_family = self.font_family
|
|
404
|
+
|
|
405
|
+
# ── Factory ──────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
@classmethod
|
|
408
|
+
def light(
|
|
409
|
+
cls,
|
|
410
|
+
color_scheme: Optional[ColorScheme] = None,
|
|
411
|
+
font_family: str = "Helvetica",
|
|
412
|
+
**kwargs,
|
|
413
|
+
) -> "ThemeData":
|
|
414
|
+
"""Cria um ThemeData com paleta Light."""
|
|
415
|
+
return cls(
|
|
416
|
+
color_scheme=color_scheme or ColorScheme.light(),
|
|
417
|
+
font_family=font_family,
|
|
418
|
+
**kwargs,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
@classmethod
|
|
422
|
+
def dark(
|
|
423
|
+
cls,
|
|
424
|
+
color_scheme: Optional[ColorScheme] = None,
|
|
425
|
+
font_family: str = "Helvetica",
|
|
426
|
+
**kwargs,
|
|
427
|
+
) -> "ThemeData":
|
|
428
|
+
"""Cria um ThemeData com paleta Dark."""
|
|
429
|
+
return cls(
|
|
430
|
+
color_scheme=color_scheme or ColorScheme.dark(),
|
|
431
|
+
font_family=font_family,
|
|
432
|
+
**kwargs,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# ── Atalhos rápidos para cores ───────────────────────────
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def primary(self) -> str:
|
|
439
|
+
return self.color_scheme.primary
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def background(self) -> str:
|
|
443
|
+
return self.color_scheme.background
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def surface(self) -> str:
|
|
447
|
+
return self.color_scheme.surface
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def error(self) -> str:
|
|
451
|
+
return self.color_scheme.error
|
|
452
|
+
|
|
453
|
+
# ── Aplicação no ttk.Style ───────────────────────────────
|
|
454
|
+
|
|
455
|
+
def apply(self, root: tk.Tk) -> None:
|
|
456
|
+
"""
|
|
457
|
+
Aplica o tema inteiro no ttk.Style.
|
|
458
|
+
Deve ser chamado APÓS tk.Tk() ser criado — nunca antes,
|
|
459
|
+
pois ttk.Style() sem um root existente cria uma janela tk implícita.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
root: instância do tk.Tk. Obrigatório quando ainda não há
|
|
463
|
+
uma instância Tk ativa no processo.
|
|
464
|
+
"""
|
|
465
|
+
# Garante que existe um root antes de instanciar o Style.
|
|
466
|
+
# Se nenhum root foi passado, usa o root padrão do Tk, que
|
|
467
|
+
# só existe se tk.Tk() já foi chamado antes.
|
|
468
|
+
if root is None and not tk._default_root: # noqa: SLF001
|
|
469
|
+
# Sem root ativo: não há como aplicar estilos ttk com segurança.
|
|
470
|
+
# O chamador deve passar o root explicitamente.
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
cs = self.color_scheme
|
|
474
|
+
tt = self.text_theme
|
|
475
|
+
bt = self.button_theme
|
|
476
|
+
idt = self.input_decoration_theme
|
|
477
|
+
abt = self.app_bar_theme
|
|
478
|
+
ct = self.card_theme
|
|
479
|
+
dt = self.divider_theme
|
|
480
|
+
|
|
481
|
+
style = ttk.Style(root) if root else ttk.Style()
|
|
482
|
+
|
|
483
|
+
# ── Tema base ────────────────────────────────────────
|
|
484
|
+
# Usa "clam" como base: permite sobrescrever mais configurações
|
|
485
|
+
try:
|
|
486
|
+
style.theme_use("clam")
|
|
487
|
+
except tk.TclError:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
# ── Janela principal ─────────────────────────────────
|
|
491
|
+
if root:
|
|
492
|
+
root.configure(bg=cs.background)
|
|
493
|
+
|
|
494
|
+
# ── TFrame ───────────────────────────────────────────
|
|
495
|
+
style.configure(
|
|
496
|
+
"TFrame",
|
|
497
|
+
background=cs.background,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
style.configure(
|
|
501
|
+
"Surface.TFrame",
|
|
502
|
+
background=cs.surface,
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
style.configure(
|
|
506
|
+
"Card.TFrame",
|
|
507
|
+
background=ct.color or cs.surface,
|
|
508
|
+
relief="raised",
|
|
509
|
+
borderwidth=ct.elevation,
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# ── TLabel ───────────────────────────────────────────
|
|
513
|
+
body_font = tt.body_small.to_tk_font()
|
|
514
|
+
title_font = tt.title_small.to_tk_font()
|
|
515
|
+
label_font = tt.label_small.to_tk_font()
|
|
516
|
+
display_font = tt.display_small.to_tk_font()
|
|
517
|
+
headline_font = tt.headline_small.to_tk_font()
|
|
518
|
+
|
|
519
|
+
style.configure(
|
|
520
|
+
"TLabel",
|
|
521
|
+
background=cs.background,
|
|
522
|
+
foreground=cs.on_background,
|
|
523
|
+
font=body_font,
|
|
524
|
+
padding=2,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# Variantes semânticas de label
|
|
528
|
+
_label_variants = {
|
|
529
|
+
"Display.TLabel": (display_font, cs.on_background),
|
|
530
|
+
"Headline.TLabel": (headline_font, cs.on_background),
|
|
531
|
+
"Title.TLabel": (title_font, cs.on_surface),
|
|
532
|
+
"Body.TLabel": (body_font, cs.on_background),
|
|
533
|
+
"Caption.TLabel": (tt.label_small.to_tk_font(), cs.on_surface_variant),
|
|
534
|
+
"Error.TLabel": (label_font, cs.error),
|
|
535
|
+
"Primary.TLabel": (label_font, cs.primary),
|
|
536
|
+
"OnPrimary.TLabel": (label_font, cs.on_primary),
|
|
537
|
+
}
|
|
538
|
+
for name, (font, fg) in _label_variants.items():
|
|
539
|
+
style.configure(
|
|
540
|
+
name,
|
|
541
|
+
background=cs.background,
|
|
542
|
+
foreground=fg,
|
|
543
|
+
font=font,
|
|
544
|
+
padding=2,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Label sobre superfície colorida
|
|
548
|
+
style.configure(
|
|
549
|
+
"AppBar.TLabel",
|
|
550
|
+
background=abt.background_color or cs.surface,
|
|
551
|
+
foreground=abt.foreground_color or cs.on_surface,
|
|
552
|
+
font=(abt.title_text_style or tt.title_large).to_tk_font(),
|
|
553
|
+
padding=(8, 8),
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# ── TButton (ElevatedButton) ─────────────────────────
|
|
557
|
+
style.configure(
|
|
558
|
+
"TButton",
|
|
559
|
+
background=cs.primary,
|
|
560
|
+
foreground=cs.on_primary,
|
|
561
|
+
font=tt.label_small.to_tk_font(),
|
|
562
|
+
padding=(bt.padding_x, bt.padding_y),
|
|
563
|
+
relief=bt.elevated_relief,
|
|
564
|
+
borderwidth=0,
|
|
565
|
+
)
|
|
566
|
+
style.map(
|
|
567
|
+
"TButton",
|
|
568
|
+
background=[
|
|
569
|
+
("active", cs.primary_container),
|
|
570
|
+
("disabled", cs.surface_variant),
|
|
571
|
+
],
|
|
572
|
+
foreground=[
|
|
573
|
+
("active", cs.on_primary_container),
|
|
574
|
+
("disabled", cs.on_surface_variant),
|
|
575
|
+
],
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# ElevatedButton explícito
|
|
579
|
+
style.configure(
|
|
580
|
+
"Elevated.TButton",
|
|
581
|
+
background=cs.primary,
|
|
582
|
+
foreground=cs.on_primary,
|
|
583
|
+
font=tt.label_large.to_tk_font(),
|
|
584
|
+
padding=(bt.padding_x, bt.padding_y),
|
|
585
|
+
relief=bt.elevated_relief,
|
|
586
|
+
)
|
|
587
|
+
style.map(
|
|
588
|
+
"Elevated.TButton",
|
|
589
|
+
background=[
|
|
590
|
+
("active", cs.primary_container),
|
|
591
|
+
("disabled", cs.surface_variant),
|
|
592
|
+
],
|
|
593
|
+
foreground=[
|
|
594
|
+
("active", cs.on_primary_container),
|
|
595
|
+
("disabled", cs.on_surface_variant),
|
|
596
|
+
],
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# OutlinedButton
|
|
600
|
+
style.configure(
|
|
601
|
+
"Outlined.TButton",
|
|
602
|
+
background=cs.surface,
|
|
603
|
+
foreground=cs.primary,
|
|
604
|
+
font=tt.label_large.to_tk_font(),
|
|
605
|
+
padding=(bt.padding_x, bt.padding_y),
|
|
606
|
+
relief=bt.outlined_relief,
|
|
607
|
+
borderwidth=bt.border_width if hasattr(bt, "border_width") else 1,
|
|
608
|
+
)
|
|
609
|
+
style.map(
|
|
610
|
+
"Outlined.TButton",
|
|
611
|
+
background=[
|
|
612
|
+
("active", cs.primary_container),
|
|
613
|
+
("disabled", cs.surface),
|
|
614
|
+
],
|
|
615
|
+
foreground=[
|
|
616
|
+
("active", cs.primary),
|
|
617
|
+
("disabled", cs.on_surface_variant),
|
|
618
|
+
],
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# TextButton
|
|
622
|
+
style.configure(
|
|
623
|
+
"Text.TButton",
|
|
624
|
+
background=cs.background,
|
|
625
|
+
foreground=cs.primary,
|
|
626
|
+
font=tt.label_large.to_tk_font(),
|
|
627
|
+
padding=(bt.padding_x, bt.padding_y),
|
|
628
|
+
relief=bt.text_relief,
|
|
629
|
+
borderwidth=0,
|
|
630
|
+
)
|
|
631
|
+
style.map(
|
|
632
|
+
"Text.TButton",
|
|
633
|
+
background=[
|
|
634
|
+
("active", cs.primary_container),
|
|
635
|
+
("disabled", cs.background),
|
|
636
|
+
],
|
|
637
|
+
foreground=[
|
|
638
|
+
("active", cs.on_primary_container),
|
|
639
|
+
("disabled", cs.on_surface_variant),
|
|
640
|
+
],
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
# FloatingActionButton
|
|
644
|
+
style.configure(
|
|
645
|
+
"FAB.TButton",
|
|
646
|
+
background=cs.primary_container,
|
|
647
|
+
foreground=cs.on_primary_container,
|
|
648
|
+
font=tt.label_large.to_tk_font(),
|
|
649
|
+
padding=(16, 16),
|
|
650
|
+
relief="raised",
|
|
651
|
+
borderwidth=0,
|
|
652
|
+
)
|
|
653
|
+
style.map(
|
|
654
|
+
"FAB.TButton",
|
|
655
|
+
background=[("active", cs.primary)],
|
|
656
|
+
foreground=[("active", cs.on_primary)],
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
# Danger / Error button
|
|
660
|
+
style.configure(
|
|
661
|
+
"Danger.TButton",
|
|
662
|
+
background=cs.error,
|
|
663
|
+
foreground=cs.on_error,
|
|
664
|
+
font=tt.label_large.to_tk_font(),
|
|
665
|
+
padding=(bt.padding_x, bt.padding_y),
|
|
666
|
+
relief=bt.elevated_relief,
|
|
667
|
+
)
|
|
668
|
+
style.map(
|
|
669
|
+
"Danger.TButton",
|
|
670
|
+
background=[("active", cs.error_container)],
|
|
671
|
+
foreground=[("active", cs.on_error_container)],
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# ── TEntry (TextField) ───────────────────────────────
|
|
675
|
+
fill = idt.fill_color or cs.surface_variant
|
|
676
|
+
border = idt.border_color or cs.outline
|
|
677
|
+
focused_border = idt.focused_border_color or cs.primary
|
|
678
|
+
|
|
679
|
+
style.configure(
|
|
680
|
+
"TEntry",
|
|
681
|
+
fieldbackground=fill,
|
|
682
|
+
foreground=cs.on_surface,
|
|
683
|
+
selectbackground=cs.primary,
|
|
684
|
+
selectforeground=cs.on_primary,
|
|
685
|
+
insertcolor=cs.primary,
|
|
686
|
+
font=body_font,
|
|
687
|
+
padding=(idt.padding_x, idt.padding_y),
|
|
688
|
+
relief="flat",
|
|
689
|
+
borderwidth=idt.border_width,
|
|
690
|
+
bordercolor=border,
|
|
691
|
+
)
|
|
692
|
+
style.map(
|
|
693
|
+
"TEntry",
|
|
694
|
+
fieldbackground=[("focus", cs.surface)],
|
|
695
|
+
bordercolor=[("focus", focused_border)],
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# ── TCombobox (DropdownButton) ───────────────────────
|
|
699
|
+
style.configure(
|
|
700
|
+
"TCombobox",
|
|
701
|
+
fieldbackground=fill,
|
|
702
|
+
foreground=cs.on_surface,
|
|
703
|
+
background=cs.surface,
|
|
704
|
+
selectbackground=cs.primary,
|
|
705
|
+
selectforeground=cs.on_primary,
|
|
706
|
+
font=body_font,
|
|
707
|
+
padding=(8, 6),
|
|
708
|
+
relief="flat",
|
|
709
|
+
)
|
|
710
|
+
style.map(
|
|
711
|
+
"TCombobox",
|
|
712
|
+
fieldbackground=[("readonly", fill)],
|
|
713
|
+
foreground=[("readonly", cs.on_surface)],
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# ── TCheckbutton ─────────────────────────────────────
|
|
717
|
+
style.configure(
|
|
718
|
+
"TCheckbutton",
|
|
719
|
+
background=cs.background,
|
|
720
|
+
foreground=cs.on_background,
|
|
721
|
+
font=body_font,
|
|
722
|
+
focuscolor=cs.primary,
|
|
723
|
+
indicatorcolor=cs.primary,
|
|
724
|
+
)
|
|
725
|
+
style.map(
|
|
726
|
+
"TCheckbutton",
|
|
727
|
+
background=[("active", cs.background)],
|
|
728
|
+
indicatorcolor=[
|
|
729
|
+
("selected", cs.primary),
|
|
730
|
+
("!selected", cs.surface_variant),
|
|
731
|
+
],
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
# ── TRadiobutton ─────────────────────────────────────
|
|
735
|
+
style.configure(
|
|
736
|
+
"TRadiobutton",
|
|
737
|
+
background=cs.background,
|
|
738
|
+
foreground=cs.on_background,
|
|
739
|
+
font=body_font,
|
|
740
|
+
focuscolor=cs.primary,
|
|
741
|
+
)
|
|
742
|
+
style.map(
|
|
743
|
+
"TRadiobutton",
|
|
744
|
+
background=[("active", cs.background)],
|
|
745
|
+
indicatorcolor=[
|
|
746
|
+
("selected", cs.primary),
|
|
747
|
+
("!selected", cs.surface_variant),
|
|
748
|
+
],
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
# ── TProgressbar ─────────────────────────────────────
|
|
752
|
+
style.configure(
|
|
753
|
+
"TProgressbar",
|
|
754
|
+
troughcolor=cs.surface_variant,
|
|
755
|
+
background=cs.primary,
|
|
756
|
+
bordercolor=cs.outline,
|
|
757
|
+
lightcolor=cs.primary,
|
|
758
|
+
darkcolor=cs.primary_container,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
# ── Horizontal.TSeparator (Divider) ──────────────────
|
|
762
|
+
style.configure(
|
|
763
|
+
"TSeparator",
|
|
764
|
+
background=dt.color or cs.outline_variant,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# ── TScrollbar ───────────────────────────────────────
|
|
768
|
+
style.configure(
|
|
769
|
+
"TScrollbar",
|
|
770
|
+
background=cs.surface_variant,
|
|
771
|
+
troughcolor=cs.background,
|
|
772
|
+
arrowcolor=cs.on_surface_variant,
|
|
773
|
+
bordercolor=cs.background,
|
|
774
|
+
lightcolor=cs.surface_variant,
|
|
775
|
+
darkcolor=cs.surface_variant,
|
|
776
|
+
)
|
|
777
|
+
style.map(
|
|
778
|
+
"TScrollbar",
|
|
779
|
+
background=[("active", cs.primary)],
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# ── Treeview (TableView) ─────────────────────────────
|
|
783
|
+
style.configure(
|
|
784
|
+
"Treeview",
|
|
785
|
+
background=cs.surface,
|
|
786
|
+
foreground=cs.on_surface,
|
|
787
|
+
fieldbackground=cs.surface,
|
|
788
|
+
font=body_font,
|
|
789
|
+
rowheight=28,
|
|
790
|
+
borderwidth=0,
|
|
791
|
+
)
|
|
792
|
+
style.configure(
|
|
793
|
+
"Treeview.Heading",
|
|
794
|
+
background=cs.surface_variant,
|
|
795
|
+
foreground=cs.on_surface_variant,
|
|
796
|
+
font=tt.label_medium.to_tk_font(),
|
|
797
|
+
relief="flat",
|
|
798
|
+
padding=(8, 6),
|
|
799
|
+
)
|
|
800
|
+
style.map(
|
|
801
|
+
"Treeview",
|
|
802
|
+
background=[("selected", cs.primary_container)],
|
|
803
|
+
foreground=[("selected", cs.on_primary_container)],
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# ── AppBar Frame ─────────────────────────────────────
|
|
807
|
+
style.configure(
|
|
808
|
+
"AppBar.TFrame",
|
|
809
|
+
background=abt.background_color or cs.surface,
|
|
810
|
+
relief="flat" if abt.elevation == 0 else "raised",
|
|
811
|
+
borderwidth=abt.elevation,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
# ── Notebook (TabBar) ─────────────────────────────────
|
|
815
|
+
style.configure(
|
|
816
|
+
"TNotebook",
|
|
817
|
+
background=cs.background,
|
|
818
|
+
borderwidth=0,
|
|
819
|
+
)
|
|
820
|
+
style.configure(
|
|
821
|
+
"TNotebook.Tab",
|
|
822
|
+
background=cs.surface_variant,
|
|
823
|
+
foreground=cs.on_surface_variant,
|
|
824
|
+
font=tt.label_medium.to_tk_font(),
|
|
825
|
+
padding=(12, 6),
|
|
826
|
+
)
|
|
827
|
+
style.map(
|
|
828
|
+
"TNotebook.Tab",
|
|
829
|
+
background=[("selected", cs.primary_container)],
|
|
830
|
+
foreground=[("selected", cs.on_primary_container)],
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
# ── Scale (Slider) ────────────────────────────────────
|
|
834
|
+
style.configure(
|
|
835
|
+
"TScale",
|
|
836
|
+
background=cs.background,
|
|
837
|
+
troughcolor=cs.surface_variant,
|
|
838
|
+
sliderrelief="flat",
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
# ============================================================
|
|
843
|
+
# THEME PROVIDER (ChangeNotifier)
|
|
844
|
+
# ============================================================
|
|
845
|
+
|
|
846
|
+
class ThemeProvider(ChangeNotifier):
|
|
847
|
+
"""
|
|
848
|
+
Provider reativo do tema — permite trocar ThemeData em runtime.
|
|
849
|
+
|
|
850
|
+
Padrão de uso:
|
|
851
|
+
provider = ThemeProvider.from_mode(ThemeMode.SYSTEM)
|
|
852
|
+
app.register_provider(provider)
|
|
853
|
+
|
|
854
|
+
# Para alternar:
|
|
855
|
+
provider.set_mode(ThemeMode.DARK)
|
|
856
|
+
# Todos os Consumer(ThemeProvider) serão reconstruídos.
|
|
857
|
+
|
|
858
|
+
Nota: a re-aplicação do tema via `apply()` muda os ttk.Style globais,
|
|
859
|
+
mas widgets já montados precisam ser desmontados/remontados para refletir
|
|
860
|
+
as novas cores — isso depende do mecanismo de rebuild do pyflutter.
|
|
861
|
+
"""
|
|
862
|
+
|
|
863
|
+
def __init__(
|
|
864
|
+
self,
|
|
865
|
+
light_theme: Optional[ThemeData] = None,
|
|
866
|
+
dark_theme: Optional[ThemeData] = None,
|
|
867
|
+
mode: ThemeMode = ThemeMode.SYSTEM,
|
|
868
|
+
) -> None:
|
|
869
|
+
super().__init__()
|
|
870
|
+
self._light = light_theme or ThemeData.light()
|
|
871
|
+
self._dark = dark_theme or ThemeData.dark()
|
|
872
|
+
self._mode = mode
|
|
873
|
+
self._root: Optional[tk.Tk] = None
|
|
874
|
+
|
|
875
|
+
# ── Factory ──────────────────────────────────────────────
|
|
876
|
+
|
|
877
|
+
@classmethod
|
|
878
|
+
def from_mode(
|
|
879
|
+
cls,
|
|
880
|
+
mode: ThemeMode = ThemeMode.SYSTEM,
|
|
881
|
+
light_theme: Optional[ThemeData] = None,
|
|
882
|
+
dark_theme: Optional[ThemeData] = None,
|
|
883
|
+
) -> "ThemeProvider":
|
|
884
|
+
return cls(light_theme=light_theme, dark_theme=dark_theme, mode=mode)
|
|
885
|
+
|
|
886
|
+
# ── Estado atual ─────────────────────────────────────────
|
|
887
|
+
|
|
888
|
+
@property
|
|
889
|
+
def is_dark(self) -> bool:
|
|
890
|
+
if self._mode == ThemeMode.DARK:
|
|
891
|
+
return True
|
|
892
|
+
if self._mode == ThemeMode.LIGHT:
|
|
893
|
+
return False
|
|
894
|
+
return _detect_system_dark_mode()
|
|
895
|
+
|
|
896
|
+
@property
|
|
897
|
+
def theme(self) -> ThemeData:
|
|
898
|
+
"""Retorna o ThemeData ativo (light ou dark)."""
|
|
899
|
+
return self._dark if self.is_dark else self._light
|
|
900
|
+
|
|
901
|
+
# ── Getters de conveniência (espelha context.theme no Flutter) ──
|
|
902
|
+
|
|
903
|
+
@property
|
|
904
|
+
def color_scheme(self) -> ColorScheme:
|
|
905
|
+
return self.theme.color_scheme
|
|
906
|
+
|
|
907
|
+
@property
|
|
908
|
+
def text_theme(self) -> TextTheme:
|
|
909
|
+
return self.theme.text_theme
|
|
910
|
+
|
|
911
|
+
@property
|
|
912
|
+
def primary(self) -> str:
|
|
913
|
+
return self.theme.primary
|
|
914
|
+
|
|
915
|
+
@property
|
|
916
|
+
def background(self) -> str:
|
|
917
|
+
return self.theme.background
|
|
918
|
+
|
|
919
|
+
# ── Mutação ──────────────────────────────────────────────
|
|
920
|
+
|
|
921
|
+
def set_mode(self, mode: ThemeMode) -> None:
|
|
922
|
+
"""Alterna entre Light, Dark e System e notifica os listeners."""
|
|
923
|
+
if mode != self._mode:
|
|
924
|
+
self._mode = mode
|
|
925
|
+
if self._root:
|
|
926
|
+
self.theme.apply(self._root)
|
|
927
|
+
self.notify_listeners()
|
|
928
|
+
|
|
929
|
+
def toggle(self) -> None:
|
|
930
|
+
"""Alterna entre Dark e Light (ignora System)."""
|
|
931
|
+
self.set_mode(
|
|
932
|
+
ThemeMode.LIGHT if self.is_dark else ThemeMode.DARK
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
def set_light_theme(self, theme: ThemeData) -> None:
|
|
936
|
+
self._light = theme
|
|
937
|
+
self.notify_listeners()
|
|
938
|
+
|
|
939
|
+
def set_dark_theme(self, theme: ThemeData) -> None:
|
|
940
|
+
self._dark = theme
|
|
941
|
+
self.notify_listeners()
|
|
942
|
+
|
|
943
|
+
# ── Aplicação ────────────────────────────────────────────
|
|
944
|
+
|
|
945
|
+
def attach_root(self, root: tk.Tk) -> None:
|
|
946
|
+
"""
|
|
947
|
+
Vincula o provider à janela raiz do Tk.
|
|
948
|
+
Deve ser chamado em TkMaterialApp.run() antes do mainloop.
|
|
949
|
+
"""
|
|
950
|
+
self._root = root
|
|
951
|
+
self.apply()
|
|
952
|
+
|
|
953
|
+
def apply(self) -> None:
|
|
954
|
+
"""Aplica o tema ativo no ttk.Style."""
|
|
955
|
+
self.theme.apply(self._root)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
# ============================================================
|
|
959
|
+
# ATALHO GLOBAL (equivalente ao Theme.of(context))
|
|
960
|
+
# ============================================================
|
|
961
|
+
|
|
962
|
+
class Theme:
|
|
963
|
+
"""
|
|
964
|
+
Classe utilitária com factory methods — mantém compatibilidade
|
|
965
|
+
com o código existente que usava Theme.PRIMARY, Theme.apply(), etc.
|
|
966
|
+
|
|
967
|
+
Para novos projetos, prefira ThemeProvider + ThemeData.
|
|
968
|
+
"""
|
|
969
|
+
|
|
970
|
+
# Constantes legadas (compatibilidade)
|
|
971
|
+
PRIMARY: str = ColorScheme.light().primary
|
|
972
|
+
BACKGROUND: str = ColorScheme.light().background
|
|
973
|
+
|
|
974
|
+
@staticmethod
|
|
975
|
+
def light(font_family: str = "Helvetica") -> ThemeData:
|
|
976
|
+
return ThemeData.light(font_family=font_family)
|
|
977
|
+
|
|
978
|
+
@staticmethod
|
|
979
|
+
def dark(font_family: str = "Helvetica") -> ThemeData:
|
|
980
|
+
return ThemeData.dark(font_family=font_family)
|
|
981
|
+
|
|
982
|
+
@staticmethod
|
|
983
|
+
def apply(root: Optional[tk.Tk] = None) -> None:
|
|
984
|
+
"""
|
|
985
|
+
Compatibilidade legada: aplica um tema light padrão.
|
|
986
|
+
Requer que tk.Tk() já tenha sido criado, ou que `root` seja passado.
|
|
987
|
+
"""
|
|
988
|
+
ThemeData.light().apply(root)
|
|
989
|
+
|
|
990
|
+
@staticmethod
|
|
991
|
+
def of(context) -> ThemeData:
|
|
992
|
+
"""
|
|
993
|
+
Equivalente ao Theme.of(context) do Flutter.
|
|
994
|
+
Recupera o ThemeData ativo a partir do BuildContext.
|
|
995
|
+
|
|
996
|
+
Uso:
|
|
997
|
+
theme = Theme.of(context)
|
|
998
|
+
color = theme.primary
|
|
999
|
+
"""
|
|
1000
|
+
try:
|
|
1001
|
+
provider: ThemeProvider = context.read(ThemeProvider)
|
|
1002
|
+
return provider.theme
|
|
1003
|
+
except (KeyError, AttributeError):
|
|
1004
|
+
return ThemeData.light()
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
# ============================================================
|
|
1008
|
+
# INTEGRAÇÃO COM TkMaterialApp
|
|
1009
|
+
# ============================================================
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def make_themed_app(base_app_class: type[BaseMaterialApp]):
|
|
1013
|
+
"""
|
|
1014
|
+
Mixin/decorator que estende qualquer subclasse de BaseMaterialApp
|
|
1015
|
+
para aplicar o ThemeProvider automaticamente dentro de run(),
|
|
1016
|
+
após tk.Tk() ser criado — sem risco de janela dupla.
|
|
1017
|
+
|
|
1018
|
+
Uso:
|
|
1019
|
+
from pyflutter.widgets import TkMaterialApp
|
|
1020
|
+
from pyflutter.theme import make_themed_app
|
|
1021
|
+
|
|
1022
|
+
ThemedApp = make_themed_app(TkMaterialApp)
|
|
1023
|
+
app = ThemedApp(home=MyPage(), title="App")
|
|
1024
|
+
app.register_provider(ThemeProvider(...))
|
|
1025
|
+
app.run() # aplica o tema no momento certo
|
|
1026
|
+
"""
|
|
1027
|
+
|
|
1028
|
+
class ThemedApp(base_app_class):
|
|
1029
|
+
|
|
1030
|
+
def run(self) -> None:
|
|
1031
|
+
|
|
1032
|
+
self.root = tk.Tk()
|
|
1033
|
+
self.root.title(self.title)
|
|
1034
|
+
|
|
1035
|
+
# Aplica o tema AGORA — root já existe, sem janela dupla.
|
|
1036
|
+
try:
|
|
1037
|
+
provider: ThemeProvider = self.providers.get(ThemeProvider)
|
|
1038
|
+
if provider is not None:
|
|
1039
|
+
provider.attach_root(self.root)
|
|
1040
|
+
except Exception:
|
|
1041
|
+
pass
|
|
1042
|
+
|
|
1043
|
+
self.get_navigator().push(self.home)
|
|
1044
|
+
self.root.mainloop()
|
|
1045
|
+
|
|
1046
|
+
def get_navigator(self) -> Navigator:
|
|
1047
|
+
return super().get_navigator()
|
|
1048
|
+
|
|
1049
|
+
def register_notifier(self, provider: T) -> None:
|
|
1050
|
+
return super().register_notifier(provider)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
ThemedApp.__name__ = f"Themed{base_app_class.__name__}"
|
|
1056
|
+
ThemedApp.__qualname__ = ThemedApp.__name__
|
|
1057
|
+
return ThemedApp
|
|
1058
|
+
|