Vuo  2.0.2
VuoInputEditorSession.cc
Go to the documentation of this file.
1 
10 #include "VuoInputEditorSession.hh"
11 
15 #include "VuoComposition.hh"
16 #include "VuoEditor.hh"
17 #include "VuoEditorComposition.hh"
18 #include "VuoEditorUtilities.hh"
19 #include "VuoInputEditor.hh"
20 #include "VuoInputEditorManager.hh"
24 
25 #include <json-c/json.h>
26 
31  VuoPublishedPortSidebar *sidebar, QMainWindow *window)
32 {
33  this->inputEditorManager = inputEditorManager;
34  this->composition = composition;
35  this->sidebar = sidebar;
36  this->window = window;
37  portWithOpenInputEditor = nullptr;
38 }
39 
45 map<VuoRendererPort *, pair<string, string> > VuoInputEditorSession::execute(VuoRendererPort *port, bool forwardTabTraversal)
46 {
47  // Show the initial input editor, plus any that are opened by tabbing from it.
48  // Tab-cycled input editor executions are stacked.
49 
50  showInputEditor(port, forwardTabTraversal);
51 
52  // Now that we're back to the bottom of the stack, collect all the changes that were made to input values.
53  // This is a temporary workaround required because of the stacking. It enables the caller to
54  // merge the entire tab cycle's worth of editing into a single Undo command to prevent individual
55  // Undo commands from being issued out of order.
56 
57  composition->setIgnoreApplicationStateChangeEvents(false);
58 
59  map<VuoRendererPort *, pair<string, string> > startAndFinalValues;
60  for (auto i : startValueForPort)
61  startAndFinalValues[i.first].first = i.second;
62  for (auto i : finalValueForPort)
63  startAndFinalValues[i.first].second = i.second;
64 
65  // Simulate a mouse release event, since the input editor might have stolen it.
66  {
67  QGraphicsSceneMouseEvent releaseEvent(QEvent::GraphicsSceneMouseRelease);
68  releaseEvent.setScenePos(firstPortLeftCenterGlobal);
69  releaseEvent.setButton(Qt::LeftButton);
70  QApplication::sendEvent(this, &releaseEvent);
71 
72  QCoreApplication::processEvents();
73  }
74 
75  return startAndFinalValues;
76 }
77 
81 void VuoInputEditorSession::showInputEditor(VuoRendererPort *port, bool forwardTabTraversal)
82 {
83  VuoInputEditor *inputEditor = nullptr;
84  VuoType *type = port->getDataType();
85 
86  string originalValueAsString;
87  json_object *details = nullptr;
88 
89  VuoRendererPublishedPort *publishedPort = dynamic_cast<VuoRendererPublishedPort *>(port);
90  if (publishedPort)
91  {
92  VuoCompilerPublishedPort *publishedInputPort = static_cast<VuoCompilerPublishedPort *>( publishedPort->getBase()->getCompiler() );
93  originalValueAsString = publishedInputPort->getInitialValue();
94 
95  bool isPublishedInput = !port->getInput();
96  details = static_cast<VuoCompilerPublishedPort *>(publishedPort->getBase()->getCompiler())->getDetails(isPublishedInput);
97  }
98  else
99  {
100  originalValueAsString = port->getConstantAsString();
102  if (portClass)
103  details = portClass->getDataClass()->getDetails();
104  }
105 
106  inputEditor = inputEditorManager->newInputEditor(type, details);
107  if (! inputEditor)
108  return;
109 
110  inputEditor->setParent(window);
111 
112  inputEditor->installEventFilter(this);
113 
114  QPoint portCenterGlobal;
115  if (publishedPort)
116  {
117  portCenterGlobal = sidebar->getGlobalPosOfPublishedPort(publishedPort);
118  }
119  else
120  {
121  QPoint portCenterInScene = port->scenePos().toPoint();
122  QPoint portCenterInView = composition->views()[0]->mapFromScene(portCenterInScene);
123  portCenterGlobal = composition->views()[0]->mapToGlobal(portCenterInView);
124  }
125  QPoint portLeftCenterGlobal = portCenterGlobal - QPoint(port->getPortRect().width()/2., 0);
126 
127  if (startValueForPort.empty())
128  firstPortLeftCenterGlobal = portLeftCenterGlobal;
129 
130  json_object *originalValue = json_tokener_parse(originalValueAsString.c_str());
131 
132  VuoRendererNode *parentNode = port->getRenderedParentNode();
133  vector<pair<QString, json_object *> > portConstants;
134  if (parentNode)
135  portConstants = parentNode->getConstantPortValues();
136 
137  composition->disableNondetachedPortPopovers();
138 
139  if (!publishedPort)
140  composition->enablePopoverForPort(port);
141 
142  portWithOpenInputEditor = port;
143 
144  connect(inputEditor, SIGNAL(valueChanged(json_object *)), this, SLOT(updateValueForEditedPort(json_object *)));
145 
146  // @todo: https://b33p.net/kosada/node/9091
147  if (!publishedPort)
148  {
149  connect(inputEditor, &VuoInputEditor::tabbedBackwardPastFirstWidget, this, &VuoInputEditorSession::showPreviousInputEditor);
150  connect(inputEditor, &VuoInputEditor::tabbedPastLastWidget, this, &VuoInputEditorSession::showNextInputEditor);
151  }
152 
153  // Only the first input editor invoked for a given port within an editing session
154  // gets to record the port's initial value.
155  if (startValueForPort.find(port) == startValueForPort.end())
156  {
157  startValueForPort[port] = originalValueAsString;
158 
159  // Ignore application state change events delivered as a result of showing the input editor.
160  // Otherwise, detached port popovers respond to the change in state and break the tab cycle.
161  composition->setIgnoreApplicationStateChangeEvents(true);
162  }
163 
164  if (!details || json_object_get_type(details) != json_type_object)
165  details = json_object_new_object();
166 
167  // If we're opening an input editor for a list item, propagate range-related details from the list drawer's host port.
169  if (listDrawer)
170  {
171  json_object *hostDetails = nullptr;
172  VuoPort *hostPort = listDrawer->getRenderedHostPort();
173  VuoCompilerInputEventPortClass *hostPortClass = dynamic_cast<VuoCompilerInputEventPortClass *>(hostPort->getClass()->getCompiler());
174  if (hostPortClass && hostPortClass->getDataClass())
175  hostDetails = hostPortClass->getDataClass()->getDetails();
176 
177  if (hostDetails)
178  {
179  // "suggestedMin"
180  json_object *suggestedMinValue = nullptr;
181  if (json_object_object_get_ex(hostDetails, "suggestedMin", &suggestedMinValue))
182  json_object_object_add(details, "suggestedMin", json_object_get(suggestedMinValue));
183 
184  // "suggestedMax"
185  json_object *suggestedMaxValue = nullptr;
186  if (json_object_object_get_ex(hostDetails, "suggestedMax", &suggestedMaxValue))
187  json_object_object_add(details, "suggestedMax", json_object_get(suggestedMaxValue));
188 
189  // "suggestedStep"
190  json_object *suggestedStepValue = nullptr;
191  if (json_object_object_get_ex(hostDetails, "suggestedStep", &suggestedStepValue))
192  json_object_object_add(details, "suggestedStep", json_object_get(suggestedStepValue));
193 
194  // Port details not propagated (future work?): default, auto, autoSupersedesDefault
195  }
196  }
197 
198  // Embed some interface settings in the port details object passed to the input editor.
199  VuoEditor *editor = static_cast<VuoEditor *>(qApp);
200  json_object_object_add(details, "isDark", json_object_new_boolean(editor->isInterfaceDark()));
201  json_object_object_add(details, "forwardTabTraversal", json_object_new_boolean(forwardTabTraversal));
202 
203  map<QString, json_object *> portNameToConstantValueMap(portConstants.begin(), portConstants.end());
204  json_object *newValue = inputEditor->show(portLeftCenterGlobal, originalValue, details, portNameToConstantValueMap);
205  string newValueAsString = json_object_to_json_string_ext(newValue, JSON_C_TO_STRING_PLAIN);
206 
207  // Only the final input editor invoked (i.e., the first to finish executing) for a given port
208  // within an editing session gets to record the port's final value.
209  if (finalValueForPort.find(port) == finalValueForPort.end())
210  finalValueForPort[port] = newValueAsString;
211 
212  disconnect(inputEditor, 0, this, 0);
213  inputEditor->deleteLater();
214 
215  composition->disableNondetachedPortPopovers();
216 }
217 
223 void VuoInputEditorSession::showPreviousInputEditor()
224 {
225  // If we ever allow multiple input editors to be open at once, we will need
226  // to do something more sophisticated to determine the reference port. For now,
227  // assume that it is the port that most recently had its input editor invoked.
228  if (! portWithOpenInputEditor)
229  return;
230 
231  VuoRendererPort *referencePort = portWithOpenInputEditor;
232  QString portName = referencePort->getBase()->getClass()->getName().c_str();
233  VuoRendererNode *parentNode = referencePort->getRenderedParentNode();
234 
235  // Exit the tab cycle if the user is tabbing away from a port whose constant value changes
236  // trigger modifications to the composition structure. These changes must be pushed onto the
237  // 'Undo' stack at the same time as the constant value change, and that does not currently
238  // happen until the input editor tab cycle exits.
239  if (composition->requiresStructuralChangesAfterValueChangeAtPort(referencePort))
240  return;
241 
242  vector<pair<QString, json_object *> > portConstants = parentNode->getConstantPortValues();
243 
244  // Filter out ports whose input editors don't emit @c tabbedPastLastWidget() and/or
245  // @c tabbedBackwardPastFirstWidget() signals. These input editors break the tab cycle.
246  vector<QString> portsWithEligibleInputEditors;
247  for (vector<pair<QString, json_object *> >::iterator i = portConstants.begin(); i != portConstants.end(); ++i)
248  {
249  VuoRendererPort *currentPort = parentNode->getBase()->getInputPortWithName(i->first.toUtf8().constData())->getRenderer();
250  json_object *currentDetails = nullptr;
251  VuoCompilerInputEventPortClass *currentPortClass = dynamic_cast<VuoCompilerInputEventPortClass *>(currentPort->getBase()->getClass()->getCompiler());
252  if (currentPortClass)
253  currentDetails = currentPortClass->getDataClass()->getDetails();
254  VuoInputEditor *currentPortDataEditor = inputEditorManager->newInputEditor(currentPort->getDataType(), currentDetails);
255  if (currentPortDataEditor)
256  {
257  if (currentPortDataEditor->supportsTabbingBetweenPorts())
258  portsWithEligibleInputEditors.push_back(i->first);
259 
260  currentPortDataEditor->deleteLater();
261  }
262  }
263 
264  if (portsWithEligibleInputEditors.empty())
265  return;
266 
267  QString previousPortName = "";
268  bool referencePortJustTraversed = false;
269  for (vector<QString>::reverse_iterator ri = portsWithEligibleInputEditors.rbegin(); ri != portsWithEligibleInputEditors.rend(); ++ri)
270  {
271  QString currentPortName = *ri;
272 
273  if (referencePortJustTraversed)
274  {
275  previousPortName = currentPortName;
276  referencePortJustTraversed = false;
277  break;
278  }
279 
280  referencePortJustTraversed = (currentPortName == portName);
281  }
282 
283  if (referencePortJustTraversed)
284  previousPortName = portsWithEligibleInputEditors.back();
285 
286  VuoRendererPort *previousPort = parentNode->getBase()->getInputPortWithName(previousPortName.toUtf8().constData())->getRenderer();
287  showInputEditor(previousPort, false);
288 }
289 
295 void VuoInputEditorSession::showNextInputEditor()
296 {
297  // If we ever allow multiple input editors to be open at once, we will need
298  // to do something more sophisticated to determine the reference port. For now,
299  // assume that it is the port that most recently had its input editor invoked.
300  if (! portWithOpenInputEditor)
301  return;
302 
303  VuoRendererPort *referencePort = portWithOpenInputEditor;
304  QString portName = referencePort->getBase()->getClass()->getName().c_str();
305  VuoRendererNode *parentNode = referencePort->getRenderedParentNode();
306 
307  // Exit the tab cycle if the user is tabbing away from a port whose constant value changes
308  // trigger modifications to the composition structure. These changes must be pushed onto the
309  // 'Undo' stack at the same time as the constant value change, and that does not currently
310  // happen until the input editor tab cycle exits.
311  if (composition->requiresStructuralChangesAfterValueChangeAtPort(referencePort))
312  return;
313 
314  vector<pair<QString, json_object *> > portConstants = parentNode->getConstantPortValues();
315 
316  // Filter out ports whose input editors don't emit @c tabbedPastLastWidget() and/or
317  // @c tabbedBackwardPastFirstWidget() signals. These input editors break the tab cycle.
318  vector<QString> portsWithEligibleInputEditors;
319  for (vector<pair<QString, json_object *> >::iterator i = portConstants.begin(); i != portConstants.end(); ++i)
320  {
321  VuoRendererPort *currentPort = parentNode->getBase()->getInputPortWithName(i->first.toUtf8().constData())->getRenderer();
322  json_object *currentDetails = nullptr;
323  VuoCompilerInputEventPortClass *currentPortClass = dynamic_cast<VuoCompilerInputEventPortClass *>(currentPort->getBase()->getClass()->getCompiler());
324  if (currentPortClass)
325  currentDetails = currentPortClass->getDataClass()->getDetails();
326  VuoInputEditor *currentPortDataEditor = inputEditorManager->newInputEditor(currentPort->getDataType(), currentDetails);
327  if (currentPortDataEditor)
328  {
329  if (currentPortDataEditor->supportsTabbingBetweenPorts())
330  portsWithEligibleInputEditors.push_back(i->first);
331 
332  currentPortDataEditor->deleteLater();
333  }
334  }
335 
336  if (portsWithEligibleInputEditors.empty())
337  return;
338 
339  QString nextPortName = "";
340  bool referencePortJustTraversed = false;
341  foreach (QString currentPortName, portsWithEligibleInputEditors)
342  {
343  if (referencePortJustTraversed)
344  {
345  nextPortName = currentPortName;
346  referencePortJustTraversed = false;
347  break;
348  }
349 
350  referencePortJustTraversed = (currentPortName == portName);
351  }
352 
353  if (referencePortJustTraversed)
354  nextPortName = portsWithEligibleInputEditors.front();
355 
356  VuoRendererPort *nextPort = parentNode->getBase()->getInputPortWithName(nextPortName.toUtf8().constData())->getRenderer();
357  showInputEditor(nextPort, true);
358 }
359 
364 void VuoInputEditorSession::updateValueForEditedPort(json_object *newValue)
365 {
366  if (! portWithOpenInputEditor)
367  return;
368 
369  // Disable real-time constant value updates while input editing is in progress if changes to this port's
370  // constant value trigger modifications to the composition structure. These changes must be pushed onto the
371  // 'Undo' stack at the same time as the constant value change, and that does not currently
372  // happen until the input editor tab cycle exits.
373  if (composition->requiresStructuralChangesAfterValueChangeAtPort(portWithOpenInputEditor))
374  return;
375 
376  string newValueAsString = json_object_to_json_string_ext(newValue, JSON_C_TO_STRING_PLAIN);
377  if (newValueAsString != portWithOpenInputEditor->getConstantAsString())
378  {
379  VuoCompilerPort *compilerPort = static_cast<VuoCompilerPort *>(portWithOpenInputEditor->getBase()->getCompiler());
380  composition->updatePortConstant(compilerPort, newValueAsString);
381  }
382 }
383 
387 bool VuoInputEditorSession::eventFilter(QObject *object, QEvent *event)
388 {
389  // Filter drag-and-drop-related events destined for VuoText input editors:
390  // Modify dropped URLs so that (in the absence of keyboard modifiers) the paths are relative to
391  // the composition's storage directory; pass the modified drop event back to the input editor.
392  if (object->metaObject() && !strcmp(object->metaObject()->className(), "VuoInputEditorText"))
393  {
394  if (event->type() == QEvent::Drop)
395  {
396  QDropEvent *filteredDropEvent = static_cast<QDropEvent *>(event);
397 
398  // Retrieve the composition directory so that file paths may be specified relative to it.
399  // Note: Providing the directory's canonical path as the argument to the
400  // QDir constructor is necessary in order for QDir::relativeFilePath() to
401  // work correctly when the non-canonical path contains symbolic links (e.g.,
402  // '/tmp' -> '/private/tmp' for example compositions).
403  QDir compositionDir(QDir(composition->getBase()->getDirectory().c_str()).canonicalPath());
404 
405  // Use the absolute file path if the "Option" key was pressed.
406  bool useAbsoluteFilePaths = VuoEditorUtilities::optionKeyPressedForEvent(event);
407 
408  const QMimeData *mimeData = filteredDropEvent->mimeData();
409  QList<QUrl> urls = mimeData->urls();
410  if (urls.size() == 1)
411  {
412  QString filePath = (useAbsoluteFilePaths? urls[0].path() : compositionDir.relativeFilePath(urls[0].path()));
413 
414  QList<QUrl> modifiedUrls;
415  modifiedUrls.append(filePath);
416  QMimeData *modifiedMimeData = new QMimeData();
417  modifiedMimeData->setUrls(modifiedUrls);
418 
419  filteredDropEvent->accept();
420 
421  QDropEvent *modifiedDropEvent = new QDropEvent(
422  filteredDropEvent->pos(),
423  filteredDropEvent->possibleActions(),
424  modifiedMimeData,
425  filteredDropEvent->mouseButtons(),
426  filteredDropEvent->keyboardModifiers(),
427  filteredDropEvent->type());
428 
429  object->removeEventFilter(this);
430  QApplication::sendEvent(object, modifiedDropEvent);
431  object->installEventFilter(this);
432  }
433  else
434  event->ignore();
435 
436  return true;
437  }
438 
439  else if (event->type() == QEvent::DragEnter)
440  {
441  QDropEvent *filteredDragEnterEvent = static_cast<QDragEnterEvent *>(event);
442  const QMimeData *mimeData = filteredDragEnterEvent->mimeData();
443 
444  // Accept drags of single files.
445  if (mimeData->hasFormat("text/uri-list"))
446  {
447  QList<QUrl> urls = mimeData->urls();
448  if (urls.size() == 1)
449  event->accept();
450  else
451  filteredDragEnterEvent->setDropAction(Qt::IgnoreAction);
452  }
453  else
454  filteredDragEnterEvent->setDropAction(Qt::IgnoreAction);
455 
456  return true;
457  }
458 
459  else if (event->type() == QEvent::DragMove)
460  {
461  event->accept();
462  return true;
463  }
464 
465  else if (event->type() == QEvent::DragLeave)
466  {
467  (static_cast<QDragLeaveEvent *>(event))->accept();
468  return true;
469  }
470  }
471 
472  return QObject::eventFilter(object, event);
473 }