1
/***************************************************************************
2
* Copyright (c) 2015 Victor Titov (DeepSOIC) <vv.titov@gmail.com> *
4
* This file is part of the FreeCAD CAx development system. *
6
* This library is free software; you can redistribute it and/or *
7
* modify it under the terms of the GNU Library General Public *
8
* License as published by the Free Software Foundation; either *
9
* version 2 of the License, or (at your option) any later version. *
11
* This library is distributed in the hope that it will be useful, *
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
* GNU Library General Public License for more details. *
16
* You should have received a copy of the GNU Library General Public *
17
* License along with this library; see the file COPYING.LIB. If not, *
18
* write to the Free Software Foundation, Inc., 59 Temple Place, *
19
* Suite 330, Boston, MA 02111-1307, USA *
21
***************************************************************************
23
* Minor modifications made by Pablo Gil (pablogil) in order to create *
24
* a Maya or Unity 3D mouse navigation style: *
25
* ALT + left mouse button = orbit *
26
* ALT + right mouse button = zoom *
27
* ALT + middle mouse button = pan *
29
* Thanks Victor for your help! *
31
***************************************************************************/
34
*A few notes on this style. (by DeepSOIC)
36
* In this style, LMB serves dual purpose. It is selecting objects, as well as
37
* spinning the view. The trick that enables it is as follows: The mousedown
38
* event is consumed an saved, but otherwise remains unprocessed. If a drag is
39
* detected while the button is down, the event is finally consumed (the saved
40
* one is discarded), and spinning starts. If there is no drag detected before
41
* the button is released, the saved mousedown is propagated to inherited,
42
* followed by the mouseup. The same trick is used for RMB, so up to two
43
* mousedown can be postponed.
45
* This navigation style does not exactly follow the structure of other
46
* navigation styles, it does not fill many of the global variables defined in
49
* This mode does not support locking cursor position on screen when
50
* navigating, since with absolute pointing devices like pen and touch it makes
51
* no sense (this style was specifically crafted for such devices).
53
* In this style, setViewing is not used (because I could not figure out how to
54
* use it properly, and it seems to just work without it).
56
* This style wasn't tested with space during development (I don't have one).
59
#include "PreCompiled.h"
61
# include <QApplication>
64
#include <Base/Console.h>
66
#include "NavigationStyle.h"
67
#include "SoTouchEvents.h"
68
#include "View3DInventorViewer.h"
73
// ----------------------------------------------------------------------------------
75
/* TRANSLATOR Gui::MayaGestureNavigationStyle */
77
TYPESYSTEM_SOURCE(Gui::MayaGestureNavigationStyle, Gui::UserNavigationStyle)
79
MayaGestureNavigationStyle::MayaGestureNavigationStyle()
81
mouseMoveThreshold = QApplication::startDragDistance();
82
mouseMoveThresholdBroken = false;
83
mousedownConsumedCount = 0;
84
thisClickIsComplex = false;
88
MayaGestureNavigationStyle::~MayaGestureNavigationStyle() = default;
90
const char* MayaGestureNavigationStyle::mouseButtons(ViewerMode mode)
93
case NavigationStyle::SELECTION:
94
return QT_TR_NOOP("Tap OR click left mouse button.");
95
case NavigationStyle::PANNING:
96
return QT_TR_NOOP("Drag screen with two fingers OR press ALT + middle mouse button.");
97
case NavigationStyle::DRAGGING:
98
return QT_TR_NOOP("Drag screen with one finger OR press ALT + left mouse button. In Sketcher and other edit modes, hold Alt in addition.");
99
case NavigationStyle::ZOOMING:
100
return QT_TR_NOOP("Pinch (place two fingers on the screen and drag them apart from or towards each other) OR scroll middle mouse button OR press ALT + right mouse button OR PgUp/PgDown on keyboard.");
102
return "No description";
107
* \brief MayaGestureNavigationStyle::testMoveThreshold tests if the mouse has moved far enough to constder it a drag.
108
* \param currentPos current position of mouse cursor, in local pixel coordinates.
109
* \return true if the mouse was moved far enough. False if it's within the boundary. Ignores MayaGestureNavigationStyle::mouseMoveThresholdBroken flag.
111
bool MayaGestureNavigationStyle::testMoveThreshold(const SbVec2s currentPos) const {
112
SbVec2s movedBy = currentPos - this->mousedownPos;
113
return SbVec2f(movedBy).length() >= this->mouseMoveThreshold;
116
SbBool MayaGestureNavigationStyle::processSoEvent(const SoEvent * const ev)
118
// Events when in "ready-to-seek" mode are ignored, except those
119
// which influence the seek mode itself -- these are handled further
120
// up the inheritance hierarchy.
121
if (this->isSeekMode()) {
122
return inherited::processSoEvent(ev);
124
// Switch off viewing mode (Bug #0000911)
125
if (!this->isSeekMode()&& !this->isAnimating() && this->isViewing() )
126
this->setViewing(false); // by default disable viewing mode to render the scene
127
//setViewing() is never used in this style, so the previous if is very unlikely to be hit.
129
const SoType type(ev->getTypeId());
130
//define some shortcuts...
131
bool evIsButton = type.isDerivedFrom(SoMouseButtonEvent::getClassTypeId());
132
bool evIsKeyboard = type.isDerivedFrom(SoKeyboardEvent::getClassTypeId());
133
bool evIsLoc2 = type.isDerivedFrom(SoLocation2Event::getClassTypeId());//mouse movement
134
bool evIsLoc3 = type.isDerivedFrom(SoMotion3Event::getClassTypeId());//spaceball/joystick movement
135
bool evIsGesture = type.isDerivedFrom(SoGestureEvent::getClassTypeId());//touchscreen gesture
137
const SbVec2f prevnormalized = this->lastmouseposition;
138
const SbVec2s pos(ev->getPosition());//not valid for gestures
139
const SbVec2f posn = this->normalizePixelPos(pos);
140
//pos: local coordinates of event, in pixels
141
//posn: normalized local coordinates of event ((0,0) = lower left corner, (1,1) = upper right corner)
142
float ratio = viewer->getSoRenderManager()->getViewportRegion().getViewportAspectRatio();
144
if (evIsButton || evIsLoc2){
145
this->lastmouseposition = posn;
148
const ViewerMode curmode = this->currentmode;
149
//ViewerMode newmode = curmode;
151
//make a unified mouse+modifiers state value (combo)
153
BUTTON1DOWN = 1 << 0,
154
BUTTON2DOWN = 1 << 1,
155
BUTTON3DOWN = 1 << 2,
159
MASKBUTTONS = BUTTON1DOWN | BUTTON2DOWN | BUTTON3DOWN,
160
MASKMODIFIERS = CTRLDOWN | SHIFTDOWN | ALTDOWN
162
unsigned int comboBefore = //before = state before this event
163
(this->button1down ? BUTTON1DOWN : 0) |
164
(this->button2down ? BUTTON2DOWN : 0) |
165
(this->button3down ? BUTTON3DOWN : 0) |
166
(this->ctrldown ? CTRLDOWN : 0) |
167
(this->shiftdown ? SHIFTDOWN : 0) |
168
(this->altdown ? ALTDOWN : 0);
170
//test for complex clicks
171
int cntMBBefore = (comboBefore & BUTTON1DOWN ? 1 : 0 ) //cntMBBefore = how many buttons were down when this event arrived?
172
+(comboBefore & BUTTON2DOWN ? 1 : 0 )
173
+(comboBefore & BUTTON3DOWN ? 1 : 0 );
174
if (cntMBBefore>=2) this->thisClickIsComplex = true;
175
if (cntMBBefore==0) {//a good chance to reset some click-related stuff
176
this->thisClickIsComplex = false;
177
this->mousedownConsumedCount = 0;//shouldn't be necessary, just a fail-safe.
180
// Mismatches in state of the modifier keys happens if the user
181
// presses or releases them outside the viewer window.
182
syncModifierKeys(ev);
183
//before this block, mouse button states in NavigationStyle::buttonXdown reflected those before current event arrived.
184
//track mouse button states
186
auto const event = (const SoMouseButtonEvent *) ev;
187
const int button = event->getButton();
188
const SbBool press //the button was pressed (if false -> released)
189
= event->getState() == SoButtonEvent::DOWN ? true : false;
191
case SoMouseButtonEvent::BUTTON1:
192
this->button1down = press;
194
case SoMouseButtonEvent::BUTTON2:
195
this->button2down = press;
197
case SoMouseButtonEvent::BUTTON3:
198
this->button3down = press;
200
//whatever else, we don't track
203
//after this block, the new states of the buttons are already in.
205
unsigned int comboAfter = //after = state after this event (current, essentially)
206
(this->button1down ? BUTTON1DOWN : 0) |
207
(this->button2down ? BUTTON2DOWN : 0) |
208
(this->button3down ? BUTTON3DOWN : 0) |
209
(this->ctrldown ? CTRLDOWN : 0) |
210
(this->shiftdown ? SHIFTDOWN : 0) |
211
(this->altdown ? ALTDOWN : 0);
213
//test for complex clicks (again)
214
int cntMBAfter = (comboAfter & BUTTON1DOWN ? 1 : 0 ) //cntMBAfter = how many buttons were down when this event arrived?
215
+(comboAfter & BUTTON2DOWN ? 1 : 0 )
216
+(comboAfter & BUTTON3DOWN ? 1 : 0 );
217
if (cntMBAfter>=2) this->thisClickIsComplex = true;
218
//if (cntMBAfter==0) this->thisClickIsComplex = false;//don't reset the flag now, we need to know that this mouseUp was an end of a click that was complex. The flag will reset by the before-check in the next event.
220
//test for move detection
221
if (evIsLoc2 || evIsButton){
222
this->mouseMoveThresholdBroken |= this->testMoveThreshold(pos);
227
auto gesture = static_cast<const SoGestureEvent*>(ev);
228
switch(gesture->state) {
229
case SoGestureEvent::SbGSStart:
230
//assert(!inGesture);//start of another gesture before the first finished? Happens all the time for Pan gesture... No idea why! --DeepSOIC
233
case SoGestureEvent::SbGSUpdate:
234
assert(inGesture);//gesture update without start?
237
case SoGestureEvent::SbGSEnd:
238
assert(inGesture);//gesture ended without starting?
241
case SoGestureEvent::SbGsCanceled:
242
assert(inGesture);//gesture canceled without starting?
246
assert(0);//shouldn't happen
252
inGesture = false;//reset the flag when mouse clicks are received, to ensure enabling mouse navigation back.
253
setViewingMode(NavigationStyle::SELECTION);//exit navigation asap, to proceed with regular processing of the click
257
bool suppressLMBDrag = false;
258
if(viewer->isEditing()){
259
//in edit mode, disable lmb dragging (spinning). Holding Alt enables it.
260
suppressLMBDrag = !(comboAfter & ALTDOWN);
263
//----------all this were preparations. Now comes the event handling! ----------
265
SbBool processed = false;//a return value for the BlahblahblahNavigationStyle::processSoEvent
266
bool propagated = false;//an internal flag indicating that the event has been already passed to inherited, to suppress the automatic doing of this at the end.
267
//goto finalize = return processed. Might be important to do something before done (none now).
269
// give the nodes in the foreground root the chance to handle events (e.g color bar)
270
if (!viewer->isEditing()) {
271
processed = handleEventInForeground(ev);
276
// Mode-independent keyboard handling
278
auto const event = (const SoKeyboardEvent *) ev;
279
const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;
280
switch (event->getKey()) {
281
case SoKeyboardEvent::H:
284
SbBool ret = NavigationStyle::lookAtPoint(event->getPosition());
286
this->interactiveCountDec();
288
"No object under cursor! Can't set new center of rotation.\n");
299
//mode-independent spaceball/joystick handling
301
auto const event = static_cast<const SoMotion3Event *>(ev);
303
this->processMotionEvent(event);
309
//all mode-dependent stuff is within this switch.
311
case NavigationStyle::SELECTION:
312
// Prevent interrupting rubber-band selection in sketcher
313
if (viewer->isEditing()) {
315
auto const event = (const SoMouseButtonEvent*)ev;
316
const SbBool press = event->getState() == SoButtonEvent::DOWN;
317
const int button = event->getButton();
319
if (!press && button == SoMouseButtonEvent::BUTTON1) {
320
setViewingMode(NavigationStyle::IDLE);
325
if (this->button1down) {
330
case NavigationStyle::IDLE:
331
// Prevent interrupting rubber-band selection in sketcher
332
if (viewer->isEditing()) {
334
auto const event = (const SoMouseButtonEvent*)ev;
335
const SbBool press = event->getState() == SoButtonEvent::DOWN;
336
const int button = event->getButton();
338
if (press && button == SoMouseButtonEvent::BUTTON1 && !this->altdown) {
339
setViewingMode(NavigationStyle::SELECTION);
345
case NavigationStyle::INTERACT: {
346
//idle and interaction
350
auto const event = (const SoKeyboardEvent *) ev;
351
const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;
353
switch(event->getKey()){
354
case SoKeyboardEvent::S:
355
case SoKeyboardEvent::HOME:
356
case SoKeyboardEvent::LEFT_ARROW:
357
case SoKeyboardEvent::UP_ARROW:
358
case SoKeyboardEvent::RIGHT_ARROW:
359
case SoKeyboardEvent::DOWN_ARROW:
360
processed = inherited::processSoEvent(ev);
363
case SoKeyboardEvent::PAGE_UP:
365
doZoom(viewer->getSoRenderManager()->getCamera(), getDelta(), posn);
369
case SoKeyboardEvent::PAGE_DOWN:
371
doZoom(viewer->getSoRenderManager()->getCamera(), -getDelta(), posn);
383
// Mouse Button / Spaceball Button handling
385
auto const event = (const SoMouseButtonEvent *) ev;
386
const int button = event->getButton();
387
const SbBool press //the button was pressed (if false -> released)
388
= event->getState() == SoButtonEvent::DOWN ? true : false;
390
case SoMouseButtonEvent::BUTTON1:
391
case SoMouseButtonEvent::BUTTON2:
393
if(this->thisClickIsComplex && this->mouseMoveThresholdBroken){
394
//this should prevent re-attempts to enter navigation when doing more clicks after a move.
396
//on LMB-down or RMB-down, we don't know yet if we should propagate it or process it. Save the event to be refired later, when it becomes clear.
397
//reset/start move detection machine
398
this->mousedownPos = pos;
399
this->mouseMoveThresholdBroken = false;
400
pan(viewer->getSoRenderManager()->getCamera());//set up panningplane
401
int &cnt = this->mousedownConsumedCount;
402
this->mousedownConsumedEvents[cnt] = *event;//hopefully, a shallow copy is enough. There are no pointers stored in events, apparently. Will lose a subclass, though.
405
if(cnt>static_cast<int>(sizeof(mousedownConsumedEvents))){
406
cnt=sizeof(mousedownConsumedEvents);//we are in trouble
408
processed = true;//just consume this event, and wait for the move threshold to be broken to start dragging/panning
411
if (button == SoMouseButtonEvent::BUTTON2 && !this->thisClickIsComplex) {
412
if (!viewer->isEditing() && this->isPopupMenuEnabled()) {
414
this->openPopupMenu(event->getPosition());
418
//re-synthesize all previously-consumed mouseDowns, if any. They might have been re-synthesized already when threshold was broken.
419
for( int i=0; i < this->mousedownConsumedCount; i++ ){
420
inherited::processSoEvent(& (this->mousedownConsumedEvents[i]));//simulate the previously-comsumed mousedown.
422
this->mousedownConsumedCount = 0;
423
processed = inherited::processSoEvent(ev);//explicitly, just for clarity that we are sending a full click sequence.
428
case SoMouseButtonEvent::BUTTON3://press the wheel
429
// starts PANNING mode
430
if(press & this->altdown){
431
setViewingMode(NavigationStyle::PANNING);
433
// if not PANNING then look at point
434
SbBool ret = NavigationStyle::lookAtPoint(event->getPosition());
436
this->interactiveCountDec();
438
"No object under cursor! Can't set new center of rotation.\n");
446
//mouse moves - test for move threshold breaking
448
if (this->mouseMoveThresholdBroken && (this->button1down || this->button2down) && mousedownConsumedCount > 0) {
449
//mousemovethreshold has JUST been broken
451
//test if we should enter navigation
452
if ((this->button1down && !suppressLMBDrag && this->altdown) || (this->button2down && this->altdown)) {
453
//yes, we are entering navigation.
454
//throw away consumed mousedowns.
455
this->mousedownConsumedCount = 0;
457
// start DRAGGING mode (orbit)
458
// if not pressing left mouse button then it assumes is right mouse button and starts ZOOMING mode
459
setRotationCenter(getFocalPoint());
460
setViewingMode(this->button1down ? NavigationStyle::DRAGGING : NavigationStyle::ZOOMING);
463
//no, we are not entering navigation.
464
//re-synthesize all previously-consumed mouseDowns, if any, and propagate this mousemove.
465
for( int i=0; i < this->mousedownConsumedCount; i++ ){
466
inherited::processSoEvent(& (this->mousedownConsumedEvents[i]));//simulate the previously-comsumed mousedown.
468
this->mousedownConsumedCount = 0;
469
processed = inherited::processSoEvent(ev);//explicitly, just for clarity that we are sending a full click sequence.
473
if (mousedownConsumedCount > 0)
474
processed = true;//if we are still deciding if it's a drag or not, consume mouseMoves.
478
if (evIsGesture && /*!this->button1down &&*/ !this->button2down){//ignore gestures when mouse buttons are down. Button1down check was disabled because of wrong state after doubleclick on sketcher constraint to edit datum
479
auto gesture = static_cast<const SoGestureEvent*>(ev);
480
if (gesture->state == SoGestureEvent::SbGSStart
481
|| gesture->state == SoGestureEvent::SbGSUpdate) {//even if we didn't get a start, assume the first update is a start (sort-of fail-safe).
482
if (type.isDerivedFrom(SoGesturePanEvent::getClassTypeId())) {
483
pan(viewer->getSoRenderManager()->getCamera());//set up panning plane
484
setViewingMode(NavigationStyle::PANNING);
486
} else if (type.isDerivedFrom(SoGesturePinchEvent::getClassTypeId())) {
487
pan(viewer->getSoRenderManager()->getCamera());//set up panning plane
488
setRotationCenter(getFocalPoint());
489
setViewingMode(NavigationStyle::DRAGGING);
491
} //all other gestures - ignore!
495
//loc2 (mousemove) - ignore.
497
} break;//end of idle and interaction
498
case NavigationStyle::DRAGGING:
499
case NavigationStyle::ZOOMING:
500
case NavigationStyle::PANNING:{
505
// Mouse Button / Spaceball Button handling
507
auto const event = (const SoMouseButtonEvent *) ev;
508
const int button = event->getButton();
510
case SoMouseButtonEvent::BUTTON1:
511
case SoMouseButtonEvent::BUTTON2:
512
case SoMouseButtonEvent::BUTTON3: // allows to release button3 into SELECTION mode
513
if(comboAfter & BUTTON1DOWN || comboAfter & BUTTON2DOWN) {
514
//don't leave navigation till all buttons have been released
515
setRotationCenter(getFocalPoint());
516
setViewingMode((comboAfter & BUTTON1DOWN) ? NavigationStyle::DRAGGING : NavigationStyle::PANNING);
518
} else { //all buttons are released
519
//end of dragging/panning/whatever
520
setViewingMode(NavigationStyle::SELECTION);
522
} //end of else (some buttons down)
527
//the essence part 1!
528
//mouse movement into camera motion. Suppress if in gesture. Ignore until threshold is surpassed.
529
if (evIsLoc2 && ! this->inGesture && this->mouseMoveThresholdBroken) {
530
if (curmode == NavigationStyle::ZOOMING) {//doesn't happen
531
this->zoomByCursor(posn, prevnormalized);
533
} else if (curmode == NavigationStyle::PANNING) {
534
panCamera(viewer->getSoRenderManager()->getCamera(), ratio, this->panningplane, posn, prevnormalized);
536
} else if (curmode == NavigationStyle::DRAGGING) {
537
if (comboAfter & BUTTON1DOWN && comboAfter & BUTTON2DOWN) {
538
//two mouse buttons down - tilting!
539
NavigationStyle::doRotate(viewer->getSoRenderManager()->getCamera(),
540
(posn - prevnormalized)[0]*(-2),
543
} else {//one mouse button - normal spinning
544
//this will also handle the single-finger drag (there's no gesture used, pseudomouse is enough)
545
//this->addToLog(event->getPosition(), event->getTime());
546
this->spin_simplified(viewer->getSoRenderManager()->getCamera(),
547
posn, prevnormalized);
553
//the essence part 2!
554
//gesture into camera motion
556
auto gesture = static_cast<const SoGestureEvent*>(ev);
558
if (gesture->state == SoGestureEvent::SbGSEnd) {
559
setViewingMode(NavigationStyle::SELECTION);
561
} else if (gesture->state == SoGestureEvent::SbGSUpdate){
562
if(type.isDerivedFrom(SoGesturePinchEvent::getClassTypeId())){
563
auto const event = static_cast<const SoGesturePinchEvent*>(ev);
564
if (this->zoomAtCursor){
565
//this is just dealing with the pan part of pinch gesture. Taking care of zooming to pos is done in doZoom.
566
SbVec2f panDist = this->normalizePixelPos(event->deltaCenter.getValue());
567
NavigationStyle::panCamera(viewer->getSoRenderManager()->getCamera(), ratio, this->panningplane, panDist, SbVec2f(0,0));
569
NavigationStyle::doZoom(viewer->getSoRenderManager()->getCamera(),-logf(event->deltaZoom),this->normalizePixelPos(event->curCenter));
570
if (event->deltaAngle != 0)
571
NavigationStyle::doRotate(viewer->getSoRenderManager()->getCamera(),event->deltaAngle,this->normalizePixelPos(event->curCenter));
574
if(type.isDerivedFrom(SoGesturePanEvent::getClassTypeId())){
575
auto const event = static_cast<const SoGesturePanEvent*>(ev);
576
//this is just dealing with the pan part of pinch gesture. Taking care of zooming to pos is done in doZoom.
577
SbVec2f panDist = this->normalizePixelPos(event->deltaOffset);
578
NavigationStyle::panCamera(viewer->getSoRenderManager()->getCamera(), ratio, this->panningplane, panDist, SbVec2f(0,0));
582
//shouldn't happen. Gestures are not expected to start in the middle of navigation.
583
//we'll consume it, without reacting.
588
} break;//end of actual navigation
589
case NavigationStyle::SEEK_WAIT_MODE:{
591
auto const event = (const SoMouseButtonEvent *) ev;
592
const int button = event->getButton();
593
const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;
594
if (button == SoMouseButtonEvent::BUTTON1 && press) {
595
this->seekToPoint(pos); // implicitly calls interactiveCountInc()
596
this->setViewingMode(NavigationStyle::SEEK_MODE);
600
} ; //not end of SEEK_WAIT_MODE. Fall through by design!!!
602
case NavigationStyle::SPINNING:
603
case NavigationStyle::SEEK_MODE: {
606
if (evIsButton || evIsGesture || evIsKeyboard || evIsLoc3)
607
setViewingMode(NavigationStyle::SELECTION);
609
} break; //end of animation modes
610
case NavigationStyle::BOXZOOM:
612
//all the rest - will be pass on to inherited, later.
616
if (! processed && ! propagated) {
617
processed = inherited::processSoEvent(ev);
621
//-----------------------end of event handling---------------------