Skip to content

Commit bfe60f3

Browse files
committed
refactor: unify dpi scaling rules and error handling
1 parent 51b24e3 commit bfe60f3

File tree

8 files changed

+2996
-1423
lines changed

8 files changed

+2996
-1423
lines changed

tests/test_dpi.py

Lines changed: 1664 additions & 457 deletions
Large diffs are not rendered by default.

tkface/dialog/datepicker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@ def update_dpi_scaling(self):
706706
return
707707

708708
width, height, x, y = map(int, match.groups())
709+
# Use unified scaling rule: DPI_scaling only
709710
scaled_width = int(width * self.dpi_scaling_factor)
710711
scaled_height = int(height * self.dpi_scaling_factor)
711712
# Don't scale position coordinates (x, y) - keep them as is

tkface/dialog/pathchooser.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,6 @@ def _create_dialog_window(parent: Optional[tk.Tk]) -> tk.Toplevel:
4848
return dialog
4949

5050

51-
def _setup_dialog_properties(dialog: tk.Toplevel, title: str, scaled_sizes: dict):
52-
"""Setup dialog window properties."""
53-
dialog.title(title)
54-
dialog.resizable(True, True)
55-
dialog.minsize(scaled_sizes["min_width"], scaled_sizes["min_height"])
56-
dialog.geometry(f"{scaled_sizes['default_width']}x{scaled_sizes['default_height']}")
57-
58-
5951
def _position_dialog(
6052
dialog: tk.Toplevel,
6153
parent: Optional[tk.Tk],
@@ -138,19 +130,6 @@ def askpath(
138130
# Create dialog window
139131
dialog = _create_dialog_window(parent)
140132

141-
# Cache DPI scaling calculations for performance (similar to messagebox.py)
142-
scaled_sizes = win.calculate_dpi_sizes(
143-
{
144-
"min_width": 900,
145-
"min_height": 450,
146-
"default_width": 1000,
147-
"default_height": 450,
148-
"padding": 10,
149-
},
150-
dialog,
151-
max_scale=1.5,
152-
)
153-
154133
# Set dialog properties
155134
if title is None:
156135
if select == "file":
@@ -162,7 +141,26 @@ def askpath(
162141
if multiple:
163142
title += "s"
164143

165-
_setup_dialog_properties(dialog, title, scaled_sizes)
144+
# Set basic dialog properties
145+
dialog.title(title)
146+
dialog.resizable(True, True)
147+
148+
# Get DPI scaling factor and apply it manually to avoid double-scaling
149+
try:
150+
scaling_factor = win.get_scaling_factor(dialog)
151+
except Exception as e:
152+
scaling_factor = 1.0
153+
154+
# Calculate scaled sizes
155+
scaled_width = int(700 * scaling_factor)
156+
scaled_height = int(500 * scaling_factor)
157+
158+
# Apply manual scaling to avoid double-scaling issues
159+
dialog.minsize(scaled_width, int(scaled_height * 0.5))
160+
dialog.geometry(f"{scaled_width}x{scaled_height}")
161+
162+
# Force update to ensure geometry is applied
163+
dialog.update_idletasks()
166164

167165
# Make dialog modal
168166
if parent:
@@ -185,12 +183,16 @@ def askpath(
185183
browser.pack(
186184
fill=tk.BOTH,
187185
expand=True,
188-
padx=scaled_sizes["padding"],
189-
pady=scaled_sizes["padding"],
186+
padx=10,
187+
pady=10,
190188
)
191189

192190
# Position the dialog (after content is created)
191+
# Note: _position_dialog may override our geometry, so we'll set it again after positioning
193192
_position_dialog(dialog, parent, x, y, x_offset, y_offset)
193+
194+
# Re-apply our desired size after positioning (in case _position_dialog overrode it)
195+
dialog.geometry(f"{scaled_width}x{scaled_height}")
194196

195197
# Result storage
196198
result = []

tkface/widget/calendar/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ def _get_scaled_font(self, base_font):
273273
try:
274274
if isinstance(base_font, tuple):
275275
family, size, *style = base_font
276+
# Use scale_font_size which now handles positive sizes correctly
276277
scaled_size = scale_font_size(size, self, self.dpi_scaling_factor)
277278
return (family, scaled_size, *style)
278279
return base_font

tkface/widget/calendar/view.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ def _create_calendar_grid(calendar_instance, month_frame, month_index):
451451
bd=0,
452452
bg=calendar_instance.theme_colors["day_header_bg"],
453453
fg=calendar_instance.theme_colors["day_header_fg"],
454+
width=3, # Fixed width for consistent column sizing
454455
)
455456
empty_header.grid(row=0, column=0, sticky="nsew", padx=1, pady=1)
456457
for day, day_name in enumerate(day_names):
@@ -464,6 +465,7 @@ def _create_calendar_grid(calendar_instance, month_frame, month_index):
464465
bd=0,
465466
bg=calendar_instance.theme_colors["day_header_bg"],
466467
fg=calendar_instance.theme_colors["day_header_fg"],
468+
width=3, # Fixed width for consistent column sizing
467469
)
468470
col = day + 1 if calendar_instance.show_week_numbers else day
469471
day_header.grid(row=0, column=col, sticky="nsew", padx=1, pady=1)
@@ -480,6 +482,7 @@ def _create_calendar_grid(calendar_instance, month_frame, month_index):
480482
bd=0,
481483
bg=calendar_instance.theme_colors["week_number_bg"],
482484
fg=calendar_instance.theme_colors["week_number_fg"],
485+
width=3, # Fixed width for consistent column sizing
483486
)
484487
week_label.grid(row=week + 1, column=0, sticky="nsew", padx=1, pady=1)
485488
calendar_instance.week_labels.append(week_label)
@@ -496,6 +499,7 @@ def _create_calendar_grid(calendar_instance, month_frame, month_index):
496499
bg=calendar_instance.theme_colors["day_bg"],
497500
fg=calendar_instance.theme_colors["day_fg"],
498501
cursor="hand2",
502+
width=3, # Fixed width for consistent column sizing
499503
)
500504
col = day + 1 if calendar_instance.show_week_numbers else day
501505
day_label.grid(row=week + 1, column=col, sticky="nsew", padx=1, pady=1)

tkface/widget/pathbrowser/view.py

Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ def _create_main_paned_window(pathbrowser_instance):
8484
pathbrowser_instance.paned = ttk.PanedWindow(
8585
pathbrowser_instance, orient=tk.HORIZONTAL
8686
)
87-
pathbrowser_instance.paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
87+
pathbrowser_instance.paned.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
8888

8989
# Left pane - Directory tree
9090
pathbrowser_instance.tree_frame = ttk.Frame(pathbrowser_instance.paned)
91-
pathbrowser_instance.paned.add(pathbrowser_instance.tree_frame, weight=1)
91+
pathbrowser_instance.paned.add(pathbrowser_instance.tree_frame, weight=2)
9292

9393
# Directory tree with icons and better styling
9494
pathbrowser_instance.tree = ttk.Treeview(
@@ -97,13 +97,55 @@ def _create_main_paned_window(pathbrowser_instance):
9797
selectmode="browse",
9898
height=10, # Reduced height for smaller dialog
9999
)
100-
pathbrowser_instance.tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
100+
pathbrowser_instance.tree.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
101+
102+
# Set initial sash position to give 200px to the tree view (DPI-scaled)
103+
# This will be applied after the paned window is fully created
104+
def set_dpi_scaled_sash():
105+
try:
106+
# Get DPI scaling factor
107+
from tkface.win.dpi import get_scaling_factor
108+
scaling_factor = get_scaling_factor(pathbrowser_instance)
109+
dpi_scaled_width = int(200 * scaling_factor)
110+
pathbrowser_instance.paned.sashpos(0, dpi_scaled_width)
111+
except Exception:
112+
# Fallback to unscaled 200px if DPI detection fails
113+
pathbrowser_instance.paned.sashpos(0, 200)
114+
115+
pathbrowser_instance.after(100, set_dpi_scaled_sash)
101116

102117
# Configure tree style for better appearance
103118
style = ttk.Style()
104119
# Set row height based on OS - Windows uses original spacing, others use compact
105120
row_height = 25 if utils.IS_WINDOWS else 20
106121
style.configure("Treeview", rowheight=row_height)
122+
123+
# Bind to tree selection to adjust indent based on current level
124+
def adjust_tree_indent(event=None):
125+
"""Adjust tree indent based on current directory level."""
126+
try:
127+
current_dir = pathbrowser_instance.state.current_dir
128+
# Count directory depth from root
129+
path_parts = Path(current_dir).parts
130+
depth = len(path_parts) - 1 # Subtract 1 for root
131+
132+
# Calculate indent: base indent minus depth-based offset
133+
# This reduces left padding for deeper directories
134+
base_indent = 20
135+
depth_offset = min(depth * 2, 15) # Max 15px reduction
136+
adjusted_indent = max(base_indent - depth_offset, 5) # Min 5px
137+
138+
# Apply the adjusted indent
139+
style.configure("Treeview", indent=adjusted_indent)
140+
141+
except Exception as e:
142+
logger.debug("Failed to adjust tree indent: %s", e)
143+
144+
# Bind to directory changes to adjust indent
145+
pathbrowser_instance.tree.bind("<<TreeviewSelect>>", adjust_tree_indent)
146+
147+
# Initial adjustment
148+
pathbrowser_instance.after(100, adjust_tree_indent)
107149

108150

109151
def _create_file_list(pathbrowser_instance):
@@ -149,23 +191,23 @@ def _create_file_list(pathbrowser_instance):
149191
)
150192

151193
pathbrowser_instance.file_tree.column("#0", width=200, minwidth=150)
152-
pathbrowser_instance.file_tree.column("size", width=80, minwidth=60)
194+
pathbrowser_instance.file_tree.column("size", width=70, minwidth=50)
153195
pathbrowser_instance.file_tree.column("modified", width=120, minwidth=100)
154-
pathbrowser_instance.file_tree.column("type", width=100, minwidth=80)
196+
pathbrowser_instance.file_tree.column("type", width=60, minwidth=50)
155197

156198
# Apply OS-specific row height to file tree as well
157199
style = ttk.Style()
158-
row_height = 25 if utils.IS_WINDOWS else 20
200+
row_height = 20
159201
style.configure("Treeview", rowheight=row_height)
160202

161-
pathbrowser_instance.file_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
203+
pathbrowser_instance.file_tree.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
162204

163205

164206
def _create_status_and_buttons(pathbrowser_instance):
165207
"""Create status bar and bottom buttons in a horizontal layout."""
166208
# Bottom frame containing status bar and buttons
167209
pathbrowser_instance.bottom_frame = ttk.Frame(pathbrowser_instance)
168-
pathbrowser_instance.bottom_frame.pack(fill=tk.X, padx=5, pady=5, side=tk.BOTTOM)
210+
pathbrowser_instance.bottom_frame.pack(fill=tk.X, padx=2, pady=2, side=tk.BOTTOM)
169211

170212
# Status bar (left side)
171213
pathbrowser_instance.status_var = tk.StringVar()
@@ -267,11 +309,13 @@ def _create_toolbar(pathbrowser_instance):
267309

268310
def create_pathbrowser_widgets(pathbrowser_instance):
269311
"""Create the widget layout for PathBrowser."""
312+
# Pack bottom areas first so they remain visible even when window is small
270313
_create_path_navigation(pathbrowser_instance)
271-
_create_main_paned_window(pathbrowser_instance)
272-
_create_file_list(pathbrowser_instance)
273314
_create_status_and_buttons(pathbrowser_instance)
274315
_create_toolbar(pathbrowser_instance)
316+
# Main content takes the remaining space
317+
_create_main_paned_window(pathbrowser_instance)
318+
_create_file_list(pathbrowser_instance)
275319

276320
# Initialize view mode
277321
pathbrowser_instance.view_mode = "details"
@@ -397,25 +441,48 @@ def setup_pathbrowser_bindings(pathbrowser_instance):
397441
def load_directory_tree(pathbrowser_instance):
398442
"""Load the directory tree."""
399443
pathbrowser_instance.tree.delete(*pathbrowser_instance.tree.get_children())
444+
# Flatten view to start at the current directory level to avoid left-side slack
445+
try:
446+
base_path = Path(pathbrowser_instance.state.current_dir)
447+
448+
# List only immediate subdirectories of the current directory as top-level
449+
dirs = []
450+
for item in base_path.iterdir():
451+
if item.is_dir():
452+
dirs.append((item.name, str(item)))
400453

401-
# Add root directories with better icons and labels
402-
if utils.IS_WINDOWS: # Windows
403-
for drive in string.ascii_uppercase:
404-
drive_path = f"{drive}:\\"
405-
if Path(drive_path).exists():
406-
# Get drive label if possible
454+
# Sort and add directories
455+
dirs.sort(key=lambda x: x[0].lower())
456+
457+
for child_name, child_path in dirs:
458+
if not pathbrowser_instance.tree.exists(child_path):
459+
# Insert as top-level item (no ancestors, no extra left indent)
407460
pathbrowser_instance.tree.insert(
408-
"", "end", drive_path, text=drive_path, open=False
461+
"", "end", child_path, text=child_name, open=False
409462
)
410-
# Pre-populate root level directories for better UX
411-
populate_tree_node(pathbrowser_instance, drive_path)
412-
else: # Unix-like
413-
pathbrowser_instance.tree.insert("", "end", "/", text="/", open=False)
414-
# Pre-populate root level directories for better UX
415-
populate_tree_node(pathbrowser_instance, "/")
416-
417-
# Expand current directory path
418-
expand_path(pathbrowser_instance, pathbrowser_instance.state.current_dir)
463+
464+
# Add placeholder if the directory has subdirectories (for lazy loading)
465+
with suppress(OSError, PermissionError):
466+
has_subdirs = False
467+
for subitem in base_path.joinpath(child_name).iterdir():
468+
if subitem.is_dir():
469+
has_subdirs = True
470+
break
471+
if has_subdirs:
472+
placeholder_id = f"{child_path}_placeholder"
473+
if not pathbrowser_instance.tree.exists(placeholder_id):
474+
pathbrowser_instance.tree.insert(
475+
child_path,
476+
"end",
477+
placeholder_id,
478+
text=lang.get("Loading...", pathbrowser_instance),
479+
open=False,
480+
)
481+
except (OSError, PermissionError) as e:
482+
logger.warning("Failed to load directory tree for %s: %s", base_path, e)
483+
pathbrowser_instance.status_var.set(
484+
f"{lang.get('Cannot access:', pathbrowser_instance)} {base_path.name}"
485+
)
419486

420487

421488
def populate_tree_node(pathbrowser_instance, parent):

tkface/win/__init__.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,18 @@
1818
get_button_label_with_shortcut,
1919
)
2020
from .dpi import (
21-
add_scalable_property,
2221
calculate_dpi_sizes,
2322
configure_ttk_widgets_for_dpi,
24-
disable_auto_dpi_scaling,
2523
dpi,
26-
enable_auto_dpi_scaling,
2724
enable_dpi_awareness,
2825
enable_dpi_geometry,
2926
get_actual_window_size,
3027
get_effective_dpi,
31-
get_scalable_properties,
3228
get_scaling_factor,
33-
is_auto_dpi_scaling_enabled,
3429
logical_to_physical,
35-
patch_tk_widgets_to_ttk,
3630
physical_to_logical,
37-
remove_scalable_property,
3831
scale_font_size,
3932
scale_icon,
40-
scale_widget_dimensions,
41-
scale_widget_tree,
4233
)
4334
from .unround import (
4435
disable_auto_unround,
@@ -60,15 +51,6 @@
6051
"get_effective_dpi",
6152
"logical_to_physical",
6253
"physical_to_logical",
63-
"enable_auto_dpi_scaling",
64-
"disable_auto_dpi_scaling",
65-
"is_auto_dpi_scaling_enabled",
66-
"scale_widget_dimensions",
67-
"scale_widget_tree",
68-
"get_scalable_properties",
69-
"add_scalable_property",
70-
"remove_scalable_property",
71-
"patch_tk_widgets_to_ttk",
7254
"configure_button_for_windows",
7355
"get_button_label_with_shortcut",
7456
"FlatButton",

0 commit comments

Comments
 (0)