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

window.py

00001 """ A top-level workbench window. """


# Standard library imports.
import cPickle
import logging
from os.path import exists, join

# Enthought library imports.
from enthought.envisage.api import Application
from enthought.envisage.resource.api import ResourceManager
from enthought.pyface.api import ApplicationWindow
from enthought.traits.api import Constant, Event, Instance, List, Str, Tuple
from enthought.traits.api import Vetoable

# Local imports.
from editor import Editor
from perspective import Perspective
from view import View
from window_layout import WindowLayout


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


00027 class Window(ApplicationWindow):
    """ A top-level workbench window. """

    #### 'Window' interface ###################################################

    # The Envisage application that the window is part of.
    #
    # fixme: This is Envisage specific should be factored out into a derived
    # class. It is not used in this module, it exists solely to allow views,
    # editors and actions etc. to find the application that they are part of.
    # class.
    application = Instance(Application)

    # The Id of the editor area.
    editor_area_id = Constant('enthought.envisage.workbench.editors')

    # The (initial) size of the editor area (the user is free to resize it of
    # course).
    editor_area_size = Tuple((100, 100))

    # The resource manager used by the window. The resource manager is used
    # to find editors etc.
    #
    # fixme: Should we replace this with an editor factory? Then this class
    # won't be dependent on Envisage...
    resource_manager = Instance(ResourceManager, ())

    # The current selection within the window.
    selection = List

    # A directory on the local file system that we can read and write to at
    # will. This is used to persist layout information etc.
    state_location = Str

    # The window layout is responsible for creating and managing the internal
    # structure of the window (i.e., it knows how to add and remove views and
    # editors etc).
    window_layout = Instance(WindowLayout)

    # The workbench that created the window.
    #
    # fixme: This is Envisage specific should be factored out into a derived
    # class. It is not used in this module, it exists solely to allow views,
    # editors and actions etc. to find the workbench that they are part of.
    workbench = Instance('enthought.envisage.workbench.workbench.Workbench')

    # A suffix to be applied to the window title.
    #
    # fixme: This is horrible! In a plugin world you can't have people adding
    # suffixes to the window title - it just don't scale, errr, at all! This
    # needs to be removed - if you want it in your app then put it in there!
    title_suffix = Str

    #### Editors #######################

    # The visible (open) editors.
    editors = List(Editor)

    # The active editor.
    #
    # This is defined as the most recent editor to receive focus, or None if no
    # editors are open or the previous active editor was closed in a manner
    # that did not cause focus to change to another editor.
    active_editor = Instance(Editor)

    # Fired when an editor is about to be opened (or restored).
    editor_opening = Event(Editor)

    # Fired when an editor has been opened (or restored).
    editor_opened = Event(Editor)

    # Fired when an editor is about to be closed.
    editor_closing = Event(Editor)

    # Fired when an editor has been closed.
    editor_closed = Event(Editor)

    #### Views #########################

    # The available views (note that this is *all* of the views, not just those
    # currently visible).
    views = List(View)

    # The active view.
    #
    # This is defined as the most recent view to receive focus, or None if no
    # views are open or the previous active view was closed in a manner that
    # did not cause focus to change to another view.
    active_view = Instance(View)

    #### Perspectives ##################

    # The available perspectives. If no perspectives are specified then the
    # default perspective (an instance of 'Perspective' is used).
    perspectives = List(Perspective)

    # The active perspective.
    active_perspective = Instance(Perspective)

    # The Id of the default perspective.
    #
    # There are two situations in which this is used:
    #
    # 1. When the window is being created from scratch (i.e., not restored).
    #
    # If this is the empty string, then the first perspective in the list of
    # perspectives is shown (if there are no perspectives then an instance of
    # the default 'Perspective' class is used).
    #
    # If this is not the empty string then the perspective with this Id is
    # shown.
    #
    # 2. When the window is being restored.
    #
    # If this is the empty string then the last perspective that was visible
    # when the window last closed is shown.
    #
    # If this is not the empty string then the perspective with this Id is
    # shown.
    default_perspective_id = Str

    ###########################################################################
    # 'Window' interface.
    ###########################################################################

00152     def open(self):
        """ Opens the window.

        Overridden to make the 'opening' event vetoable.

        Returns True if the window opened successfully; False if the open event
        was vetoed.

        """

        logger.debug('opening workbench window [%s]', self)

        # Trait notification.
        self.opening = event = Vetoable()

        # fixme: Hack to mimic vetoable events!
        if not event.veto:
            if self.control is None:
                self._create()

            self.control.Show(True)

            # Trait notification.
            self.opened = self

            logger.debug('opened workbench window [%s]', self)

        else:
            logger.debug('opening of workbench window [%s] vetoed', self)

        # fixme: This is not actually part of the Pyface 'Window' API (but
        # maybe it should be). We return this to indicate whether the window
        # actually opened.
        return self.control is not None

00187     def close(self):
        """ Closes the window.

        Overridden to make the 'closing' event vetoable.

        Returns True if the window closed successfully (or was not even
        open!); False if the close event was vetoed.

        """

        logger.debug('closing workbench window [%s]', self)

        if self.control is not None:
            # Trait notification.
            self.closing = event = Vetoable()

            # fixme: Hack to mimic vetoable events!
            if not event.veto:
                # Give views and editors a chance to cleanup after themselves.
                for view in self.views:
                    if view.control is not None:
                        view.destroy_control()

                for editor in self.editors:
                    if editor.control is not None:
                        self.window_layout.close_editor(editor)

                # Cleanup the toolkit-specific control.
                self.control.Destroy()

                # Cleanup the window layout (event handlers, etc.)
                self.window_layout.close()

                # Cleanup our reference to the control so that we can (at least
                # in theory!) be opened again.
                self.control = None

                # Trait notification.
                self.closed = self

                logger.debug('closed workbench window [%s]', self)

            else:
                logger.debug('closing of workbench window [%s] vetoed', self)

        else:
            logger.debug('workbench window [%s] is not open', self)

        # fixme: This is not actually part of the Pyface 'Window' API (but
        # maybe it should be). We return this to indicate whether the window
        # actually closed.
        return self.control is None

    ###########################################################################
    # Protected 'Window' interface.
    ###########################################################################

00244     def _create_contents(self, parent):
        """ Creates the window contents. """

        # Create the initial window layout.
        self.window_layout.create_initial_layout()

        # Save the initial window layout so that we can reset it when changing
        # to a perspective that has not been seen yet.
        self._initial_layout = self.window_layout.get_view_memento()

        # When the application starts up we try to make it look just like it
        # did the last time it was closed down (i.e., the layout of views and
        # editors etc).
        #
        # If we have a saved layout then try to restore it (it may not be
        # totally possible, e.g. an editor may have been open for a resource
        # that has been deleted outside of the application, but we try to do
        # the best we can!).
        filename = join(self.state_location, 'active_perspective_id')
        if exists(filename):
            self.restore_layout()

        # Otherwise, we have no saved layout so let's create one.
        else:
            self._create_layout()

        return

    ###########################################################################
    # 'Window' interface.
    ###########################################################################

    #### Initializers #########################################################

00278     def _window_layout_default(self):
        """ Trait initializer. """

        window_layout = WindowLayout(window=self)

        # fixme: Is there a more 'traitsy' way to do this?
        def propogate(obj, trait_name):
            def handler(event):
                setattr(self, trait_name, event)
                return

            obj.on_trait_change(handler, trait_name)

            return

        propogate(window_layout, 'editor_opening')
        propogate(window_layout, 'editor_opened')
        propogate(window_layout, 'editor_closing')
        propogate(window_layout, 'editor_closed')

        return window_layout

    #### Methods ##############################################################

00302     def activate_editor(self, editor):
        """ Activates an editor. """

        self.window_layout.activate_editor(editor)

        return

00309     def activate_view(self, view):
        """ Activates a view. """

        self.window_layout.activate_view(view)

        return

00316     def add_editor(self, editor, title=None):
        """ Adds an editor.

        If no title is specified, the editor's name is used.

        """

        if title is None:
            title = editor.name

        self.window_layout.add_editor(editor, title)
        self.editors.append(editor)

        return

00331     def add_view(self, view, position, relative_to=None, size=(-1, -1)):
        """ Adds a view. """

        self.window_layout.add_view(view, position, relative_to, size)

        return

00338     def close_editor(self, editor):
        """ Closes an editor. """

        self.window_layout.close_editor(editor)

        return

00345     def close_view(self, view):
        """ Closes a view.

        fixme: Currently views are never 'closed' in the same sense as an
        editor is closed. Views are merely hidden.

        """

        self.hide_view(view)

        return

00357     def create_editor(self, resource):
        """ Creates an editor for a resource.

        Returns None if no editor can be created for the resource.

        """

        # Determine the type of the resource.
        resource_type = self.resource_manager.get_type_of(resource)

        # If no resource type is found then, errrr, what?!?
        if resource_type is not None:
            # Ask the resource type to create an editor for us.
            #
            # fixme: We currently pass the window in to this call as we have
            # the usual race condition in the setting of traits... In this case
            # the 'TextEditor' class needs the window defined before we set the
            # resource! Aaaaggghhhh! The 'ProjectEditor' in VMS also relies on
            # the fact that the resource is set in the contructor, so we
            # can't just create the editor by passing in no traits and then
            # set them in the order that we would like!
            editor = resource_type.create_editor(resource, window=self)
            if editor is not None:
                # fixme: This is redundant at the moment - see the above
                # comment. I've left it in so that it is a bit easier to find
                # just where the editor's window is set, and hopefully if
                # we can sort out how to create and initialize editors more
                # cleanly then this is the line that should stay!
                editor.window = self

            else:
                logger.warn('no editor for resource [%s]' % str(resource))

        else:
            logger.warn('no resource type for resource [%s]' % str(resource))
            editor = None

        return editor

00396     def edit(self, resource, use_existing=True):
        """ Edits a resource.

        If 'use_existing' is True and the resource is already being edited in
        the window then the existing editor will be activated (i.e., given
        focus, brought to the front, etc.).

        If 'use_existing' is False, then a new editor will be created even if
        one already exists.

        """

        if use_existing:
            # Is the resource already being edited in the window?
            editor = self.get_editor_by_resource(resource)

            # If not then create an editor for it.
            if editor is None:
                editor = self._create_editor(resource)

            # Otherwise, activate the existing editor (i.e., bring it to the
            # front, give it the focus etc).
            else:
                self.activate_editor(editor)

        else:
            editor = self._create_editor(resource)

        return editor

00426     def get_editor_by_id(self, id):
        """ Returns the editor with the specified Id.

        Returns None if no such editor exists.

        """

        for editor in self.editors:
            if editor.id == id:
                break

        else:
            editor = None

        return editor

    # fixme: This is a better way to do this, but it doesn't work for VMS cad
    # editors - find out why!
##     def get_editor_by_resource(self, resource):
##         """ Returns the editor that is editing a resource.

##         Returns None if no such editor exists.

##         """

##         resource_type = self.resource_manager.get_type_of(resource)
##         if resource_type is not None:
##             editor = self.get_editor_by_id(resource_type.get_id(resource))

##         else:
##             editor = None

##         return editor

00460     def get_editor_by_resource(self, resource):
        """ Returns the editor that is editing a resource.

        Returns None if no such editor exists.

        """

        for editor in self.editors:
            if self._is_editing(editor, resource):
                break

        else:
            editor = None

        return editor

00476     def get_perspective_by_id(self, id):
        """ Returns the perspective with the specified Id.

        Returns None if no such perspective exists.

        """

        for perspective in self.perspectives:
            if perspective.id == id:
                break

        else:
            if id == Perspective.DEFAULT_ID:
                perspective = Perspective()

            else:
                perspective = None

        return perspective

00496     def remove_perspective_by_id(self, id):
        """ Removes the perspective with the specified Id.
        """
        perspectives = self.perspectives
        for index, perspective in enumerate(perspectives):
            if perspective.id == id:
                del perspectives[index]
                if perspective is self.active_perspective:
                    if len(perspectives) > 0:
                        self.active_perspective = perspectives[0]
                    else:
                        self.active_perspective = None

                # Also delete the perspective from the UserPerspective object:
                self.workbench.user_perspective.remove( id )

                return True

        return False

00516     def get_view_by_id(self, id):
        """ Returns the view with the specified Id.

        Returns None if no such view exists.

        """

        for view in self.views:
            if view.id == id:
                break

        else:
            view = None

        return view

00532     def hide_editor_area(self):
        """ Hides the editor area. """

        self.window_layout.hide_editor_area()

        return

00539     def hide_view(self, view):
        """ Hides a view. """

        self.window_layout.hide_view(view)

        return

00546     def refresh(self):
        """ Refreshes the window to reflect any changes. """

        self.window_layout.refresh()

        return

00553     def reset_editors(self):
        """ Activates the first editor in every tab. """

        self.window_layout.reset_editors()

        return

00560     def reset_views(self):
        """ Activates the first view in every tab. """

        self.window_layout.reset_views()

        return

00567     def show_editor_area(self):
        """ Shows the editor area. """

        self.window_layout.show_editor_area()

        return

00574     def show_view(self, view):
        """ Shows a view. """

        self.window_layout.show_view(view)

        return

    #### Methods for saving and restoring the layout ##########################

    # fixme: This is called from workbench on exit to persist the window
    # layout.
    #
    # fixme: Can we replace these with memento methods and let the workbench
    # decide how to persist the memento?!?
00588     def save_layout(self):
        """ Saves the window layout. """

        # Save the Id of the active perspective.
        f = file(join(self.state_location, 'active_perspective_id'), 'w')
        f.write(self.active_perspective.id)
        f.close()

        # Save the layout of the view area.
        memento = self.window_layout.get_view_memento()

        f = file(join(self.state_location, self.active_perspective.id), 'w')
        cPickle.dump(memento, f)
        f.close()

        # Save the layout of the editor area.
        memento = self.window_layout.get_editor_memento()

        f = file(join(self.state_location, 'editors'), 'w')
        cPickle.dump(memento, f)
        f.close()

        return

    # fixme: This is only ever called internally!
00613     def restore_layout(self):
        """ Restores the window layout. """

        # We prefer the default perspective, if one is configured, over any
        # other perspective as our initial layout.
        perspective = None
        if len(self.default_perspective_id) > 0:
            id = self.default_perspective_id
            perspective = self.get_perspective_by_id(id)
            if perspective is None:
                logger.warn(
                    'Default perspective [%s] is no longer available', id
                )

        # If there was no default perspective, then we prefer to use the
        # last used perspective as our initial layout.
        if perspective is None:
            f = None
            try:
                f = file(join(self.state_location, 'active_perspective_id'),
                    'r')
                id = f.read()
                perspective = self.get_perspective_by_id(id)
                if perspective is None:
                    logger.warn(
                        'Last used perspective [%s] is no longer available', id
                    )

            except:
                if f:
                    f.close()

        # If there was no last used perspective, then try the first perspective
        # we know about as our initial layout.
        if perspective is None:
            if len(self.perspectives) > 0:
                perspective = self.perspectives[0]

        # If we have no known perspectives, make a new blank one up.
        if perspective is None:
            logger.warn('No known perspectives.  Creating a new one.')
            perspective = Perspective()

        # The layout of the view area is restored in the 'active_perspective'
        # trait change handler!
        self.active_perspective = perspective

        # Restore the layout of the editor area.
        filename = join(self.state_location, 'editors')
        if exists(filename):
            f = file(filename)
            memento = cPickle.load(f)
            f.close()

            self.window_layout.set_editor_memento(memento)

        return

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

00675     def _create_layout(self):
        """ Creates the initial window layout. """

        if len(self.perspectives) > 0:
            if len(self.default_perspective_id) > 0:
                perspective = self.get_perspective_by_id(
                    self.default_perspective_id
                )

            else:
                perspective = self.perspectives[0]

        else:
            perspective = Perspective()

        # All the work is done in the 'active_perspective' trait change
        # handler!
        self.active_perspective = perspective

        return

00696     def _create_editor(self, resource):
        """ Creates an editor for a resource and adds it to the window. """

        editor = self.create_editor(resource)
        if editor is not None:
            self.add_editor(editor)
            self.activate_editor(editor)

        else:
            logger.warn('no editor for resource [%s]', resource)

        return editor

00709     def _is_editing(self, editor, resource):
        """ Returns True if the editor is editing the resource. """

        resource_type_x = self.resource_manager.get_type_of(editor.resource)
        resource_type_y = self.resource_manager.get_type_of(resource)

        if resource_type_x is not None and resource_type_x is resource_type_y:
            id_x = resource_type_x.get_id(editor.resource)
            id_y = resource_type_y.get_id(resource)

            is_editing = id_x == id_y

        else:
            is_editing = False

        return is_editing

00726     def _add_view_listeners(self, view):
        """ Adds any required listeners to a view. """

        view.window = self

        # Update our selection when the selection of any contained view
        # changes.
        #
        # fixme: Not sure this is really what we want to do but it is what
        # we did in the old UI plugin and the selection listener still does
        # only listen to the window's selection.
        view.on_trait_change(self._on_view_selection_changed, 'selection')

        return

00741     def _remove_view_listeners(self, view):
        """ Removes any required listeners from a view. """

        view.window = None

        # Remove the selection listener.
        view.on_trait_change(
            self._on_view_selection_changed, 'selection', remove=True
        )

        return

    #### Trait change handlers ################################################

    #### Static ####

00757     def _active_perspective_changed(self, old, new):
        """ Static trait change handler. """

        logger.debug('perspective changed from [%s] to [%s]', old, new)

        # Hide the old perspective...
        if old is not None:
            # fixme: This is a bit ugly but... when we restore the layout we
            # ignore the default view visibility.
            for view in self.views:
                view.visible = False

            # Save the current layout of the perspective.
            memento = self.window_layout.get_view_memento()

            f = file(join(self.state_location, old.id), 'w')
            cPickle.dump(memento, f)
            f.close()

        # ... and show the new one.
        if new is not None:
            # Save the Id of the active perspective.
            f = file(join(self.state_location, 'active_perspective_id'), 'w')
            f.write(new.id)
            f.close()

            # If the perspective has been seen before then restore it.
            filename = join(self.state_location, new.id)
            if exists(filename):
                # Load the window layout from the specified file.
                f = file(filename)
                memento = cPickle.load(f)
                f.close()

                self.window_layout.set_view_memento(memento)

            # Otherwise, this is the first time the perspective has been seen
            # so create it.
            else:
                if old is not None:
                    # Reset the window layout to its initial state.
                    self.window_layout.set_view_memento(self._initial_layout)

                # Create the perspective in the window.
                new.create(self)

                # Show the editor area?
                if new.show_editor_area:
                    self.show_editor_area()

                else:
                    self.hide_editor_area()

            # This forces the dock window to update its layout.
            if old is not None:
                self.refresh()

        return

    # fixme: This is horrible! In a plugin world you can't have people adding
    # suffixes to the window title - it just don't scale, errr, at all! This
    # needs to be removed - if you want it in your app then put it in there!
00819     def _title_suffix_changed(self, old, new):
        """ Static trait change handler. """

        logger.debug('title suffix changed from [%s] to [%s]' % (old, new))

        title = self.title
        if old and len(old) > 0:
            from string import rfind
            index = rfind(self.title, ' ' + old)
            if index > -1:
                title = self.title[0:index]
        self.title = title + ' ' + new

        return

00834     def _views_changed(self, old, new):
        """ Static trait change handler. """

        # Cleanup any old views.
        for view in old:
            self._remove_view_listeners(view)

        # Initialize any new views.
        for view in new:
            self._add_view_listeners(view)

        return

00847     def _views_items_changed(self, event):
        """ Static trait change handler. """

        # Cleanup any old views.
        for view in event.removed:
            self._remove_view_listeners(view)

        # Initialize any new views.
        for view in event.added:
            self._add_view_listeners(view)

        return

    #### Dynamic ####

00862     def _on_editor_activated(self, editor):
        """ Dynamic trait change handler. """

        self.active_editor = editor

        return

00869     def _on_view_selection_changed(self, obj, trait_name, old, new):
        """ Dynamic trait change handler. """

        logger.debug('new workbench window selection [%s]', new)
        self.selection = new

        return

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

Generated by  Doxygen 1.6.0   Back to index