Source code for pyface.tasks.task_window
# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
import logging
from traits.api import (
Bool,
Callable,
HasStrictTraits,
Instance,
List,
Property,
Str,
Vetoable,
observe,
)
from pyface.action.i_menu_bar_manager import IMenuBarManager
from pyface.action.i_status_bar_manager import IStatusBarManager
from pyface.action.i_tool_bar_manager import IToolBarManager
from pyface.application_window import ApplicationWindow
from pyface.tasks.action.task_action_manager_builder import (
TaskActionManagerBuilder,
)
from pyface.tasks.i_dock_pane import IDockPane
from pyface.tasks.i_task_pane import ITaskPane
from pyface.tasks.i_task_window_backend import ITaskWindowBackend
from pyface.tasks.task import Task, TaskLayout
from pyface.tasks.task_window_layout import TaskWindowLayout
# Logging.
logger = logging.getLogger(__name__)
[docs]class TaskWindow(ApplicationWindow):
""" The the top-level window to which tasks can be assigned.
A TaskWindow is responsible for creating and the managing the controls of
its tasks.
"""
# IWindow interface ----------------------------------------------------
#: Unless a title is specifically assigned, delegate to the active task.
title = Property(Str, observe=["active_task.name", "_title"])
# TaskWindow interface ------------------------------------------------
#: The pane (central or dock) in the active task that currently has focus.
active_pane = Instance(ITaskPane)
#: The active task for this window.
active_task = Instance(Task)
#: The list of all tasks currently attached to this window. All panes of
#: the inactive tasks are hidden.
tasks = List(Task)
#: The central pane of the active task, which is always visible.
central_pane = Instance(ITaskPane)
#: The list of all dock panes in the active task, which may or may not be
#: visible.
dock_panes = List(IDockPane)
#: The factory for the window's TaskActionManagerBuilder, which is
#: instantiated to translate menu and tool bar schemas into Pyface action
#: managers. This attribute can overridden to introduce custom logic into
#: the translation process, although this is not usually necessary.
action_manager_builder_factory = Callable(TaskActionManagerBuilder)
# Protected traits -----------------------------------------------------
_active_state = Instance("pyface.tasks.task_window.TaskState")
_states = List(Instance("pyface.tasks.task_window.TaskState"))
_title = Str()
_window_backend = Instance(ITaskWindowBackend)
# ------------------------------------------------------------------------
# 'Widget' interface.
# ------------------------------------------------------------------------
[docs] def destroy(self):
""" Overridden to ensure that all task panes are cleanly destroyed.
"""
if self.control is not None:
# Allow the TaskWindowBackend to clean up first.
self._window_backend.destroy()
# Don't use 'remove_task' here to avoid changing the active state and
# thereby removing the window's menus and toolbars. This can lead to
# undesirable animations when the window is being closed.
for state in self._states:
self._destroy_state(state)
super().destroy()
# ------------------------------------------------------------------------
# 'Window' interface.
# ------------------------------------------------------------------------
[docs] def open(self):
""" Opens the window.
Overridden to make the 'opening' event vetoable and to activate a task
if one has not already been activated. Returns whether the window was
opened.
"""
self.opening = event = Vetoable()
if not event.veto:
# Create the control, if necessary.
if self.control is None:
self.create()
# Activate a task, if necessary.
if self._active_state is None and self._states:
self.activate_task(self._states[0].task)
self.show(True)
self.opened = self
return self.control is not None and not event.veto
# ------------------------------------------------------------------------
# Protected 'IApplicationWindow' interface.
# ------------------------------------------------------------------------
def _create_contents(self, parent):
""" Delegate to the TaskWindowBackend.
"""
return self._window_backend.create_contents(parent)
# ------------------------------------------------------------------------
# 'TaskWindow' interface.
# ------------------------------------------------------------------------
[docs] def activate_task(self, task):
""" Activates a task that has already been added to the window.
"""
state = self._get_state(task)
if state and state != self._active_state:
# Hide the panes of the currently active task, if necessary.
if self._active_state is not None:
self._window_backend.hide_task(self._active_state)
# Initialize the new task, if necessary.
if not state.initialized:
task.initialized()
state.initialized = True
# Display the panes of the new task.
self._window_backend.show_task(state)
# Activate the new task. The menus, toolbars, and status bar will be
# replaced at this time.
self._active_state = state
task.activated()
elif not state:
logger.warning(
"Cannot activate task %r: task does not belong to the "
"window." % task
)
[docs] def add_task(self, task):
""" Adds a task to the window. The task is not activated.
"""
if task.window is not None:
logger.error(
"Cannot add task %r: task has already been added "
"to a window!" % task
)
return
task.window = self
state = TaskState(task=task, layout=task.default_layout)
self._states.append(state)
# Make sure the underlying control has been created, even if it is not
# yet visible.
if self.control is None:
self.create()
# Create the central pane.
state.central_pane = task.create_central_pane()
state.central_pane.task = task
state.central_pane.create(self.control)
# Create the dock panes.
state.dock_panes = task.create_dock_panes()
for dock_pane_factory in task.extra_dock_pane_factories:
state.dock_panes.append(dock_pane_factory(task=task))
for dock_pane in state.dock_panes:
dock_pane.task = task
dock_pane.create(self.control)
# Build the menu and tool bars.
builder = self.action_manager_builder_factory(task=task)
state.menu_bar_manager = builder.create_menu_bar_manager()
state.status_bar_manager = task.status_bar
state.tool_bar_managers = builder.create_tool_bar_managers()
[docs] def remove_task(self, task):
""" Removes a task that has already been added to the window. All the
task's panes are destroyed.
"""
state = self._get_state(task)
if state:
# If the task is active, make sure it is de-activated before
# deleting its controls.
if self._active_state == state:
self._window_backend.hide_task(state)
self._active_state = None
self._destroy_state(state)
self._states.remove(state)
else:
logger.warning(
"Cannot remove task %r: task does not belong to the "
"window." % task
)
[docs] def focus_next_pane(self):
""" Shifts focus to the "next" pane, taking into account the active pane
and the pane geometry.
"""
if self._active_state:
panes = self._get_pane_ring()
index = 0
if self.active_pane:
index = (panes.index(self.active_pane) + 1) % len(panes)
panes[index].set_focus()
[docs] def focus_previous_pane(self):
""" Shifts focus to the "previous" pane, taking into account the active
pane and the pane geometry.
"""
if self._active_state:
panes = self._get_pane_ring()
index = -1
if self.active_pane:
index = panes.index(self.active_pane) - 1
panes[index].set_focus()
[docs] def get_central_pane(self, task):
""" Returns the central pane for the specified task.
"""
state = self._get_state(task)
return state.central_pane if state else None
[docs] def get_dock_pane(self, id, task=None):
""" Returns the dock pane in the task with the specified ID, or
None if no such dock pane exists. If a task is not specified, the
active task is used.
"""
if task is None:
state = self._active_state
else:
state = self._get_state(task)
return state.get_dock_pane(id) if state else None
[docs] def get_dock_panes(self, task):
""" Returns the dock panes for the specified task.
"""
state = self._get_state(task)
return state.dock_panes[:] if state else []
[docs] def get_task(self, id):
""" Returns the task with the specified ID, or None if no such task
exists.
"""
state = self._get_state(id)
return state.task if state else None
# Methods for saving and restoring the layout -------------------------#
[docs] def get_layout(self):
""" Returns a TaskLayout (for the active task) that reflects the state
of the window.
"""
if self._active_state:
return self._window_backend.get_layout()
return None
[docs] def set_layout(self, layout):
""" Applies a TaskLayout (which should be suitable for the active task)
to the window.
"""
if self._active_state:
self._window_backend.set_layout(layout)
[docs] def reset_layout(self):
""" Restores the active task's default TaskLayout.
"""
if self.active_task:
self.set_layout(self.active_task.default_layout)
[docs] def get_window_layout(self):
""" Returns a TaskWindowLayout for the current state of the window.
"""
result = TaskWindowLayout(
position=self.position, size=self.size, size_state=self.size_state
)
for state in self._states:
if state == self._active_state:
result.active_task = state.task.id
layout = self._window_backend.get_layout()
else:
layout = state.layout.clone_traits()
layout.id = state.task.id
result.items.append(layout)
return result
[docs] def set_window_layout(self, window_layout):
""" Applies a TaskWindowLayout to the window.
"""
# Set window size before laying it out.
self.position = window_layout.position
self.size = window_layout.size
self.size_state = window_layout.size_state
# Store layouts for the tasks, including the active task.
for layout in window_layout.items:
if isinstance(layout, str):
continue
state = self._get_state(layout.id)
if state:
state.layout = layout
else:
logger.warning(
"Cannot apply layout for task %r: task does not "
"belong to the window." % layout.id
)
# Attempt to activate the requested task.
state = self._get_state(window_layout.get_active_task())
if state:
# If the requested active task is already active, calling
# ``activate`` is a no-op, so we must force a re-layout.
if state == self._active_state:
self._window_backend.set_layout(state.layout)
else:
self.activate_task(state.task)
# ------------------------------------------------------------------------
# Protected 'TaskWindow' interface.
# ------------------------------------------------------------------------
def _destroy_state(self, state):
""" Destroy all controls associated with a Task state.
"""
# Notify the task that it is about to be destroyed.
state.task.prepare_destroy()
# Destroy action managers associated with the task, unless the task is
# active, in which case this will be handled by our superclass.
if state != self._active_state:
if state.menu_bar_manager:
state.menu_bar_manager.destroy()
for tool_bar_manager in state.tool_bar_managers:
tool_bar_manager.destroy()
# Destroy all controls associated with the task.
for dock_pane in state.dock_panes:
dock_pane.destroy()
state.central_pane.destroy()
state.task.window = None
def _get_pane_ring(self):
""" Returns a list of visible panes ordered for focus switching.
"""
# Proceed clockwise through the dock areas.
# TODO: Also take into account ordering within dock areas.
panes = []
if self._active_state:
layout = self.get_layout()
panes.append(self.central_pane)
for area in ("top", "right", "bottom", "left"):
item = getattr(layout, area)
if item:
panes.extend(
[
self.get_dock_pane(pane_item.id)
for pane_item in item.iterleaves()
]
)
return panes
def _get_state(self, id_or_task):
""" Returns the TaskState that contains the specified Task, or None if
no such state exists.
"""
for state in self._states:
if state.task == id_or_task or state.task.id == id_or_task:
return state
return None
# Trait initializers ---------------------------------------------------
def __window_backend_default(self):
from pyface.tasks.task_window_backend import TaskWindowBackend
return TaskWindowBackend(window=self)
# Trait property getters/setters ---------------------------------------
def _get_title(self):
if self._title or self.active_task is None:
return self._title
return self.active_task.name
def _set_title(self, title):
self._title = title
# Trait change handlers ------------------------------------------------
@observe("_active_state")
def _update_traits_given_new_active_state(self, event):
state = event.new
if state is None:
self.active_task = self.central_pane = None
self.dock_panes = []
self.menu_bar_manager = self.status_bar_manager = None
self.tool_bar_managers = []
else:
self.active_task = state.task
self.central_pane = state.central_pane
self.dock_panes = state.dock_panes
self.menu_bar_manager = state.menu_bar_manager
self.status_bar_manager = state.status_bar_manager
self.tool_bar_managers = state.tool_bar_managers
@observe("central_pane:has_focus, dock_panes:items:has_focus")
def _focus_updated(self, event):
if event.new:
self.active_pane = event.object
@observe("_states.items")
def _states_updated(self, event):
self.tasks = [state.task for state in self._states]
[docs]class TaskState(HasStrictTraits):
""" An object used internally by TaskWindow to maintain the state associated
with an attached Task.
"""
#: The Task that the state comes from.
task = Instance(Task)
#: The layout of panes in the TaskWindow.
layout = Instance(TaskLayout)
#: Whether the task state has been initialized.
initialized = Bool(False)
#: The central pane of the TaskWindow
central_pane = Instance(ITaskPane)
#: The list of all dock panes.
dock_panes = List(Instance(IDockPane))
#: The TaskWindow's menu bar manager instance.
menu_bar_manager = Instance(IMenuBarManager)
#: The TaskWindow's status bar instance.
status_bar_manager = Instance(IStatusBarManager)
#: The TaskWindow's tool bar instances.
tool_bar_managers = List(Instance(IToolBarManager))
[docs] def get_dock_pane(self, id):
""" Returns the dock pane with the specified id, or None if no such dock
pane exists.
"""
for pane in self.dock_panes:
if pane.id == id:
return pane
return None