|
14 | 14 | #include <qlogging.h> |
15 | 15 | #include <qloggingcategory.h> |
16 | 16 | #include <qobject.h> |
| 17 | +#include <qproperty.h> |
| 18 | +#include <qqml.h> |
17 | 19 | #include <qtenvironmentvariables.h> |
18 | 20 | #include <qtmetamacros.h> |
19 | 21 | #include <qtypes.h> |
20 | 22 | #include <qvariant.h> |
21 | 23 |
|
22 | 24 | #include "../../../core/model.hpp" |
23 | 25 | #include "../../../core/qmlscreen.hpp" |
| 26 | +#include "../../toplevel_management/handle.hpp" |
| 27 | +#include "hyprland_toplevel.hpp" |
24 | 28 | #include "monitor.hpp" |
| 29 | +#include "toplevel_mapping.hpp" |
25 | 30 | #include "workspace.hpp" |
26 | 31 |
|
27 | 32 | namespace qs::hyprland::ipc { |
@@ -62,11 +67,16 @@ HyprlandIpc::HyprlandIpc() { |
62 | 67 | QObject::connect(&this->eventSocket, &QLocalSocket::errorOccurred, this, &HyprlandIpc::eventSocketError); |
63 | 68 | QObject::connect(&this->eventSocket, &QLocalSocket::stateChanged, this, &HyprlandIpc::eventSocketStateChanged); |
64 | 69 | QObject::connect(&this->eventSocket, &QLocalSocket::readyRead, this, &HyprlandIpc::eventSocketReady); |
| 70 | + |
| 71 | + auto *instance = HyprlandToplevelMappingManager::instance(); |
| 72 | + QObject::connect(instance, &HyprlandToplevelMappingManager::toplevelAddressed, this, &HyprlandIpc::toplevelAddressed); |
| 73 | + |
65 | 74 | // clang-format on |
66 | 75 |
|
67 | 76 | this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly); |
68 | 77 | this->refreshMonitors(true); |
69 | 78 | this->refreshWorkspaces(true); |
| 79 | + this->refreshToplevels(); |
70 | 80 | } |
71 | 81 |
|
72 | 82 | QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; } |
@@ -113,6 +123,36 @@ void HyprlandIpc::eventSocketReady() { |
113 | 123 | } |
114 | 124 | } |
115 | 125 |
|
| 126 | +void HyprlandIpc::toplevelAddressed( |
| 127 | + wayland::toplevel_management::impl::ToplevelHandle* handle, |
| 128 | + quint64 address |
| 129 | +) { |
| 130 | + auto* waylandToplevel = |
| 131 | + wayland::toplevel_management::ToplevelManager::instance()->forImpl(handle); |
| 132 | + |
| 133 | + if (!waylandToplevel) return; |
| 134 | + |
| 135 | + auto* attached = qobject_cast<HyprlandToplevel*>( |
| 136 | + qmlAttachedPropertiesObject<HyprlandToplevel>(waylandToplevel, false) |
| 137 | + ); |
| 138 | + |
| 139 | + auto* hyprToplevel = this->findToplevelByAddress(address, true); |
| 140 | + |
| 141 | + if (attached) { |
| 142 | + if (attached->address()) { |
| 143 | + qCDebug(logHyprlandIpc) << "Toplevel" << attached->addressStr() << "already has address" |
| 144 | + << address; |
| 145 | + |
| 146 | + return; |
| 147 | + } |
| 148 | + |
| 149 | + attached->setAddress(address); |
| 150 | + attached->setHyprlandHandle(hyprToplevel); |
| 151 | + } |
| 152 | + |
| 153 | + hyprToplevel->setWaylandHandle(waylandToplevel->implHandle()); |
| 154 | +} |
| 155 | + |
116 | 156 | void HyprlandIpc::makeRequest( |
117 | 157 | const QByteArray& request, |
118 | 158 | const std::function<void(bool, QByteArray)>& callback |
@@ -166,6 +206,8 @@ ObjectModel<HyprlandMonitor>* HyprlandIpc::monitors() { return &this->mMonitors; |
166 | 206 |
|
167 | 207 | ObjectModel<HyprlandWorkspace>* HyprlandIpc::workspaces() { return &this->mWorkspaces; } |
168 | 208 |
|
| 209 | +ObjectModel<HyprlandToplevel>* HyprlandIpc::toplevels() { return &this->mToplevels; } |
| 210 | + |
169 | 211 | QVector<QByteArrayView> HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) { |
170 | 212 | auto args = QVector<QByteArrayView>(); |
171 | 213 |
|
@@ -218,6 +260,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { |
218 | 260 | if (event->name == "configreloaded") { |
219 | 261 | this->refreshMonitors(true); |
220 | 262 | this->refreshWorkspaces(true); |
| 263 | + this->refreshToplevels(); |
221 | 264 | } else if (event->name == "monitoraddedv2") { |
222 | 265 | auto args = event->parseView(3); |
223 | 266 |
|
@@ -390,6 +433,133 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { |
390 | 433 | // the fullscreen state changed, but this falls apart if you move a fullscreen |
391 | 434 | // window between workspaces. |
392 | 435 | this->refreshWorkspaces(false); |
| 436 | + } else if (event->name == "openwindow") { |
| 437 | + auto args = event->parseView(4); |
| 438 | + auto ok = false; |
| 439 | + auto windowAddress = args.at(0).toULongLong(&ok, 16); |
| 440 | + |
| 441 | + if (!ok) return; |
| 442 | + |
| 443 | + auto workspaceName = QString::fromUtf8(args.at(1)); |
| 444 | + auto windowTitle = QString::fromUtf8(args.at(2)); |
| 445 | + auto windowClass = QString::fromUtf8(args.at(3)); |
| 446 | + |
| 447 | + auto* workspace = this->findWorkspaceByName(workspaceName, false); |
| 448 | + if (!workspace) { |
| 449 | + qCWarning(logHyprlandIpc) << "Got openwindow for workspace" << workspaceName |
| 450 | + << "which was not previously tracked."; |
| 451 | + return; |
| 452 | + } |
| 453 | + |
| 454 | + auto* toplevel = this->findToplevelByAddress(windowAddress, false); |
| 455 | + const bool existed = toplevel != nullptr; |
| 456 | + |
| 457 | + if (!toplevel) toplevel = new HyprlandToplevel(this); |
| 458 | + toplevel->updateInitial(windowAddress, windowTitle, workspaceName); |
| 459 | + |
| 460 | + workspace->insertToplevel(toplevel); |
| 461 | + |
| 462 | + if (!existed) { |
| 463 | + this->mToplevels.insertObject(toplevel); |
| 464 | + qCDebug(logHyprlandIpc) << "New toplevel created with address" << windowAddress << ", title" |
| 465 | + << windowTitle << ", workspace" << workspaceName; |
| 466 | + } |
| 467 | + } else if (event->name == "closewindow") { |
| 468 | + auto args = event->parseView(1); |
| 469 | + auto ok = false; |
| 470 | + auto windowAddress = args.at(0).toULongLong(&ok, 16); |
| 471 | + |
| 472 | + if (!ok) return; |
| 473 | + |
| 474 | + const auto& mList = this->mToplevels.valueList(); |
| 475 | + auto toplevelIter = std::ranges::find_if(mList, [windowAddress](HyprlandToplevel* m) { |
| 476 | + return m->address() == windowAddress; |
| 477 | + }); |
| 478 | + |
| 479 | + if (toplevelIter == mList.end()) { |
| 480 | + qCWarning(logHyprlandIpc) << "Got closewindow for address" << windowAddress |
| 481 | + << "which was not previously tracked."; |
| 482 | + return; |
| 483 | + } |
| 484 | + |
| 485 | + auto* toplevel = *toplevelIter; |
| 486 | + auto index = toplevelIter - mList.begin(); |
| 487 | + this->mToplevels.removeAt(index); |
| 488 | + |
| 489 | + // Remove from workspace |
| 490 | + auto* workspace = toplevel->bindableWorkspace().value(); |
| 491 | + if (workspace) { |
| 492 | + workspace->toplevels()->removeObject(toplevel); |
| 493 | + } |
| 494 | + |
| 495 | + delete toplevel; |
| 496 | + } else if (event->name == "movewindowv2") { |
| 497 | + auto args = event->parseView(3); |
| 498 | + auto ok = false; |
| 499 | + auto windowAddress = args.at(0).toULongLong(&ok, 16); |
| 500 | + auto workspaceName = QString::fromUtf8(args.at(2)); |
| 501 | + |
| 502 | + auto* toplevel = this->findToplevelByAddress(windowAddress, false); |
| 503 | + if (!toplevel) { |
| 504 | + qCWarning(logHyprlandIpc) << "Got movewindowv2 event for client with address" << windowAddress |
| 505 | + << "which was not previously tracked."; |
| 506 | + return; |
| 507 | + } |
| 508 | + |
| 509 | + HyprlandWorkspace* workspace = this->findWorkspaceByName(workspaceName, false); |
| 510 | + if (!workspace) { |
| 511 | + qCWarning(logHyprlandIpc) << "Got movewindowv2 event for workspace" << args.at(2) |
| 512 | + << "which was not previously tracked."; |
| 513 | + return; |
| 514 | + } |
| 515 | + |
| 516 | + auto* oldWorkspace = toplevel->bindableWorkspace().value(); |
| 517 | + toplevel->setWorkspace(workspace); |
| 518 | + |
| 519 | + if (oldWorkspace) { |
| 520 | + oldWorkspace->removeToplevel(toplevel); |
| 521 | + } |
| 522 | + |
| 523 | + workspace->insertToplevel(toplevel); |
| 524 | + } else if (event->name == "windowtitlev2") { |
| 525 | + auto args = event->parseView(2); |
| 526 | + auto ok = false; |
| 527 | + auto windowAddress = args.at(0).toULongLong(&ok, 16); |
| 528 | + auto windowTitle = QString::fromUtf8(args.at(1)); |
| 529 | + |
| 530 | + if (!ok) return; |
| 531 | + |
| 532 | + // It happens that Hyprland sends windowtitlev2 events before event |
| 533 | + // "openwindow" is emitted, so let's preemptively create it |
| 534 | + auto* toplevel = this->findToplevelByAddress(windowAddress, true); |
| 535 | + if (!toplevel) { |
| 536 | + qCWarning(logHyprlandIpc) << "Got windowtitlev2 event for client with address" |
| 537 | + << windowAddress << "which was not previously tracked."; |
| 538 | + return; |
| 539 | + } |
| 540 | + |
| 541 | + toplevel->bindableTitle().setValue(windowTitle); |
| 542 | + } else if (event->name == "activewindowv2") { |
| 543 | + auto args = event->parseView(1); |
| 544 | + auto ok = false; |
| 545 | + auto windowAddress = args.at(0).toULongLong(&ok, 16); |
| 546 | + |
| 547 | + if (!ok) return; |
| 548 | + |
| 549 | + // Did not observe "activewindowv2" event before "openwindow", |
| 550 | + // but better safe than sorry, so create if missing. |
| 551 | + auto* toplevel = this->findToplevelByAddress(windowAddress, true); |
| 552 | + this->bActiveToplevel = toplevel; |
| 553 | + } else if (event->name == "urgent") { |
| 554 | + auto args = event->parseView(1); |
| 555 | + auto ok = false; |
| 556 | + auto windowAddress = args.at(0).toULongLong(&ok, 16); |
| 557 | + |
| 558 | + if (!ok) return; |
| 559 | + |
| 560 | + // It happens that Hyprland sends urgent before "openwindow" |
| 561 | + auto* toplevel = this->findToplevelByAddress(windowAddress, true); |
| 562 | + toplevel->bindableUrgent().setValue(true); |
393 | 563 | } |
394 | 564 | } |
395 | 565 |
|
@@ -496,6 +666,71 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) { |
496 | 666 | }); |
497 | 667 | } |
498 | 668 |
|
| 669 | +HyprlandToplevel* HyprlandIpc::findToplevelByAddress(quint64 address, bool createIfMissing) { |
| 670 | + const auto& mList = this->mToplevels.valueList(); |
| 671 | + HyprlandToplevel* toplevel = nullptr; |
| 672 | + |
| 673 | + auto toplevelIter = |
| 674 | + std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == address; }); |
| 675 | + |
| 676 | + toplevel = toplevelIter == mList.end() ? nullptr : *toplevelIter; |
| 677 | + |
| 678 | + if (!toplevel && createIfMissing) { |
| 679 | + qCDebug(logHyprlandIpc) << "Toplevel with address" << address |
| 680 | + << "requested before creation, performing early init"; |
| 681 | + |
| 682 | + toplevel = new HyprlandToplevel(this); |
| 683 | + toplevel->updateInitial(address, "", ""); |
| 684 | + this->mToplevels.insertObject(toplevel); |
| 685 | + } |
| 686 | + |
| 687 | + return toplevel; |
| 688 | +} |
| 689 | + |
| 690 | +void HyprlandIpc::refreshToplevels() { |
| 691 | + if (this->requestingToplevels) return; |
| 692 | + this->requestingToplevels = true; |
| 693 | + |
| 694 | + this->makeRequest("j/clients", [this](bool success, const QByteArray& resp) { |
| 695 | + this->requestingToplevels = false; |
| 696 | + if (!success) return; |
| 697 | + |
| 698 | + qCDebug(logHyprlandIpc) << "Parsing j/clients response"; |
| 699 | + auto json = QJsonDocument::fromJson(resp).array(); |
| 700 | + |
| 701 | + const auto& mList = this->mToplevels.valueList(); |
| 702 | + |
| 703 | + for (auto entry: json) { |
| 704 | + auto object = entry.toObject().toVariantMap(); |
| 705 | + |
| 706 | + bool ok = false; |
| 707 | + auto address = object.value("address").toString().toULongLong(&ok, 16); |
| 708 | + |
| 709 | + if (!ok) { |
| 710 | + qCWarning(logHyprlandIpc) << "Invalid address in j/clients entry:" << object; |
| 711 | + continue; |
| 712 | + } |
| 713 | + |
| 714 | + auto toplevelsIter = |
| 715 | + std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == address; }); |
| 716 | + |
| 717 | + auto* toplevel = toplevelsIter == mList.end() ? nullptr : *toplevelsIter; |
| 718 | + auto exists = toplevel != nullptr; |
| 719 | + |
| 720 | + if (!exists) toplevel = new HyprlandToplevel(this); |
| 721 | + toplevel->updateFromObject(object); |
| 722 | + |
| 723 | + if (!exists) { |
| 724 | + qCDebug(logHyprlandIpc) << "New toplevel created with address" << address; |
| 725 | + this->mToplevels.insertObject(toplevel); |
| 726 | + } |
| 727 | + |
| 728 | + auto* workspace = toplevel->bindableWorkspace().value(); |
| 729 | + workspace->insertToplevel(toplevel); |
| 730 | + } |
| 731 | + }); |
| 732 | +} |
| 733 | + |
499 | 734 | HyprlandMonitor* |
500 | 735 | HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { |
501 | 736 | const auto& mList = this->mMonitors.valueList(); |
|
0 commit comments