Add basic smooth follow controls

This commit is contained in:
wheaney 2025-09-17 22:34:47 -07:00
parent ea01dd79bd
commit db2d865432
10 changed files with 140 additions and 21 deletions

View File

@ -67,6 +67,13 @@
<label>Remove virtual displays on disable</label> <label>Remove virtual displays on disable</label>
<description>Whether to remove any virtual displays when the effect is disabled</description> <description>Whether to remove any virtual displays when the effect is disabled</description>
</entry> </entry>
<entry name="SmoothFollowThreshold" type="Int">
<default>15</default>
<min>1</min>
<max>45</max>
<label>Follow threshold (degrees)</label>
<description>How closely the display follows</description>
</entry>
<entry name="LookAheadOverride" type="Int"> <entry name="LookAheadOverride" type="Int">
<default>-1</default> <default>-1</default>
<min>-1</min> <min>-1</min>

View File

@ -118,6 +118,10 @@ BreezyDesktopEffect::BreezyDesktopEffect()
this->setZoomOnFocusEnabled(!m_zoomOnFocusEnabled); this->setZoomOnFocusEnabled(!m_zoomOnFocusEnabled);
} }
); );
setupGlobalShortcut(
BreezyShortcuts::TOGGLE_FOLLOW_MODE,
[this]() { this->toggleSmoothFollow(); }
);
connect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage); connect(effects, &EffectsHandler::cursorShapeChanged, this, &BreezyDesktopEffect::updateCursorImage);
updateCursorImage(); updateCursorImage();
@ -194,9 +198,9 @@ void BreezyDesktopEffect::setupGlobalShortcut(const BreezyShortcuts::Shortcut &s
} }
void BreezyDesktopEffect::recenter() { void BreezyDesktopEffect::recenter() {
XRDriverIPC::instance().writeControlFlags({ QJsonObject flags;
{"recenter_screen", true} flags.insert(QStringLiteral("recenter_screen"), true);
}); XRDriverIPC::instance().writeControlFlags(flags);
} }
void BreezyDesktopEffect::reconfigure(ReconfigureFlags) void BreezyDesktopEffect::reconfigure(ReconfigureFlags)
@ -378,6 +382,12 @@ void BreezyDesktopEffect::setZoomOnFocusEnabled(bool enabled) {
} }
} }
void BreezyDesktopEffect::toggleSmoothFollow() {
QJsonObject flags;
flags.insert(QStringLiteral("toggle_breezy_desktop_smooth_follow"), true);
XRDriverIPC::instance().writeControlFlags(flags);
}
bool BreezyDesktopEffect::imuResetState() const { bool BreezyDesktopEffect::imuResetState() const {
return m_imuResetState; return m_imuResetState;
} }
@ -421,6 +431,8 @@ void BreezyDesktopEffect::setFocusedDisplayDistance(qreal distance) {
if (distance != m_focusedDisplayDistance) { if (distance != m_focusedDisplayDistance) {
m_focusedDisplayDistance = std::clamp(distance, 0.2, m_allDisplaysDistance); m_focusedDisplayDistance = std::clamp(distance, 0.2, m_allDisplaysDistance);
Q_EMIT focusedDisplayDistanceChanged(); Q_EMIT focusedDisplayDistanceChanged();
if (m_smoothFollowEnabled) updateDriverDisplayDistance(m_focusedDisplayDistance);
} }
} }
@ -661,7 +673,15 @@ void BreezyDesktopEffect::updateImuRotation() {
if (m_smoothFollowEnabled != nextSmoothFollowEnabled) { if (m_smoothFollowEnabled != nextSmoothFollowEnabled) {
m_smoothFollowEnabled = nextSmoothFollowEnabled; m_smoothFollowEnabled = nextSmoothFollowEnabled;
Q_EMIT smoothFollowEnabledChanged(); Q_EMIT smoothFollowEnabledChanged();
}
if (nextSmoothFollowEnabled) updateDriverDisplayDistance(m_focusedDisplayDistance);
} else if (enabled && !wasEnabled) Q_EMIT smoothFollowEnabledChanged();
}
void BreezyDesktopEffect::updateDriverDisplayDistance(float distance) {
QJsonObject flags;
flags.insert(QStringLiteral("breezy_desktop_display_distance"), distance);
XRDriverIPC::instance().writeControlFlags(flags);
} }
QString BreezyDesktopEffect::cursorImageSource() const QString BreezyDesktopEffect::cursorImageSource() const

View File

@ -132,6 +132,8 @@ namespace KWin
void setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut, void setupGlobalShortcut(const BreezyShortcuts::Shortcut &shortcut,
std::function<void()> triggeredFunc); std::function<void()> triggeredFunc);
void recenter(); void recenter();
void toggleSmoothFollow();
void updateDriverDisplayDistance(float distance);
QString m_cursorImageSource; QString m_cursorImageSource;
QSize m_cursorImageSize; QSize m_cursorImageSize;

View File

@ -107,13 +107,16 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu
addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE); addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE);
addShortcutAction(actionCollection, BreezyShortcuts::RECENTER); addShortcutAction(actionCollection, BreezyShortcuts::RECENTER);
addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE_ZOOM_ON_FOCUS); addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE_ZOOM_ON_FOCUS);
addShortcutAction(actionCollection, BreezyShortcuts::TOGGLE_FOLLOW_MODE);
ui.shortcutsEditor->addCollection(actionCollection); ui.shortcutsEditor->addCollection(actionCollection);
connect(ui.shortcutsEditor, &KShortcutsEditor::keyChange, this, &BreezyDesktopEffectConfig::markAsChanged); connect(ui.shortcutsEditor, &KShortcutsEditor::keyChange, this, &BreezyDesktopEffectConfig::markAsChanged);
connect(ui.EffectEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::updateDriverEnabled); connect(ui.EffectEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::updateDriverEnabled);
connect(ui.SmoothFollowEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::updateSmoothFollowEnabled);
connect(ui.kcfg_ZoomOnFocusEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_ZoomOnFocusEnabled, &QCheckBox::toggled, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_FocusedDisplayDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_AllDisplaysDistance, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplaySpacing, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_SmoothFollowThreshold, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::updateSmoothFollowThreshold);
connect(ui.kcfg_DisplayHorizontalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplayHorizontalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_DisplayVerticalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_DisplayVerticalOffset, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
connect(ui.kcfg_LookAheadOverride, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save); connect(ui.kcfg_LookAheadOverride, &QSlider::valueChanged, this, &BreezyDesktopEffectConfig::save);
@ -151,7 +154,9 @@ BreezyDesktopEffectConfig::BreezyDesktopEffectConfig(QObject *parent, const KPlu
labelStatus->setVisible(false); labelStatus->setVisible(false);
bool success = XRDriverIPC::instance().verifyToken(edit->text().trimmed().toStdString()); bool success = XRDriverIPC::instance().verifyToken(edit->text().trimmed().toStdString());
if (success) { if (success) {
XRDriverIPC::instance().writeControlFlags({{"refresh_device_license", true}}); QJsonObject flags;
flags.insert(QStringLiteral("refresh_device_license"), true);
XRDriverIPC::instance().writeControlFlags(flags);
} }
showStatus(labelStatus, success, success ? tr("Your license has been refreshed.") : tr("Invalid or expired token.")); showStatus(labelStatus, success, success ? tr("Your license has been refreshed.") : tr("Invalid or expired token."));
setRequestInProgress({edit, sender()}, false); setRequestInProgress({edit, sender()}, false);
@ -216,7 +221,8 @@ void BreezyDesktopEffectConfig::save()
updateConfigFromUi(); updateConfigFromUi();
BreezyDesktopConfig::self()->save(); BreezyDesktopConfig::self()->save();
KCModule::save(); KCModule::save();
ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked()); ui.kcfg_FocusedDisplayDistance->setEnabled(
ui.kcfg_ZoomOnFocusEnabled->isChecked() || ui.SmoothFollowEnabled->isChecked());
m_updatingFromConfig = false; m_updatingFromConfig = false;
updateUnmanagedState(); updateUnmanagedState();
@ -250,6 +256,7 @@ void BreezyDesktopEffectConfig::updateUiFromConfig()
ui.kcfg_RemoveVirtualDisplaysOnDisable->setChecked(BreezyDesktopConfig::self()->removeVirtualDisplaysOnDisable()); ui.kcfg_RemoveVirtualDisplaysOnDisable->setChecked(BreezyDesktopConfig::self()->removeVirtualDisplaysOnDisable());
ui.kcfg_ZoomOnFocusEnabled->setChecked(BreezyDesktopConfig::self()->zoomOnFocusEnabled()); ui.kcfg_ZoomOnFocusEnabled->setChecked(BreezyDesktopConfig::self()->zoomOnFocusEnabled());
ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked()); ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked());
ui.kcfg_SmoothFollowThreshold->setValue(BreezyDesktopConfig::self()->smoothFollowThreshold());
} }
void BreezyDesktopEffectConfig::updateUiFromDefaultConfig() void BreezyDesktopEffectConfig::updateUiFromDefaultConfig()
@ -446,6 +453,13 @@ void BreezyDesktopEffectConfig::pollDriverState()
m_connectedDeviceBrand = stateJson.value(QStringLiteral("connected_device_brand")).toString(); m_connectedDeviceBrand = stateJson.value(QStringLiteral("connected_device_brand")).toString();
m_connectedDeviceModel = stateJson.value(QStringLiteral("connected_device_model")).toString(); m_connectedDeviceModel = stateJson.value(QStringLiteral("connected_device_model")).toString();
const bool smoothFollow = smoothFollowEnabled(stateJsonOpt);
if (ui.SmoothFollowEnabled->isChecked() != smoothFollow) {
ui.SmoothFollowEnabled->setChecked(smoothFollow);
ui.kcfg_FocusedDisplayDistance->setEnabled(ui.kcfg_ZoomOnFocusEnabled->isChecked() || smoothFollow);
}
const bool wasDeviceConnected = m_deviceConnected; const bool wasDeviceConnected = m_deviceConnected;
m_deviceConnected = !m_connectedDeviceBrand.isEmpty() && !m_connectedDeviceModel.isEmpty(); m_deviceConnected = !m_connectedDeviceBrand.isEmpty() && !m_connectedDeviceModel.isEmpty();
if (!m_driverStateInitialized || m_deviceConnected != wasDeviceConnected) { if (!m_driverStateInitialized || m_deviceConnected != wasDeviceConnected) {
@ -471,6 +485,13 @@ bool BreezyDesktopEffectConfig::multitapEnabled(std::optional<QJsonObject> confi
return configJson.value(QStringLiteral("multi_tap_enabled")).toBool(); return configJson.value(QStringLiteral("multi_tap_enabled")).toBool();
} }
bool BreezyDesktopEffectConfig::smoothFollowEnabled(std::optional<QJsonObject> stateJsonOpt)
{
if (!stateJsonOpt) return false;
auto stateJson = stateJsonOpt.value();
return stateJson.value(QStringLiteral("breezy_desktop_smooth_follow_enabled")).toBool();
}
void BreezyDesktopEffectConfig::updateMultitapEnabled() void BreezyDesktopEffectConfig::updateMultitapEnabled()
{ {
auto configJsonOpt = XRDriverIPC::instance().retrieveConfig(); auto configJsonOpt = XRDriverIPC::instance().retrieveConfig();
@ -486,6 +507,26 @@ void BreezyDesktopEffectConfig::updateMultitapEnabled()
XRDriverIPC::instance().writeConfig(newConfig); XRDriverIPC::instance().writeConfig(newConfig);
} }
void BreezyDesktopEffectConfig::updateSmoothFollowEnabled()
{
auto stateJsonOpt = XRDriverIPC::instance().retrieveDriverState();
if (smoothFollowEnabled(stateJsonOpt) == ui.SmoothFollowEnabled->isChecked()) {
return;
}
bool enabled = ui.SmoothFollowEnabled->isChecked();
QJsonObject flags;
flags.insert(QStringLiteral("enable_breezy_desktop_smooth_follow"), enabled);
XRDriverIPC::instance().writeControlFlags(flags);
}
void BreezyDesktopEffectConfig::updateSmoothFollowThreshold()
{
BreezyDesktopEffectConfig::save();
QJsonObject flags;
flags.insert(QStringLiteral("breezy_desktop_follow_threshold"), ui.kcfg_SmoothFollowThreshold->value());
XRDriverIPC::instance().writeControlFlags(flags);
}
void BreezyDesktopEffectConfig::showStatus(QLabel *label, bool success, const QString &message) { void BreezyDesktopEffectConfig::showStatus(QLabel *label, bool success, const QString &message) {
if (!label) return; if (!label) return;
QPalette pal = label->palette(); QPalette pal = label->palette();

View File

@ -30,12 +30,15 @@ public Q_SLOTS:
private: private:
void updateDriverEnabled(); void updateDriverEnabled();
void updateMultitapEnabled(); void updateMultitapEnabled();
void updateSmoothFollowEnabled();
void updateSmoothFollowThreshold();
void updateUiFromConfig(); void updateUiFromConfig();
void updateUiFromDefaultConfig(); void updateUiFromDefaultConfig();
void updateConfigFromUi(); void updateConfigFromUi();
void updateUnmanagedState(); void updateUnmanagedState();
bool driverEnabled(std::optional<QJsonObject> configJsonOpt); bool driverEnabled(std::optional<QJsonObject> configJsonOpt);
bool multitapEnabled(std::optional<QJsonObject> configJsonOpt); bool multitapEnabled(std::optional<QJsonObject> configJsonOpt);
bool smoothFollowEnabled(std::optional<QJsonObject> stateJsonOpt);
void pollDriverState(); void pollDriverState();
void refreshLicenseUi(const QJsonObject &rootObj); void refreshLicenseUi(const QJsonObject &rootObj);
void checkEffectLoaded(); void checkEffectLoaded();
@ -55,6 +58,8 @@ private:
bool m_updatingFromConfig = false; bool m_updatingFromConfig = false;
bool m_driverStateInitialized = false; bool m_driverStateInitialized = false;
bool m_deviceConnected = false; bool m_deviceConnected = false;
bool m_smoothFollowEnabled = false;
int m_smoothFollowThreshold = 15;
QString m_connectedDeviceBrand; QString m_connectedDeviceBrand;
QString m_connectedDeviceModel; QString m_connectedDeviceModel;
QTimer m_statePollTimer; // periodic driver state polling QTimer m_statePollTimer; // periodic driver state polling

View File

@ -72,14 +72,24 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="SmoothFollowEnabled">
<property name="text">
<string>Follow mode</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelFocusedDisplayDistance"> <widget class="QLabel" name="labelFocusedDisplayDistance">
<property name="text"> <property name="text">
<string>Focused Display Distance:</string> <string>Focused Display Distance:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="3" column="1">
<widget class="LabeledSlider" name="kcfg_FocusedDisplayDistance"> <widget class="LabeledSlider" name="kcfg_FocusedDisplayDistance">
<property name="decimalShift"> <property name="decimalShift">
<double>2</double> <double>2</double>
@ -101,14 +111,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<widget class="QLabel" name="labelAllDisplaysDistance"> <widget class="QLabel" name="labelAllDisplaysDistance">
<property name="text"> <property name="text">
<string>All Displays Distance:</string> <string>All Displays Distance:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="1"> <item row="4" column="1">
<widget class="LabeledSlider" name="kcfg_AllDisplaysDistance"> <widget class="LabeledSlider" name="kcfg_AllDisplaysDistance">
<property name="decimalShift"> <property name="decimalShift">
<double>2</double> <double>2</double>
@ -130,14 +140,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="5" column="0">
<widget class="QLabel" name="labelDisplaySpacing"> <widget class="QLabel" name="labelDisplaySpacing">
<property name="text"> <property name="text">
<string>Display Spacing:</string> <string>Display Spacing:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="5" column="1">
<widget class="QSlider" name="kcfg_DisplaySpacing"> <widget class="QSlider" name="kcfg_DisplaySpacing">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -147,7 +157,36 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="6" column="0">
<widget class="QLabel" name="labelFollowThreshold">
<property name="text">
<string>Follow threshold:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="LabeledSlider" name="kcfg_SmoothFollowThreshold">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tracking">
<bool>false</bool>
</property>
<property name="tickPosition">
<enum>QSlider::NoTicks</enum>
</property>
<property name="tickStartOffset">
<double>9</double>
</property>
<property name="tickInterval">
<double>10</double>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="labelVirtualDisplays"> <widget class="QLabel" name="labelVirtualDisplays">
<property name="text"> <property name="text">
<string>Add Virtual Display:</string> <string>Add Virtual Display:</string>
@ -160,7 +199,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="7" column="1">
<widget class="QWidget" name="widgetVirtualDisplayButtons"> <widget class="QWidget" name="widgetVirtualDisplayButtons">
<property name="visible"> <property name="visible">
<bool>false</bool> <bool>false</bool>
@ -193,7 +232,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="6" column="0" colspan="2"> <item row="8" column="0" colspan="2">
<widget class="QWidget" name="widgetVirtualDisplayList"> <widget class="QWidget" name="widgetVirtualDisplayList">
<property name="visible"><bool>false</bool></property> <property name="visible"><bool>false</bool></property>
<property name="enabled"><bool>false</bool></property> <property name="enabled"><bool>false</bool></property>
@ -206,7 +245,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="7" column="0" colspan="2"> <item row="9" column="0" colspan="2">
<widget class="KShortcutsEditor" name="shortcutsEditor" native="true"> <widget class="KShortcutsEditor" name="shortcutsEditor" native="true">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred">

View File

@ -28,4 +28,10 @@ namespace BreezyShortcuts {
QStringLiteral("Toggle Zoom on Focus"), QStringLiteral("Toggle Zoom on Focus"),
QStringLiteral("Toggle Zoom on Focus") QStringLiteral("Toggle Zoom on Focus")
}; };
const Shortcut TOGGLE_FOLLOW_MODE = {
Qt::CTRL | Qt::META | Qt::Key_Return,
QStringLiteral("Toggle Follow Mode"),
QStringLiteral("Toggle Follow Mode")
};
} }

View File

@ -89,9 +89,8 @@ bool XRDriverIPC::writeConfig(const QJsonObject &configUpdate) {
return !out.isEmpty(); return !out.isEmpty();
} }
bool XRDriverIPC::writeControlFlags(const std::map<std::string, bool> &flags) { bool XRDriverIPC::writeControlFlags(const QJsonObject &flags) {
QJsonObject obj; for (const auto &kv : flags) obj.insert(QString::fromStdString(kv.first), kv.second); QByteArray payload = QJsonDocument(flags).toJson(QJsonDocument::Compact);
QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact);
QByteArray out = invokePython(QStringLiteral("write_control_flags"), payload, {}); QByteArray out = invokePython(QStringLiteral("write_control_flags"), payload, {});
return !out.isEmpty(); return !out.isEmpty();
} }

View File

@ -82,7 +82,7 @@ public:
std::optional<QJsonObject> retrieveConfig(); std::optional<QJsonObject> retrieveConfig();
std::optional<QJsonObject> retrieveDriverState(); std::optional<QJsonObject> retrieveDriverState();
bool writeConfig(const QJsonObject &configUpdate); bool writeConfig(const QJsonObject &configUpdate);
bool writeControlFlags(const std::map<std::string, bool> &flags); bool writeControlFlags(const QJsonObject &flags);
bool requestToken(const std::string &email); bool requestToken(const std::string &email);
bool verifyToken(const std::string &token); bool verifyToken(const std::string &token);

@ -1 +1 @@
Subproject commit 7fecfc604b553b9155837c58aaae423ba00afc63 Subproject commit d047bdf7b7300751a09464b6f62d67cb3e291031