From ce37bcef6df6ea164c30b5e538d1d7491eab5633 Mon Sep 17 00:00:00 2001 From: Lasan Mahaliyana Date: Tue, 30 Jun 2026 21:21:34 +0530 Subject: [PATCH] added global event bus. refactored targets screen --- sharing/linux/app/AppContent.qml | 114 ++++--------------------- sharing/linux/app/BUILD | 3 + sharing/linux/app/Drop.qml | 5 +- sharing/linux/app/EventBus.qml | 9 ++ sharing/linux/app/SearchingDevices.qml | 88 +++++++++++++++++++ sharing/linux/app/ShareTarget.qml | 46 +++++++--- sharing/linux/app/Sidebar.qml | 5 +- sharing/linux/app/Targets.qml | 6 ++ sharing/linux/app/main.cc | 65 +++++++++++++- sharing/linux/app/qmldir | 1 + sharing/linux/app/resources.qrc | 3 + 11 files changed, 225 insertions(+), 120 deletions(-) create mode 100644 sharing/linux/app/EventBus.qml create mode 100644 sharing/linux/app/SearchingDevices.qml create mode 100644 sharing/linux/app/qmldir diff --git a/sharing/linux/app/AppContent.qml b/sharing/linux/app/AppContent.qml index 850bdd5d..5e0bc922 100644 --- a/sharing/linux/app/AppContent.qml +++ b/sharing/linux/app/AppContent.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import QtQuick.Shapes +import "." RowLayout { id: top @@ -31,13 +32,23 @@ RowLayout { transferring = false; } } + + Connections { + target: EventBus + + function onFileSelected(path) { + top.pendingPath = path; + startSharing(); + } + + function onCancelPendingShareRequested() { + top.cancelPendingShare(); + } + } + Component.onCompleted: { backend.startReceive(); } - Component.onDestruction: { - backend.stopReceive(); - backend.stopDiscovery(); - } function cancelPendingShare() { pendingPath = ""; @@ -54,9 +65,6 @@ RowLayout { Sidebar { pendingPath: top.pendingPath - onCancelPendingShareRequested: { - top.cancelPendingShare(); - } } StackLayout { id: contentStack @@ -64,97 +72,9 @@ RowLayout { Layout.fillHeight: true currentIndex: top.currentIndex - Drop { - onFileSelected: path => { - top.pendingPath = path; - startSharing(); - } - } + Drop {} - Rectangle { - color: "transparent" - Layout.fillWidth: true - Layout.fillHeight: true - - Rectangle { - id: pulseButton - property real pulseSize: Math.min(parent.width, parent.height) * 0.5 - - width: pulseSize - height: pulseSize - radius: pulseSize / 2 - color: "#bde8f9" - anchors.centerIn: parent - - SequentialAnimation { - id: pulseAnimation - loops: Animation.Infinite - running: true - - ParallelAnimation { - NumberAnimation { - target: pulseButton - property: "scale" - from: 1.0 - to: 1.5 - duration: 1000 - easing.type: Easing.InOutSine - } - - NumberAnimation { - target: pulseButton - property: "opacity" - from: 1.0 - to: 0 - duration: 1000 - easing.type: Easing.InOutSine - } - } - - ParallelAnimation { - NumberAnimation { - target: pulseButton - property: "scale" - from: 1.5 - to: 1.0 - duration: 1000 - easing.type: Easing.InOutSine - } - - NumberAnimation { - target: pulseButton - property: "opacity" - from: 0 - to: 1.0 - duration: 1000 - easing.type: Easing.InOutSine - } - } - } - } - - ColumnLayout { - anchors.centerIn: parent - width: parent.width - 64 - spacing: 40 - - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 10 - - Text { - text: "Searching for devices..." - font.pointSize: 24 - font.weight: 600 - color: "#377B95" - } - } - - Targets { - Layout.alignment: Qt.AlignHCenter - } - } - } + SearchingDevices {} IncomingShare { filename: top.filename diff --git a/sharing/linux/app/BUILD b/sharing/linux/app/BUILD index 4076c2c8..fa40f691 100644 --- a/sharing/linux/app/BUILD +++ b/sharing/linux/app/BUILD @@ -23,10 +23,13 @@ qt_resource_via_qrc( qrc_file = "resources.qrc", files = [ "main.qml", + "qmldir", "AppContent.qml", + "EventBus.qml", "Drop.qml", "Sidebar.qml", "ShareTarget.qml", + "SearchingDevices.qml", "Targets.qml", "IncomingShare.qml", "googlesans_var.ttf", diff --git a/sharing/linux/app/Drop.qml b/sharing/linux/app/Drop.qml index 4795cd1a..97ea2332 100644 --- a/sharing/linux/app/Drop.qml +++ b/sharing/linux/app/Drop.qml @@ -4,10 +4,10 @@ import QtQuick.Controls import QtQuick.Shapes import Qt.labs.platform as Platform import QtCore +import "." Rectangle { id: dropZone - signal fileSelected(string filePath) color: "transparent" Platform.FileDialog { id: fileDialog @@ -17,13 +17,12 @@ Rectangle { nameFilters: ["All files (*)"] onAccepted: { - fileSelected(fileDialog.file); + EventBus.fileSelected(fileDialog.file); } onRejected: { console.log("File picker canceled"); } } - //signal fileDropped(string path) DropArea { anchors.fill: parent diff --git a/sharing/linux/app/EventBus.qml b/sharing/linux/app/EventBus.qml new file mode 100644 index 00000000..4133d026 --- /dev/null +++ b/sharing/linux/app/EventBus.qml @@ -0,0 +1,9 @@ +pragma Singleton + +import QtQuick + +QtObject { + signal fileSelected(string filePath) + signal cancelPendingShareRequested() + signal shareTargetSelected(var shareTargetId) +} diff --git a/sharing/linux/app/SearchingDevices.qml b/sharing/linux/app/SearchingDevices.qml new file mode 100644 index 00000000..fa59bdd3 --- /dev/null +++ b/sharing/linux/app/SearchingDevices.qml @@ -0,0 +1,88 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +Rectangle { + color: "transparent" + Layout.fillWidth: true + Layout.fillHeight: true + + Rectangle { + id: pulseButton + property real pulseSize: Math.min(parent.width, parent.height) * 0.5 + + width: pulseSize + height: pulseSize + radius: pulseSize / 2 + color: "#bde8f9" + anchors.centerIn: parent + + SequentialAnimation { + id: pulseAnimation + loops: Animation.Infinite + running: true + + ParallelAnimation { + NumberAnimation { + target: pulseButton + property: "scale" + from: 0.75 + to: 1.5 + duration: 600 + easing.type: Easing.InOutSine + } + + NumberAnimation { + target: pulseButton + property: "opacity" + from: 1.0 + to: 0 + duration: 600 + easing.type: Easing.InOutSine + } + } + + ParallelAnimation { + NumberAnimation { + target: pulseButton + property: "scale" + from: 1.5 + to: 0.75 + duration: 1500 + easing.type: Easing.InOutSine + } + + NumberAnimation { + target: pulseButton + property: "opacity" + from: 0 + to: 1.0 + duration: 1500 + easing.type: Easing.InOutSine + } + } + } + } + + ColumnLayout { + anchors.centerIn: parent + width: parent.width - 64 + spacing: 40 + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 10 + + Text { + text: "Searching for devices..." + font.pointSize: 24 + font.weight: 600 + color: "#377B95" + } + } + + Targets { + Layout.alignment: Qt.AlignHCenter + } + } +} diff --git a/sharing/linux/app/ShareTarget.qml b/sharing/linux/app/ShareTarget.qml index 7669181a..cdf1be11 100644 --- a/sharing/linux/app/ShareTarget.qml +++ b/sharing/linux/app/ShareTarget.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls +import "." Rectangle { id: rootItem @@ -12,18 +13,8 @@ Rectangle { // --- CUSTOM ARGUMENTS (PROPERTIES) --- property string deviceName: "Unknown Device" property string iconSource: "qrc:icons/smartphone.svg" - property string shareTargetId: "1" - - signal clickShareTarget() - - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - rootItem.clickShareTarget(rootItem.shareTargetId) - } - } + property var shareTargetId: 0 + property bool hovered: targetMouseArea.containsMouse ColumnLayout { anchors.top: parent.top @@ -31,6 +22,7 @@ Rectangle { anchors.right: parent.right spacing: 5 + // --- ICON CONTAINER --- Item { id: iconContainer @@ -38,13 +30,21 @@ Rectangle { implicitHeight: 60 Layout.alignment: Qt.AlignHCenter + // Icon Circle Background Rectangle { id: iconCircle anchors.fill: parent - color: "#6EA7B6" + color: rootItem.hovered ? "#195871" : "#6EA7B6" radius: width / 2 + Behavior on color { + ColorAnimation { + duration: 80 + easing.type: Easing.OutQuad + } + } + Button { id: iconButton icon.source: rootItem.iconSource @@ -59,6 +59,16 @@ Rectangle { } } } + + MouseArea { + id: targetMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + EventBus.shareTargetSelected(rootItem.shareTargetId); + } + } } // --- TEXT COMPONENT --- @@ -67,7 +77,15 @@ Rectangle { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap - color: "#1A1C1E" // Using the text charcoal color from your palette + color: rootItem.hovered ? "#06384C" : "#1A1C1E" + + Behavior on color { + ColorAnimation { + duration: 120 + easing.type: Easing.OutQuad + } + } } } + } diff --git a/sharing/linux/app/Sidebar.qml b/sharing/linux/app/Sidebar.qml index 5e8509c6..d6ac6ecb 100644 --- a/sharing/linux/app/Sidebar.qml +++ b/sharing/linux/app/Sidebar.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import QtQuick.Shapes +import "." Rectangle { id: sidebar @@ -12,8 +13,6 @@ Rectangle { color: "#CBF0FF" - signal cancelPendingShareRequested() - ColumnLayout { anchors.margins: 20 anchors.fill: parent @@ -110,7 +109,7 @@ Rectangle { id: cancelbutton Layout.fillWidth: true text: "Cancel" - onClicked: sidebar.cancelPendingShareRequested() + onClicked: EventBus.cancelPendingShareRequested() contentItem: Text { text: cancelbutton.text font.pointSize: 14 diff --git a/sharing/linux/app/Targets.qml b/sharing/linux/app/Targets.qml index d6578916..1a88c61d 100644 --- a/sharing/linux/app/Targets.qml +++ b/sharing/linux/app/Targets.qml @@ -12,22 +12,27 @@ Row { id: dummyTargets ListElement { + targetId: 1 deviceName: "Lasan's Pixel" type: 1 } ListElement { + targetId: 2 deviceName: "Work Laptop" type: 2 } ListElement { + targetId: 3 deviceName: "Kitchen Tablet" type: 3 } ListElement { + targetId: 4 deviceName: "A55" type: 1 } ListElement { + targetId: 5 deviceName: "Long Device Name For Wrapping" type: 2 } @@ -48,6 +53,7 @@ Row { spacing: 5 ShareTarget { + shareTargetId: shareTargetsRow.useDummyTargets ? model.targetId : model.id deviceName: model.deviceName iconSource: { if (model.type === 2) diff --git a/sharing/linux/app/main.cc b/sharing/linux/app/main.cc index f4b97b1a..5b9b9303 100644 --- a/sharing/linux/app/main.cc +++ b/sharing/linux/app/main.cc @@ -1,3 +1,9 @@ +#include +#include +#include + +#include + #include #include #include @@ -5,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +25,15 @@ constexpr char kQmlSourceDirEnv[] = "NEARBY_QML_SOURCE_DIR"; constexpr char kDefaultQmlSourceDir[] = "/home/lasan/Dev/nearby_latest/sharing/linux/app"; +int g_signal_pipe[2] = {-1, -1}; + +void HandleTerminationSignal(int signal) { + const char value = static_cast(signal); + if (g_signal_pipe[1] != -1) { + static_cast(write(g_signal_pipe[1], &value, sizeof(value))); + } +} + bool IsHotReloadEnabled() { const QByteArray value = qgetenv(kQmlHotReloadEnv); return value == "1" || value.toLower() == "true"; @@ -65,12 +81,56 @@ bool RequiresFullReload(const QFileInfo& changed_file) { return changed_file.fileName() == QStringLiteral("main.qml"); } +void InstallTerminationCleanup(QApplication& app, Backend& backend) { + auto cleanup_done = std::make_shared(false); + const auto cleanup = [&backend, cleanup_done]() { + if (*cleanup_done) { + return; + } + *cleanup_done = true; + backend.stopDiscovery(); + backend.stopReceive(); + }; + + QObject::connect(&app, &QCoreApplication::aboutToQuit, &app, cleanup); + + if (pipe(g_signal_pipe) != 0) { + return; + } + fcntl(g_signal_pipe[0], F_SETFL, + fcntl(g_signal_pipe[0], F_GETFL, 0) | O_NONBLOCK); + fcntl(g_signal_pipe[1], F_SETFL, + fcntl(g_signal_pipe[1], F_GETFL, 0) | O_NONBLOCK); + + auto* notifier = new QSocketNotifier(g_signal_pipe[0], + QSocketNotifier::Read, &app); + QObject::connect(notifier, &QSocketNotifier::activated, &app, + [&app, notifier, cleanup](int socket) { + notifier->setEnabled(false); + char buffer[32]; + while (read(socket, buffer, sizeof(buffer)) > 0) { + } + cleanup(); + app.quit(); + }); + + struct sigaction action {}; + action.sa_handler = HandleTerminationSignal; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + sigaction(SIGINT, &action, nullptr); + sigaction(SIGTERM, &action, nullptr); +} + } // namespace int main(int argc, char* argv[]) { QApplication app(argc, argv); QQmlApplicationEngine engine; + Backend backend; + InstallTerminationCleanup(app, backend); + const int fontId = QFontDatabase::addApplicationFont(":/googlesans_var.ttf"); if (fontId != -1) { @@ -83,6 +143,8 @@ int main(int argc, char* argv[]) { } if (IsHotReloadEnabled()) { + engine.rootContext()->setContextProperty("backend", &backend); + const QDir qml_source_dir = QmlSourceDir(); const QUrl source_url = QUrl::fromLocalFile(qml_source_dir.absoluteFilePath("main.qml")); @@ -132,10 +194,7 @@ int main(int argc, char* argv[]) { return app.exec(); } - // 1. Create the backend instance in C++ - Backend backend; - // 2. Inject it into the QML root context engine.rootContext()->setContextProperty("backend", &backend); // Loaded via the qrc scheme since it's compiled into the binary const QUrl url(QStringLiteral("qrc:/main.qml")); diff --git a/sharing/linux/app/qmldir b/sharing/linux/app/qmldir new file mode 100644 index 00000000..956d882b --- /dev/null +++ b/sharing/linux/app/qmldir @@ -0,0 +1 @@ +singleton EventBus 1.0 EventBus.qml diff --git a/sharing/linux/app/resources.qrc b/sharing/linux/app/resources.qrc index 67ce68e3..dca1bb4d 100644 --- a/sharing/linux/app/resources.qrc +++ b/sharing/linux/app/resources.qrc @@ -1,10 +1,13 @@ main.qml + qmldir AppContent.qml + EventBus.qml Sidebar.qml Drop.qml ShareTarget.qml + SearchingDevices.qml IncomingShare.qml Targets.qml googlesans_var.ttf