Spaces:
Paused
refactor(ui): π¨ restructure layout system with fixed height constraints and grid ratios
Browse filesThis commit restructures the model filter GUI layout to use a grid-based system with proper weight ratios and minimum height constraints, ensuring stable proportions during window resizing.
- Reduce minimum window dimensions from 850x600 to 600x400 for better flexibility
- Implement 3:1 grid weight ratio between model lists (weight=3) and rule panels (weight=1)
- Add grid_propagate(False) to key containers to prevent content from dictating frame sizes
- Reorganize RulePanel layout: title at top, input frame at bottom (packed first to reserve space), rule list in middle
- Set minimum row sizes: 200px for model lists, 55px for rule panels
- Remove obsolete RuleChip class (141 lines) - functionality now handled by VirtualRuleList
- Adjust padding and spacing values for more compact layout
The new grid-based approach ensures consistent proportional sizing and prevents UI elements from being squished or expanding unpredictably during window resizing.
- src/proxy_app/model_filter_gui.py +36 -171
|
@@ -32,8 +32,8 @@ from dotenv import load_dotenv, set_key, unset_key
|
|
| 32 |
# Window settings
|
| 33 |
WINDOW_TITLE = "Model Filter Configuration"
|
| 34 |
WINDOW_DEFAULT_SIZE = "1000x750"
|
| 35 |
-
WINDOW_MIN_WIDTH =
|
| 36 |
-
WINDOW_MIN_HEIGHT =
|
| 37 |
|
| 38 |
# Color scheme (dark mode)
|
| 39 |
BG_PRIMARY = "#1a1a2e" # Main background
|
|
@@ -1377,6 +1377,9 @@ class VirtualSyncModelLists(ctk.CTkFrame):
|
|
| 1377 |
|
| 1378 |
def _create_content(self):
|
| 1379 |
"""Build the dual list layout."""
|
|
|
|
|
|
|
|
|
|
| 1380 |
# Configure grid
|
| 1381 |
self.grid_columnconfigure(0, weight=1)
|
| 1382 |
self.grid_columnconfigure(1, weight=1)
|
|
@@ -2017,147 +2020,6 @@ class VirtualRuleList:
|
|
| 2017 |
)
|
| 2018 |
|
| 2019 |
|
| 2020 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2021 |
-
# RULE CHIP COMPONENT
|
| 2022 |
-
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2023 |
-
|
| 2024 |
-
|
| 2025 |
-
class RuleChip(ctk.CTkFrame):
|
| 2026 |
-
"""
|
| 2027 |
-
Individual rule display showing pattern, affected count, and delete button.
|
| 2028 |
-
|
| 2029 |
-
The pattern text is colored with the rule's assigned color.
|
| 2030 |
-
"""
|
| 2031 |
-
|
| 2032 |
-
def __init__(
|
| 2033 |
-
self,
|
| 2034 |
-
master,
|
| 2035 |
-
rule: FilterRule,
|
| 2036 |
-
on_delete: Callable[[str], None],
|
| 2037 |
-
on_click: Callable[[FilterRule], None],
|
| 2038 |
-
):
|
| 2039 |
-
super().__init__(
|
| 2040 |
-
master,
|
| 2041 |
-
fg_color=BG_TERTIARY,
|
| 2042 |
-
corner_radius=6,
|
| 2043 |
-
border_width=1,
|
| 2044 |
-
border_color=BORDER_COLOR,
|
| 2045 |
-
)
|
| 2046 |
-
|
| 2047 |
-
self.rule = rule
|
| 2048 |
-
self.on_delete = on_delete
|
| 2049 |
-
self.on_click = on_click
|
| 2050 |
-
self._is_highlighted = False
|
| 2051 |
-
self._tooltip = None # Store tooltip reference to avoid duplicates
|
| 2052 |
-
|
| 2053 |
-
self._create_content()
|
| 2054 |
-
|
| 2055 |
-
# Click binding
|
| 2056 |
-
self.bind("<Button-1>", self._handle_click)
|
| 2057 |
-
|
| 2058 |
-
def _create_content(self):
|
| 2059 |
-
"""Build chip content."""
|
| 2060 |
-
# Container for horizontal layout
|
| 2061 |
-
self.content = ctk.CTkFrame(self, fg_color="transparent")
|
| 2062 |
-
self.content.pack(fill="x", padx=8, pady=6)
|
| 2063 |
-
|
| 2064 |
-
# Pattern text (colored)
|
| 2065 |
-
self.pattern_label = ctk.CTkLabel(
|
| 2066 |
-
self.content,
|
| 2067 |
-
text=self.rule.pattern,
|
| 2068 |
-
font=(FONT_FAMILY, FONT_SIZE_NORMAL),
|
| 2069 |
-
text_color=self.rule.color,
|
| 2070 |
-
anchor="w",
|
| 2071 |
-
)
|
| 2072 |
-
self.pattern_label.pack(side="left", fill="x", expand=True)
|
| 2073 |
-
self.pattern_label.bind("<Button-1>", self._handle_click)
|
| 2074 |
-
|
| 2075 |
-
# Affected count
|
| 2076 |
-
self.count_label = ctk.CTkLabel(
|
| 2077 |
-
self.content,
|
| 2078 |
-
text=f"({self.rule.affected_count})",
|
| 2079 |
-
font=(FONT_FAMILY, FONT_SIZE_SMALL),
|
| 2080 |
-
text_color=TEXT_MUTED,
|
| 2081 |
-
width=35,
|
| 2082 |
-
)
|
| 2083 |
-
self.count_label.pack(side="left", padx=(5, 5))
|
| 2084 |
-
self.count_label.bind("<Button-1>", self._handle_click)
|
| 2085 |
-
|
| 2086 |
-
# Delete button
|
| 2087 |
-
delete_btn = ctk.CTkButton(
|
| 2088 |
-
self.content,
|
| 2089 |
-
text="Γ",
|
| 2090 |
-
font=(FONT_FAMILY, FONT_SIZE_LARGE, "bold"),
|
| 2091 |
-
fg_color="transparent",
|
| 2092 |
-
hover_color=ACCENT_RED,
|
| 2093 |
-
text_color=TEXT_MUTED,
|
| 2094 |
-
width=24,
|
| 2095 |
-
height=24,
|
| 2096 |
-
corner_radius=4,
|
| 2097 |
-
command=self._handle_delete,
|
| 2098 |
-
)
|
| 2099 |
-
delete_btn.pack(side="right")
|
| 2100 |
-
|
| 2101 |
-
# Tooltip showing affected models - create once, update later
|
| 2102 |
-
self._update_tooltip()
|
| 2103 |
-
|
| 2104 |
-
# Bind tooltip events to child widgets (not delete button)
|
| 2105 |
-
for widget in [self.content, self.pattern_label, self.count_label]:
|
| 2106 |
-
widget.bind("<Enter>", self._on_tooltip_enter)
|
| 2107 |
-
widget.bind("<Leave>", self._on_tooltip_leave)
|
| 2108 |
-
|
| 2109 |
-
def _on_tooltip_enter(self, event=None):
|
| 2110 |
-
"""Forward enter event to tooltip."""
|
| 2111 |
-
if self._tooltip:
|
| 2112 |
-
self._tooltip._schedule_show(event)
|
| 2113 |
-
|
| 2114 |
-
def _on_tooltip_leave(self, event=None):
|
| 2115 |
-
"""Forward leave event to tooltip."""
|
| 2116 |
-
if self._tooltip:
|
| 2117 |
-
self._tooltip._hide(event)
|
| 2118 |
-
|
| 2119 |
-
def _handle_click(self, event=None):
|
| 2120 |
-
"""Handle click on rule chip."""
|
| 2121 |
-
self.on_click(self.rule)
|
| 2122 |
-
|
| 2123 |
-
def _handle_delete(self):
|
| 2124 |
-
"""Handle delete button click."""
|
| 2125 |
-
self.on_delete(self.rule.pattern)
|
| 2126 |
-
|
| 2127 |
-
def update_count(self, count: int, affected_models: List[str]):
|
| 2128 |
-
"""Update the affected count and tooltip."""
|
| 2129 |
-
self.rule.affected_count = count
|
| 2130 |
-
self.rule.affected_models = affected_models
|
| 2131 |
-
self.count_label.configure(text=f"({count})")
|
| 2132 |
-
self._update_tooltip()
|
| 2133 |
-
|
| 2134 |
-
def _update_tooltip(self):
|
| 2135 |
-
"""Update tooltip with affected models."""
|
| 2136 |
-
if self.rule.affected_models:
|
| 2137 |
-
if len(self.rule.affected_models) <= 5:
|
| 2138 |
-
models_text = "\n".join(self.rule.affected_models)
|
| 2139 |
-
else:
|
| 2140 |
-
models_text = "\n".join(self.rule.affected_models[:5])
|
| 2141 |
-
models_text += f"\n... and {len(self.rule.affected_models) - 5} more"
|
| 2142 |
-
text = f"Matches:\n{models_text}"
|
| 2143 |
-
else:
|
| 2144 |
-
text = "No models match this pattern"
|
| 2145 |
-
|
| 2146 |
-
# Reuse existing tooltip or create new one
|
| 2147 |
-
if self._tooltip is None:
|
| 2148 |
-
self._tooltip = ToolTip(self, text)
|
| 2149 |
-
else:
|
| 2150 |
-
self._tooltip.update_text(text)
|
| 2151 |
-
|
| 2152 |
-
def set_highlighted(self, highlighted: bool):
|
| 2153 |
-
"""Set highlighted state."""
|
| 2154 |
-
self._is_highlighted = highlighted
|
| 2155 |
-
if highlighted:
|
| 2156 |
-
self.configure(border_color=self.rule.color, border_width=2)
|
| 2157 |
-
else:
|
| 2158 |
-
self.configure(border_color=BORDER_COLOR, border_width=1)
|
| 2159 |
-
|
| 2160 |
-
|
| 2161 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2162 |
# RULE PANEL COMPONENT
|
| 2163 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -2191,30 +2053,18 @@ class RulePanel(ctk.CTkFrame):
|
|
| 2191 |
|
| 2192 |
def _create_content(self):
|
| 2193 |
"""Build panel content."""
|
| 2194 |
-
# Title (compact)
|
| 2195 |
title_label = ctk.CTkLabel(
|
| 2196 |
self,
|
| 2197 |
text=self.title,
|
| 2198 |
font=(FONT_FAMILY, FONT_SIZE_SMALL, "bold"),
|
| 2199 |
text_color=TEXT_PRIMARY,
|
| 2200 |
)
|
| 2201 |
-
title_label.pack(anchor="w", padx=10, pady=(
|
| 2202 |
|
| 2203 |
-
#
|
| 2204 |
-
self.rule_list = VirtualRuleList(
|
| 2205 |
-
self,
|
| 2206 |
-
rule_type=self.rule_type,
|
| 2207 |
-
on_rule_click=self.on_rule_clicked,
|
| 2208 |
-
on_rule_delete=self._on_rule_delete,
|
| 2209 |
-
)
|
| 2210 |
-
self.rule_list.pack(fill="both", expand=True, padx=6, pady=(0, 3))
|
| 2211 |
-
|
| 2212 |
-
# Set minimum height for rule list to ensure it's visible
|
| 2213 |
-
self.rule_list.frame.configure(height=70)
|
| 2214 |
-
|
| 2215 |
-
# Input frame with fixed height (won't squish on resize)
|
| 2216 |
input_frame = ctk.CTkFrame(self, fg_color="transparent", height=32)
|
| 2217 |
-
input_frame.pack(fill="x", padx=6, pady=(
|
| 2218 |
input_frame.pack_propagate(False) # Prevent children from changing frame height
|
| 2219 |
|
| 2220 |
# Pattern input
|
|
@@ -2228,7 +2078,7 @@ class RulePanel(ctk.CTkFrame):
|
|
| 2228 |
placeholder_text_color=TEXT_MUTED,
|
| 2229 |
height=28,
|
| 2230 |
)
|
| 2231 |
-
self.input_entry.pack(side="left", fill="
|
| 2232 |
self.input_entry.bind("<Return>", self._on_add_clicked)
|
| 2233 |
self.input_entry.bind("<KeyRelease>", self._on_input_key)
|
| 2234 |
|
|
@@ -2245,6 +2095,15 @@ class RulePanel(ctk.CTkFrame):
|
|
| 2245 |
)
|
| 2246 |
add_btn.pack(side="right")
|
| 2247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2248 |
def _on_input_key(self, event=None):
|
| 2249 |
"""Handle key release in input field - for real-time preview."""
|
| 2250 |
text = self.input_entry.get().strip()
|
|
@@ -2365,20 +2224,24 @@ class ModelFilterGUI(ctk.CTk):
|
|
| 2365 |
self.after(100, self._activate_window)
|
| 2366 |
|
| 2367 |
def _create_main_layout(self):
|
| 2368 |
-
"""Create the main layout with grid for
|
| 2369 |
-
# Main content frame
|
| 2370 |
-
# This allows proportional sizing between model lists and rule panels
|
| 2371 |
self.content_frame = ctk.CTkFrame(self, fg_color="transparent")
|
| 2372 |
-
self.content_frame.pack(fill="both", expand=True, padx=
|
| 2373 |
|
| 2374 |
-
# Configure grid weights for
|
| 2375 |
-
# Using 3:1 ratio so models get significantly more space than rules
|
| 2376 |
self.content_frame.grid_columnconfigure(0, weight=1)
|
| 2377 |
-
|
| 2378 |
-
|
| 2379 |
-
self.content_frame.grid_rowconfigure(
|
| 2380 |
-
|
| 2381 |
-
self.content_frame.grid_rowconfigure(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2382 |
|
| 2383 |
# Create all sections
|
| 2384 |
self._create_header()
|
|
@@ -2527,6 +2390,8 @@ class ModelFilterGUI(ctk.CTk):
|
|
| 2527 |
"""Create the ignore and whitelist rule panels."""
|
| 2528 |
self.rules_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
|
| 2529 |
self.rules_frame.grid(row=3, column=0, sticky="nsew", pady=(0, 5))
|
|
|
|
|
|
|
| 2530 |
self.rules_frame.grid_columnconfigure(0, weight=1)
|
| 2531 |
self.rules_frame.grid_columnconfigure(1, weight=1)
|
| 2532 |
self.rules_frame.grid_rowconfigure(0, weight=1)
|
|
|
|
| 32 |
# Window settings
|
| 33 |
WINDOW_TITLE = "Model Filter Configuration"
|
| 34 |
WINDOW_DEFAULT_SIZE = "1000x750"
|
| 35 |
+
WINDOW_MIN_WIDTH = 600
|
| 36 |
+
WINDOW_MIN_HEIGHT = 400
|
| 37 |
|
| 38 |
# Color scheme (dark mode)
|
| 39 |
BG_PRIMARY = "#1a1a2e" # Main background
|
|
|
|
| 1377 |
|
| 1378 |
def _create_content(self):
|
| 1379 |
"""Build the dual list layout."""
|
| 1380 |
+
# Don't let content dictate size - let parent grid control height
|
| 1381 |
+
self.grid_propagate(False)
|
| 1382 |
+
|
| 1383 |
# Configure grid
|
| 1384 |
self.grid_columnconfigure(0, weight=1)
|
| 1385 |
self.grid_columnconfigure(1, weight=1)
|
|
|
|
| 2020 |
)
|
| 2021 |
|
| 2022 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2023 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2024 |
# RULE PANEL COMPONENT
|
| 2025 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 2053 |
|
| 2054 |
def _create_content(self):
|
| 2055 |
"""Build panel content."""
|
| 2056 |
+
# Title at top (compact)
|
| 2057 |
title_label = ctk.CTkLabel(
|
| 2058 |
self,
|
| 2059 |
text=self.title,
|
| 2060 |
font=(FONT_FAMILY, FONT_SIZE_SMALL, "bold"),
|
| 2061 |
text_color=TEXT_PRIMARY,
|
| 2062 |
)
|
| 2063 |
+
title_label.pack(side="top", anchor="w", padx=10, pady=(4, 2))
|
| 2064 |
|
| 2065 |
+
# Input frame at BOTTOM - pack BEFORE rule_list to reserve space
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2066 |
input_frame = ctk.CTkFrame(self, fg_color="transparent", height=32)
|
| 2067 |
+
input_frame.pack(side="bottom", fill="x", padx=6, pady=(2, 4))
|
| 2068 |
input_frame.pack_propagate(False) # Prevent children from changing frame height
|
| 2069 |
|
| 2070 |
# Pattern input
|
|
|
|
| 2078 |
placeholder_text_color=TEXT_MUTED,
|
| 2079 |
height=28,
|
| 2080 |
)
|
| 2081 |
+
self.input_entry.pack(side="left", fill="both", expand=True, padx=(0, 6))
|
| 2082 |
self.input_entry.bind("<Return>", self._on_add_clicked)
|
| 2083 |
self.input_entry.bind("<KeyRelease>", self._on_input_key)
|
| 2084 |
|
|
|
|
| 2095 |
)
|
| 2096 |
add_btn.pack(side="right")
|
| 2097 |
|
| 2098 |
+
# Virtual rule list fills REMAINING middle space - pack LAST
|
| 2099 |
+
self.rule_list = VirtualRuleList(
|
| 2100 |
+
self,
|
| 2101 |
+
rule_type=self.rule_type,
|
| 2102 |
+
on_rule_click=self.on_rule_clicked,
|
| 2103 |
+
on_rule_delete=self._on_rule_delete,
|
| 2104 |
+
)
|
| 2105 |
+
self.rule_list.pack(side="top", fill="both", expand=True, padx=6, pady=(0, 2))
|
| 2106 |
+
|
| 2107 |
def _on_input_key(self, event=None):
|
| 2108 |
"""Handle key release in input field - for real-time preview."""
|
| 2109 |
text = self.input_entry.get().strip()
|
|
|
|
| 2224 |
self.after(100, self._activate_window)
|
| 2225 |
|
| 2226 |
def _create_main_layout(self):
|
| 2227 |
+
"""Create the main layout with grid weights for 3:1 ratio."""
|
| 2228 |
+
# Main content frame - regular frame with grid layout
|
|
|
|
| 2229 |
self.content_frame = ctk.CTkFrame(self, fg_color="transparent")
|
| 2230 |
+
self.content_frame.pack(fill="both", expand=True, padx=15, pady=(5, 8))
|
| 2231 |
|
| 2232 |
+
# Configure grid with proper weights for 3:1 ratio
|
|
|
|
| 2233 |
self.content_frame.grid_columnconfigure(0, weight=1)
|
| 2234 |
+
|
| 2235 |
+
# Row 0: Header - fixed height
|
| 2236 |
+
self.content_frame.grid_rowconfigure(0, weight=0)
|
| 2237 |
+
# Row 1: Search - fixed height
|
| 2238 |
+
self.content_frame.grid_rowconfigure(1, weight=0)
|
| 2239 |
+
# Row 2: Model lists - weight=3 for 3:1 ratio, minimum 100px
|
| 2240 |
+
self.content_frame.grid_rowconfigure(2, weight=3, minsize=200)
|
| 2241 |
+
# Row 3: Rule panels - weight=1 for 3:1 ratio, minimum 55px
|
| 2242 |
+
self.content_frame.grid_rowconfigure(3, weight=1, minsize=55)
|
| 2243 |
+
# Row 4: Status bar - fixed height
|
| 2244 |
+
self.content_frame.grid_rowconfigure(4, weight=0)
|
| 2245 |
|
| 2246 |
# Create all sections
|
| 2247 |
self._create_header()
|
|
|
|
| 2390 |
"""Create the ignore and whitelist rule panels."""
|
| 2391 |
self.rules_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
|
| 2392 |
self.rules_frame.grid(row=3, column=0, sticky="nsew", pady=(0, 5))
|
| 2393 |
+
# Don't let content dictate size - let parent grid control height
|
| 2394 |
+
self.rules_frame.grid_propagate(False)
|
| 2395 |
self.rules_frame.grid_columnconfigure(0, weight=1)
|
| 2396 |
self.rules_frame.grid_columnconfigure(1, weight=1)
|
| 2397 |
self.rules_frame.grid_rowconfigure(0, weight=1)
|