f@0: /*
f@0: CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
f@0:
f@0: Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
f@0:
f@0: This program is free software: you can redistribute it and/or modify
f@0: it under the terms of the GNU General Public License as published by
f@0: the Free Software Foundation, either version 3 of the License, or
f@0: (at your option) any later version.
f@0:
f@0: This program is distributed in the hope that it will be useful,
f@0: but WITHOUT ANY WARRANTY; without even the implied warranty of
f@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
f@0: GNU General Public License for more details.
f@0:
f@0: You should have received a copy of the GNU General Public License
f@0: along with this program. If not, see .
f@0: */
f@0: package uk.ac.qmul.eecs.ccmi.gui;
f@0:
f@0: import java.awt.geom.Point2D;
f@0: import java.awt.geom.Rectangle2D;
f@0: import java.util.ResourceBundle;
f@0:
f@0: import javax.swing.SwingUtilities;
f@0:
f@0: import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel;
f@0: import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement;
f@0: import uk.ac.qmul.eecs.ccmi.haptics.HapticListener;
f@0: import uk.ac.qmul.eecs.ccmi.haptics.HapticListenerThread;
f@0: import uk.ac.qmul.eecs.ccmi.haptics.HapticListenerCommand;
f@0: import uk.ac.qmul.eecs.ccmi.main.DiagramEditorApp;
f@0: import uk.ac.qmul.eecs.ccmi.network.Command;
f@0: import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource;
f@0: import uk.ac.qmul.eecs.ccmi.sound.SoundEvent;
f@0: import uk.ac.qmul.eecs.ccmi.sound.SoundFactory;
f@0: import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory;
f@0: import uk.ac.qmul.eecs.ccmi.utils.InteractionLog;
f@0:
f@0: /**
f@0: *
f@0: * An instance of HapticListener for the diagram editor. By this class visual diagrams
f@0: * can be manipulated by an haptic device. This class extends the {@code Thread} class,
f@0: * and can therefore be run on a separate thread listening to the haptic commands coming from the thread
f@0: * managing the haptic device. The commands affecting the Swing components will
f@0: * be queued for execution on the code Event Dispatching Thread event queue.
f@0: *
f@0: */
f@0: public class HapticKindle extends HapticListenerThread {
f@0:
f@0: public HapticKindle(){
f@0: super();
f@0: cmdImpl = new CommandImplementation();
f@0: /* unselect always ends up to the same instruction. Therefore don't create a new runnable *
f@0: * each time the command is issued, but rather keep and reuse always the same class */
f@0: unselectRunnable = new Runnable(){
f@0: @Override
f@0: public void run(){
f@0: cmdImpl.executeCommand(HapticListenerCommand.UNSELECT, 0, 0, 0, 0, 0);
f@0: }
f@0: };
f@0: }
f@0:
f@0: /**
f@0: * Implementation of the {@code executeCommand} method. All the commands that involve the
f@0: * diagram model, are executed in the Event Dispatching Thread through {@code SwingUtilities} invoke
f@0: * methods. This prevents race conditions on the model and on diagram elements.
f@0: *
f@0: * @see HapticListenerThread#executeCommand(HapticListenerCommand, int, double, double, double, double)
f@0: */
f@0: @Override
f@0: public void executeCommand(final HapticListenerCommand cmd, final int ID, final double x, final double y, final double startX, final double startY) {
f@0: switch(cmd){
f@0: case PLAY_ELEMENT_SOUND :
f@0: case PLAY_ELEMENT_SPEECH :
f@0: case SELECT :
f@0: case INFO :
f@0: case ERROR :
f@0: SwingUtilities.invokeLater(new Runnable(){
f@0: public void run(){
f@0: cmdImpl.executeCommand(cmd, ID, x, y, startX, startY);
f@0: }
f@0: });
f@0: break;
f@0: case UNSELECT :
f@0: SwingUtilities.invokeLater(unselectRunnable);
f@0: break;
f@0: case PICK_UP :
f@0: case MOVE : {
f@0: /* when this block is executed we already have the lock *
f@0: * on the element from the PICK_UP command execution */
f@0: try {
f@0: SwingUtilities.invokeAndWait(new Runnable(){
f@0: @Override
f@0: public void run(){
f@0: cmdImpl.executeCommand(cmd, ID, x, y, startX, startY);
f@0: } // run()
f@0: });
f@0: } catch (Exception e) {
f@0: throw new RuntimeException(e);
f@0: }
f@0: }
f@0: break;
f@0:
f@0: case PLAY_SOUND :
f@0: cmdImpl.executeCommand(cmd, ID, x, y, startX, startY); // not in the Event Dispatching Thread
f@0: break;
f@0: }
f@0:
f@0: }
f@0:
f@0: /**
f@0: * Returns the delegate inner implementation of the {@code HapticListener} commands.
f@0: * When called directly from the returned {@code HapticListener} the commands won't be executed on a separate thread.
f@0: *
f@0: * @return the {@code HapticListener} command implementation
f@0: */
f@0: @Override
f@0: public HapticListener getNonRunnableListener(){
f@0: return cmdImpl;
f@0: }
f@0:
f@0:
f@0: private Runnable unselectRunnable;
f@0: private CommandImplementation cmdImpl;
f@0: private static String INTERACTION_LOG_SOURCE = "HAPTIC";
f@0:
f@0: /* An inner class with the implementation of all the commands. HapticKindle runs *
f@0: * on its own thread and delegates the real commands implementation to this class */
f@0: private static class CommandImplementation implements HapticListener{
f@0: @Override
f@0: public void executeCommand(HapticListenerCommand cmd, int ID, double x,
f@0: double y, double startX, double startY) {
f@0: final EditorFrame frame = DiagramEditorApp.getFrame();
f@0: switch(cmd){
f@0: case PLAY_ELEMENT_SOUND : {
f@0: if((frame == null)||(frame.getActiveTab() == null))
f@0: return;
f@0: CollectionModel collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
f@0: DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
f@0: /* can be null if the tab has been switched or closed in the meantime */
f@0: if(de == null)
f@0: return;
f@0: SoundFactory.getInstance().play(de.getSound());
f@0: }break;
f@0: case PLAY_ELEMENT_SPEECH : {
f@0: if((frame == null)||(frame.getActiveTab() == null))
f@0: return;
f@0: CollectionModel collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
f@0: DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
f@0: if(de == null)
f@0: return;
f@0: SoundFactory.getInstance().play(de.getSound());
f@0: NarratorFactory.getInstance().speak(de.getName());
f@0: iLog("touch",((de instanceof Node) ? "node " : "edge ")+de.getName());
f@0: }break;
f@0: case SELECT : {
f@0: if((frame == null)||(frame.getActiveTab() == null))
f@0: return;
f@0: CollectionModel collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
f@0: DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
f@0: if(de == null)
f@0: return;
f@0: frame.selectHapticHighligh(de);
f@0: }break;
f@0: case UNSELECT : {
f@0: if((frame == null)||(frame.getActiveTab() == null))
f@0: return;
f@0: frame.selectHapticHighligh(null);
f@0: }break;
f@0: case MOVE : {
f@0: /* when this block is executed we already have the lock *
f@0: * on the element from the PICK_UP command execution */
f@0: if((frame == null)||(frame.getActiveTab() == null))
f@0: return;
f@0: CollectionModel collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
f@0: DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
f@0: if(de == null)
f@0: return;
f@0: DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater();
f@0: if(de instanceof Node){
f@0: Node n = (Node)de;
f@0: Rectangle2D bounds = n.getBounds();
f@0: Point2D.Double p = new Point2D.Double(bounds.getCenterX(),bounds.getCenterY());
f@0: double dx = x - p.getX();
f@0: double dy = y - p.getY();
f@0: n.getMonitor().lock();
f@0: modelUpdater.translate(n, p, dx, dy,DiagramEventSource.HAPT);
f@0: modelUpdater.stopMove(n,DiagramEventSource.HAPT);
f@0: n.getMonitor().unlock();
f@0:
f@0: StringBuilder builder = new StringBuilder();
f@0: builder.append(DiagramElement.toLogString(n)).append(" ").append(p.getX())
f@0: .append(' ').append(p.getY());
f@0: iLog("move node start",builder.toString());
f@0: builder = new StringBuilder();
f@0: builder.append(DiagramElement.toLogString(n)).append(' ')
f@0: .append(x).append(' ').append(y);
f@0: iLog("move node end",builder.toString());
f@0: }else{
f@0: Edge e = (Edge)de;
f@0: modelUpdater.startMove(e, new Point2D.Double(startX,startY),DiagramEventSource.HAPT);
f@0: Point2D p = new Point2D.Double(x,y);
f@0: e.getMonitor().lock();
f@0: modelUpdater.bend(e, p,DiagramEventSource.HAPT);
f@0: modelUpdater.stopMove(e,DiagramEventSource.HAPT);
f@0: e.getMonitor().unlock();
f@0:
f@0: StringBuilder builder = new StringBuilder();
f@0: builder.append(DiagramElement.toLogString(e)).append(' ').append(startX)
f@0: .append(' ').append(startY);
f@0: iLog("bend edge start",builder.toString());
f@0: builder = new StringBuilder();
f@0: builder.append(DiagramElement.toLogString(e)).append(' ')
f@0: .append(x).append(' ').append(y);
f@0: iLog("bend edge end",builder.toString());
f@0: }
f@0: modelUpdater.yieldLock(de,
f@0: Lock.MOVE,
f@0: new DiagramEventActionSource(
f@0: DiagramEventSource.HAPT,
f@0: de instanceof Node ? Command.Name.STOP_NODE_MOVE : Command.Name.STOP_EDGE_MOVE,
f@0: de.getId(),
f@0: de.getName()
f@0: ));
f@0: SoundFactory.getInstance().play(SoundEvent.HOOK_OFF);
f@0: }break;
f@0: case INFO : {
f@0: if((frame == null)||(frame.getActiveTab() == null))
f@0: return;
f@0: CollectionModel collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
f@0: DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
f@0: if(de == null)
f@0: return;
f@0: SoundFactory.getInstance().stop();
f@0: NarratorFactory.getInstance().speak(de.detailedSpokenText());
f@0: iLog("request detailed info",((de instanceof Node) ? "node " : "edge ")+de.getName());
f@0: }break;
f@0: case PLAY_SOUND : {
f@0: switch(HapticListenerCommand.Sound.fromInt(ID) ){
f@0: case MAGNET_OFF :
f@0: SoundFactory.getInstance().play(SoundEvent.MAGNET_OFF);
f@0: iLog("sticky mode off","");
f@0: break;
f@0: case MAGNET_ON :
f@0: SoundFactory.getInstance().play(SoundEvent.MAGNET_ON);
f@0: iLog("sticky mode on","");
f@0: break;
f@0: case DRAG : SoundFactory.getInstance().play(SoundEvent.DRAG);
f@0: break;
f@0: }
f@0: }break;
f@0: case PICK_UP :{
f@0: if((frame == null)||(frame.getActiveTab() == null))
f@0: return;
f@0: CollectionModel collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
f@0: DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
f@0: if(de == null)
f@0: return;
f@0: DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater();
f@0: if(!modelUpdater.getLock(de,
f@0: Lock.MOVE,
f@0: new DiagramEventActionSource(DiagramEventSource.HAPT, de instanceof Edge ? Command.Name.TRANSLATE_EDGE : Command.Name.TRANSLATE_NODE ,de.getId(),de.getName()))){
f@0: iLog("Could not get lock on element for motion", DiagramElement.toLogString(de));
f@0: NarratorFactory.getInstance().speak("Object is being moved by another user");
f@0: return;
f@0: }
f@0: frame.hPickUp(de);
f@0: SoundFactory.getInstance().play(SoundEvent.HOOK_ON);
f@0: iLog("hook on","");
f@0: }break;
f@0: case ERROR : {
f@0: if((frame == null)||(frame.getActiveTab() == null))
f@0: return;
f@0: frame.backupOpenDiagrams();
f@0: NarratorFactory.getInstance().speak(ResourceBundle.getBundle(EditorFrame.class.getName()).getString("speech.haptic_device_crashed"));
f@0: }break;
f@0: }
f@0: }
f@0:
f@0: private void iLog(String action, String args){
f@0: InteractionLog.log(INTERACTION_LOG_SOURCE,action,args);
f@0: }
f@0: }
f@0:
f@0: }