1 """

2 Ribbon configuration builder for pyPhotoAlbum

3

4 This module scans classes for methods decorated with @ribbon_action

5 and automatically builds the ribbon configuration structure.

6 """

7

8 from typing import Dict, List, Any, Type

9 from collections import defaultdict

10

11

12 def 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

119 def 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

148 def 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

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