Thursday, 9 August 2012

Contextual Component Actions

If our score has multiple objects on the page, e.g. 3 text areas, 1 manuscript area, and you have an action called for example "setBackGroundColour" in each object.

If you have a menuitem and toolbar component that fire this action, how does it know which of the objects to fire it into? The answer has to be the currently focused one.

How does it know what the currently focused one is? That's a problem because by selecting a menu item or the toolbar component, you actually change focus.

There's a solution using delegate actions presented on the web, but this doesn't seem to exactly fit the bill. It's not the first way of solving this problem that sprang to mind, but the solution has a basis in this  article http://www.javalobby.org/java/forums/t19448.html

My thoughts were, what if we put a FocusListener on a target object, which, when focus is gained it tells the action, it's the current target. It should probably tell the Action that it's no longer the target when it loses focus. This is a co-operative approach, all objects must implement the protocol defined or it breaks down. So what happens though, as surely you would lose focus when you click the menubar? The DefaultEditorKit must solve this somehow with it's cut, copy, paste actions.

So let's work this out, we extend the default AbstractAction to include some new capabilities. A setTarget method which can be called to make it null, ie called from within a lost focus method, or called with an Action specifying the Action whose actionPerformed method should be invoked, ie called when focus gained.

This means the focus lost and gained methods for an object need to know the list of actions to set as target actions. Also means they must know the list of master actions to control.

Suddenly starting to seem complex with lots of list passing and storing going on. Further reading shows that this is considered in http://www.javalobby.org/java/forums/t19448.html. It works fundamentally by tracking the focus owner, checking every time focus changes if the new focus owner has an action of the monitored name.

So for us we have a set of application level actions (things file open, window, help); then some document level actions (zoom, new staff, new text) and then each page level component in the document has a set of actions. What the page level component needs to do is tell the document level actions that if it gets fired to pass it their way.

Now the immediate challenge with that is some of these page level components (e.g. the JMusicComponent) have 40 or 50 actions. That's an awful lot to be switching on and off every time focus is changed. That would mean there's a corresponding document level action for each of those.

There's probably another level of hierarchy too, as within the page level component there are lots of other widgets which potentially have actions. At that level in some of the components we are talking widgets, meaning objects that are not based on a swing component, especially the 2D drawing based objects. Not being based on Swing components at that level means no focus transitions to plug into, there will need to be a handcrafted way of achieving the same.

This is when I started thinking about swapping menuitem action references. Each JMenuItem has an associated action, what if we could swap those at run time, would that save writing all those wrapper actions to manage the arbitration and switching? The way the app is structured, all the menu items get created in one go when we create or open a document. All the code for doing this sits at the frame  level, reaching into the document for actions or reaching up to the application level for actions. This makes the frame level part of the document level, i.e. it manages the frame, and puts a panel within it which we call the view.

This provides little scope for dynamically adding or removing menu items. Guess it's easier to have actions greyed out like this, as no matter where in the app you enable or disable an action it will work on the menu item.

So it looks like the way ahead is to reproduce every single components actions that need to have menuitems or buttons associated with them. One last thing to explore is the permanent or temporary loss of focus - it could be that going for menus and buttons results in temporary loss of focus which is something we could hook into, for example, on temporary loss, we leave our actions enabled, on permanent loss we nullify them out. On regain of permanent focus we plug our action set back in. On testing this it works a treat.

Here's the base class used that all document level actions that need to be pointed at the last permanently focused component are derived from:

package score;

import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;

public class SwitchingAction extends AbstractAction {

 private static final long serialVersionUID = 1L;
 private Action targetAction=null;

 public SwitchingAction() {
  super();
 }
 
 public SwitchingAction(String name) {
  super(name);
 }
 
 public SwitchingAction(String name, Icon icon) {
  super(name,icon);
 }
 
 @Override
 public void actionPerformed(ActionEvent arg0) {
  if (targetAction != null) {
   targetAction.actionPerformed(new ActionEvent(arg0.getSource(), 
            arg0.getID(), 
            arg0.getActionCommand(), 
            arg0.getWhen(), 
            arg0.getModifiers()) 
          );
  }

 }

 public Action getTargetAction() {
  return targetAction;
 }

 public void setTargetAction(Action targetAction) {
  this.targetAction = targetAction;

  if (targetAction==null) {
   this.setEnabled(false);
  } else {
   this.setEnabled(targetAction.isEnabled());
  }
 }

}



Then in a component that needs to worry about getting it's actions switched in when it has permanent focus ...

 @SuppressWarnings("serial")
 Action setBoldAction = new AbstractAction() {

  @Override
  public void actionPerformed(ActionEvent arg0) {
   System.out.println("Going bold");
   setTextFace(Font.BOLD);
  }
 };
 
        [snip]

        // in the object constructor ...
 docText.addFocusListener(this);

        [snip]


 @Override
 public void focusGained(FocusEvent arg0) {
  // TODO Auto-generated method stub
  System.out.println(arg0);
  if (!arg0.isTemporary()) {
   // tell the document action we want the action events
   DocActions docActions=this.getDocActions();
   SwitchingAction sba=docActions.getDocSetBoldAction();
   sba.setTargetAction(setBoldAction);
  }
 }

 @Override
 public void focusLost(FocusEvent arg0) {
  // TODO Auto-generated method stub
  System.out.println(arg0);
  if (!arg0.isTemporary()) {
   // tell the document action to forget us
   DocActions docActions=this.getDocActions();
   SwitchingAction sba=docActions.getDocSetBoldAction();
   sba.setTargetAction(null);
  }
 }


We have all the document level actions, including the switching ones defined in a DocActions object for ease of reference.





1 comment:

  1. And found out when testing the implementation, we need to mirror selected state of an action too, when they're used in a menu. Had to jump through hoops a little bit trying to be clever and using the one underlying action for font family name, relying on the actionCommand() string to identify what was wanted.

    Meant testing font name and ensuring selected state set appropriately before switching the action back in, then in SwitchingAction, pulling the selected state out with getValue. More lines of code than you'd want. There must be an easier way!

    ReplyDelete