diff --git a/ChangeLog b/ChangeLog index c55cb36935..ad26b00cba 100644 --- a/ChangeLog +++ b/ChangeLog @@ -25,6 +25,7 @@ Unreleased Version 2.2.2-pre * Fix: Clamp palette swatch sizes to more reasonable bounds. Thanks MachKerman for reporting. * Server Feature: Allow checking session, operator and server account passwords via the API. This can be used to password-protect recordings or similar. Thanks Meru for contributing. * Fix: Correct some UI scaling problems with brush outlines, canvas centering, dock toggling, pixel grid and transform handles. Thanks annoy for reporting. + * Feature: Extended touch tap gestures, among them two-finger tap to undo and three-finger tap to redo. Can be configured in the preferences under the Touch tab. Thanks InconsolableCellist and many others for suggesting. 2024-08-09 Version 2.2.2-beta.3 * Fix: Use more accurate timers for performance profiles if the platform supports it. diff --git a/src/desktop/CMakeLists.txt b/src/desktop/CMakeLists.txt index a326cfd8ee..89b6df667b 100644 --- a/src/desktop/CMakeLists.txt +++ b/src/desktop/CMakeLists.txt @@ -157,6 +157,8 @@ target_sources(drawpile PRIVATE dialogs/settingsdialog/shortcuts.h dialogs/settingsdialog/tools.cpp dialogs/settingsdialog/tools.h + dialogs/settingsdialog/touch.cpp + dialogs/settingsdialog/touch.h dialogs/startdialog.cpp dialogs/startdialog.h dialogs/startdialog/browse.cpp diff --git a/src/desktop/dialogs/settingsdialog.cpp b/src/desktop/dialogs/settingsdialog.cpp index b7899c3666..9f3f4fb4de 100644 --- a/src/desktop/dialogs/settingsdialog.cpp +++ b/src/desktop/dialogs/settingsdialog.cpp @@ -9,6 +9,7 @@ #include "desktop/dialogs/settingsdialog/servers.h" #include "desktop/dialogs/settingsdialog/shortcuts.h" #include "desktop/dialogs/settingsdialog/tools.h" +#include "desktop/dialogs/settingsdialog/touch.h" #include "desktop/dialogs/settingsdialog/userinterface.h" #include "desktop/main.h" #include "desktop/settings.h" @@ -89,6 +90,8 @@ SettingsDialog::SettingsDialog( new settingsdialog::UserInterface(m_settings, this), true}, {"dialog-input-devices", tr("Input"), new settingsdialog::Input(m_settings, this), true}, + {"hand", tr("Touch"), new settingsdialog::Touch(m_settings, this), + true}, {"tools", tr("Tools"), new settingsdialog::Tools(m_settings, this), true}, {"network-modem", tr("Network"), diff --git a/src/desktop/dialogs/settingsdialog/input.cpp b/src/desktop/dialogs/settingsdialog/input.cpp index 0a7815160c..d423c740f2 100644 --- a/src/desktop/dialogs/settingsdialog/input.cpp +++ b/src/desktop/dialogs/settingsdialog/input.cpp @@ -127,40 +127,6 @@ void Input::initTablet( form->addRow(tr("Driver:"), driver); #endif - utils::addFormSpacer(form); - - auto *touchMode = utils::addRadioGroup( - form, tr("Touch mode:"), true, - { - {tr("Touchscreen"), false}, - {tr("Gestures"), true}, - }); - settings.bindTouchGestures(touchMode); - - auto *oneTouch = utils::addRadioGroup( - form, tr("One-finger input:"), true, - { - {tr("None"), int(desktop::settings::OneFingerTouchAction::Nothing)}, - {tr("Draw"), int(desktop::settings::OneFingerTouchAction::Draw)}, - {tr("Pan"), int(desktop::settings::OneFingerTouchAction::Pan)}, - {tr("Guess"), int(desktop::settings::OneFingerTouchAction::Guess)}, - }); - settings.bindOneFingerTouch(oneTouch); - settings.bindTouchGestures( - oneTouch->button(int(desktop::settings::OneFingerTouchAction::Draw)), - &QWidget::setDisabled); - - auto *twoTouch = new utils::EncapsulatedLayout; - twoTouch->setContentsMargins(0, 0, 0, 0); - auto *zoom = new QCheckBox(tr("Pinch to zoom")); - settings.bindTwoFingerZoom(zoom); - twoTouch->addWidget(zoom); - auto *rotate = new QCheckBox(tr("Twist to rotate")); - settings.bindTwoFingerRotate(rotate); - twoTouch->addWidget(rotate); - twoTouch->addStretch(); - form->addRow(tr("Touch gestures:"), twoTouch); - auto *testerLayout = new QVBoxLayout; auto *testerLabel = new QLabel(tr("Test your tablet here:")); testerLabel->setAlignment(Qt::AlignHCenter); diff --git a/src/desktop/dialogs/settingsdialog/touch.cpp b/src/desktop/dialogs/settingsdialog/touch.cpp new file mode 100644 index 0000000000..c5e143bbda --- /dev/null +++ b/src/desktop/dialogs/settingsdialog/touch.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "desktop/dialogs/settingsdialog/touch.h" +#include "desktop/settings.h" +#include "desktop/utils/widgetutils.h" +#include +#include +#include +#include +#include + +namespace dialogs { +namespace settingsdialog { + +Touch::Touch(desktop::settings::Settings &settings, QWidget *parent) + : Page(parent) +{ + init(settings); +} + +void Touch::setUp(desktop::settings::Settings &settings, QVBoxLayout *layout) +{ + initMode(settings, utils::addFormSection(layout)); + utils::addFormSeparator(layout); + initTouchActions(settings, utils::addFormSection(layout)); + utils::addFormSeparator(layout); + initTapActions(settings, utils::addFormSection(layout)); +} + +void Touch::initMode(desktop::settings::Settings &settings, QFormLayout *form) +{ + QButtonGroup *touchMode = utils::addRadioGroup( + form, tr("Touch mode:"), true, + { + {tr("Touchscreen"), false}, + {tr("Gestures"), true}, + }); + settings.bindTouchGestures(touchMode); +} + +void Touch::initTapActions( + desktop::settings::Settings &settings, QFormLayout *form) +{ + QComboBox *oneFingerTap = new QComboBox; + QComboBox *twoFingerTap = new QComboBox; + QComboBox *threeFingerTap = new QComboBox; + QComboBox *fourFingerTap = new QComboBox; + for(QComboBox *tap : + {oneFingerTap, twoFingerTap, threeFingerTap, fourFingerTap}) { + tap->addItem( + tr("No action"), int(desktop::settings::TouchTapAction::Nothing)); + tap->addItem(tr("Undo"), int(desktop::settings::TouchTapAction::Undo)); + tap->addItem(tr("Redo"), int(desktop::settings::TouchTapAction::Redo)); + tap->addItem( + tr("Hide docks"), + int(desktop::settings::TouchTapAction::HideDocks)); + tap->addItem( + tr("Toggle color picker"), + int(desktop::settings::TouchTapAction::ColorPicker)); + tap->addItem( + tr("Toggle eraser"), + int(desktop::settings::TouchTapAction::Eraser)); + tap->addItem( + tr("Toggle erase mode"), + int(desktop::settings::TouchTapAction::EraseMode)); + tap->addItem( + tr("Toggle recolor mode"), + int(desktop::settings::TouchTapAction::RecolorMode)); + } + + settings.bindOneFingerTap(oneFingerTap, Qt::UserRole); + settings.bindTwoFingerTap(twoFingerTap, Qt::UserRole); + settings.bindThreeFingerTap(threeFingerTap, Qt::UserRole); + settings.bindFourFingerTap(fourFingerTap, Qt::UserRole); + + settings.bindTouchGestures(twoFingerTap, &QComboBox::setDisabled); + settings.bindTouchGestures(threeFingerTap, &QComboBox::setDisabled); + settings.bindTouchGestures(fourFingerTap, &QComboBox::setDisabled); + + form->addRow(tr("One-finger tap:"), oneFingerTap); + form->addRow(tr("Two-finger tap:"), twoFingerTap); + form->addRow(tr("Three-finger tap:"), threeFingerTap); + form->addRow(tr("Four-finger tap:"), fourFingerTap); +} + +void Touch::initTouchActions( + desktop::settings::Settings &settings, QFormLayout *form) +{ + QComboBox *oneFingerTouch = new QComboBox; + oneFingerTouch->setSizeAdjustPolicy(QComboBox::AdjustToContents); + oneFingerTouch->addItem( + tr("No action"), int(desktop::settings::OneFingerTouchAction::Nothing)); + oneFingerTouch->addItem( + tr("Draw"), int(desktop::settings::OneFingerTouchAction::Draw)); + oneFingerTouch->addItem( + tr("Pan canvas"), int(desktop::settings::OneFingerTouchAction::Pan)); + oneFingerTouch->addItem( + tr("Guess"), int(desktop::settings::OneFingerTouchAction::Guess)); + settings.bindOneFingerTouch(oneFingerTouch, Qt::UserRole); + form->addRow(tr("One-finger touch:"), oneFingerTouch); + + QComboBox *twoFingerPinch = new QComboBox; + twoFingerPinch->setSizeAdjustPolicy(QComboBox::AdjustToContents); + twoFingerPinch->addItem( + tr("No action"), int(desktop::settings::TwoFingerPinchAction::Nothing)); + twoFingerPinch->addItem( + tr("Zoom"), int(desktop::settings::TwoFingerPinchAction::Zoom)); + settings.bindTwoFingerPinch(twoFingerPinch, Qt::UserRole); + form->addRow(tr("Two-finger pinch:"), twoFingerPinch); + + QComboBox *twoFingerTwist = new QComboBox; + twoFingerTwist->setSizeAdjustPolicy(QComboBox::AdjustToContents); + twoFingerTwist->addItem( + tr("No action"), int(desktop::settings::TwoFingerPinchAction::Nothing)); + twoFingerTwist->addItem( + tr("Rotate canvas"), + int(desktop::settings::TwoFingerTwistAction::Rotate)); + twoFingerTwist->addItem( + tr("Free rotate canvas"), + int(desktop::settings::TwoFingerTwistAction::RotateNoSnap)); + twoFingerTwist->addItem( + tr("Ratchet rotate canvas"), + int(desktop::settings::TwoFingerTwistAction::RotateDiscrete)); + settings.bindTwoFingerTwist(twoFingerTwist, Qt::UserRole); + form->addRow(tr("Two-finger twist:"), twoFingerTwist); +} + +} // namespace settingsdialog +} // namespace dialogs diff --git a/src/desktop/dialogs/settingsdialog/touch.h b/src/desktop/dialogs/settingsdialog/touch.h new file mode 100644 index 0000000000..a4a4492d83 --- /dev/null +++ b/src/desktop/dialogs/settingsdialog/touch.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef DESKTOP_DIALOGS_SETTINGSDIALOG_TOUCH_H +#define DESKTOP_DIALOGS_SETTINGSDIALOG_TOUCH_H +#include "desktop/dialogs/settingsdialog/page.h" + +class QFormLayout; + +namespace desktop { +namespace settings { +class Settings; +} +} + +namespace dialogs { +namespace settingsdialog { + +class Touch final : public Page { + Q_OBJECT +public: + Touch(desktop::settings::Settings &settings, QWidget *parent = nullptr); + +protected: + void + setUp(desktop::settings::Settings &settings, QVBoxLayout *layout) override; + +private: + void initMode(desktop::settings::Settings &settings, QFormLayout *form); + + void + initTapActions(desktop::settings::Settings &settings, QFormLayout *form); + + void + initTouchActions(desktop::settings::Settings &settings, QFormLayout *form); +}; + +} +} + +#endif diff --git a/src/desktop/dialogs/systeminfodialog.cpp b/src/desktop/dialogs/systeminfodialog.cpp index 18f51c02c6..027b0a26c0 100644 --- a/src/desktop/dialogs/systeminfodialog.cpp +++ b/src/desktop/dialogs/systeminfodialog.cpp @@ -179,10 +179,26 @@ QString SystemInfoDialog::getSystemInfo() const QStringLiteral("One-finger touch action: %1\n") .arg(QMetaEnum::fromType() .valueToKey(settings.oneFingerTouch())); - info += QStringLiteral("Two-finger zoom: %1\n") - .arg(boolToYesNo(settings.twoFingerZoom())); - info += QStringLiteral("Two-finger rotate: %1\n") - .arg(boolToYesNo(settings.twoFingerRotate())); + info += + QStringLiteral("Two-finger pinch action: %1\n") + .arg(QMetaEnum::fromType() + .valueToKey(settings.twoFingerPinch())); + info += + QStringLiteral("Two-finger twist action: %1\n") + .arg(QMetaEnum::fromType() + .valueToKey(settings.twoFingerTwist())); + info += QStringLiteral("One-finger tap action: %1\n") + .arg(QMetaEnum::fromType() + .valueToKey(settings.oneFingerTap())); + info += QStringLiteral("Two-finger tap action: %1\n") + .arg(QMetaEnum::fromType() + .valueToKey(settings.twoFingerTap())); + info += QStringLiteral("Three-finger tap action: %1\n") + .arg(QMetaEnum::fromType() + .valueToKey(settings.threeFingerTap())); + info += QStringLiteral("Four-finger tap action: %1\n") + .arg(QMetaEnum::fromType() + .valueToKey(settings.fourFingerTap())); info += QStringLiteral("Touch gestures: %1\n") .arg(boolToYesNo(settings.touchGestures())); info += QStringLiteral("\n"); diff --git a/src/desktop/mainwindow.cpp b/src/desktop/mainwindow.cpp index 2ac778b035..1559af05de 100644 --- a/src/desktop/mainwindow.cpp +++ b/src/desktop/mainwindow.cpp @@ -2257,8 +2257,12 @@ void MainWindow::toggleTabletEventLog() DP_event_log_write_meta("Tablet enabled: %d", settings.tabletEvents()); DP_event_log_write_meta("Tablet eraser action: %d", settings.tabletEraserAction()); DP_event_log_write_meta("One-finger touch action: %d", settings.oneFingerTouch()); - DP_event_log_write_meta("Two-finger rotate: %d", settings.twoFingerRotate()); - DP_event_log_write_meta("Two-finger zoom: %d", settings.twoFingerZoom()); + DP_event_log_write_meta("Two-finger pinch action: %d", settings.twoFingerPinch()); + DP_event_log_write_meta("Two-finger twist action: %d", settings.twoFingerTwist()); + DP_event_log_write_meta("One-finger tap action: %d", settings.oneFingerTap()); + DP_event_log_write_meta("Two-finger tap action: %d", settings.twoFingerTap()); + DP_event_log_write_meta("Three-finger tap action: %d", settings.threeFingerTap()); + DP_event_log_write_meta("Four-finger tap action: %d", settings.fourFingerTap()); DP_event_log_write_meta("Gestures: %d", settings.touchGestures()); } else { showErrorMessageWithDetails(tr("Error opening tablet event log."), DP_error()); @@ -3192,6 +3196,43 @@ void MainWindow::handleToggleAction(int action) } // clang-format on +void MainWindow::handleTouchTapAction(int action) +{ + switch(action) { + case int(desktop::settings::TouchTapAction::Undo): + getAction("undo")->trigger(); + break; + case int(desktop::settings::TouchTapAction::Redo): + getAction("redo")->trigger(); + break; + case int(desktop::settings::TouchTapAction::HideDocks): + getAction("hidedocks")->trigger(); + break; + case int(desktop::settings::TouchTapAction::ColorPicker): + if(m_dockToolSettings->currentTool() == tools::Tool::PICKER) { + m_dockToolSettings->setPreviousTool(); + } else { + m_dockToolSettings->setTool(tools::Tool::PICKER); + } + break; + case int(desktop::settings::TouchTapAction::Eraser): + if(m_dockToolSettings->currentTool() == tools::Tool::ERASER) { + m_dockToolSettings->setPreviousTool(); + } else { + m_dockToolSettings->setTool(tools::Tool::ERASER); + } + break; + case int(desktop::settings::TouchTapAction::EraseMode): + getAction("currenterasemode")->trigger(); + break; + case int(desktop::settings::TouchTapAction::RecolorMode): + getAction("currentrecolormode")->trigger(); + break; + default: + qWarning("Unknown tap action %d", action); + break; + } +} void MainWindow::setNotificationsMuted(bool muted) { diff --git a/src/desktop/mainwindow.h b/src/desktop/mainwindow.h index c54b6fc1ff..9e0b781582 100644 --- a/src/desktop/mainwindow.h +++ b/src/desktop/mainwindow.h @@ -178,6 +178,7 @@ public slots: void dropImage(const QImage &image); void dropUrl(const QUrl &url); void handleToggleAction(int action); + void handleTouchTapAction(int action); void savePreResetImageAs(); void discardPreResetImage(); diff --git a/src/desktop/scene/canvasview.cpp b/src/desktop/scene/canvasview.cpp index bdc475a17c..f7c657d26f 100644 --- a/src/desktop/scene/canvasview.cpp +++ b/src/desktop/scene/canvasview.cpp @@ -175,6 +175,9 @@ CanvasView::CanvasView(QWidget *parent) connect( m_touch, &TouchHandler::touchZoomedRotated, this, &CanvasView::touchZoomRotate, Qt::DirectConnection); + connect( + m_touch, &TouchHandler::touchTapActionActivated, this, + &CanvasView::touchTapActionActivated, Qt::DirectConnection); } @@ -182,11 +185,13 @@ void CanvasView::setTouchUseGestureEvents(bool useGestureEvents) { if(useGestureEvents && !m_useGestureEvents) { DP_EVENT_LOG("grab gesture events"); + viewport()->grabGesture(Qt::TapGesture); viewport()->grabGesture(Qt::PanGesture); viewport()->grabGesture(Qt::PinchGesture); m_useGestureEvents = true; } else if(!useGestureEvents && m_useGestureEvents) { DP_EVENT_LOG("ungrab gesture events"); + viewport()->ungrabGesture(Qt::TapGesture); viewport()->ungrabGesture(Qt::PanGesture); viewport()->ungrabGesture(Qt::PinchGesture); m_useGestureEvents = false; diff --git a/src/desktop/scene/canvasview.h b/src/desktop/scene/canvasview.h index 218c208cfd..fb262c5ddd 100644 --- a/src/desktop/scene/canvasview.h +++ b/src/desktop/scene/canvasview.h @@ -155,6 +155,7 @@ class CanvasView final : public QGraphicsView { void savePreResetStateDismissed(); void toggleActionActivated(int action); + void touchTapActionActivated(int action); public slots: //! Set the size of the brush preview outline diff --git a/src/desktop/scene/scenewrapper.cpp b/src/desktop/scene/scenewrapper.cpp index fd2d4a45db..be5809c8a8 100644 --- a/src/desktop/scene/scenewrapper.cpp +++ b/src/desktop/scene/scenewrapper.cpp @@ -386,6 +386,9 @@ void SceneWrapper::connectMainWindow(MainWindow *mainWindow) connect( m_view, &CanvasView::toggleActionActivated, mainWindow, &MainWindow::handleToggleAction); + connect( + m_view, &CanvasView::touchTapActionActivated, mainWindow, + &MainWindow::handleTouchTapAction); connect( m_view, &CanvasView::reconnectRequested, mainWindow, &MainWindow::reconnect); diff --git a/src/desktop/settings.cpp b/src/desktop/settings.cpp index 729679459f..26ab61b9b8 100644 --- a/src/desktop/settings.cpp +++ b/src/desktop/settings.cpp @@ -135,6 +135,70 @@ namespace oneFingerTouch { } } +namespace twoFingerPinch { + QVariant get(const SettingMeta &meta, QSettings &settings) + { + if (findKey(settings, meta.baseKey, meta.version)) { + bool ok; + int value = any::get(meta, settings).toInt(&ok); + if(ok) { + switch(value) { + case int(TwoFingerPinchAction::Nothing): + case int(TwoFingerPinchAction::Zoom): + return value; + default: + break; + } + } + return int(TwoFingerPinchAction::Zoom); + } + + std::optional touchPinchKey = findKey( + settings, "settings/input/touchpinch", SettingMeta::Version::V0); + if(touchPinchKey.has_value() && + settings.value(touchPinchKey->key).toBool()) { + return settings.value(touchPinchKey->key).toBool() + ? int(TwoFingerPinchAction::Zoom) + : int(TwoFingerPinchAction::Nothing); + } + + return int(TwoFingerPinchAction::Zoom); + } +} + +namespace twoFingerTwist { + QVariant get(const SettingMeta &meta, QSettings &settings) + { + if (findKey(settings, meta.baseKey, meta.version)) { + bool ok; + int value = any::get(meta, settings).toInt(&ok); + if(ok) { + switch(value) { + case int(TwoFingerTwistAction::Nothing): + case int(TwoFingerTwistAction::Rotate): + case int(TwoFingerTwistAction::RotateNoSnap): + case int(TwoFingerTwistAction::RotateDiscrete): + return value; + default: + break; + } + } + return int(TwoFingerTwistAction::Rotate); + } + + std::optional touchTwistKey = findKey( + settings, "settings/input/touchtwist", SettingMeta::Version::V0); + if(touchTwistKey.has_value() && + settings.value(touchTwistKey->key).toBool()) { + return settings.value(touchTwistKey->key).toBool() + ? int(TwoFingerTwistAction::Rotate) + : int(TwoFingerTwistAction::Nothing); + } + + return int(TwoFingerTwistAction::Rotate); + } +} + namespace tabletDriver { QVariant get(const SettingMeta &meta, QSettings &settings) { diff --git a/src/desktop/settings.h b/src/desktop/settings.h index d85143d1e1..e97f4c0f5e 100644 --- a/src/desktop/settings.h +++ b/src/desktop/settings.h @@ -59,6 +59,32 @@ enum class OneFingerTouchAction : int { }; Q_ENUM_NS(OneFingerTouchAction) +enum class TwoFingerPinchAction : int { + Nothing, + Zoom, +}; +Q_ENUM_NS(TwoFingerPinchAction) + +enum class TwoFingerTwistAction : int { + Nothing, + Rotate, + RotateNoSnap, + RotateDiscrete, +}; +Q_ENUM_NS(TwoFingerTwistAction) + +enum class TouchTapAction : int { + Nothing, + Undo, + Redo, + HideDocks, + ColorPicker, + Eraser, + EraseMode, + RecolorMode, +}; +Q_ENUM_NS(TouchTapAction) + enum class ThemePalette : int { System, Light, diff --git a/src/desktop/settings_table.h b/src/desktop/settings_table.h index 6fc02fd827..a16f9e9e39 100644 --- a/src/desktop/settings_table.h +++ b/src/desktop/settings_table.h @@ -179,8 +179,16 @@ SETTING(notifSoundLogin , NotifSoundLogin , "notifications/l SETTING(notifSoundLogout , NotifSoundLogout , "notifications/logout" , true) SETTING(notifSoundPrivateChat , NotifSoundPrivateChat , "notifications/privatechat" , true) SETTING(notifSoundUnlock , NotifSoundUnlock , "notifications/unlock" , true) -SETTING_GETSET(oneFingerTouch , OneFingerTouch , "settings/input/onefinertouch" , int(ONE_FINGER_TOUCH_DEFAULT) +SETTING_GETSET(oneFingerTouch , OneFingerTouch , "settings/input/onefingertouch" , int(ONE_FINGER_TOUCH_DEFAULT) , &oneFingerTouch::get, &any::set) +SETTING_GETSET(twoFingerPinch , TwoFingerPinch , "settings/input/twofingerpinch" , int(TwoFingerPinchAction::Zoom) + , &twoFingerPinch::get, &any::set) +SETTING_GETSET(twoFingerTwist , TwoFingerTwist , "settings/input/twofingertwist" , int(TwoFingerTwistAction::Rotate) + , &twoFingerTwist::get, &any::set) +SETTING(oneFingerTap , OneFingerTap , "settings/input/onefingertap" , int(TouchTapAction::Nothing)) +SETTING(twoFingerTap , TwoFingerTap , "settings/input/twofingertap" , int(TouchTapAction::Undo)) +SETTING(threeFingerTap , ThreeFingerTap , "settings/input/threefingertap" , int(TouchTapAction::Redo)) +SETTING(fourFingerTap , FourFingerTap , "settings/input/fourfingertap" , int(TouchTapAction::HideDocks)) SETTING(tabletPressTimerDelay , TabletPressTimerDelay , "settings/input/tabletpresstimerdelay" , 500) SETTING(touchGestures , TouchGestures , "settings/input/touchgestures" , false) SETTING(onionSkinsFrameCount , OnionSkinsFrameCount , "onionskins/framecount" , 8) @@ -216,8 +224,6 @@ SETTING_FULL(V2, themeStyle , ThemeStyle , "settings/theme/ , &any::get , &any::set, &themeStyle::notify) SETTING(toolToggle , ToolToggle , "settings/tooltoggle" , true) SETTING(toolset , Toolset , "tools/toolset" , (QMap())) -SETTING(twoFingerRotate , TwoFingerRotate , "settings/input/touchtwist" , true) -SETTING(twoFingerZoom , TwoFingerZoom , "settings/input/touchpinch" , true) SETTING(updateCheckEnabled , UpdateCheckEnabled , "settings/updatecheck" , true) SETTING(userMarkerPersistence , UserMarkerPersistence , "settings/usermarkerpersistence" , 1000) SETTING(videoExportCustomFfmpeg , VideoExportCustomFfmpeg , "videoexport/customffmpeg" , QString()) diff --git a/src/desktop/utils/touchhandler.cpp b/src/desktop/utils/touchhandler.cpp index a1ef508126..8ea7b58406 100644 --- a/src/desktop/utils/touchhandler.cpp +++ b/src/desktop/utils/touchhandler.cpp @@ -12,11 +12,23 @@ extern "C" { TouchHandler::TouchHandler(QObject *parent) : QObject(parent) , m_oneFingerTouchAction(int(ONE_FINGER_TOUCH_DEFAULT)) + , m_twoFingerPinchAction(int(desktop::settings::TwoFingerPinchAction::Zoom)) + , m_twoFingerTwistAction( + int(desktop::settings::TwoFingerTwistAction::Rotate)) + , m_oneFingerTapAction(int(desktop::settings::TouchTapAction::Nothing)) + , m_twoFingerTapAction(int(desktop::settings::TouchTapAction::Undo)) + , m_threeFingerTapAction(int(desktop::settings::TouchTapAction::Redo)) + , m_fourFingerTapAction(int(desktop::settings::TouchTapAction::HideDocks)) + , m_tapTimer(Qt::CoarseTimer) { desktop::settings::Settings &settings = dpApp().settings(); settings.bindOneFingerTouch(this, &TouchHandler::setOneFingerTouchAction); - settings.bindTwoFingerZoom(this, &TouchHandler::setEnableTouchPinch); - settings.bindTwoFingerRotate(this, &TouchHandler::setEnableTouchTwist); + settings.bindTwoFingerPinch(this, &TouchHandler::setTwoFingerPinchAction); + settings.bindTwoFingerTwist(this, &TouchHandler::setTwoFingerTwistAction); + settings.bindOneFingerTap(this, &TouchHandler::setOneFingerTapAction); + settings.bindTwoFingerTap(this, &TouchHandler::setTwoFingerTapAction); + settings.bindThreeFingerTap(this, &TouchHandler::setThreeFingerTapAction); + settings.bindFourFingerTap(this, &TouchHandler::setFourFingerTapAction); } bool TouchHandler::isTouchDrawEnabled() const @@ -55,6 +67,23 @@ bool TouchHandler::isTouchDrawOrPanEnabled() const } } +bool TouchHandler::isTouchPinchEnabled() const +{ + return m_twoFingerPinchAction != + int(desktop::settings::TwoFingerPinchAction::Nothing); +} + +bool TouchHandler::isTouchTwistEnabled() const +{ + return m_twoFingerTwistAction != + int(desktop::settings::TwoFingerTwistAction::Nothing); +} + +bool TouchHandler::isTouchPinchOrTwistEnabled() const +{ + return isTouchPinchEnabled() || isTouchTwistEnabled(); +} + void TouchHandler::handleTouchBegin(QTouchEvent *event) { const QList &points = compat::touchPoints(*event); @@ -62,7 +91,10 @@ void TouchHandler::handleTouchBegin(QTouchEvent *event) QPointF posf = compat::touchPos(points.first()); m_touchDrawBuffer.clear(); + m_touchDragging = false; m_touchRotating = false; + m_maxTouchPoints = pointsCount; + m_tapTimer.setRemainingTime(TAP_MAX_DELAY_MS); if(isTouchDrawEnabled() && pointsCount == 1 && !compat::isTouchPad(event)) { DP_EVENT_LOG( "touch_draw_begin x=%f y=%f touching=%d type=%d device=%s " @@ -71,7 +103,9 @@ void TouchHandler::handleTouchBegin(QTouchEvent *event) qUtf8Printable(compat::touchDeviceName(event)), qUtf8Printable(compat::debug(points)), qulonglong(event->timestamp())); - if(isTouchPanEnabled() || m_enableTouchPinch || m_enableTouchTwist) { + if(isTouchPanEnabled() || isTouchPinchOrTwistEnabled() || + m_oneFingerTapAction != + int(desktop::settings::TouchTapAction::Nothing)) { // Buffer the touch first, since it might end up being the // beginning of an action that involves multiple fingers. m_touchDrawBuffer.append( @@ -100,6 +134,9 @@ void TouchHandler::handleTouchUpdate( { const QList &points = compat::touchPoints(*event); int pointsCount = points.size(); + if(pointsCount > m_maxTouchPoints) { + m_maxTouchPoints = pointsCount; + } if(isTouchDrawEnabled() && ((pointsCount == 1 && m_touchMode == TouchMode::Unknown) || @@ -128,9 +165,9 @@ void TouchHandler::handleTouchUpdate( // buffer an excessive amount of touches yet. Buffer the touched // point and wait a bit more as to what's going to happen. bool shouldAppend = - bufferCount < TOUCH_DRAW_BUFFER_COUNT && - QLineF(m_touchDrawBuffer.first().second, posf).length() < - TOUCH_DRAW_DISTANCE; + bufferCount < DRAW_BUFFER_COUNT && + squareDist(m_touchDrawBuffer.first().second - posf) < + TAP_SLOP_SQUARED; if(shouldAppend) { m_touchDrawBuffer.append( {QDateTime::currentMSecsSinceEpoch(), posf}); @@ -144,17 +181,38 @@ void TouchHandler::handleTouchUpdate( m_touchMode = TouchMode::Moving; QPointF startCenter, lastCenter, center; - for(const auto &tp : compat::touchPoints(*event)) { - startCenter += compat::touchStartPos(tp); + for(const compat::TouchPoint &tp : compat::touchPoints(*event)) { + QPointF startPos = compat::touchStartPos(tp); + startCenter += startPos; lastCenter += compat::touchLastPos(tp); - center += compat::touchPos(tp); + QPointF pos = compat::touchPos(tp); + center += pos; + // This might be a tap gesture. Don't start a drag until there's + // been sufficient movement on any of the fingers. + if(!m_touchDragging && + squareDist(startPos - pos) > TAP_SLOP_SQUARED) { + m_touchDragging = true; + } + } + + if(!m_touchDragging) { + if(m_tapTimer.hasExpired()) { + // The user has had their fingers down for at least a second, + // this is clearly no longer a tap, but some longer gesture. + m_touchDragging = true; + } else { + // Maybe still a tap gesture, wait for more movement. + return; + } } + startCenter /= pointsCount; lastCenter /= pointsCount; center /= pointsCount; DP_EVENT_LOG( - "touch_update x=%f y=%f touching=%d type=%d device=%s points=%s " + "touch_update x=%f y=%f touching=%d type=%d device=%s " + "points=%s " "timestamp=%llu", center.x(), center.y(), int(m_touching), compat::touchDeviceType(event), @@ -174,8 +232,7 @@ void TouchHandler::handleTouchUpdate( // operation and aren't going to be drawing until all fingers leave // the surface anyway, so panning is the only sensible option. bool haveMultiTouch = pointsCount >= 2; - bool havePinchOrTwist = - haveMultiTouch && (m_enableTouchPinch || m_enableTouchTwist); + bool havePinchOrTwist = haveMultiTouch && isTouchPinchOrTwistEnabled(); bool havePan = havePinchOrTwist || (isTouchDrawOrPanEnabled() && (haveMultiTouch || !compat::isTouchPad(event))); @@ -199,14 +256,14 @@ void TouchHandler::handleTouchUpdate( startAvgDist = sqrt(startAvgDist); qreal touchZoom = zoom; - if(m_enableTouchPinch) { + if(isTouchPinchEnabled()) { avgDist = sqrt(avgDist); qreal dZoom = avgDist / startAvgDist; touchZoom = m_touchStartZoom * dZoom; } qreal touchRotation = rotation; - if(m_enableTouchTwist) { + if(isTouchTwistEnabled()) { QLineF l1( compat::touchStartPos(points.first()), compat::touchStartPos(points.last())); @@ -223,7 +280,8 @@ void TouchHandler::handleTouchUpdate( if(startAvgDist * dpr > 80.0 && (qAbs(dAngle) > 3.0 || m_touchRotating)) { m_touchRotating = true; - touchRotation = m_touchStartRotate + dAngle; + touchRotation = + adjustTwistRotation(m_touchStartRotate + dAngle); } } @@ -237,7 +295,10 @@ void TouchHandler::handleTouchEnd(QTouchEvent *event, bool cancel) event->accept(); const QList &points = compat::touchPoints(*event); if(isTouchDrawEnabled() && - ((m_touchMode == TouchMode::Unknown && !m_touchDrawBuffer.isEmpty()) || + ((m_touchMode == TouchMode::Unknown && !m_touchDrawBuffer.isEmpty() && + (m_touchDragging || + m_oneFingerTapAction == + int(desktop::settings::TouchTapAction::Nothing))) || m_touchMode == TouchMode::Drawing)) { DP_EVENT_LOG( "touch_draw_%s touching=%d type=%d device=%s points=%s " @@ -251,7 +312,7 @@ void TouchHandler::handleTouchEnd(QTouchEvent *event, bool cancel) emit touchReleased( QDateTime::currentMSecsSinceEpoch(), compat::touchPos(compat::touchPoints(*event).first())); - } else { + } else if(m_touchDragging) { DP_EVENT_LOG( "touch_%s touching=%d type=%d device=%s points=%s timestamp=%llu", cancel ? "cancel" : "end", int(m_touching), @@ -259,6 +320,26 @@ void TouchHandler::handleTouchEnd(QTouchEvent *event, bool cancel) qUtf8Printable(compat::touchDeviceName(event)), qUtf8Printable(compat::debug(points)), qulonglong(event->timestamp())); + } else { + DP_EVENT_LOG( + "touch_tap_%s maxTouchPoints=%d touching=%d type=%d device=%s " + "points=%s timestamp=%llu", + cancel ? "cancel" : "end", m_maxTouchPoints, int(m_touching), + compat::touchDeviceType(event), + qUtf8Printable(compat::touchDeviceName(event)), + qUtf8Printable(compat::debug(points)), + qulonglong(event->timestamp())); + if(!cancel) { + if(m_maxTouchPoints == 1) { + emitTapAction(m_oneFingerTapAction); + } else if(m_maxTouchPoints == 2) { + emitTapAction(m_twoFingerTapAction); + } else if(m_maxTouchPoints == 3) { + emitTapAction(m_threeFingerTapAction); + } else if(m_maxTouchPoints >= 4) { + emitTapAction(m_fourFingerTapAction); + } + } } m_touching = false; } @@ -266,6 +347,17 @@ void TouchHandler::handleTouchEnd(QTouchEvent *event, bool cancel) void TouchHandler::handleGesture( QGestureEvent *event, qreal zoom, qreal rotation) { + const QTapGesture *tap = + static_cast(event->gesture(Qt::TapGesture)); + if(tap) { + Qt::GestureState tapState = tap->state(); + DP_EVENT_LOG( + "tap state=0x%x touching=%d", unsigned(tapState), int(m_touching)); + if(tapState == Qt::GestureFinished) { + emitTapAction(m_oneFingerTapAction); + } + } + const QPinchGesture *pinch = static_cast(event->gesture(Qt::PinchGesture)); bool hadPinchUpdate = false; @@ -290,19 +382,20 @@ void TouchHandler::handleGesture( emit touchScrolledBy(-d.x(), -d.y()); } - bool haveZoom = m_enableTouchPinch && + bool haveZoom = isTouchPinchEnabled() && cf.testFlag(QPinchGesture::ScaleFactorChanged); bool haveRotation = - m_enableTouchTwist && + isTouchTwistEnabled() && cf.testFlag(QPinchGesture::RotationAngleChanged); if(haveZoom || haveRotation) { qreal gestureZoom = haveZoom ? m_gestureStartZoom * pinch->totalScaleFactor() : m_gestureStartZoom; - qreal gestureRotation = - haveRotation - ? m_gestureStartRotation + pinch->totalRotationAngle() - : m_gestureStartRotation; + qreal gestureRotation = haveRotation + ? adjustTwistRotation( + m_gestureStartRotation + + pinch->totalRotationAngle()) + : m_gestureStartRotation; emit touchZoomedRotated(gestureZoom, gestureRotation); } @@ -348,27 +441,57 @@ void TouchHandler::handleGesture( void TouchHandler::setOneFingerTouchAction(int oneFingerTouchAction) { - switch(oneFingerTouchAction) { - case int(desktop::settings::OneFingerTouchAction::Nothing): - case int(desktop::settings::OneFingerTouchAction::Draw): - case int(desktop::settings::OneFingerTouchAction::Pan): - case int(desktop::settings::OneFingerTouchAction::Guess): - m_oneFingerTouchAction = oneFingerTouchAction; - break; - default: - qWarning("Unknown one finger touch action %d", oneFingerTouchAction); - break; - } + m_oneFingerTouchAction = oneFingerTouchAction; +} + +void TouchHandler::setTwoFingerPinchAction(int twoFingerPinchAction) +{ + m_twoFingerPinchAction = twoFingerPinchAction; +} + +void TouchHandler::setTwoFingerTwistAction(int twoFingerTwistAction) +{ + m_twoFingerTwistAction = twoFingerTwistAction; +} + +void TouchHandler::setOneFingerTapAction(int oneFingerTapAction) +{ + m_oneFingerTapAction = oneFingerTapAction; } -void TouchHandler::setEnableTouchPinch(bool enableTouchPinch) +void TouchHandler::setTwoFingerTapAction(int twoFingerTapAction) { - m_enableTouchPinch = enableTouchPinch; + m_twoFingerTapAction = twoFingerTapAction; } -void TouchHandler::setEnableTouchTwist(bool enableTouchTwist) +void TouchHandler::setThreeFingerTapAction(int threeFingerTapAction) { - m_enableTouchTwist = enableTouchTwist; + m_threeFingerTapAction = threeFingerTapAction; +} + +void TouchHandler::setFourFingerTapAction(int fourFingerTapAction) +{ + m_fourFingerTapAction = fourFingerTapAction; +} + +qreal TouchHandler::adjustTwistRotation(qreal degrees) const +{ + if(m_twoFingerTwistAction == + int(desktop::settings::TwoFingerTwistAction::Rotate)) { + return qAbs(std::fmod(degrees, 360.0)) < 5.0 ? 0.0 : degrees; + } else if( + m_twoFingerTwistAction == + int(desktop::settings::TwoFingerTwistAction::RotateDiscrete)) { + qreal step = 15.0; + qreal offset = std::fmod(degrees, step); + if(offset <= step / 2.0) { + return qFloor(degrees / step) * step; + } else { + return qCeil(degrees / step) * step; + } + } else { + return degrees; + } } void TouchHandler::flushTouchDrawBuffer() @@ -384,3 +507,10 @@ void TouchHandler::flushTouchDrawBuffer() m_touchDrawBuffer.clear(); } } + +void TouchHandler::emitTapAction(int action) +{ + if(action != int(desktop::settings::TouchTapAction::Nothing)) { + emit touchTapActionActivated(action); + } +} diff --git a/src/desktop/utils/touchhandler.h b/src/desktop/utils/touchhandler.h index 3f7a0ae3b2..95bc59daf0 100644 --- a/src/desktop/utils/touchhandler.h +++ b/src/desktop/utils/touchhandler.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #ifndef DESKTOP_UTILS_TOUCHHANDLER_H #define DESKTOP_UTILS_TOUCHHANDLER_H +#include #include #include @@ -17,8 +18,9 @@ class TouchHandler : public QObject { bool isTouchDrawEnabled() const; bool isTouchPanEnabled() const; bool isTouchDrawOrPanEnabled() const; - bool isTouchPinchEnabled() const { return m_enableTouchPinch; } - bool isTouchTwistEnabled() const { return m_enableTouchTwist; } + bool isTouchPinchEnabled() const; + bool isTouchTwistEnabled() const; + bool isTouchPinchOrTwistEnabled() const; bool isTouching() const { return m_touching; } @@ -35,10 +37,12 @@ class TouchHandler : public QObject { void touchReleased(long long timeMsec, const QPointF &posf); void touchScrolledBy(qreal dx, qreal dy); void touchZoomedRotated(qreal zoom, qreal rotation); + void touchTapActionActivated(int action); private: - static constexpr qreal TOUCH_DRAW_DISTANCE = 10.0; - static constexpr int TOUCH_DRAW_BUFFER_COUNT = 20; + static constexpr qreal TAP_SLOP_SQUARED = 16.0 * 16.0; + static constexpr int TAP_MAX_DELAY_MS = 1000; + static constexpr int DRAW_BUFFER_COUNT = 20; enum class TouchMode { Unknown, Drawing, Moving }; @@ -48,17 +52,29 @@ class TouchHandler : public QObject { } void setOneFingerTouchAction(int oneFingerTouchAction); - void setEnableTouchPinch(bool enableTouchPinch); - void setEnableTouchTwist(bool enableTouchTwist); + void setTwoFingerPinchAction(int twoFingerPinchAction); + void setTwoFingerTwistAction(int twoFingerTwistAction); + void setOneFingerTapAction(int oneFingerTapAction); + void setTwoFingerTapAction(int twoFingerTapAction); + void setThreeFingerTapAction(int threeFingerTapAction); + void setFourFingerTapAction(int fourFingerTapAction); + qreal adjustTwistRotation(qreal degrees) const; void flushTouchDrawBuffer(); + void emitTapAction(int action); - bool m_enableTouchPinch = true; - bool m_enableTouchTwist = true; bool m_touching = false; + bool m_touchDragging = false; bool m_touchRotating = false; bool m_anyTabletEventsReceived = false; int m_oneFingerTouchAction; + int m_twoFingerPinchAction; + int m_twoFingerTwistAction; + int m_oneFingerTapAction; + int m_twoFingerTapAction; + int m_threeFingerTapAction; + int m_fourFingerTapAction; + int m_maxTouchPoints = 0; TouchMode m_touchMode = TouchMode::Unknown; QVector> m_touchDrawBuffer; QPointF m_touchStartPos; @@ -67,6 +83,7 @@ class TouchHandler : public QObject { QPointF m_gestureStartPos; qreal m_gestureStartZoom = 0.0; qreal m_gestureStartRotation = 0.0; + QDeadlineTimer m_tapTimer; }; #endif diff --git a/src/desktop/view/canvascontroller.cpp b/src/desktop/view/canvascontroller.cpp index fe5788295a..8277e13e28 100644 --- a/src/desktop/view/canvascontroller.cpp +++ b/src/desktop/view/canvascontroller.cpp @@ -87,6 +87,9 @@ CanvasController::CanvasController(CanvasScene *scene, QWidget *parent) connect( m_touch, &TouchHandler::touchZoomedRotated, this, &CanvasController::touchZoomRotate, Qt::DirectConnection); + connect( + m_touch, &TouchHandler::touchTapActionActivated, this, + &CanvasController::touchTapActionActivated, Qt::DirectConnection); resetCanvasTransform(); } diff --git a/src/desktop/view/canvascontroller.h b/src/desktop/view/canvascontroller.h index 4c216831fa..01e24d4227 100644 --- a/src/desktop/view/canvascontroller.h +++ b/src/desktop/view/canvascontroller.h @@ -189,6 +189,7 @@ class CanvasController : public QObject { void quickAdjust(qreal value); void cursorChanged(const QCursor &cursor); void toggleActionActivated(int action); + void touchTapActionActivated(int action); void saveInProgressChanged(bool saveInProgress); private: diff --git a/src/desktop/view/canvasview.cpp b/src/desktop/view/canvasview.cpp index b87afada7d..db9a1cf314 100644 --- a/src/desktop/view/canvasview.cpp +++ b/src/desktop/view/canvasview.cpp @@ -311,11 +311,13 @@ void CanvasView::setTouchUseGestureEvents(bool touchUseGestureEvents) { if(touchUseGestureEvents && !m_touchUseGestureEvents) { DP_EVENT_LOG("grab gesture events"); + viewport()->grabGesture(Qt::TapGesture); viewport()->grabGesture(Qt::PanGesture); viewport()->grabGesture(Qt::PinchGesture); m_touchUseGestureEvents = true; } else if(!touchUseGestureEvents && m_touchUseGestureEvents) { DP_EVENT_LOG("ungrab gesture events"); + viewport()->ungrabGesture(Qt::TapGesture); viewport()->ungrabGesture(Qt::PanGesture); viewport()->ungrabGesture(Qt::PinchGesture); m_touchUseGestureEvents = false; diff --git a/src/desktop/view/viewwrapper.cpp b/src/desktop/view/viewwrapper.cpp index 6eea3b25d0..dee7cb2c2e 100644 --- a/src/desktop/view/viewwrapper.cpp +++ b/src/desktop/view/viewwrapper.cpp @@ -374,6 +374,9 @@ void ViewWrapper::connectMainWindow(MainWindow *mainWindow) connect( m_controller, &CanvasController::toggleActionActivated, mainWindow, &MainWindow::handleToggleAction); + connect( + m_controller, &CanvasController::touchTapActionActivated, mainWindow, + &MainWindow::handleTouchTapAction); connect( m_view, &CanvasView::reconnectRequested, mainWindow, &MainWindow::reconnect);