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 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)