Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ project(logitune VERSION ${LOGITUNE_VERSION} LANGUAGES CXX)
# first `cmake -B build`. Skipped for source-tarball builds (AUR, OBS) that
# have no .git, and no-ops if already pointing at hooks/.
# ---------------------------------------------------------------------------
if(Git_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
if(Git_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git" AND NOT LOGITUNE_NO_GIT_WRITES)
execute_process(
COMMAND ${GIT_EXECUTABLE} config --local --get core.hooksPath
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
Expand Down
8 changes: 5 additions & 3 deletions devices/mx-master-4/descriptor.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"reprogControls": true,
"gestureV2": false,
"mouseGesture": false,
"hapticFeedback": false,
"hapticFeedback": true,
"forceSensingButton": false,
"crown": false,
"reportRate": false,
Expand Down Expand Up @@ -45,7 +45,8 @@
{ "controlId": "0x0056", "buttonIndex": 4, "defaultName": "Forward", "defaultActionType": "default", "configurable": true },
{ "controlId": "0x00C3", "buttonIndex": 5, "defaultName": "Gesture button", "defaultActionType": "gesture-trigger", "configurable": true },
{ "controlId": "0x00C4", "buttonIndex": 6, "defaultName": "Shift wheel mode", "defaultActionType": "smartshift-toggle", "configurable": true },
{ "controlId": "0x0000", "buttonIndex": 7, "defaultName": "Thumb wheel", "defaultActionType": "default", "configurable": true }
{ "controlId": "0x0000", "buttonIndex": 7, "defaultName": "Thumb wheel", "defaultActionType": "default", "configurable": true },
{ "controlId": "0x01A0", "buttonIndex": 8, "defaultName": "Haptic button", "defaultActionType": "actions-ring", "configurable": true }
],
"hotspots": {
"buttons": [
Expand All @@ -54,7 +55,8 @@
{ "buttonIndex": 7, "xPct": 0.55, "yPct": 0.515, "side": "right", "labelOffsetYPct": 0.20 },
{ "buttonIndex": 4, "xPct": 0.43, "yPct": 0.50, "side": "left", "labelOffsetYPct": 0.0 },
{ "buttonIndex": 3, "xPct": 0.47, "yPct": 0.60, "side": "left", "labelOffsetYPct": 0.20 },
{ "buttonIndex": 5, "xPct": 0.39, "yPct": 0.38, "side": "left", "labelOffsetYPct": -0.05 }
{ "buttonIndex": 5, "xPct": 0.39, "yPct": 0.38, "side": "left", "labelOffsetYPct": -0.05 },
{ "buttonIndex": 8, "xPct": 0.36, "yPct": 0.73, "side": "left", "labelOffsetYPct": -0.08 }
],
"scroll": [
{ "buttonIndex": -1, "xPct": 0.73, "yPct": 0.23, "side": "right", "labelOffsetYPct": 0.0, "kind": "scrollwheel" },
Expand Down
1 change: 1 addition & 0 deletions devices/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"default",
"keystroke",
"gesture-trigger",
"actions-ring",
"smartshift-toggle",
"dpi-cycle",
"app-launch",
Expand Down
2 changes: 1 addition & 1 deletion pkg/obs/debian.control
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Homepage: https://github.com/mmaher88/logitune

Package: logitune
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, libqt6core6 (>= 6.4), libqt6quick6, libqt6svg6, libqt6dbus6, libqt6widgets6, libudev1, qml6-module-qtqml, qml6-module-qtqml-workerscript, qml6-module-qtquick, qml6-module-qtquick-controls, qml6-module-qtquick-dialogs, qml6-module-qtquick-layouts, qml6-module-qtquick-templates, qml6-module-qtquick-window
Depends: ${shlibs:Depends}, ${misc:Depends}, libqt6core6 (>= 6.4), libqt6quick6, libqt6svg6, libqt6dbus6, libqt6widgets6, libudev1, qml6-module-qt-labs-folderlistmodel, qml6-module-qtcore, qml6-module-qtqml, qml6-module-qtqml-workerscript, qml6-module-qtquick, qml6-module-qtquick-controls, qml6-module-qtquick-dialogs, qml6-module-qtquick-layouts, qml6-module-qtquick-templates, qml6-module-qtquick-window
Suggests: gnome-shell
Description: Logitech device configurator for Linux
Logitune is a Logitech Options+ alternative for Linux. Configure HID++
Expand Down
2 changes: 1 addition & 1 deletion scripts/package-deb.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Version: $VERSION
Section: utils
Priority: optional
Architecture: $ARCH
Depends: libqt6core6 (>= 6.4), libqt6qml6, libqt6quick6, libqt6svg6, libqt6dbus6, libqt6widgets6, libudev1, qml6-module-qtqml, qml6-module-qtqml-workerscript, qml6-module-qtquick, qml6-module-qtquick-controls, qml6-module-qtquick-dialogs, qml6-module-qtquick-layouts, qml6-module-qtquick-templates, qml6-module-qtquick-window
Depends: libqt6core6 (>= 6.4), libqt6qml6, libqt6quick6, libqt6svg6, libqt6dbus6, libqt6widgets6, libudev1, qml6-module-qt-labs-folderlistmodel, qml6-module-qtcore, qml6-module-qtqml, qml6-module-qtqml-workerscript, qml6-module-qtquick, qml6-module-qtquick-controls, qml6-module-qtquick-dialogs, qml6-module-qtquick-layouts, qml6-module-qtquick-templates, qml6-module-qtquick-window
Recommends: gnome-shell-extension-appindicator
Maintainer: Mina Maher <mina.maher88@hotmail.com>
Description: Logitech device configurator for Linux
Expand Down
13 changes: 12 additions & 1 deletion src/app/AppRoot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ void AppRoot::startMonitoring(bool simulateAll, bool editMode)

void AppRoot::wireSignals()
{
m_actionsRing = std::make_unique<ActionsRingService>(&m_profileEngine, &m_deviceManager,
&m_actionExecutor, &m_deviceResolver, m_desktop, &m_profileOrchestrator);
connect(&m_buttonDispatcher, &ButtonActionDispatcher::actionsRingTriggered,
m_actionsRing.get(), &ActionsRingService::trigger);
connect(&m_profileEngine, &ProfileEngine::deviceDisplayProfileChanged,
m_actionsRing.get(), &ActionsRingService::editorChanged);
connect(&m_deviceModel, &DeviceModel::selectedChanged, m_actionsRing.get(),
&ActionsRingService::editorChanged);
// User edits in the UI (button reassignments, focus-follow from desktop,
// tab selection, engine-driven display refresh) route to the orchestrator.
connect(&m_buttonModel, &ButtonModel::userActionChanged,
Expand Down Expand Up @@ -276,7 +284,10 @@ void AppRoot::onPhysicalDeviceAdded(PhysicalDevice *device)
connect(device, &PhysicalDevice::gestureRawXY,
&m_buttonDispatcher, &ButtonActionDispatcher::onGestureRaw);
connect(device, &PhysicalDevice::divertedButtonPressed,
&m_buttonDispatcher, &ButtonActionDispatcher::onDivertedButtonPressed);
&m_buttonDispatcher, [this, device](uint16_t cid, bool pressed) {
m_buttonDispatcher.dispatchButton(device->primary(), device->deviceSerial(),
device->descriptor(), cid, pressed);
});
connect(device, &PhysicalDevice::thumbWheelRotation,
&m_buttonDispatcher, &ButtonActionDispatcher::onThumbWheelRotation);

Expand Down
3 changes: 3 additions & 0 deletions src/app/AppRoot.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "services/DeviceCommandHandler.h"
#include "services/ButtonActionDispatcher.h"
#include "services/ProfileOrchestrator.h"
#include "services/ActionsRingService.h"
#include <QObject>
#include <memory>

Expand Down Expand Up @@ -68,6 +69,7 @@ class AppRoot : public QObject {
ProfileModel *profileModel() { return &m_profileModel; }
SettingsModel *settingsModel() { return &m_settingsModel; }
EditorModel *editorModel() const { return m_editorModel.get(); }
ActionsRingService *actionsRing() { return m_actionsRing.get(); }

private slots:
void onPhysicalDeviceAdded(PhysicalDevice *device);
Expand Down Expand Up @@ -107,6 +109,7 @@ private slots:
ActionExecutor m_actionExecutor;
ButtonActionDispatcher m_buttonDispatcher;
ProfileOrchestrator m_profileOrchestrator;
std::unique_ptr<ActionsRingService> m_actionsRing;

std::unique_ptr<EditorModel> m_editorModel;
};
Expand Down
26 changes: 25 additions & 1 deletion src/app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ add_library(logitune-app-lib STATIC
models/ActionModel.cpp
models/ProfileModel.cpp
models/EditorModel.cpp
models/InstalledAppsModel.cpp
services/ActiveDeviceResolver.cpp
services/DeviceCommandHandler.cpp
services/ButtonActionDispatcher.cpp
services/ProfileOrchestrator.cpp
services/ActionsRingService.cpp
dialogs/GitHubIssueBuilder.cpp
dialogs/CrashReportDialog.cpp
TrayManager.cpp
SingleInstance.cpp
MainWindowController.cpp
)

target_include_directories(logitune-app-lib PUBLIC
Expand All @@ -23,7 +27,12 @@ target_include_directories(logitune-app-lib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/services
)

target_link_libraries(logitune-app-lib PUBLIC logitune-core Qt6::Quick Qt6::Widgets)
target_link_libraries(logitune-app-lib PUBLIC logitune-core Qt6::Quick Qt6::Widgets Qt6::Network)
find_package(LayerShellQt QUIET)
if(TARGET LayerShellQt::Interface)
target_link_libraries(logitune-app-lib PUBLIC LayerShellQt::Interface)
target_compile_definitions(logitune-app-lib PRIVATE LOGITUNE_LAYER_SHELL)
endif()
target_compile_definitions(logitune-app-lib PUBLIC
PROJECT_VERSION="${PROJECT_VERSION}"
PROJECT_VERSION_FULL="${LOGITUNE_DISPLAY_VERSION}"
Expand Down Expand Up @@ -70,6 +79,20 @@ qt_add_qml_module(logitune-app-lib
qml/components/KeystrokeCapture.qml
qml/components/ButtonCallout.qml
qml/components/ActionsPanel.qml
qml/components/actionsring/Ring.qml
qml/components/actionsring/RingButton.qml
qml/components/actionsring/Glyph.qml
qml/components/actionsring/RingEditor.qml
qml/components/actionsring/RingSlotEditor.qml
qml/components/actionsring/RingEditorState.qml
qml/components/actionsring/RingActionLibrary.qml
qml/components/actionsring/RingPreview.qml
qml/components/actionsring/RingEditorBubble.qml
qml/components/actionsring/RingParameterCard.qml
qml/components/actionsring/RingTextField.qml
qml/components/actionsring/RingFlatButton.qml
qml/components/actionsring/RingIconPicker.qml
qml/components/actionsring/RingFileBrowser.qml
qml/components/DeviceCard.qml
qml/components/DeviceRender.qml
qml/components/AppProfileBar.qml
Expand All @@ -83,6 +106,7 @@ qt_add_qml_module(logitune-app-lib
qml/pages/ButtonsPage.qml
qml/pages/EasySwitchPage.qml
qml/pages/SettingsPage.qml
qml/pages/HapticsPage.qml
RESOURCES
qml/assets/logitune-icon.svg
qml/assets/logitune-tray.svg
Expand Down
65 changes: 65 additions & 0 deletions src/app/MainWindowController.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#include "MainWindowController.h"

#include <QQmlExpression>
#include <QTimer>

#ifdef __GLIBC__
#include <malloc.h>
#endif

namespace logitune {

MainWindowController::MainWindowController(QQmlApplicationEngine &engine, bool dark)
: m_engine(engine), m_dark(dark)
{
}

bool MainWindowController::showMainWindow()
{
if (!m_window) {
m_engine.load(QUrl(QStringLiteral("qrc:/Logitune/qml/Main.qml")));
if (m_engine.rootObjects().isEmpty())
return false;
m_window = qobject_cast<QQuickWindow*>(m_engine.rootObjects().last());
if (!m_window)
return false;

auto *window = m_window.data();
connect(window, &QQuickWindow::closing, this, [this, window]() {
m_window = nullptr;
// Defer deletion until Qt has finished delivering the close event.
window->deleteLater();
});
connect(window, &QObject::destroyed, this, [this]() {
QTimer::singleShot(0, this, [this]() {
m_engine.collectGarbage();
// Keep live singleton types valid while releasing unused UI components.
m_engine.trimComponentCache();
#ifdef __GLIBC__
// Return freed UI heap pages after deferred render cleanup has run.
QTimer::singleShot(100, this, [this]() {
if (!m_window)
malloc_trim(0);
});
#endif
});
});

if (!m_themeInitialized) {
QQmlExpression theme(QQmlEngine::contextForObject(window), window,
m_dark ? QStringLiteral("Theme.dark = true")
: QStringLiteral("Theme.dark = false"));
theme.evaluate();
if (theme.hasError())
qWarning() << "Theme expression error:" << theme.error().toString();
m_themeInitialized = true;
}
}

m_window->show();
m_window->raise();
m_window->requestActivate();
return true;
}

} // namespace logitune
24 changes: 24 additions & 0 deletions src/app/MainWindowController.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <QObject>
#include <QPointer>
#include <QQmlApplicationEngine>
#include <QQuickWindow>

namespace logitune {

class MainWindowController : public QObject
{
public:
explicit MainWindowController(QQmlApplicationEngine &engine, bool dark = false);

bool showMainWindow();

private:
QQmlApplicationEngine &m_engine;
QPointer<QQuickWindow> m_window;
bool m_dark;
bool m_themeInitialized = false;
};

} // namespace logitune
71 changes: 71 additions & 0 deletions src/app/SingleInstance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include "SingleInstance.h"

#include <QElapsedTimer>
#include <QLocalSocket>
#include <QThread>

namespace logitune {

SingleInstance::SingleInstance(const QString &lockPath, const QString &serverName)
: m_lockFile(lockPath), m_serverName(serverName)
{
m_server.setSocketOptions(QLocalServer::UserAccessOption);
connect(&m_server, &QLocalServer::newConnection, this, [this]() {
while (auto *socket = m_server.nextPendingConnection()) {
connect(socket, &QLocalSocket::disconnected, socket, &QObject::deleteLater);
auto receive = [this, socket]() {
if (socket->canReadLine()) {
const auto command = socket->readLine();
if (command == "show\n")
emit showRequested();
else if (command == "quit\n")
emit quitRequested();
else if (command == "ring\n")
emit ringRequested();
socket->disconnectFromServer();
} else if (socket->bytesAvailable() > 64) {
socket->disconnectFromServer();
}
};
connect(socket, &QLocalSocket::readyRead, this, receive);
receive();
}
});
}

std::optional<int> SingleInstance::start(Command command)
{
if (m_lockFile.tryLock(100)) {
if (command == Command::Quit) {
m_lockFile.unlock();
return 0;
}
// Only the lock owner may remove a socket left by a crashed instance.
QLocalServer::removeServer(m_serverName);
if (!m_server.listen(m_serverName))
return 1;
return std::nullopt;
}

// A concurrent first launch may own the lock before its socket is ready.
QElapsedTimer timeout;
timeout.start();
do {
QLocalSocket socket;
socket.connectToServer(m_serverName);
if (socket.waitForConnected(100)) {
const QByteArray message = command == Command::Quit ? "quit\n"
: command == Command::Ring ? "ring\n" : "show\n";
if (socket.write(message) != message.size())
return 1;
if (socket.bytesToWrite() && !socket.waitForBytesWritten(1000))
return 1;
return 0;
}
QThread::msleep(50);
} while (timeout.elapsed() < 2000);

return 1;
}

} // namespace logitune
32 changes: 32 additions & 0 deletions src/app/SingleInstance.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#pragma once

#include <QObject>
#include <QLocalServer>
#include <QLockFile>
#include <QString>
#include <optional>

namespace logitune {

class SingleInstance : public QObject
{
Q_OBJECT
public:
enum class Command { Show, Quit, Ring };
SingleInstance(const QString &lockPath, const QString &serverName);

// An exit code means this launch must not initialize the devices or UI.
std::optional<int> start(Command command = Command::Show);

signals:
void showRequested();
void quitRequested();
void ringRequested();

private:
QLockFile m_lockFile;
QLocalServer m_server;
QString m_serverName;
};

} // namespace logitune
Loading