tkwin11 0.2.3__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.
- tkwin11/__init__.py +72 -0
- tkwin11/button.py +218 -0
- tkwin11/cards.py +130 -0
- tkwin11/charts.py +578 -0
- tkwin11/classic.py +115 -0
- tkwin11/core.py +1416 -0
- tkwin11/dataview.py +218 -0
- tkwin11/dpi.py +555 -0
- tkwin11/dropdowns.py +169 -0
- tkwin11/expansion.py +89 -0
- tkwin11/extras.py +487 -0
- tkwin11/feedback.py +280 -0
- tkwin11/fluent.py +409 -0
- tkwin11/flyouts.py +128 -0
- tkwin11/forms.py +527 -0
- tkwin11/inputs.py +154 -0
- tkwin11/layouts.py +60 -0
- tkwin11/media.py +412 -0
- tkwin11/menus.py +61 -0
- tkwin11/motion.py +126 -0
- tkwin11/navigation.py +248 -0
- tkwin11/placeholder.py +173 -0
- tkwin11/popups.py +219 -0
- tkwin11/render.py +198 -0
- tkwin11/restore_animation.py +189 -0
- tkwin11/scrolling.py +99 -0
- tkwin11/selection.py +157 -0
- tkwin11/slider.py +460 -0
- tkwin11/states.py +49 -0
- tkwin11/switches.py +284 -0
- tkwin11/table.py +400 -0
- tkwin11/tcl_bridge.py +137 -0
- tkwin11/titlebar.py +59 -0
- tkwin11/tooltip.py +116 -0
- tkwin11/widgets.py +303 -0
- tkwin11/window_chrome.py +801 -0
- tkwin11-0.2.3.dist-info/METADATA +88 -0
- tkwin11-0.2.3.dist-info/RECORD +40 -0
- tkwin11-0.2.3.dist-info/WHEEL +4 -0
- tkwin11-0.2.3.dist-info/licenses/LICENSE +22 -0
tkwin11/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""TkWin11: Windows 11 Fluent-style theme and widgets for standard tkinter/ttk.
|
|
2
|
+
|
|
3
|
+
Standard library only. Existing programs need one line before the first Tk():
|
|
4
|
+
|
|
5
|
+
import tkwin11; tkwin11.install()
|
|
6
|
+
|
|
7
|
+
New programs can call enable_dpi_awareness() before Tk(), then
|
|
8
|
+
apply_theme(root) before creating widgets:
|
|
9
|
+
|
|
10
|
+
import tkinter as tk
|
|
11
|
+
from tkwin11 import enable_dpi_awareness, apply_theme
|
|
12
|
+
|
|
13
|
+
enable_dpi_awareness()
|
|
14
|
+
root = tk.Tk()
|
|
15
|
+
apply_theme(root)
|
|
16
|
+
|
|
17
|
+
Every component can also be imported from its own module, e.g.
|
|
18
|
+
``from tkwin11.charts import LineChart``.
|
|
19
|
+
"""
|
|
20
|
+
__version__ = '0.2.3'
|
|
21
|
+
|
|
22
|
+
from .menus import bind_context_menu
|
|
23
|
+
from .scrolling import AutoScrollbar
|
|
24
|
+
from .inputs import set_readonly
|
|
25
|
+
from .placeholder import set_placeholder, PlaceholderEntry
|
|
26
|
+
from .core import (PALETTE, THEME, enable_dpi_awareness, icon, apply_theme, Card, SegmentedControl,
|
|
27
|
+
Sidebar, ScrollArea, Tooltip, Dropdown, Accordion, SettingRow, BarChart, CardList,
|
|
28
|
+
ProgressSlider, Modal)
|
|
29
|
+
from .widgets import (RoundedText, SectionCard, ResponsiveTable, configure_table, auto_hide_text_scrollbar,
|
|
30
|
+
focus_when_mapped, initial_window_bounds, align_tree_scrollbar)
|
|
31
|
+
from .popups import ChoicePopup, ThemedCombobox, ThemedMenu
|
|
32
|
+
from .extras import SquareSwitch, ToggleButton, ToolButton, MenuButton, OptionMenu, RoundedListbox
|
|
33
|
+
from .table import SortableTreeview, enable_sorting, Spreadsheet
|
|
34
|
+
from .charts import LineChart, ColumnChart, ScatterChart, DonutChart, PieChart
|
|
35
|
+
from .forms import (Badge, ValidatedEntry, ResultDialog, MessageDialog, InputDialog, Dialogs, DateEntry,
|
|
36
|
+
CalendarPopup, ValueSlider)
|
|
37
|
+
from .feedback import ToastNotification, notify, TextProgress, Meter
|
|
38
|
+
from .dataview import DataTable
|
|
39
|
+
from .media import ColorPicker, ColorDialog, FontPicker, FontDialog, ImageViewer
|
|
40
|
+
from .window_chrome import WindowChrome
|
|
41
|
+
from .slider import FluentSlider
|
|
42
|
+
from .button import FluentButton
|
|
43
|
+
from .titlebar import FluentTitleBar
|
|
44
|
+
from .fluent import (configure_fluent, FluentPage, SettingsCard, NavigationView,
|
|
45
|
+
FluentWindowChrome, FluentSwitch, ExpanderCard, KeyChord, FormFlyout)
|
|
46
|
+
from .classic import install, uninstall, theme_classic
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
'set_readonly', 'set_placeholder', 'PlaceholderEntry', 'bind_context_menu',
|
|
50
|
+
'AutoScrollbar',
|
|
51
|
+
'PALETTE', 'THEME', 'enable_dpi_awareness', 'icon', 'apply_theme',
|
|
52
|
+
'Card', 'SegmentedControl', 'Sidebar', 'ScrollArea', 'Tooltip', 'Dropdown', 'Accordion', 'SettingRow',
|
|
53
|
+
'BarChart', 'CardList', 'ProgressSlider', 'Modal',
|
|
54
|
+
'RoundedText', 'SectionCard', 'ResponsiveTable', 'configure_table', 'auto_hide_text_scrollbar',
|
|
55
|
+
'focus_when_mapped', 'initial_window_bounds', 'align_tree_scrollbar',
|
|
56
|
+
'ChoicePopup', 'ThemedCombobox', 'ThemedMenu',
|
|
57
|
+
'SquareSwitch', 'ToggleButton', 'ToolButton', 'MenuButton', 'OptionMenu', 'RoundedListbox',
|
|
58
|
+
'SortableTreeview', 'enable_sorting', 'Spreadsheet',
|
|
59
|
+
'LineChart', 'ColumnChart', 'ScatterChart', 'DonutChart', 'PieChart',
|
|
60
|
+
'Badge', 'ValidatedEntry', 'ResultDialog', 'MessageDialog', 'InputDialog', 'Dialogs', 'DateEntry',
|
|
61
|
+
'CalendarPopup', 'ValueSlider',
|
|
62
|
+
'ToastNotification', 'notify', 'TextProgress', 'Meter',
|
|
63
|
+
'DataTable',
|
|
64
|
+
'ColorPicker', 'ColorDialog', 'FontPicker', 'FontDialog', 'ImageViewer',
|
|
65
|
+
'WindowChrome',
|
|
66
|
+
'FluentSlider',
|
|
67
|
+
'FluentButton',
|
|
68
|
+
'FluentTitleBar',
|
|
69
|
+
'configure_fluent', 'FluentPage', 'SettingsCard', 'NavigationView',
|
|
70
|
+
'FluentWindowChrome', 'FluentSwitch', 'ExpanderCard', 'KeyChord', 'FormFlyout',
|
|
71
|
+
'install', 'uninstall', 'theme_classic',
|
|
72
|
+
]
|
tkwin11/button.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Shared native ttk button feedback, including legacy construction."""
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
from tkinter import ttk
|
|
4
|
+
from .core import PALETTE, _patch
|
|
5
|
+
from .dpi import additive_styles
|
|
6
|
+
from .tcl_bridge import on_map
|
|
7
|
+
from .motion import Tween, blend
|
|
8
|
+
|
|
9
|
+
STYLES={'TButton','Primary.TButton','Quiet.TButton','Close.TButton','CardIcon.TButton','Dropdown.TButton','Toggle.TButton','Toolbutton','TMenubutton','TOptionMenu','Popup.Quiet.TButton'}
|
|
10
|
+
SEMANTIC={'Primary':PALETTE['accent'],'Success':'#159576','Warning':'#B77717','Danger':'#D54B58','Info':'#197FA8','Neutral':'#657080'}
|
|
11
|
+
STYLES.update(name+'.'+variant+'TButton' for name in SEMANTIC for variant in ('','Outline.','Link.'))
|
|
12
|
+
STYLES.update(('Segment.TButton','Selected.Segment.TButton'))
|
|
13
|
+
|
|
14
|
+
def _face(root,fill,edge,background,radius):
|
|
15
|
+
cache=getattr(root,'_button_face_cache',None)
|
|
16
|
+
if cache is None:
|
|
17
|
+
from collections import OrderedDict
|
|
18
|
+
cache=root._button_face_cache=OrderedDict()
|
|
19
|
+
key=(fill.lower(),edge.lower(),background.lower(),radius)
|
|
20
|
+
image=cache.get(key)
|
|
21
|
+
if image is None:
|
|
22
|
+
image=cache[key]=_patch(root,fill,edge,background,radius)
|
|
23
|
+
if len(cache)>192:cache.popitem(last=False)
|
|
24
|
+
else:cache.move_to_end(key)
|
|
25
|
+
return image
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ButtonController:
|
|
29
|
+
def __init__(self,widget):
|
|
30
|
+
self.widget=widget;self.root=widget._root()
|
|
31
|
+
self.resources=self.root._dpi_resources;self.resources.attach(widget)
|
|
32
|
+
self.adapter=self.resources.widgets[str(widget)]
|
|
33
|
+
self.public_style=str(widget.cget('style'))
|
|
34
|
+
self.base=self.public_style or widget.winfo_class()
|
|
35
|
+
self.fill=PALETTE['bg'];self.focus_visible=False
|
|
36
|
+
self.animator=Tween(widget);self.job=None;self.closed=False
|
|
37
|
+
self.variable=None
|
|
38
|
+
self.trace_command=widget.register(lambda *args:self.schedule())
|
|
39
|
+
self.scale=self.resources.base_dpi/96
|
|
40
|
+
pool=getattr(self.root,'_button_slots',None)
|
|
41
|
+
if pool is None:pool=self.root._button_slots=[]
|
|
42
|
+
self.slot=next((slot for slot in pool if not slot['used']),None)
|
|
43
|
+
if self.slot is None:
|
|
44
|
+
serial=len(pool)+1;element=f'ButtonMotion{serial}.surface'
|
|
45
|
+
face=_patch(self.root,self.fill,PALETTE['border'],PALETTE['bg'],round(4*self.scale))
|
|
46
|
+
style=ttk.Style(widget);border=round(4*self.scale)+1
|
|
47
|
+
# padding=0 like the theme's own button surfaces this replaces; ttk
|
|
48
|
+
# would default it to the border and grow the button on attach.
|
|
49
|
+
with additive_styles(widget):
|
|
50
|
+
style.element_create(element,'image',face,border=border,padding=0,width=border*2,height=border*2,sticky='nsew')
|
|
51
|
+
self.slot={'serial':serial,'element':element,'face':face,'used':False};pool.append(self.slot)
|
|
52
|
+
self.slot['used']=True
|
|
53
|
+
self.serial=self.slot['serial'];self.element=self.slot['element'];self.face=self.slot['face']
|
|
54
|
+
widget._tkwin11_button=self
|
|
55
|
+
self.use_style(self.public_style)
|
|
56
|
+
for sequence in ('<Enter>','<Leave>','<ButtonPress-1>','<ButtonRelease-1>','<FocusIn>','<FocusOut>','<KeyPress>','<KeyRelease>'):
|
|
57
|
+
widget.bind(sequence,self.event,add='+')
|
|
58
|
+
widget.bind('<Destroy>',self.destroy,add='+');self.sync()
|
|
59
|
+
|
|
60
|
+
def use_style(self,base):
|
|
61
|
+
self.public_style=str(base)
|
|
62
|
+
self.base=str(base) or self.widget.winfo_class()
|
|
63
|
+
if self.base not in STYLES:
|
|
64
|
+
self.style=self.base;self.adapter.values['style']=self.base
|
|
65
|
+
self.animator.cancel();self.adapter.apply();return
|
|
66
|
+
self.style=f'ButtonMotion{self.serial}.{self.base}'
|
|
67
|
+
style=ttk.Style(self.widget);layout=style.layout(self.base)
|
|
68
|
+
if layout:
|
|
69
|
+
_,options=layout[0];layout[0]=(self.element,options)
|
|
70
|
+
# Pooled slots are reused by every popup row. Re-declaring an identical
|
|
71
|
+
# layout would still broadcast <<ThemeChanged>> to the whole app.
|
|
72
|
+
layouts=self.slot.setdefault('layouts',{})
|
|
73
|
+
# The slot is this widget's alone, and apply() below relayouts it.
|
|
74
|
+
with additive_styles(self.widget):
|
|
75
|
+
if layouts.get(self.base)!=repr(layout):
|
|
76
|
+
style.layout(self.style,layout);layouts[self.base]=repr(layout)
|
|
77
|
+
self.adapter.values['style']=self.style;self.adapter.apply()
|
|
78
|
+
self.schedule()
|
|
79
|
+
|
|
80
|
+
def event(self,event):
|
|
81
|
+
if event.type==tk.EventType.ButtonPress:self.root._button_keyboard=False
|
|
82
|
+
elif event.type==tk.EventType.KeyPress:self.root._button_keyboard=True
|
|
83
|
+
elif event.type==tk.EventType.FocusIn:
|
|
84
|
+
import sys
|
|
85
|
+
if sys.platform=='win32':
|
|
86
|
+
import ctypes
|
|
87
|
+
if ctypes.windll.user32.GetKeyState(9)&0x8000:self.root._button_keyboard=True
|
|
88
|
+
self.schedule()
|
|
89
|
+
|
|
90
|
+
def image_spec(self,value):
|
|
91
|
+
parts=self.widget.tk.splitlist(value)
|
|
92
|
+
if len(parts)!=1:return value
|
|
93
|
+
name=str(parts[0])
|
|
94
|
+
if str(self.widget.tk.call('image','type',name))!='photo':return value
|
|
95
|
+
cache=getattr(self.root,'_button_disabled_icons',None)
|
|
96
|
+
if cache is None:cache=self.root._button_disabled_icons={}
|
|
97
|
+
if name not in cache:
|
|
98
|
+
factory=self.resources.image_factories.get(name)
|
|
99
|
+
disabled_factory=getattr(factory,'disabled_factory',None)
|
|
100
|
+
if disabled_factory is not None:
|
|
101
|
+
image=disabled_factory(1);self.resources.image_factories[str(image)]=disabled_factory
|
|
102
|
+
cache[name]=image
|
|
103
|
+
return (name,'disabled',str(image))
|
|
104
|
+
spec=getattr(factory,'cache_key',())
|
|
105
|
+
if spec and spec[0]=='icon':
|
|
106
|
+
from .core import icon
|
|
107
|
+
# Preserve the vector factory and partial alpha for disabled
|
|
108
|
+
# icons too; pixel mutation would discard their DPI factory.
|
|
109
|
+
cache[name]=icon(self.root,spec[1],spec[2],color=blend(spec[3],PALETTE['bg'],.65))
|
|
110
|
+
return (name,'disabled',str(cache[name]))
|
|
111
|
+
width=int(self.widget.tk.call('image','width',name));height=int(self.widget.tk.call('image','height',name))
|
|
112
|
+
if width*height>16384:return value
|
|
113
|
+
image=tk.PhotoImage(master=self.root,width=width,height=height)
|
|
114
|
+
image.tk.call(image,'copy',name)
|
|
115
|
+
bg=tuple(int(PALETTE['bg'][i:i+2],16) for i in (1,3,5))
|
|
116
|
+
for y in range(height):
|
|
117
|
+
for x in range(width):
|
|
118
|
+
if self.widget.tk.getboolean(self.widget.tk.call(name,'transparency','get',x,y)):continue
|
|
119
|
+
color=self.widget.tk.call(name,'get',x,y)
|
|
120
|
+
muted='#'+''.join(f'{round(int(a)*.35+b*.65):02x}' for a,b in zip(color,bg))
|
|
121
|
+
image.put(muted,(x,y))
|
|
122
|
+
cache[name]=image
|
|
123
|
+
return (name,'disabled',str(cache[name]))
|
|
124
|
+
|
|
125
|
+
def schedule(self,event=None):
|
|
126
|
+
if not self.closed and self.job is None:self.job=self.widget.after_idle(self.sync)
|
|
127
|
+
|
|
128
|
+
def sync(self):
|
|
129
|
+
if self.closed:return
|
|
130
|
+
if self.job is not None:self.widget.after_cancel(self.job);self.job=None
|
|
131
|
+
if self.base not in STYLES:return
|
|
132
|
+
if isinstance(self.widget,ttk.Checkbutton):
|
|
133
|
+
variable=str(self.widget.cget('variable'))
|
|
134
|
+
if variable!=self.variable:
|
|
135
|
+
if self.variable:
|
|
136
|
+
self.widget.tk.call('trace','remove','variable',self.variable,'write',self.trace_command)
|
|
137
|
+
self.variable=variable
|
|
138
|
+
if variable:self.widget.tk.call('trace','add','variable',variable,'write',self.trace_command)
|
|
139
|
+
states=set(self.widget.state());disabled='disabled' in states
|
|
140
|
+
primary=self.base=='Primary.TButton';quiet=self.base in ('Quiet.TButton','Toolbutton','Popup.Quiet.TButton')
|
|
141
|
+
selected='selected' in states
|
|
142
|
+
background=PALETTE['selected'] if self.base=='CardIcon.TButton' else PALETTE['bg']
|
|
143
|
+
normal=PALETTE['accent'] if primary else PALETTE['selected'] if selected else background
|
|
144
|
+
if disabled:target='#C4C4C4' if primary else background if quiet else PALETTE['hover']
|
|
145
|
+
elif 'pressed' in states:target='#3184CA' if primary else '#DCEAF7' if selected else PALETTE['active']
|
|
146
|
+
elif 'active' in states:target=PALETTE['accent_hover'] if primary else '#DCEAF7' if selected else '#FFE8E8' if self.base=='Close.TButton' else PALETTE['hover']
|
|
147
|
+
else:target=normal
|
|
148
|
+
self.focus_visible='focus' in states and not disabled and getattr(self.root,'_button_keyboard',False)
|
|
149
|
+
edge=PALETTE['fg'] if self.focus_visible else target if quiet or primary else PALETTE['accent'] if selected and not disabled else PALETTE['border']
|
|
150
|
+
if self.base in ('Segment.TButton','Selected.Segment.TButton'):
|
|
151
|
+
background=PALETTE['hover']
|
|
152
|
+
normal=PALETTE['bg'] if self.base.startswith('Selected.') else background
|
|
153
|
+
target=background if disabled else PALETTE['active'] if 'pressed' in states else PALETTE['hover'] if 'active' in states else normal
|
|
154
|
+
edge=PALETTE['fg'] if self.focus_visible else target
|
|
155
|
+
prefix=self.base.split('.')[0]
|
|
156
|
+
if prefix in SEMANTIC and self.base!='Primary.TButton':
|
|
157
|
+
color=PALETTE['accent'] if prefix=='Primary' else SEMANTIC[prefix]
|
|
158
|
+
outline='.Outline.' in self.base;link='.Link.' in self.base
|
|
159
|
+
if disabled:target=background if link else PALETTE['hover']
|
|
160
|
+
elif 'pressed' in states:target=blend(color,'#FFFFFF',.72 if outline or link else .2)
|
|
161
|
+
elif 'active' in states:target=blend(color,'#FFFFFF',.86 if outline or link else .1)
|
|
162
|
+
else:target=background if outline or link else color
|
|
163
|
+
edge=PALETTE['fg'] if self.focus_visible else target if link or not outline else PALETTE['border'] if disabled else color
|
|
164
|
+
signature=(target,edge,background)
|
|
165
|
+
if getattr(self,'_paint_target',None)==signature:return
|
|
166
|
+
self._paint_target=signature
|
|
167
|
+
start=self.fill
|
|
168
|
+
def paint(amount):
|
|
169
|
+
# Eight steps per transition keep the 83 ms fade smooth while every
|
|
170
|
+
# frame becomes a cache hit after first use (no PhotoImage churn).
|
|
171
|
+
self.fill=target if amount>=1 else blend(start,target,round(amount*8)/8)
|
|
172
|
+
self.widget.tk.call(self.face,'copy',_face(self.root,self.fill,edge,background,round(4*self.scale)))
|
|
173
|
+
if disabled or start.lower()==target.lower():self.animator.cancel();paint(1)
|
|
174
|
+
else:self.animator.start(0.,1.,paint,duration=83,linear=True)
|
|
175
|
+
|
|
176
|
+
def destroy(self,event):
|
|
177
|
+
if event.widget is not self.widget:return
|
|
178
|
+
self.closed=True
|
|
179
|
+
if self.job is not None:self.widget.after_cancel(self.job);self.job=None
|
|
180
|
+
self.animator.cancel()
|
|
181
|
+
if self.variable:
|
|
182
|
+
self.widget.tk.call('trace','remove','variable',self.variable,'write',self.trace_command)
|
|
183
|
+
self.widget.deletecommand(self.trace_command)
|
|
184
|
+
self.slot['used']=False
|
|
185
|
+
|
|
186
|
+
def attach_button(widget):
|
|
187
|
+
if not isinstance(widget,(ttk.Button,ttk.Checkbutton,ttk.Menubutton)):return None
|
|
188
|
+
resources=getattr(widget._root(),'_dpi_resources',None)
|
|
189
|
+
if resources is None or resources.manual(widget):return None
|
|
190
|
+
current=getattr(widget,'_tkwin11_button',None)
|
|
191
|
+
if current:return current
|
|
192
|
+
base=str(widget.cget('style')) or widget.winfo_class()
|
|
193
|
+
if base not in STYLES:return None
|
|
194
|
+
if isinstance(widget,ttk.Checkbutton) and base not in ('Toggle.TButton','Toolbutton'):return None
|
|
195
|
+
return ButtonController(widget)
|
|
196
|
+
|
|
197
|
+
def install_button_compatibility(root):
|
|
198
|
+
if not getattr(root,'_button_compatibility',False):
|
|
199
|
+
on_map(root,attach_button)
|
|
200
|
+
root.bind_all('<KeyPress>',lambda event:setattr(root,'_button_keyboard',True),add='+')
|
|
201
|
+
def pointer(event):
|
|
202
|
+
root._button_keyboard=False
|
|
203
|
+
focused=root.focus_get()
|
|
204
|
+
controller=getattr(focused,'_tkwin11_button',None) or getattr(focused,'_tkwin11_choice',None)
|
|
205
|
+
if controller is not None:controller.schedule()
|
|
206
|
+
root.bind_all('<ButtonPress-1>',pointer,add='+')
|
|
207
|
+
root._button_compatibility=True
|
|
208
|
+
def walk(widget):
|
|
209
|
+
attach_button(widget)
|
|
210
|
+
for child in widget.winfo_children():walk(child)
|
|
211
|
+
walk(root)
|
|
212
|
+
|
|
213
|
+
class FluentButton(ttk.Button):
|
|
214
|
+
"""Identical controller and interaction to themed ttk.Button."""
|
|
215
|
+
def __init__(self,master=None,**kwargs):
|
|
216
|
+
super().__init__(master,**kwargs)
|
|
217
|
+
self.controller=attach_button(self)
|
|
218
|
+
self.animator=self.controller.animator if self.controller is not None else Tween(self)
|
tkwin11/cards.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Shared card interaction; passive cards stay outside the keyboard tab order."""
|
|
2
|
+
import tkinter as tk
|
|
3
|
+
from tkinter import ttk
|
|
4
|
+
from .dpi import additive_styles
|
|
5
|
+
from .tcl_bridge import on_map
|
|
6
|
+
from .motion import Tween
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CardInteraction:
|
|
10
|
+
def __init__(self,card,command):
|
|
11
|
+
self.card=card;self.command=command;self.hover=False;self.pressed=False
|
|
12
|
+
self.saved={};self.bound=set();self.tween=Tween(card);self.fill=None
|
|
13
|
+
card.configure(takefocus=bool(command))
|
|
14
|
+
card.bind('<FocusIn>',self.refresh,add='+');card.bind('<FocusOut>',self.cancel_press,add='+')
|
|
15
|
+
card.bind('<Return>',self.keyboard);card.bind('<space>',self.keyboard)
|
|
16
|
+
card.bind('<KeyRelease-space>',self.key_release)
|
|
17
|
+
self.job=card.after_idle(self.attach)
|
|
18
|
+
card.bind('<Destroy>',self.destroy,add='+')
|
|
19
|
+
root=card._root()
|
|
20
|
+
if not getattr(root,'_dynamic_cards_installed',False):
|
|
21
|
+
root._dynamic_cards_installed=True
|
|
22
|
+
on_map(root,_mapped_card)
|
|
23
|
+
|
|
24
|
+
def attach(self):
|
|
25
|
+
self.job=None
|
|
26
|
+
card=self.card
|
|
27
|
+
def walk(widget):
|
|
28
|
+
if widget is not card and hasattr(widget,'interaction'):return
|
|
29
|
+
if widget is not card and isinstance(widget,ttk.Frame):
|
|
30
|
+
style=str(widget.cget('style'))
|
|
31
|
+
if style not in ('','TFrame') and not style.startswith('CardFill'):return
|
|
32
|
+
if widget in (getattr(card,'actions',None),getattr(card,'action',None)):return
|
|
33
|
+
if isinstance(widget,tk.Canvas) and widget is not card._surface:return
|
|
34
|
+
if widget is not card and not isinstance(widget,(ttk.Frame,ttk.Label,tk.Canvas)):return
|
|
35
|
+
if widget not in self.bound:
|
|
36
|
+
self.bound.add(widget)
|
|
37
|
+
widget.bind('<Enter>',lambda e:self.pointer(True),add='+')
|
|
38
|
+
widget.bind('<Leave>',lambda e:self.pointer(False),add='+')
|
|
39
|
+
widget.bind('<ButtonPress-1>',self.press,add='+')
|
|
40
|
+
widget.bind('<ButtonRelease-1>',self.release,add='+')
|
|
41
|
+
for child in widget.winfo_children():walk(child)
|
|
42
|
+
known=len(self.bound)
|
|
43
|
+
walk(card)
|
|
44
|
+
# Repaint only when new children joined (or on first attach).
|
|
45
|
+
self.refresh(force=self.fill is None or len(self.bound)!=known)
|
|
46
|
+
|
|
47
|
+
def cancel_press(self,event=None):
|
|
48
|
+
self.pressed=False;self.refresh()
|
|
49
|
+
def pointer(self,inside):
|
|
50
|
+
self.hover=inside
|
|
51
|
+
if not inside:self.pressed=False
|
|
52
|
+
self.refresh()
|
|
53
|
+
def press(self,event):
|
|
54
|
+
if not self.command or self.card.instate(['disabled']):return
|
|
55
|
+
self.card._root()._button_keyboard=False
|
|
56
|
+
self.pressed=True;self.card.focus_set();self.refresh()
|
|
57
|
+
def release(self,event):
|
|
58
|
+
pressed=self.pressed;self.pressed=False;self.refresh()
|
|
59
|
+
c=self.card
|
|
60
|
+
if pressed and c.winfo_rootx()<=event.x_root<c.winfo_rootx()+c.winfo_width() and c.winfo_rooty()<=event.y_root<c.winfo_rooty()+c.winfo_height():self.invoke()
|
|
61
|
+
def keyboard(self,event):
|
|
62
|
+
if event.widget is not self.card:return
|
|
63
|
+
self.card._root()._button_keyboard=True
|
|
64
|
+
if event.keysym=='space':
|
|
65
|
+
self.pressed=True;self.refresh()
|
|
66
|
+
else:self.refresh();self.invoke()
|
|
67
|
+
return 'break'
|
|
68
|
+
def key_release(self,event):
|
|
69
|
+
if event.widget is self.card and self.pressed:
|
|
70
|
+
self.pressed=False;self.refresh();self.invoke()
|
|
71
|
+
return 'break'
|
|
72
|
+
def invoke(self):
|
|
73
|
+
if self.command and not self.card.instate(['disabled']):return self.command()
|
|
74
|
+
|
|
75
|
+
def state_changed(self):
|
|
76
|
+
disabled=self.card.instate(['disabled'])
|
|
77
|
+
from .states import descendants_disabled
|
|
78
|
+
descendants_disabled(self.card,disabled,self.saved)
|
|
79
|
+
if disabled:self.pressed=False
|
|
80
|
+
self.refresh()
|
|
81
|
+
|
|
82
|
+
def refresh(self,event=None,force=False):
|
|
83
|
+
from .core import PALETTE
|
|
84
|
+
from .motion import blend
|
|
85
|
+
c=self.card;disabled=c.instate(['disabled'])
|
|
86
|
+
fill=PALETTE['selected'] if c._selected else PALETTE['bg']
|
|
87
|
+
if self.command and not disabled:
|
|
88
|
+
if self.pressed:fill=PALETTE['active']
|
|
89
|
+
elif self.hover:fill=PALETTE['hover']
|
|
90
|
+
focus=bool(self.command and not disabled and c.focus_get() is c and getattr(c._root(),'_button_keyboard',False))
|
|
91
|
+
focus_changed=focus!=getattr(c,'_feedback_focus',False)
|
|
92
|
+
c._feedback_focus=focus
|
|
93
|
+
if not force and self.fill is not None and fill==getattr(self,'target',None):
|
|
94
|
+
# Passive cards receive Enter/Leave from every child; nothing to do.
|
|
95
|
+
if focus_changed:c._recolor()
|
|
96
|
+
return
|
|
97
|
+
self.target=fill
|
|
98
|
+
before=self.fill or fill
|
|
99
|
+
def paint(amount):
|
|
100
|
+
# Quarter steps bound the colours (corner images, frame styles) to a
|
|
101
|
+
# small reusable set instead of one new ttk style per frame.
|
|
102
|
+
color=fill if amount>=1 else blend(before,fill,round(amount*4)/4)
|
|
103
|
+
if color==self.fill and not force:return
|
|
104
|
+
self.fill=color;c._feedback_fill=color;c._recolor()
|
|
105
|
+
root=c._root();declared=getattr(root,'_card_fill_styles',None)
|
|
106
|
+
if declared is None:declared=root._card_fill_styles=set()
|
|
107
|
+
name='CardFill'+color[1:]+'.TFrame'
|
|
108
|
+
for widget in self.bound:
|
|
109
|
+
if widget is c or not widget.winfo_exists():continue
|
|
110
|
+
if isinstance(widget,ttk.Label):widget.configure(background=color)
|
|
111
|
+
elif isinstance(widget,ttk.Frame):
|
|
112
|
+
if name not in declared:
|
|
113
|
+
declared.add(name)
|
|
114
|
+
with additive_styles(c):ttk.Style(c).configure(name,background=color)
|
|
115
|
+
widget.configure(style=name)
|
|
116
|
+
if before==fill or force:self.tween.cancel();paint(1)
|
|
117
|
+
else:self.tween.start(0.,1.,paint,duration=83)
|
|
118
|
+
|
|
119
|
+
def destroy(self,event):
|
|
120
|
+
if event.widget is self.card and self.job is not None:
|
|
121
|
+
self.card.after_cancel(self.job);self.job=None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _mapped_card(widget):
|
|
125
|
+
widget=getattr(widget,'master',None)
|
|
126
|
+
while widget is not None:
|
|
127
|
+
controller=getattr(widget,'interaction',None)
|
|
128
|
+
if controller is not None and controller.job is None:
|
|
129
|
+
controller.job=widget.after_idle(controller.attach)
|
|
130
|
+
widget=getattr(widget,'master',None)
|