Coverage for pyPhotoAlbum/ribbon_builder.py: 96%
101 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
1"""
2Ribbon configuration builder for pyPhotoAlbum
4This module scans classes for methods decorated with @ribbon_action
5and automatically builds the ribbon configuration structure.
6"""
8from typing import Dict, List, Any, Type
9from collections import defaultdict
12def build_ribbon_config(window_class: Type) -> Dict[str, Any]:
13 """
14 Extract decorated methods and build ribbon configuration.
16 This function scans all methods in the window class and its mixins
17 for methods decorated with @ribbon_action, then builds a nested
18 configuration structure suitable for the RibbonWidget.
20 Args:
21 window_class: The MainWindow class with decorated methods
23 Returns:
24 Dictionary containing the ribbon configuration with structure:
25 {
26 "TabName": {
27 "groups": [
28 {
29 "name": "GroupName",
30 "actions": [
31 {
32 "label": "Button Label",
33 "action": "method_name",
34 "tooltip": "Tooltip text",
35 ...
36 }
37 ]
38 }
39 ]
40 }
41 }
42 """
43 # Structure to collect actions by tab and group
44 tabs: Dict[str, Dict[str, List[Dict[str, Any]]]] = defaultdict(lambda: defaultdict(list))
46 # Scan all methods in the class and its bases (mixins)
47 for attr_name in dir(window_class):
48 try:
49 attr = getattr(window_class, attr_name)
51 # Check if this attribute has ribbon action metadata
52 if hasattr(attr, "_ribbon_action"):
53 action_data = attr._ribbon_action
55 # Extract tab and group information
56 tab_name = action_data["tab"]
57 group_name = action_data["group"]
59 # Add action to the appropriate tab and group
60 tabs[tab_name][group_name].append(
61 {
62 "label": action_data["label"],
63 "action": action_data["action"],
64 "tooltip": action_data["tooltip"],
65 "icon": action_data.get("icon"),
66 "shortcut": action_data.get("shortcut"),
67 }
68 )
69 except (AttributeError, TypeError):
70 # Skip attributes that can't be inspected
71 continue
73 # Convert to the expected ribbon config format
74 ribbon_config = {}
76 # Define tab order (tabs will appear in this order)
77 tab_order = ["Home", "Insert", "Layout", "Arrange", "Style", "View"]
79 # Add tabs in the defined order, then add any remaining tabs
80 all_tabs = list(tabs.keys())
81 ordered_tabs = [t for t in tab_order if t in all_tabs]
82 ordered_tabs.extend([t for t in all_tabs if t not in tab_order])
84 for tab_name in ordered_tabs:
85 groups_dict = tabs[tab_name]
87 # Convert groups dictionary to list format
88 groups_list = []
90 # Define group order per tab (if needed)
91 group_orders = {
92 "Home": ["File", "Edit"],
93 "Insert": ["Media", "Snapping"],
94 "Layout": ["Page", "Templates"],
95 "Arrange": ["Align", "Distribute", "Size", "Order", "Transform"],
96 "Style": ["Corners", "Border", "Effects", "Frame", "Presets"],
97 "View": ["Zoom", "Guides"],
98 }
100 # Get the group order for this tab, or use alphabetical
101 if tab_name in group_orders:
102 group_order = group_orders[tab_name]
103 # Add any groups not in the defined order
104 all_groups = list(groups_dict.keys())
105 group_order.extend([g for g in all_groups if g not in group_order])
106 else:
107 group_order = sorted(groups_dict.keys())
109 for group_name in group_order:
110 if group_name in groups_dict:
111 actions = groups_dict[group_name]
112 groups_list.append({"name": group_name, "actions": actions})
114 ribbon_config[tab_name] = {"groups": groups_list}
116 return ribbon_config
119def get_keyboard_shortcuts(window_class: Type) -> Dict[str, str]:
120 """
121 Extract keyboard shortcuts from decorated methods.
123 Args:
124 window_class: The MainWindow class with decorated methods
126 Returns:
127 Dictionary mapping shortcut strings to method names
128 Example: {'Ctrl+N': 'new_project', 'Ctrl+S': 'save_project'}
129 """
130 shortcuts = {}
132 for attr_name in dir(window_class):
133 try:
134 attr = getattr(window_class, attr_name)
136 if hasattr(attr, "_ribbon_action"):
137 action_data = attr._ribbon_action
138 shortcut = action_data.get("shortcut")
140 if shortcut:
141 shortcuts[shortcut] = action_data["action"]
142 except (AttributeError, TypeError):
143 continue
145 return shortcuts
148def validate_ribbon_config(config: Dict[str, Any]) -> List[str]:
149 """
150 Validate the ribbon configuration structure.
152 Args:
153 config: The ribbon configuration dictionary
155 Returns:
156 List of validation error messages (empty if valid)
157 """
158 errors = []
160 if not isinstance(config, dict):
161 errors.append("Config must be a dictionary")
162 return errors
164 for tab_name, tab_data in config.items():
165 if not isinstance(tab_data, dict):
166 errors.append(f"Tab '{tab_name}' data must be a dictionary")
167 continue
169 if "groups" not in tab_data:
170 errors.append(f"Tab '{tab_name}' missing 'groups' key")
171 continue
173 groups = tab_data["groups"]
174 if not isinstance(groups, list):
175 errors.append(f"Tab '{tab_name}' groups must be a list")
176 continue
178 for i, group in enumerate(groups):
179 if not isinstance(group, dict):
180 errors.append(f"Tab '{tab_name}' group {i} must be a dictionary")
181 continue
183 if "name" not in group:
184 errors.append(f"Tab '{tab_name}' group {i} missing 'name'")
186 if "actions" not in group:
187 errors.append(f"Tab '{tab_name}' group {i} missing 'actions'")
188 continue
190 actions = group["actions"]
191 if not isinstance(actions, list):
192 errors.append(f"Tab '{tab_name}' group {i} actions must be a list")
193 continue
195 for j, action in enumerate(actions):
196 if not isinstance(action, dict):
197 errors.append(f"Tab '{tab_name}' group {i} action {j} must be a dictionary")
198 continue
200 required_keys = ["label", "action", "tooltip"]
201 for key in required_keys:
202 if key not in action:
203 errors.append(f"Tab '{tab_name}' group {i} action {j} missing '{key}'")
205 return errors
208def print_ribbon_summary(config: Dict[str, Any]):
209 """
210 Print a summary of the ribbon configuration.
212 Args:
213 config: The ribbon configuration dictionary
214 """
215 print("\n=== Ribbon Configuration Summary ===\n")
217 total_tabs = len(config)
218 total_groups = sum(len(tab_data["groups"]) for tab_data in config.values())
219 total_actions = sum(len(group["actions"]) for tab_data in config.values() for group in tab_data["groups"])
221 print(f"Total Tabs: {total_tabs}")
222 print(f"Total Groups: {total_groups}")
223 print(f"Total Actions: {total_actions}\n")
225 for tab_name, tab_data in config.items():
226 print(f"📑 {tab_name}")
227 for group in tab_data["groups"]:
228 print(f" 📦 {group['name']} ({len(group['actions'])} actions)")
229 for action in group["actions"]:
230 shortcut = f" ({action['shortcut']})" if action.get("shortcut") else ""
231 print(f" • {action['label']}{shortcut}")
232 print()