Permalink
Browse files

Merge pull request #73 from enthought/mergeable-schemas

Mergeable schemas
  • Loading branch information...
2 parents 0173533 + c061d8b commit 88cdcb6cf50b3ef79c424f57b61bc59c27b6cc88 nmichaud committed Dec 27, 2012
@@ -29,7 +29,7 @@ class ActionManager(HasTraits):
An action manager contains a list of groups, with each group containing a
list of items.
- There are currently three concrete sub-classes:-
+ There are currently three concrete sub-classes:
1) 'MenuBarManager'
2) 'MenuManager'
@@ -1,6 +1,7 @@
# Enthought library imports.
from pyface.action.api import Action, ActionItem, Group, \
MenuManager, MenuBarManager, ToolBarManager
+from pyface.util.id_helper import get_unique_id
from traits.api import Bool, Callable, Enum, HasTraits, Instance, \
List, Property, Str, Trait, Tuple, Unicode
@@ -18,6 +19,9 @@ class Schema(HasTraits):
# The schema's identifier (unique within its parent schema).
id = Str
+ def _id_default(self):
+ return get_unique_id(self)
+
# The list of sub-items in the schema. These items can be other
# (non-top-level) schema or concrete instances from the Pyface API.
items = List(SubSchema)
@@ -130,7 +134,7 @@ class ToolBarSchema(Schema):
# Should we display the name of each tool bar tool under its image?
show_tool_names = Bool(True)
- # A factory for instantiating a pyfce ToolBarManager
+ # A factory for instantiating a pyface ToolBarManager
tool_bar_manager_factory = Callable(ToolBarManager)
def create(self, children):
@@ -81,25 +81,162 @@ def prepare_item(self, item, path):
# Private interface.
###########################################################################
+ def _get_ordered_schemas(self, schemas):
+ begin = []
+ middle = []
+ end = []
+
+ for schema in schemas:
+ absolute_position = getattr(schema, 'absolute_position', None)
+ if absolute_position is None:
+ middle.append(schema)
+ elif absolute_position == 'last':
+ end.append(schema)
+ else:
+ begin.append(schema)
+
+ schemas = (before_after_sort(begin)
+ + before_after_sort(middle)
+ + before_after_sort(end))
+ return schemas
+
+ def _group_items_by_id(self, items):
+ """ Group a list of action items by their ID.
+
+ Action items are Schemas and Groups, MenuManagers, etc.
+
+ Return a dictionary {item_id: list_of_items}, and a list containing
+ all the ids ordered by their appearance in the `all_items` list. The
+ ordered IDs are used as a replacement for an ordered dictionary, to
+ keep compatibility with Python <2.7 .
+
+ """
+
+ ordered_items_ids = []
+ id_to_items = defaultdict(list)
+
+ for item in items:
+ if item.id not in id_to_items:
+ ordered_items_ids.append(item.id)
+ id_to_items[item.id].append(item)
+
+ return id_to_items, ordered_items_ids
+
+ def _group_items_by_class(self, items):
+ """ Group a list of action items by their class.
+
+ Action items are Schemas and Groups, MenuManagers, etc.
+
+ Return a dictionary {item_class: list_of_items}, and a list containing
+ all the classes ordered by their appearance in the `all_items` list.
+ The ordered classes are used as a replacement for an ordered
+ dictionary, to keep compatibility with Python <2.7 .
+
+ """
+
+ ordered_items_class = []
+ class_to_items = defaultdict(list)
+
+ for item in items:
+ if item.__class__ not in class_to_items:
+ ordered_items_class.append(item.__class__)
+ class_to_items[item.__class__].append(item)
+
+ return class_to_items, ordered_items_class
+
+ def _unpack_schema_additions(self, items):
+ """ Unpack additions, since they may themselves be schemas. """
+
+ unpacked_items = []
+
+ for item in items:
+ if isinstance(item, SchemaAddition):
+ unpacked_items.append(item.factory())
+ else:
+ unpacked_items.append(item)
+
+ return unpacked_items
+
+ def _merge_items_with_same_path(self, id_to_items, ordered_items_ids):
+ """ Merge items with the same path if possible.
+
+ Items must be subclasses of `Schema` and they must be instances of
+ the same class to be merged.
+
+ """
+
+ merged_items = []
+ for item_id in ordered_items_ids:
+ items_with_same_id = id_to_items[item_id]
+
+ # Group items by class.
+ class_to_items, ordered_items_class =\
+ self._group_items_by_class(items_with_same_id)
+
+ for items_class in ordered_items_class:
+ items_with_same_class = class_to_items[items_class]
+
+ if len(items_with_same_class) == 1:
+ merged_items.extend(items_with_same_class)
+
+ else:
+ # Only schemas can be merged.
+ if issubclass(items_class, Schema):
+ # Merge into a single schema.
+ items_content = sum(
+ (item.items for item in items_with_same_class), []
+ )
+
+ merged_item = items_with_same_class[0].clone_traits()
+ merged_item.items = items_content
+ merged_items.append(merged_item)
+
+ else:
+ merged_items.extend(items_with_same_class)
+
+ return merged_items
+
+ def _preprocess_schemas(self, schema, additions, path):
+ """ Sort and merge a schema and a set of schema additions. """
+
+ # Determine the order of the items at this path.
+ if additions[path]:
+ all_items = self._get_ordered_schemas(schema.items+additions[path])
+ else:
+ all_items = schema.items
+
+ unpacked_items = self._unpack_schema_additions(all_items)
+
+ id_to_items, ordered_items_ids = self._group_items_by_id(unpacked_items)
+
+ merged_items = self._merge_items_with_same_path(id_to_items,
+ ordered_items_ids)
+
+ return merged_items
+
def _create_action_manager_recurse(self, schema, additions, path=''):
""" Recursively create a manager for the given schema and additions map.
+
+ Items with the same path are merged together in a single entry if
+ possible (i.e., if they have the same class).
+
+ When a list of items is merged, their children are added to a clone
+ of the first item in the list. As a consequence, traits like menu
+ names etc. are inherited from the first item.
+
"""
+
# Compute the new action path.
- if path: path += '/'
- path += schema.id
+ if path:
+ path = path + '/' + schema.id
+ else:
+ path = schema.id
- # Determine the order of the items at this path.
- items = schema.items
- if additions[path]:
- items = self._get_ordered_schemas(items + additions[path])
+ preprocessed_items = self._preprocess_schemas(schema, additions, path)
# Create the actual children by calling factory items.
children = []
- for item in items:
- # Unpack additions first, since they may themselves be schemas.
- if isinstance(item, SchemaAddition):
- item = item.factory()
-
+ for item in preprocessed_items:
if isinstance(item, Schema):
item = self._create_action_manager_recurse(item,additions,path)
else:
@@ -112,29 +249,10 @@ def _create_action_manager_recurse(self, schema, additions, path=''):
item.controller = self.controller
children.append(item)
-
+
# Finally, create the pyface.action instance for this schema.
return self.prepare_item(schema.create(children), path)
- def _get_ordered_schemas(self, schemas):
- begin = []
- middle = []
- end = []
-
- for schema in schemas:
- absolute_position = getattr(schema, 'absolute_position', None)
- if absolute_position is None:
- middle.append(schema)
- elif absolute_position == 'last':
- end.append(schema)
- else:
- begin.append(schema)
-
- schemas = (before_after_sort(begin)
- + before_after_sort(middle)
- + before_after_sort(end))
- return schemas
-
#### Trait initializers ###################################################
def _controller_default(self):
View
@@ -4,7 +4,7 @@
Unicode
# Local imports.
-from action.schema import MenuSchema, MenuBarSchema, ToolBarSchema
+from action.schema import MenuBarSchema, ToolBarSchema
from action.schema_addition import SchemaAddition
from task_layout import TaskLayout
Oops, something went wrong.

0 comments on commit 88cdcb6

Please sign in to comment.