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