1# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX 2# All rights reserved. 3# 4# This software is provided without warranty under the terms of the BSD 5# license included in LICENSE.txt and may be redistributed only under 6# the conditions described in the aforementioned license. The license 7# is also available online at http://www.enthought.com/licenses/BSD.txt 8# 9# Thanks for using Enthought open source! 10 11from collections import defaultdict 12import logging 13 14 15from pyface.action.api import ActionController, ActionManager 16from traits.api import HasTraits, Instance 17 18 19from pyface.tasks.task import Task 20from pyface.tasks.topological_sort import before_after_sort 21from pyface.tasks.action.schema import Schema, ToolBarSchema 22from pyface.tasks.action.schema_addition import SchemaAddition 23 24# Logging. 25logger = logging.getLogger(__name__) 26 27 28class TaskActionManagerBuilder(HasTraits): 29 """ Builds menu bars and tool bars from menu bar and tool bar schema, along 30 with any additions provided by the task. 31 """ 32 33 # The controller to assign to the menubar and toolbars. 34 controller = Instance(ActionController) 35 36 # The Task to build menubars and toolbars for. 37 task = Instance(Task) 38 39 # ------------------------------------------------------------------------ 40 # 'TaskActionManagerBuilder' interface. 41 # ------------------------------------------------------------------------ 42 43 def create_action_manager(self, schema): 44 """ Create a manager for the given schema using the task's additions. 45 """ 46 additions_map = defaultdict(list) 47 for addition in self.task.extra_actions: 48 if addition.path: 49 additions_map[addition.path].append(addition) 50 51 manager = self._create_action_manager_recurse(schema, additions_map) 52 manager.controller = self.controller 53 return manager 54 55 def create_menu_bar_manager(self): 56 """ Create a menu bar manager from the task's menu bar schema and 57 additions. 58 """ 59 if self.task.menu_bar: 60 return self.create_action_manager(self.task.menu_bar) 61 return None 62 63 def create_tool_bar_managers(self): 64 """ Create tool bar managers from the tasks's tool bar schemas and 65 additions. 66 """ 67 schemas = self.task.tool_bars[:] 68 for addition in self.task.extra_actions: 69 if not addition.path: 70 schema = addition.factory() 71 if isinstance(schema, ToolBarSchema): 72 schemas.append(schema) 73 else: 74 logger.error( 75 "Invalid top-level schema addition: %r. Only " 76 "ToolBar schemas can be path-less.", 77 schema, 78 ) 79 return [ 80 self.create_action_manager(schema) 81 for schema in self._get_ordered_schemas(schemas) 82 ] 83 84 def prepare_item(self, item, path): 85 """ Called immediately after a concrete Pyface item has been created 86 (or, in the case of items that are not produced from schemas, 87 immediately before they are processed). 88 89 This hook can be used to perform last-minute transformations or 90 configuration. Returns a concrete Pyface item. 91 """ 92 return item 93 94 # ------------------------------------------------------------------------ 95 # Private interface. 96 # ------------------------------------------------------------------------ 97 98 def _get_ordered_schemas(self, schemas): 99 begin = [] 100 middle = [] 101 end = [] 102 103 for schema in schemas: 104 absolute_position = getattr(schema, "absolute_position", None) 105 if absolute_position is None: 106 middle.append(schema) 107 elif absolute_position == "last": 108 end.append(schema) 109 else: 110 begin.append(schema) 111 112 schemas = ( 113 before_after_sort(begin) 114 + before_after_sort(middle) 115 + before_after_sort(end) 116 ) 117 return schemas 118 119 def _group_items_by_id(self, items): 120 """ Group a list of action items by their ID. 121 122 Action items are Schemas and Groups, MenuManagers, etc. 123 124 Return a dictionary {item_id: list_of_items}, and a list containing 125 all the ids ordered by their appearance in the `all_items` list. The 126 ordered IDs are used as a replacement for an ordered dictionary, to 127 keep compatibility with Python <2.7 . 128 129 """ 130 131 ordered_items_ids = [] 132 id_to_items = defaultdict(list) 133 134 for item in items: 135 if item.id not in id_to_items: 136 ordered_items_ids.append(item.id) 137 id_to_items[item.id].append(item) 138 139 return id_to_items, ordered_items_ids 140 141 def _group_items_by_class(self, items): 142 """ Group a list of action items by their class. 143 144 Action items are Schemas and Groups, MenuManagers, etc. 145 146 Return a dictionary {item_class: list_of_items}, and a list containing 147 all the classes ordered by their appearance in the `all_items` list. 148 The ordered classes are used as a replacement for an ordered 149 dictionary, to keep compatibility with Python <2.7 . 150 151 """ 152 153 ordered_items_class = [] 154 class_to_items = defaultdict(list) 155 156 for item in items: 157 if item.__class__ not in class_to_items: 158 ordered_items_class.append(item.__class__) 159 class_to_items[item.__class__].append(item) 160 161 return class_to_items, ordered_items_class 162 163 def _unpack_schema_additions(self, items): 164 """ Unpack additions, since they may themselves be schemas. """ 165 166 unpacked_items = [] 167 168 for item in items: 169 if isinstance(item, SchemaAddition): 170 unpacked_items.append(item.factory()) 171 else: 172 unpacked_items.append(item) 173 174 return unpacked_items 175 176 def _merge_items_with_same_path(self, id_to_items, ordered_items_ids): 177 """ Merge items with the same path if possible. 178 179 Items must be subclasses of `Schema` and they must be instances of 180 the same class to be merged. 181 182 """ 183 184 merged_items = [] 185 for item_id in ordered_items_ids: 186 items_with_same_id = id_to_items[item_id] 187 188 # Group items by class. 189 class_to_items, ordered_items_class = self._group_items_by_class( 190 items_with_same_id 191 ) 192 193 for items_class in ordered_items_class: 194 items_with_same_class = class_to_items[items_class] 195 196 if len(items_with_same_class) == 1: 197 merged_items.extend(items_with_same_class) 198 199 else: 200 # Only schemas can be merged. 201 if issubclass(items_class, Schema): 202 # Merge into a single schema. 203 items_content = sum( 204 (item.items for item in items_with_same_class), [] 205 ) 206 207 merged_item = items_with_same_class[0].clone_traits() 208 merged_item.items = items_content 209 merged_items.append(merged_item) 210 211 else: 212 merged_items.extend(items_with_same_class) 213 214 return merged_items 215 216 def _preprocess_schemas(self, schema, additions, path): 217 """ Sort and merge a schema and a set of schema additions. """ 218 219 # Determine the order of the items at this path. 220 if additions[path]: 221 all_items = self._get_ordered_schemas( 222 schema.items + additions[path] 223 ) 224 else: 225 all_items = schema.items 226 227 unpacked_items = self._unpack_schema_additions(all_items) 228 229 id_to_items, ordered_items_ids = self._group_items_by_id( 230 unpacked_items 231 ) 232 233 merged_items = self._merge_items_with_same_path( 234 id_to_items, ordered_items_ids 235 ) 236 237 return merged_items 238 239 def _create_action_manager_recurse(self, schema, additions, path=""): 240 """ Recursively create a manager for the given schema and additions map. 241 242 Items with the same path are merged together in a single entry if 243 possible (i.e., if they have the same class). 244 245 When a list of items is merged, their children are added to a clone 246 of the first item in the list. As a consequence, traits like menu 247 names etc. are inherited from the first item. 248 249 """ 250 251 # Compute the new action path. 252 if path: 253 path = path + "/" + schema.id 254 else: 255 path = schema.id 256 257 preprocessed_items = self._preprocess_schemas(schema, additions, path) 258 259 # Create the actual children by calling factory items. 260 children = [] 261 for item in preprocessed_items: 262 if isinstance(item, Schema): 263 item = self._create_action_manager_recurse( 264 item, additions, path 265 ) 266 else: 267 item = self.prepare_item(item, path + "/" + item.id) 268 269 if isinstance(item, ActionManager): 270 # Give even non-root action managers a reference to the 271 # controller so that custom Groups, MenuManagers, etc. can get 272 # access to their Tasks. 273 item.controller = self.controller 274 275 children.append(item) 276 277 # Finally, create the pyface.action instance for this schema. 278 return self.prepare_item(schema.create(children), path) 279 280 # Trait initializers --------------------------------------------------- 281 282 def _controller_default(self): 283 from .task_action_controller import TaskActionController 284 285 return TaskActionController(task=self.task) 286