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: }