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

1""" 

2Ribbon configuration builder for pyPhotoAlbum 

3 

4This module scans classes for methods decorated with @ribbon_action 

5and automatically builds the ribbon configuration structure. 

6""" 

7 

8from typing import Dict, List, Any, Type 

9from collections import defaultdict 

10 

11 

12def build_ribbon_config(window_class: Type) -> Dict[str, Any]: 

13 """ 

14 Extract decorated methods and build ribbon configuration. 

15 

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. 

19 

20 Args: 

21 window_class: The MainWindow class with decorated methods 

22 

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

45 

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) 

50 

51 # Check if this attribute has ribbon action metadata 

52 if hasattr(attr, "_ribbon_action"): 

53 action_data = attr._ribbon_action 

54 

55 # Extract tab and group information 

56 tab_name = action_data["tab"] 

57 group_name = action_data["group"] 

58 

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 

72 

73 # Convert to the expected ribbon config format 

74 ribbon_config = {} 

75 

76 # Define tab order (tabs will appear in this order) 

77 tab_order = ["Home", "Insert", "Layout", "Arrange", "Style", "View"] 

78 

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

83 

84 for tab_name in ordered_tabs: 

85 groups_dict = tabs[tab_name] 

86 

87 # Convert groups dictionary to list format 

88 groups_list = [] 

89 

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 } 

99 

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

108 

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

113 

114 ribbon_config[tab_name] = {"groups": groups_list} 

115 

116 return ribbon_config 

117 

118 

119def get_keyboard_shortcuts(window_class: Type) -> Dict[str, str]: 

120 """ 

121 Extract keyboard shortcuts from decorated methods. 

122 

123 Args: 

124 window_class: The MainWindow class with decorated methods 

125 

126 Returns: 

127 Dictionary mapping shortcut strings to method names 

128 Example: {'Ctrl+N': 'new_project', 'Ctrl+S': 'save_project'} 

129 """ 

130 shortcuts = {} 

131 

132 for attr_name in dir(window_class): 

133 try: 

134 attr = getattr(window_class, attr_name) 

135 

136 if hasattr(attr, "_ribbon_action"): 

137 action_data = attr._ribbon_action 

138 shortcut = action_data.get("shortcut") 

139 

140 if shortcut: 

141 shortcuts[shortcut] = action_data["action"] 

142 except (AttributeError, TypeError): 

143 continue 

144 

145 return shortcuts 

146 

147 

148def validate_ribbon_config(config: Dict[str, Any]) -> List[str]: 

149 """ 

150 Validate the ribbon configuration structure. 

151 

152 Args: 

153 config: The ribbon configuration dictionary 

154 

155 Returns: 

156 List of validation error messages (empty if valid) 

157 """ 

158 errors = [] 

159 

160 if not isinstance(config, dict): 

161 errors.append("Config must be a dictionary") 

162 return errors 

163 

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 

168 

169 if "groups" not in tab_data: 

170 errors.append(f"Tab '{tab_name}' missing 'groups' key") 

171 continue 

172 

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 

177 

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 

182 

183 if "name" not in group: 

184 errors.append(f"Tab '{tab_name}' group {i} missing 'name'") 

185 

186 if "actions" not in group: 

187 errors.append(f"Tab '{tab_name}' group {i} missing 'actions'") 

188 continue 

189 

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 

194 

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 

199 

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}'") 

204 

205 return errors 

206 

207 

208def print_ribbon_summary(config: Dict[str, Any]): 

209 """ 

210 Print a summary of the ribbon configuration. 

211 

212 Args: 

213 config: The ribbon configuration dictionary 

214 """ 

215 print("\n=== Ribbon Configuration Summary ===\n") 

216 

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

220 

221 print(f"Total Tabs: {total_tabs}") 

222 print(f"Total Groups: {total_groups}") 

223 print(f"Total Actions: {total_actions}\n") 

224 

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