Custom Interaction Mode (dragging cables)
Table of Contents

You can create some great user experience by creating your own custom interaction mode. Let’s go through the steps to create one.
As an example I create a mode where the user can drag a cable in a perpendicular motion to a new location. The movie plays like this:

  1. When the user is near a cable, the cable will highlight.
  2. If the user clicks and holds the left mouse button, the highlit cable will be dragged.
  3. The cable will move with the motion of the mouse, but only in a perpendicular motion.
  4. When the mouse button is released, the cable is moved to the new location.
PDR.png
These are the steps to implement this:
1) Add the plugin to the config.xml of the application. The name of the plugin is "custom_mode" in this example, the name of the mode is drag_cable.
<config>
  <plugins>
      <plugin name="custom_mode" class_name="custom_interaction_mode_plugin"/>     
      <plugin name="maps" class_name="map_manager" >
      <interaction_modes>
          <mode name="select"/>
          <mode name="geometry"/>
          <mode name="zoom_in"/>
          <mode name="zoom_out"/>
          <mode name="pan"/>
          <mode name="custom_mode.drag_cable"/>
          <cycle_sequence modes="select,geometry"/>
      </interaction_modes>
      <!-- and the rest -->
      </plugin>
</config>

2) Create a plugin custom_interaction_mode_plugin. Let it inherit from :custom_interaction_mode_mixin.

3) Implement the method get_interaction_mode_action(). Let it return the sw_action that represents the button in the toolbar.

_pragma(classify_level=basic, topic={demo}) 
_method custom_interaction_mode_plugin.get_interaction_mode_action(mode_name)
    ## Return the sw_action that defines the button that activates
    ## our mode.
    _if mode_name _is :drag_cable
    _then
        _return sw_action.new(mode_name,
                      :engine, _self,
                      :tooltip, "Drag a cable to a new position",
                      :image, { mode_name, _self.module_name },
                      :value, mode_name )
    _endif 
_endmethod
$

This will add a 6th interaction mode to the toolbar.
modes.png
4) Implement the method get_interaction_mode(). Let it return the interaction_mode. The interaction_mode has many options but the core is to define the actions and install a handler to handle the actions.
_pragma(classify_level=basic, topic={demo})
_method custom_interaction_mode_plugin.get_interaction_mode( name, logical_name )
    ## Return the interaction mode that will find and drag the
    ## cable. Set the cursors, move actions, event handlers and the lot.
    _if logical_name _isnt :drag_cable _then _return _unset _endif
    …
    m << interaction_mode.new( :drag_cable, _unset )
    …
    # When moving around on the screen, the :move action will be
    # send on every move.
    m.set_move_actions(0, :move)
    …
    # Setup the drag actions, set the actions that are send during
    # the drag.
    m.set_drag_actions(:left, 0, {:start_drag, :drag, :end_drag})
    …
    # Installs the event handlers
    m.add_event_handler( :action, _self, :|handle_drag_action()| )
    _return m
_endmethod
$

The code above instructs the interaction mode to send events to handle_drag_mode(). When the mouse moves around it should send the action :move. When the user clicks and hold the mouse down, the action :start_drag is send, during the drag the action :drag is send and upon release the action :end_drag is send.
5) Implement the event handler to deal with all the actions flying around.
_pragma(classify_level=basic, topic={demo})
_method custom_interaction_mode_plugin.handle_drag_action(action,start_coord,end_coord,a_window)
    ## the master method that is invoked on every event once the
    ## the mode has started. Return :event_handled to indicate that
    ## that no other handlers need to do anthing anymore.
    _local handled? << _true 
    _if action _is :move
    _then
        _self.handle_move_action(a_window, end_coord)
    _else
        _if .cable _isnt _unset
        _then 
            _if action _is :start_drag
            _then
                _self.start_drag(a_window)
            _elif action _is :press
            _then

            _elif action _is :drag 
            _then
                _self.dragging(a_window, end_coord)
            _elif action _is :end_drag
            _then
                _self.end_drag(a_window, end_coord)
            _elif action _is :abort
            _then
                _self.abort_drag(a_window, end_coord)
            _else
                handled? << _false 
            _endif
        _else
            handled? << _false 
        _endif
    _endif
    _if handled? _then _return :event_handled _endif 
_endmethod
$

This method is ugly, but more on that at the end of this page.
These are the fundaments of creating an interaction mode. There are a few things to consider.

Chaining

I want the user to be able to zoom and pan during my interaction mode. However I don’t want to copy all the zoom and pan sourcecode in my plugin. The way to deal with this is to chain the interaction_mode to an existing interaction_mode. All events that are not handled by my interaction mode will be handled by the chained interaction mode.
These are the steps to implement this:
1) Add an event handler to catch the start of the interaction mode.

…
    m.add_event_handler( :start_interaction, _self, :|start()|)
…

2) Chain the custom mode to the pan mode.
_pragma(classify_level=basic, topic={demo})
_method custom_interaction_mode_plugin.start(mode)
    ## Make self dependent on the document to catch the refresh
    ## actions. Also chain my interaction handler to the :pan handler.

    _local handler << mode.framework
    _local chain_mode << handler.mode(:pan)
    mode.chain_to << chain_mode
_endmethod
$

3) Let the action event handler return the symbol :event_handled if the event handler handed the event. The chained interaction mode will be skipped if the action was handled.
if handled? _then _return :event_handled _endif

That’s it!

Refresh

If the screen gets refreshed during the interaction, the highlight will be messed up. The interaction_mode has no build-in way to counteract this, so we have to program it.
These are the steps to implement this:
1) Add event handers to notice the start and stop of the interaction. Make the plugin dependent on the map_view when the interaction starts and remove the dependency when the interaction stops.

…
    m.add_event_handler( :start_interaction, _self, :|start()|)
    m.add_event_handler( :end_interaction,   _self, :|end()|)
…

_pragma(classify_level=basic, topic={demo})
_method custom_interaction_mode_plugin.start(mode)
    ## Make self dependent on the document to catch the refresh
    ## actions. Also chain my interaction handler to the :pan handler.

    _local handler << mode.framework
    _local map_gui << handler.framework
    map_gui.document.add_dependent(_self, :post_render)
_endmethod
$
_pragma(classify_level=basic, topic={demo})
_method custom_interaction_mode_plugin.end(mode)
    ## remove dependency on the document.
    _local handler << mode.framework
    _local map_gui << handler.framework
    map_gui.document.remove_dependent(_self)
_endmethod
$

2) Implement the note_change() method to note the end of the refresh and redo the highlight.
_pragma(classify_level=basic, topic={demo})
_method custom_interaction_mode_plugin.note_change(p_map_view, p_what)
    _if p_what _is :post_render
    _then
        _self.redraw_after_refresh(p_map_view.window)
    _endif 
_endmethod
$

Code

The complete sourcecode of the plugin is attached here. Load it into the Cambridge application and make the required changes to the config.xml of the application. Enjoy.

Better?

The reader with a software engineering background will notice that there is design pattern in this plugin screaming to get out: the state pattern. Check this page to see the refactored plugin.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License