f@0
|
1 /*
|
f@0
|
2 CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
|
f@0
|
3
|
f@0
|
4 Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
|
f@0
|
5
|
f@0
|
6 This program is free software: you can redistribute it and/or modify
|
f@0
|
7 it under the terms of the GNU General Public License as published by
|
f@0
|
8 the Free Software Foundation, either version 3 of the License, or
|
f@0
|
9 (at your option) any later version.
|
f@0
|
10
|
f@0
|
11 This program is distributed in the hope that it will be useful,
|
f@0
|
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
f@0
|
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
f@0
|
14 GNU General Public License for more details.
|
f@0
|
15
|
f@0
|
16 You should have received a copy of the GNU General Public License
|
f@0
|
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
|
f@0
|
18 */
|
f@0
|
19 package uk.ac.qmul.eecs.ccmi.gui;
|
f@0
|
20
|
f@0
|
21 import java.awt.geom.Point2D;
|
f@0
|
22 import java.awt.geom.Rectangle2D;
|
f@0
|
23 import java.util.ResourceBundle;
|
f@0
|
24
|
f@0
|
25 import javax.swing.SwingUtilities;
|
f@0
|
26
|
f@0
|
27 import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel;
|
f@0
|
28 import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement;
|
f@0
|
29 import uk.ac.qmul.eecs.ccmi.haptics.HapticListener;
|
f@0
|
30 import uk.ac.qmul.eecs.ccmi.haptics.HapticListenerThread;
|
f@0
|
31 import uk.ac.qmul.eecs.ccmi.haptics.HapticListenerCommand;
|
f@0
|
32 import uk.ac.qmul.eecs.ccmi.main.DiagramEditorApp;
|
f@0
|
33 import uk.ac.qmul.eecs.ccmi.network.Command;
|
f@0
|
34 import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource;
|
f@0
|
35 import uk.ac.qmul.eecs.ccmi.sound.SoundEvent;
|
f@0
|
36 import uk.ac.qmul.eecs.ccmi.sound.SoundFactory;
|
f@0
|
37 import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory;
|
f@0
|
38 import uk.ac.qmul.eecs.ccmi.utils.InteractionLog;
|
f@0
|
39
|
f@0
|
40 /**
|
f@0
|
41 *
|
f@0
|
42 * An instance of HapticListener for the diagram editor. By this class visual diagrams
|
f@0
|
43 * can be manipulated by an haptic device. This class extends the {@code Thread} class,
|
f@0
|
44 * and can therefore be run on a separate thread listening to the haptic commands coming from the thread
|
f@0
|
45 * managing the haptic device. The commands affecting the Swing components will
|
f@0
|
46 * be queued for execution on the code Event Dispatching Thread event queue.
|
f@0
|
47 *
|
f@0
|
48 */
|
f@0
|
49 public class HapticKindle extends HapticListenerThread {
|
f@0
|
50
|
f@0
|
51 public HapticKindle(){
|
f@0
|
52 super();
|
f@0
|
53 cmdImpl = new CommandImplementation();
|
f@0
|
54 /* unselect always ends up to the same instruction. Therefore don't create a new runnable *
|
f@0
|
55 * each time the command is issued, but rather keep and reuse always the same class */
|
f@0
|
56 unselectRunnable = new Runnable(){
|
f@0
|
57 @Override
|
f@0
|
58 public void run(){
|
f@0
|
59 cmdImpl.executeCommand(HapticListenerCommand.UNSELECT, 0, 0, 0, 0, 0);
|
f@0
|
60 }
|
f@0
|
61 };
|
f@0
|
62 }
|
f@0
|
63
|
f@0
|
64 /**
|
f@0
|
65 * Implementation of the {@code executeCommand} method. All the commands that involve the
|
f@0
|
66 * diagram model, are executed in the Event Dispatching Thread through {@code SwingUtilities} invoke
|
f@0
|
67 * methods. This prevents race conditions on the model and on diagram elements.
|
f@0
|
68 *
|
f@0
|
69 * @see HapticListenerThread#executeCommand(HapticListenerCommand, int, double, double, double, double)
|
f@0
|
70 */
|
f@0
|
71 @Override
|
f@0
|
72 public void executeCommand(final HapticListenerCommand cmd, final int ID, final double x, final double y, final double startX, final double startY) {
|
f@0
|
73 switch(cmd){
|
f@0
|
74 case PLAY_ELEMENT_SOUND :
|
f@0
|
75 case PLAY_ELEMENT_SPEECH :
|
f@0
|
76 case SELECT :
|
f@0
|
77 case INFO :
|
f@0
|
78 case ERROR :
|
f@0
|
79 SwingUtilities.invokeLater(new Runnable(){
|
f@0
|
80 public void run(){
|
f@0
|
81 cmdImpl.executeCommand(cmd, ID, x, y, startX, startY);
|
f@0
|
82 }
|
f@0
|
83 });
|
f@0
|
84 break;
|
f@0
|
85 case UNSELECT :
|
f@0
|
86 SwingUtilities.invokeLater(unselectRunnable);
|
f@0
|
87 break;
|
f@0
|
88 case PICK_UP :
|
f@0
|
89 case MOVE : {
|
f@0
|
90 /* when this block is executed we already have the lock *
|
f@0
|
91 * on the element from the PICK_UP command execution */
|
f@0
|
92 try {
|
f@0
|
93 SwingUtilities.invokeAndWait(new Runnable(){
|
f@0
|
94 @Override
|
f@0
|
95 public void run(){
|
f@0
|
96 cmdImpl.executeCommand(cmd, ID, x, y, startX, startY);
|
f@0
|
97 } // run()
|
f@0
|
98 });
|
f@0
|
99 } catch (Exception e) {
|
f@0
|
100 throw new RuntimeException(e);
|
f@0
|
101 }
|
f@0
|
102 }
|
f@0
|
103 break;
|
f@0
|
104
|
f@0
|
105 case PLAY_SOUND :
|
f@0
|
106 cmdImpl.executeCommand(cmd, ID, x, y, startX, startY); // not in the Event Dispatching Thread
|
f@0
|
107 break;
|
f@0
|
108 }
|
f@0
|
109
|
f@0
|
110 }
|
f@0
|
111
|
f@0
|
112 /**
|
f@0
|
113 * Returns the delegate inner implementation of the {@code HapticListener} commands.
|
f@0
|
114 * When called directly from the returned {@code HapticListener} the commands won't be executed on a separate thread.
|
f@0
|
115 *
|
f@0
|
116 * @return the {@code HapticListener} command implementation
|
f@0
|
117 */
|
f@0
|
118 @Override
|
f@0
|
119 public HapticListener getNonRunnableListener(){
|
f@0
|
120 return cmdImpl;
|
f@0
|
121 }
|
f@0
|
122
|
f@0
|
123
|
f@0
|
124 private Runnable unselectRunnable;
|
f@0
|
125 private CommandImplementation cmdImpl;
|
f@0
|
126 private static String INTERACTION_LOG_SOURCE = "HAPTIC";
|
f@0
|
127
|
f@0
|
128 /* An inner class with the implementation of all the commands. HapticKindle runs *
|
f@0
|
129 * on its own thread and delegates the real commands implementation to this class */
|
f@0
|
130 private static class CommandImplementation implements HapticListener{
|
f@0
|
131 @Override
|
f@0
|
132 public void executeCommand(HapticListenerCommand cmd, int ID, double x,
|
f@0
|
133 double y, double startX, double startY) {
|
f@0
|
134 final EditorFrame frame = DiagramEditorApp.getFrame();
|
f@0
|
135 switch(cmd){
|
f@0
|
136 case PLAY_ELEMENT_SOUND : {
|
f@0
|
137 if((frame == null)||(frame.getActiveTab() == null))
|
f@0
|
138 return;
|
f@0
|
139 CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
|
f@0
|
140 DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
|
f@0
|
141 /* can be null if the tab has been switched or closed in the meantime */
|
f@0
|
142 if(de == null)
|
f@0
|
143 return;
|
f@0
|
144 SoundFactory.getInstance().play(de.getSound());
|
f@0
|
145 }break;
|
f@0
|
146 case PLAY_ELEMENT_SPEECH : {
|
f@0
|
147 if((frame == null)||(frame.getActiveTab() == null))
|
f@0
|
148 return;
|
f@0
|
149 CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
|
f@0
|
150 DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
|
f@0
|
151 if(de == null)
|
f@0
|
152 return;
|
f@0
|
153 SoundFactory.getInstance().play(de.getSound());
|
f@0
|
154 NarratorFactory.getInstance().speak(de.getName());
|
f@0
|
155 iLog("touch",((de instanceof Node) ? "node " : "edge ")+de.getName());
|
f@0
|
156 }break;
|
f@0
|
157 case SELECT : {
|
f@0
|
158 if((frame == null)||(frame.getActiveTab() == null))
|
f@0
|
159 return;
|
f@0
|
160 CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
|
f@0
|
161 DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
|
f@0
|
162 if(de == null)
|
f@0
|
163 return;
|
f@0
|
164 frame.selectHapticHighligh(de);
|
f@0
|
165 }break;
|
f@0
|
166 case UNSELECT : {
|
f@0
|
167 if((frame == null)||(frame.getActiveTab() == null))
|
f@0
|
168 return;
|
f@0
|
169 frame.selectHapticHighligh(null);
|
f@0
|
170 }break;
|
f@0
|
171 case MOVE : {
|
f@0
|
172 /* when this block is executed we already have the lock *
|
f@0
|
173 * on the element from the PICK_UP command execution */
|
f@0
|
174 if((frame == null)||(frame.getActiveTab() == null))
|
f@0
|
175 return;
|
f@0
|
176 CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
|
f@0
|
177 DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
|
f@0
|
178 if(de == null)
|
f@0
|
179 return;
|
f@0
|
180 DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater();
|
f@0
|
181 if(de instanceof Node){
|
f@0
|
182 Node n = (Node)de;
|
f@0
|
183 Rectangle2D bounds = n.getBounds();
|
f@0
|
184 Point2D.Double p = new Point2D.Double(bounds.getCenterX(),bounds.getCenterY());
|
f@0
|
185 double dx = x - p.getX();
|
f@0
|
186 double dy = y - p.getY();
|
f@0
|
187 n.getMonitor().lock();
|
f@0
|
188 modelUpdater.translate(n, p, dx, dy,DiagramEventSource.HAPT);
|
f@0
|
189 modelUpdater.stopMove(n,DiagramEventSource.HAPT);
|
f@0
|
190 n.getMonitor().unlock();
|
f@0
|
191
|
f@0
|
192 StringBuilder builder = new StringBuilder();
|
f@0
|
193 builder.append(DiagramElement.toLogString(n)).append(" ").append(p.getX())
|
f@0
|
194 .append(' ').append(p.getY());
|
f@0
|
195 iLog("move node start",builder.toString());
|
f@0
|
196 builder = new StringBuilder();
|
f@0
|
197 builder.append(DiagramElement.toLogString(n)).append(' ')
|
f@0
|
198 .append(x).append(' ').append(y);
|
f@0
|
199 iLog("move node end",builder.toString());
|
f@0
|
200 }else{
|
f@0
|
201 Edge e = (Edge)de;
|
f@0
|
202 modelUpdater.startMove(e, new Point2D.Double(startX,startY),DiagramEventSource.HAPT);
|
f@0
|
203 Point2D p = new Point2D.Double(x,y);
|
f@0
|
204 e.getMonitor().lock();
|
f@0
|
205 modelUpdater.bend(e, p,DiagramEventSource.HAPT);
|
f@0
|
206 modelUpdater.stopMove(e,DiagramEventSource.HAPT);
|
f@0
|
207 e.getMonitor().unlock();
|
f@0
|
208
|
f@0
|
209 StringBuilder builder = new StringBuilder();
|
f@0
|
210 builder.append(DiagramElement.toLogString(e)).append(' ').append(startX)
|
f@0
|
211 .append(' ').append(startY);
|
f@0
|
212 iLog("bend edge start",builder.toString());
|
f@0
|
213 builder = new StringBuilder();
|
f@0
|
214 builder.append(DiagramElement.toLogString(e)).append(' ')
|
f@0
|
215 .append(x).append(' ').append(y);
|
f@0
|
216 iLog("bend edge end",builder.toString());
|
f@0
|
217 }
|
f@0
|
218 modelUpdater.yieldLock(de,
|
f@0
|
219 Lock.MOVE,
|
f@0
|
220 new DiagramEventActionSource(
|
f@0
|
221 DiagramEventSource.HAPT,
|
f@0
|
222 de instanceof Node ? Command.Name.STOP_NODE_MOVE : Command.Name.STOP_EDGE_MOVE,
|
f@0
|
223 de.getId(),
|
f@0
|
224 de.getName()
|
f@0
|
225 ));
|
f@0
|
226 SoundFactory.getInstance().play(SoundEvent.HOOK_OFF);
|
f@0
|
227 }break;
|
f@0
|
228 case INFO : {
|
f@0
|
229 if((frame == null)||(frame.getActiveTab() == null))
|
f@0
|
230 return;
|
f@0
|
231 CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
|
f@0
|
232 DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
|
f@0
|
233 if(de == null)
|
f@0
|
234 return;
|
f@0
|
235 SoundFactory.getInstance().stop();
|
f@0
|
236 NarratorFactory.getInstance().speak(de.detailedSpokenText());
|
f@0
|
237 iLog("request detailed info",((de instanceof Node) ? "node " : "edge ")+de.getName());
|
f@0
|
238 }break;
|
f@0
|
239 case PLAY_SOUND : {
|
f@0
|
240 switch(HapticListenerCommand.Sound.fromInt(ID) ){
|
f@0
|
241 case MAGNET_OFF :
|
f@0
|
242 SoundFactory.getInstance().play(SoundEvent.MAGNET_OFF);
|
f@0
|
243 iLog("sticky mode off","");
|
f@0
|
244 break;
|
f@0
|
245 case MAGNET_ON :
|
f@0
|
246 SoundFactory.getInstance().play(SoundEvent.MAGNET_ON);
|
f@0
|
247 iLog("sticky mode on","");
|
f@0
|
248 break;
|
f@0
|
249 case DRAG : SoundFactory.getInstance().play(SoundEvent.DRAG);
|
f@0
|
250 break;
|
f@0
|
251 }
|
f@0
|
252 }break;
|
f@0
|
253 case PICK_UP :{
|
f@0
|
254 if((frame == null)||(frame.getActiveTab() == null))
|
f@0
|
255 return;
|
f@0
|
256 CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
|
f@0
|
257 DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
|
f@0
|
258 if(de == null)
|
f@0
|
259 return;
|
f@0
|
260 DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater();
|
f@0
|
261 if(!modelUpdater.getLock(de,
|
f@0
|
262 Lock.MOVE,
|
f@0
|
263 new DiagramEventActionSource(DiagramEventSource.HAPT, de instanceof Edge ? Command.Name.TRANSLATE_EDGE : Command.Name.TRANSLATE_NODE ,de.getId(),de.getName()))){
|
f@0
|
264 iLog("Could not get lock on element for motion", DiagramElement.toLogString(de));
|
f@0
|
265 NarratorFactory.getInstance().speak("Object is being moved by another user");
|
f@0
|
266 return;
|
f@0
|
267 }
|
f@0
|
268 frame.hPickUp(de);
|
f@0
|
269 SoundFactory.getInstance().play(SoundEvent.HOOK_ON);
|
f@0
|
270 iLog("hook on","");
|
f@0
|
271 }break;
|
f@0
|
272 case ERROR : {
|
f@0
|
273 if((frame == null)||(frame.getActiveTab() == null))
|
f@0
|
274 return;
|
f@0
|
275 frame.backupOpenDiagrams();
|
f@0
|
276 NarratorFactory.getInstance().speak(ResourceBundle.getBundle(EditorFrame.class.getName()).getString("speech.haptic_device_crashed"));
|
f@0
|
277 }break;
|
f@0
|
278 }
|
f@0
|
279 }
|
f@0
|
280
|
f@0
|
281 private void iLog(String action, String args){
|
f@0
|
282 InteractionLog.log(INTERACTION_LOG_SOURCE,action,args);
|
f@0
|
283 }
|
f@0
|
284 }
|
f@0
|
285
|
f@0
|
286 }
|