Vuo 2.4.4
Loading...
Searching...
No Matches
VuoInputEditorSession.cc
Go to the documentation of this file.
1
11
15#include "VuoComposition.hh"
16#include "VuoEditor.hh"
18#include "VuoEditorUtilities.hh"
19#include "VuoInputEditor.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
45map<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
81void 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
223void 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
295void 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
364void 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
387bool 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}