Logo Search packages:      
Sourcecode: mayavi2 version File versions  Download package

resource_node_type.py

00001 """ The base class for all resource node types. """


# Standard library imports.
import cPickle
import logging

# Enthought library imports.
from enthought.envisage import get_application
from enthought.envisage.action import ActionSetManager, DefaultMenuBuilder
from enthought.io import File
from enthought.naming.api import Binding, Context
from enthought.pyface.api import confirm, ImageResource, YES
from enthought.pyface.action.api import Action, ActionItem, Group, MenuManager
from enthought.pyface.tree.api import NodeType
from enthought.traits.api import Bool, Instance, Str, Type
from enthought.util.wx.drag_and_drop import clipboard

# FIXME: We should separate the action set declaration out of the
# plugin_definition file since we shouldn't be importing parts of the plugin
# definition here.
from enthought.envisage.action.action_plugin_definition import ActionSet


# Setup a logger for this module
logger = logging.getLogger(__name__)


00029 class ResourceNodeType(NodeType):
    """ The base class for all resource node types. """

    #### 'ResourceNodeType' interface #########################################

    # The context menu for nodes of this type.
    context_menu = Instance(MenuManager, ())

    # Should the system actions be added to the context menu.
    #
    # fixme: THis is here so that we can use system actions in VMS without
    # stomping on other projects. In the future this *should* be the default
    # behaviour (or at least some kind of system action framework needs to be
    # put in place).
    use_system_actions = Bool(False)

    # The resource type that we are the node type for.
    resource_type = Instance('enthought.envisage.resource.ResourceType')

    # If set, then we build our context menu by querying for actions, groups,
    # and menus, within ActionSets, which are configured for locations starting
    # with this path.  In that case, this value must be an exact match to
    # the begining of the fully-expanded location string -- it should NOT
    # include an alias.
    action_location_root = Str

    # The extension point we retrieve our action sets from.
    #
    # This is used only to filter the menu building algorithm to a smaller set
    # of possible actions to consider.  Overriding it is *NOT* required unless
    # you're app is suffering from performance issues when building context
    # menus.
    action_set_type = Type(ActionSet)


    ###########################################################################
    # 'NodeType' interface.
    ###########################################################################

    # Note that 'node' in this case is always a 'Binding' instance.
00069     def is_type_for(self, node):
        """ Returns True if a node is deemed to be of this type. """

        return self.resource_type.is_type_for(node.obj)

    #### Content ####

00076     def allows_children(self, node):
        """ Does the node allow children (ie. a folder vs a file). """

        return self._get_context_adapter(node) is not None

00081     def has_children(self, node):
        """ Returns True if a node has children, otherwise False. """

        context_adapter = self._get_context_adapter(node)
        if context_adapter is not None:
            has_children = len(context_adapter.list_names('')) > 0

        else:
            has_children = False

        return has_children

00093     def get_children(self, node):
        """ Returns the children of a node. """

        context_adapter = self._get_context_adapter(node)
        if context_adapter is not None:
            children = context_adapter.list_bindings('')

        else:
            children = []

        return children

00105     def get_monitor(self, node):
        """ Returns a monitor that detects changes to a node.

        Returns None by default, which indicates that the node is not
        monitored.

        """

        return None

    #### Cut/Copy/Delete/Paste ####

00117     def can_copy(self, node):
        """ Can a node be copied? """

        return True

00122     def can_cut(self, node):
        """ Can a node be cut? """

        return False

00127     def can_delete(self, node):
        """ Can a node be deleted? """

        return False

00132     def confirm_delete(self, node):
        """ Returns True if deletions must be confirmed. """

        return True

00137     def get_confirm_delete_message(self, node):
        """ Returns the message displayed when confirming deletion. """

        # The message shown when confirming the deletion of a test.
        message = 'Are you sure you want to delete %s?'

        return message % node.name

00145     def can_paste(self, node, data):
        """ Returns True iff an object can be pasted into a node. """

        return False

    def paste(self, node, data):
        rm_name = 'enthought.envisage.resource.IResourceManager'
        resource_manager = get_application().get_service(rm_name)
        # Note that paste actions can ONLY be performed on either context
        # objects or objects that have a context adapter.
        ctx = self._get_target_context(resource_manager, node)

        # Make sure that we are not trying to paste a folder onto itself
        # or a sub-folder onto one of its ancestors.
        if isinstance(data.obj, Context):
            if ctx.namespace_name == data.obj.namespace_name:
                raise ValueError(DST_SAME_AS_SRC)

            elif ctx.namespace_name.startswith(data.obj.namespace_name):
                raise ValueError(DST_SUB_OF_SRC)

        # Generate a unique name for the object being pasted (unique within
        # the context that it is being pasted into).
        name = ctx.get_unique_name(data.name)

        # Make sure that the value that we are pasting has the appropriate
        # name.
        #
        # fixme: What if the paste value is of a different resource type?!
        data_resource_type = resource_manager.get_type_of(data.obj)
        value = data_resource_type.node_type.get_paste_value(data)

        # fixme: this whole method is a confused mess. i hate to special
        #        case files, but the name setting and the copying need
        #        to be done in reverse order for them from everything
        #        else, or nothing updates correctly
        #from enthought.debug.fbi import fbi; fbi()

        if isinstance(value, File):
            # fixme: Hmmmmmmm!
            data_resource_type.copy(ctx, name, value)
            data_resource_type.set_name(value, name)
        else:
            data_resource_type.set_name(value, name)
            # fixme: Hmmmmmmm!
            data_resource_type.copy(ctx, name, value)

        return

00194     def get_copy_value(self, node):
        """ Get the value that is copied for a node.

        By default, returns the node itself.

        """

        return Binding(name=node.name, obj=self.resource_type.clone(node.obj))

00203     def get_paste_value(self, node):
        """ Get the value that is pasted for a node. """

        return self.resource_type.clone(node.obj)

    #### Drag and drop ####

00210     def get_drag_value(self, node):
        """ Get the value that is dragged for a node. """

        return node.obj

    #### Visualization ####

00217     def can_rename(self, node):
        """ Returns True if the node can be renamed, otherwise False. """

        # You can't rename the root context!
        return node.context is not None

00223     def get_text(self, node):
        """ Returns the label text for a node. """

        return node.name

00228     def can_set_text(self, node, text):
        """ Returns True if the node's label can be set. """

        # The parent context will NOT be None here (see 'is_editable').
        parent = node.context

        return len(text.strip()) > 0 and text not in parent.list_names('')

00236     def set_text(self, node, text):
        """ Sets the label text for a node. """

        # Update the resource.
        self.resource_type.set_name(node.obj, text)

        # Do the rename in the naming system.
        #
        # fixme: We need to sort out just who is responsible for updating the
        # context?  Should the resource type do it in 'set_name'?  The problem
        # is that many object resources have monitors to detect name changes
        # and so sometimes the monitor attempts the rename too, and the last
        # one in will fail as the rename has already taken place!  Confused?
        # You will be.
        if text not in node.context.list_names():
            node.context.rename(node.name, text)

        # Update the binding (the naming system knows nothing about this
        # binding, so its up to us to keep it up to date).
        node.name = text

        return

00259     def get_context_menu(self, node):
        """ Returns the context menu for the specified node. """

        # Get the template.
        menu = self.get_context_menu_template(node)

        # Initialize the enabled/disabled state.
        self._initialize_context_menu(menu, node)

        # Add any system actions.
        if self.use_system_actions:
            self._add_system_actions(node, menu)

        return menu

00274     def get_context_menu_template(self, node):
        """ Returns the context menu template for the specified node.

        The template has not be initialized for use with the window and can
        still be pickled.

        """

        # If an action location root is set, then build the menu from all
        # actions, groups, and menus having that root location across the
        # known action sets.
        if self.action_location_root is not None and \
               self.action_location_root != '':
            menu = self._get_action_set_context_menu(node)

        # Otherwise, build up a menu the 'old way'
        else:
            # fixme: I'm not sure the context menu should be part of the
            # resource manager.  Shouldn't we have a resource type that
            # represents all resources?
            resource_manager = self.resource_type.resource_manager

            # Start with the system template.
            menu = cPickle.loads(cPickle.dumps(resource_manager.context_menu))

            # If our associated resource type class has a parent resource type
            # with its own context menu definition, then we want to extend
            # that instead of the system template.
            #
            # NOTE: This only works if the resource type ID's are classnames!
            try:
                bases = self.resource_type.__class__.__bases__
                if len(bases) > 0:
                    parent_id = bases[0].__module__ + "." + bases[0].__name__
                    parent_resource = resource_manager.lookup(parent_id)
                    if parent_resource and parent_resource.node_type:
                        menu = parent_resource.node_type.get_context_menu(node)
            except Exception, e:
                logger.exception('Unexpected error finding parent context menu')

            # Add the type-specific menus.
            insertion_index = 1
            for group in self.context_menu.groups:
                # If this group exists in the target menu, we'll just append
                # our items to the existing group.
                existing_group = menu.find_group(group.id)
                if existing_group is not None:
                    for item in group.items[:]:
                        item = cPickle.loads( cPickle.dumps( item ))
                        existing_group.append(item)

                # Otherwise, create a copy of our group and insert it into
                # the existing menu.
                else:
                    copy = Group()
                    for item in group.items[:]:
                        try:
                            item = cPickle.loads( cPickle.dumps( item ))
                        except:
                            # If the menu items can't be copied, they also
                            # can't be modified based on the object state.
                            # That's a limitation that is ok for some.
                            pass
                        copy.append(item)
                    menu.insert(insertion_index, copy)

                insertion_index += 1

        return menu

    ###########################################################################
    # Protected 'ResourceNodeType' interface.
    ###########################################################################

00348     def _add_system_actions(self, node, menu):
        """ Adds any required system actions to the context menu. """

        menu.append(self._create_copy_group(node))
        menu.append(self._create_delete_group(node))
        menu.append(self._create_rename_group(node))

        return

00357     def _create_copy_group(self, node):
        """ Creates the copy group. """

        group = Group(
            self._create_copy_action(node),
            self._create_paste_action(node),
            id='CopyGroup'
        )

        return group

00368     def _create_delete_group(self, node):
        """ Creates the delete group. """

        group = Group(
            self._create_delete_action(node),
            id='DeleteGroup'
        )

        return group

00378     def _create_rename_group(self, node):
        """ Creates the rename group. """

        group = Group(
            self._create_rename_action(node),
            id='RenameGroup'
        )

        return group

00388     def _create_copy_action(self, node):
        """ Creates the copy action. """

        def copy():
            """ Copies a node. """

            # fixme: The clipboard is a mess! We need a proper API!
            clipboard.source = None
            clipboard.data   = self.get_copy_value(node)
            clipboard.node   = [node]

            return

        enabled = self.can_copy(node)
        return Action(name='Copy', on_perform=copy, enabled=enabled)

00404     def _create_paste_action(self, node):
        """ Creates the paste action. """

        def paste():
            """ Pastes a node. """

            self.paste(node, clipboard.data)

            return

        enabled = self.can_paste(node, clipboard.data)
        return Action(name='Paste', on_perform=paste, enabled=enabled)

00417     def _create_delete_action(self, node):
        """ Creates the delete action. """

        # Bind the name 'node_type' here for use in the perform method of the
        # 'DeleteAction' class.
        node_type = self

        class DeleteAction(Action):
            """ The delete action. """

            name = 'Delete'

            def perform(self, event):
                """ Performs the action. """

                if node_type.confirm_delete(node):
                    message = node_type.get_confirm_delete_message(node)

                    parent = event.window.control
                    if confirm(parent, message, 'Confirm Deletion') == YES:
                        node_type.delete(node)

                else:
                    node_type.delete(node)

                return

        return DeleteAction(enabled=self.can_delete(node))

00446     def _create_rename_action(self, node):
        """ Creates the rename action. """

        # fixme: Circular import issues if we import this at the top of the
        # module!
        from enthought.envisage.single_project.action.rename_action \
             import RenameAction

        return RenameAction(enabled=self.can_rename(node))

    ###########################################################################
    # Private interface.
    ###########################################################################

00460     def _get_action_set_context_menu(self, node):
        """ Returns a context menu for this node based on the actions, groups,
            and menus having our root location.
        """
        logger.debug('Building context menu using location root [%s]',
            self.action_location_root)

        menu_manager = MenuManager()
        try:
            action_sets = self._get_action_sets()
            action_set_manager = ActionSetManager(action_sets=action_sets)
            menu_builder = DefaultMenuBuilder(application=get_application())
            menu_builder.initialize_menu_manager(
                menu_manager,
                action_set_manager,
                self.action_location_root
                )
        except:
            logger.exception('Unable to build context menu')

        return menu_manager

00482     def _get_action_sets(self):
        """
        Return the action sets used to build our menu(s).

        This is basically a filtering mechanism when compared to retrieving
        all known action sets from Envisage.  Unless you're seeing performance
        issues when building menu(s) due to too many actions being parsed,
        there probably is no need to do this filtering.

        Provided as a separate method to allow implementors to customize
        the way action sets are discovered (say if the node type isn't being
        used in Envisage.)

        """

        action_sets = get_application().load_extensions(self.action_set_type)
        logger.debug('Found ActionSets [%s] for ResourceNodeType [%s]',
            action_sets, self)

        return action_sets

00503     def _get_context_adapter(self, node):
        """ Returns a context adapter for a node.
        Returns None if no such adapter is found.

        """

        # fixme: Hmmmm, this is only used when we don't create a resource tree
        # using a real initial context (ie. the application tree).
        #
        # This is the correct usage...
        if node.context is not None:
            environment = node.context.environment

        # ... and this is the hack to get around not using an initial context!
        else:
            from enthought.naming.context import ENVIRONMENT
            environment = ENVIRONMENT

        type_manager = self.resource_type.resource_manager.type_manager

        context_adapter = type_manager.object_as(
            node.obj, Context, environment=environment, context=node.context
        )

        return context_adapter

00529     def _get_target_context(self, resource_manager, node):
        """ Returns the target context for the paste action. """

        # Note that paste actions can ONLY be performed on either context
        # objects or objects that have a context adapter.
        if isinstance(node.obj, Context):
            environment = node.obj.environment
            parent      = node.obj

        else:
            environment = node.context.environment
            parent      = node.context

        ctx = resource_manager.type_manager.object_as(
            node.obj, Context, environment, parent
        )
        if ctx is None:
            raise ValueError('Can only paste into contexts')

        return ctx

00550     def _initialize_action_item(self, node, item, window):
        """ Initializes the specified node's specified action item before
            it is displayed in a context menu.

            Implemented this way to allow implementors to easily override what
            gets set on action items during initialization.
        """
        item.action.window = window

00559     def _initialize_context_menu(self, menu, node=None):
        """ Set the enabled/disabled state of actions in a context menu. """
        # Determine the current window
        # FIXME: This package shouldn't require any UI plugins!
        window = None
        try:
            # Try to find it from the UIPlugin
            from enthought.envisage.ui import UIPlugin
            window = UIPlugin.instance.active_window
        except:
            # Try the workbench plugin
            from enthought.envisage.workbench import IWORKBENCH
            workbench = get_application().get_service(IWORKBENCH)
            window = workbench.active_window

        for group in menu.groups:
            for item in group.items:
                if isinstance(item, ActionItem):
                    self._initialize_action_item(node, item, window)

                    # fixme: this is extra gorpy!
                    if hasattr(item.action, 'refresh'):
                        item.action.refresh()

                elif isinstance(item, MenuManager):
                    self._initialize_context_menu(item, node)

        return

#### EOF ######################################################################

Generated by  Doxygen 1.6.0   Back to index