Getting Started with dip.shell

A shell is an application’s main user interface that, to the developer, implements the IShell interface.

In general terms, a shell visualises actions and acts as a container for tools. Actions and tools are managed on behalf of the shell by a shell manager. Different shells may visualise actions and tools in different ways. For example a shell specifically designed for mobile devices may only allow one tool to be visible at a time.

A tool is essentially a QWidget sub-class that allows one or more logically related operations to be performed on a shell object. Tools are implicitly managed in that the lifecycle of the shell object is controlled automatically by the shell manager and not by the tool. An unmanaged tool is a tool that controls the lifecycle of the object it is operating on itself. A shell manager automatically maintains a reference to the currently active tool, if any, as active_tool.

In order to manage the lifecycle of shell objects, the shell manager uses actions provided by the shell that implement the standard New, Open, Save, Save As and Close operations. When interacting with the user these will automatically take account of the shell objects and tools that are available, and the storage where shell objects can be read from or written to.

The shell manager also uses an action that implements the standard Quit operation that automatically interacts with the user regarding any unsaved shell objects.

In the following sections we describe the different aspects of using a shell. However, in a complex real-world application you are more likely to use the features of the dip.plugins module to wrap shell objects and tools as plugins so that they are automatically added to a shell without having to explicitly change the shell’s configuration.

Creating and Displaying a Shell

dip provides QMainWindowShell which uses a QMainWindow to supply menus, tool bars etc. It can be created and displayed as follows:

from dip.shell.shells.qmainwindow import QMainWindowShell

shell = QMainWindowShell()
shell.widget.show()

As well as implementing the IShell interface a shell may be able to be configured in additional ways. For example the QMainWindowShell shell allows how tools are placed in tabbed widgets to be specified. For example:

# It's only safe to do this when we are sure the shell is a
# QMainWindowShell.
shell.tab_policy = 'never'

Adding Shell Objects

Tools normally operate on fundamental business or domain objects. For these objects to be used as a shell object they must implement (or more usually be able to be adapted to) the IShellObject interface. It is normally better to use adaptation as it means that the extra attributes needed to manage the object can be kept separate from the object itself.

The following attributes of the IShellObject interface should be maintained by the application:

  • the dirty attribute is a boolean that should be set whenever the application object changes. It will be cleared automatically
  • the name attribute is a string that is a short descriptive name of the individual object. If the object doesn’t have anything that might be used as a name then it is recommended that the string representation of the location attribute is used.

The values of the other attributes of IShellObject are maintained automatically by the shell manager.

The code below shows an example that will adapt the IExampleModel interface we introduced in an earlier section:

from dip.model import adapt, Adapter, DelegatedTo, observe
from dip.shell import IShellObject

from .i_example_model import IExampleModel

@adapt(IExampleModel, to=IShellObject)
class IExampleModelIShellObjectAdapter(Adapter)

    name = DelegatedTo('adaptee.name')

    @observe('adaptee.*')
    def _on_obj_changed(self, change):

        self.dirty = True

Because the object has a name attribute already we can simply delegate to it as follows:

name = DelegatedTo('adaptee.name')

Because the object is an instance of a Model, and because all of its attributes will be stored, we mark the object as being dirty if any of those attributes change as follows:

@observe('adaptee.*')
def _on_obj_changed(self, change):

    self.dirty = True

Shell objects are created by a factory. The following is a factory for the example model:

from dip.model import implements, Model
from dip.shell import IShellObjectFactory

@implements(IShellObjectFactory)
class ExampleModelFactory(Model)

    name = "Example"

    native_format = 'myapplication.formats.example_model'

    def __call__(self, shell):

        from .example_model import ExampleModel

        return ExampleModel()

The factory itself is a Model instance that implements the IShellObjectFactory interface as follows:

@implements(IShellObjectFactory)
class ExampleModelFactory(Model)

Each shell object’s type has a short descriptive name (which is different to the name of an individual shell object) as follows:

name = "Example"

Each shell object also has a native data format as follows:

native_format = 'myapplication.formats.example_model'

Note that this is the identifier of the ICodecsFactory we defined earlier.

Finally we implement the __call__() method that actually creates the object as follows:

def __call__(self, shell):

    from .example_model import ExampleModel

    return ExampleModel()

Shell object factories are registered with the shell by appending them to the shell manager’s shell_object_factories list, for example:

from example_model_factory import ExampleModelFactory

shell.shell_manager.shell_object_factories.append(ExampleModelFactory())

Adding Tools

Tools are handled in a very similar way to shell objects. A tool must implement (or be able to be adapted to) the ITool interface. A tool is created by a factory, i.e. an implementation of the IToolFactory interface. Tool factories are registered with the shell by appending them to the shell manager’s tool_factories list.

In the remaining part of this section we will just cover issues specific to tools.

dip.shell includes an adapter between QWidget and ITool so any tool can get access to the object it is operating on by doing the following:

obj_operated_on = ITool(self).obj

Note that the object may not implement the IShellObject interface, but it is guaranteed that it can be adapted to it. Therefore if you want to set the dirty flag you should excplicitly cast it as follows:

IShellObject(obj_operated_on).dirty = True

A tool factory should implement the handles() method which is used by the shell manager to determine if a tool can handle a particular object. If there is more than one tool that can handle an object then the shell manager will ask the user which they want to use.

dip provides ModelToolFactory as a tool factory that will automatically create a tool for editing the attributes of a Model. All that is needed is to set the model_type attribute to the exact Model sub-class to be handled. This can be done as follows:

from dip.shell import ModelToolFactory

from i_example_model import IExampleModel

example_model_tool_factory = ModelToolFactory(name="Example Model Editor",
        model_type=IExampleModel)

shell.shell_manager.tool_factories.append(example_model_tool_factory)

Adding Actions

An action, in the context of a shell, is an implementation of the IShellAction interface, or an ActionCollection. Actions are added by appending them to the actions list.

The ShellAction class is an implementation of the IShellAction interface specifically designed for use by tools (including unmanaged tools) without needing to be sub-classed. The following is an example shell action:

action = ShellAction(id='myapplication.actions.do_something',
        text="&Do Something", handles=ExampleTool,
        on_perform=lambda tool: tool.do_something())

The id attribute is the action’s identifier that is used by the shell when visualising the action, typically to place it in an appropriate menu. It is also used by the dip.automate module to automate the triggering of the action.

The text attribute is the action’s text, typically used as the text of a menu item.

The handles attribute is the type of the tool that must be currently active for the action to be enabled.

The on_perform attribute is a callable that is invoked when the action is triggered. The callable is passed the tool as its only argument.

ShellAction contains many other attributes that allow you to completely configure the action.

Adding Unmanaged Tools

An unmanaged tool is a QWidget sub-class that implements (or, more usually, can be adapted to) to the IUnmanagedTool interface. IUnmanagedTool is itself a sub-class of the IShellAction interface and unmanaged tools are added to the shell using actions just like any other action.

A fundamental difference between ordinary tools and unmanaged tools is that there is only ever one instance of an unmanaged tool. Many instances of an ordinary tool may exist at any time, each operating on a different shell object.

The action implicit in an unmanaged tool is used to toggle the visibility of the tool.

The developer tools provided with dip are all implemented as unmanaged tools.