@dusted/anqst 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/emit.js CHANGED
@@ -626,6 +626,27 @@ function buildCppTypeContext(spec) {
626
626
  }
627
627
  return normalizer.buildContext();
628
628
  }
629
+ function collectDragDropMimeConstants(spec) {
630
+ const seen = new Set();
631
+ const constants = [];
632
+ for (const service of spec.services) {
633
+ for (const member of service.members) {
634
+ if ((member.kind === "DropTarget" || member.kind === "HoverTarget") && member.payloadTypeText) {
635
+ const typeName = member.payloadTypeText.replace(/\s/g, "");
636
+ const key = `${service.name}-${typeName}`;
637
+ if (seen.has(key))
638
+ continue;
639
+ seen.add(key);
640
+ constants.push({
641
+ typeName,
642
+ serviceName: service.name,
643
+ mimeType: `application/anqst-dragdropevent_${service.name}-${typeName}`
644
+ });
645
+ }
646
+ }
647
+ }
648
+ return constants;
649
+ }
629
650
  function renderTypesHeader(spec, cppTypes) {
630
651
  const decls = cppTypes.orderedDecls.map(renderCppDecl).join("\n\n");
631
652
  const metatypes = cppTypes.structNames
@@ -634,6 +655,11 @@ function renderTypesHeader(spec, cppTypes) {
634
655
  `Q_DECLARE_METATYPE(QList<${spec.widgetName}::${name}>)`
635
656
  ])
636
657
  .join("\n");
658
+ const mimeConstants = collectDragDropMimeConstants(spec);
659
+ const mimeConstantLines = mimeConstants
660
+ .map((c) => `static constexpr const char* kDragDropMime_${c.typeName} = "${c.mimeType}";`)
661
+ .join("\n");
662
+ const mimeBlock = mimeConstantLines.length > 0 ? `\n${mimeConstantLines}\n` : "";
637
663
  return `#pragma once
638
664
  #include <QString>
639
665
  #include <QStringList>
@@ -646,19 +672,30 @@ function renderTypesHeader(spec, cppTypes) {
646
672
  namespace ${spec.widgetName} {
647
673
 
648
674
  ${decls}
649
-
675
+ ${mimeBlock}
650
676
  } // namespace ${spec.widgetName}
651
677
 
652
678
  ${metatypes}
653
679
  `;
654
680
  }
681
+ function renderWidgetUmbrellaHeader(spec) {
682
+ return `#pragma once
683
+ // Built by <AnQst_version>
684
+ #include "${spec.widgetName}Widget.h"
685
+ #include "${spec.widgetName}Types.h"
686
+ `;
687
+ }
655
688
  function renderWidgetHeader(spec, cppTypes) {
689
+ const widgetClassName = `${spec.widgetName}Widget`;
656
690
  const callbackAliases = [];
657
691
  const publicMethods = [];
692
+ const slotMethods = [];
693
+ const handleMethods = [];
694
+ const callSetterMethods = [];
658
695
  const signals = [];
659
696
  const properties = [];
660
697
  const fields = [];
661
- const outputSetters = [];
698
+ const publicSlots = [];
662
699
  const bindings = [];
663
700
  for (const service of spec.services) {
664
701
  for (const member of service.members) {
@@ -666,21 +703,20 @@ function renderWidgetHeader(spec, cppTypes) {
666
703
  const memberPascal = pascalCase(member.name);
667
704
  if (member.kind === "Call" && member.payloadTypeText) {
668
705
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
669
- const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
706
+ const args = member.parameters.map((p) => `const ${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])}& ${p.name}`).join(", ");
670
707
  callbackAliases.push(`using ${memberPascal}Handler = std::function<${cppType}(${args})>;`);
671
- publicMethods.push(`void set${memberPascal}Handler(const ${memberPascal}Handler& handler);`);
708
+ handleMethods.push(` void ${member.name}(const ${memberPascal}Handler& handler) const;`);
709
+ callSetterMethods.push(`void set${memberPascal}CallHandler(const ${memberPascal}Handler& handler);`);
672
710
  fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
673
711
  }
674
712
  else if (member.kind === "Emitter") {
675
- const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
676
- callbackAliases.push(`using ${memberPascal}Handler = std::function<void(${args})>;`);
677
- publicMethods.push(`void set${memberPascal}Handler(const ${memberPascal}Handler& handler);`);
678
- fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
713
+ const args = member.parameters.map((p) => `const ${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])}& ${p.name}`).join(", ");
714
+ signals.push(`void ${member.name}(${args});`);
679
715
  }
680
716
  else if (member.kind === "Slot") {
681
717
  const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
682
718
  const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
683
- publicMethods.push(`${ret} ${member.name}(${args}${args ? ", " : ""}bool* ok = nullptr, QString* error = nullptr);`);
719
+ slotMethods.push(`${ret} slot_${member.name}(${args});`);
684
720
  }
685
721
  else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
686
722
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
@@ -696,13 +732,25 @@ function renderWidgetHeader(spec, cppTypes) {
696
732
  fields.push(`${memberPascal}Handler m_${member.name}Handler;`);
697
733
  }
698
734
  else {
699
- outputSetters.push(`void publish${memberPascal}(const ${cppType}& value);`);
735
+ publicSlots.push(`void ${member.name}Slot(const ${cppType}& value);`);
700
736
  }
701
737
  }
738
+ else if (member.kind === "DropTarget" && member.payloadTypeText) {
739
+ const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
740
+ signals.push(`void ${member.name}(const ${cppType}& payload, double x, double y);`);
741
+ }
742
+ else if (member.kind === "HoverTarget" && member.payloadTypeText) {
743
+ const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
744
+ signals.push(`void ${member.name}(const ${cppType}& payload, double x, double y);`);
745
+ signals.push(`void ${member.name}Left();`);
746
+ }
702
747
  }
703
748
  }
704
749
  return `#pragma once
750
+ #include <QDateTime>
705
751
  #include <QHash>
752
+ #include <QMetaMethod>
753
+ #include <QQueue>
706
754
  #include <QVariant>
707
755
  #include <QVariantList>
708
756
  #include <functional>
@@ -710,28 +758,54 @@ function renderWidgetHeader(spec, cppTypes) {
710
758
  #include "${spec.widgetName}Types.h"
711
759
 
712
760
  namespace ${spec.widgetName} {
761
+ } // namespace ${spec.widgetName}
762
+
763
+ using namespace ${spec.widgetName};
713
764
 
714
- class ${spec.widgetName} : public AnQstWebHostBase {
765
+ class ${widgetClassName} : public AnQstWebHostBase {
715
766
  Q_OBJECT
716
767
  ${properties.map((p) => ` ${p}`).join("\n")}
717
768
 
718
769
  public:
719
- explicit ${spec.widgetName}(QWidget* parent = nullptr);
720
- ~${spec.widgetName}() override;
770
+ ${callbackAliases.map((s) => ` ${s}`).join("\n")}
771
+
772
+ class handle {
773
+ public:
774
+ explicit handle(${widgetClassName}* owner) : m_owner(owner) {}
775
+ ${handleMethods.join("\n")}
776
+ private:
777
+ ${widgetClassName}* m_owner;
778
+ };
779
+
780
+ explicit ${widgetClassName}(QWidget* parent = nullptr);
781
+ ~${widgetClassName}() override;
721
782
  bool enableDebug();
722
783
  static constexpr const char* kBootstrapEntryPoint = "index.html";
723
784
  static constexpr const char* kBootstrapContentRoot = "qrc:/${spec.widgetName.toLowerCase()}";
724
785
  static constexpr const char* kBootstrapBridgeObject = "${spec.widgetName}Bridge";
786
+ static constexpr int kMaxQueuedCallsPerEndpoint = 1024;
725
787
 
726
- ${callbackAliases.map((s) => ` ${s}`).join("\n")}
788
+ handle handle;
727
789
  ${publicMethods.map((s) => ` ${s}`).join("\n")}
728
- ${outputSetters.map((s) => ` ${s}`).join("\n")}
790
+
791
+ public slots:
792
+ ${slotMethods.map((s) => ` ${s}`).join("\n")}
793
+ ${publicSlots.map((s) => ` ${s}`).join("\n")}
729
794
 
730
795
  signals:
731
796
  ${signals.map((s) => ` ${s}`).join("\n")}
732
797
  void diagnosticsForwarded(const QVariantMap& payload);
733
798
 
799
+ protected:
800
+ void connectNotify(const QMetaMethod& signal) override;
801
+ void disconnectNotify(const QMetaMethod& signal) override;
802
+
734
803
  private:
804
+ struct PendingCallInvocation {
805
+ QString requestId;
806
+ QVariantList args;
807
+ QDateTime enqueuedAt;
808
+ };
735
809
  struct BridgeBindingRow {
736
810
  const char* service;
737
811
  const char* member;
@@ -741,21 +815,37 @@ private:
741
815
  static constexpr int kBridgeBindingsCount = ${bindings.length};
742
816
  static QString makeBindingKey(const QString& service, const QString& member);
743
817
  void installBridgeBindings();
818
+ bool hasEmitterListeners(const QString& service, const QString& member) const;
744
819
  QVariant handleGeneratedCall(const QString& service, const QString& member, const QVariantList& args);
745
820
  void handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args);
746
821
  void handleGeneratedInput(const QString& service, const QString& member, const QVariant& value);
747
-
822
+ QVariant waitForCallHandlerAndInvoke(
823
+ const QString& service,
824
+ const QString& member,
825
+ const QString& requestId,
826
+ int timeoutMs,
827
+ const std::function<QVariant()>& invokeNow);
828
+ void removeQueuedCallById(const QString& queueKey, const QString& requestId);
829
+ ${callSetterMethods.map((s) => ` ${s}`).join("\n")}
830
+
831
+ qulonglong m_callRequestCounter{0};
832
+ QHash<QString, QQueue<PendingCallInvocation>> m_queuedCalls;
748
833
  ${fields.map((f) => ` ${f}`).join("\n")}
749
834
  };
750
-
751
- } // namespace ${spec.widgetName}
752
835
  `;
753
836
  }
754
837
  function renderCppStub(spec, cppTypes) {
838
+ const widgetClassName = `${spec.widgetName}Widget`;
755
839
  const lines = [];
756
- lines.push(`#include "include/${spec.widgetName}.h"`);
840
+ lines.push(`#include "include/${spec.widgetName}Widget.h"`);
757
841
  lines.push(`#include <QDebug>`);
842
+ lines.push(`#include <QElapsedTimer>`);
843
+ lines.push(`#include <QEventLoop>`);
758
844
  lines.push(`#include <QMetaType>`);
845
+ lines.push(`#include <QTimer>`);
846
+ lines.push(`#include <stdexcept>`);
847
+ lines.push("");
848
+ lines.push(`using namespace ${spec.widgetName};`);
759
849
  lines.push("");
760
850
  lines.push(`extern int qInitResources_${spec.widgetName}();`);
761
851
  lines.push("");
@@ -772,9 +862,23 @@ function renderCppStub(spec, cppTypes) {
772
862
  lines.push("}");
773
863
  lines.push("}");
774
864
  lines.push("");
775
- lines.push(`namespace ${spec.widgetName} {`);
776
- lines.push("");
777
- lines.push(`const ${spec.widgetName}::BridgeBindingRow ${spec.widgetName}::kBridgeBindings[] = {`);
865
+ for (const service of spec.services) {
866
+ for (const member of service.members) {
867
+ if (member.kind !== "Call" || !member.payloadTypeText)
868
+ continue;
869
+ const pascal = pascalCase(member.name);
870
+ lines.push(`void ${widgetClassName}::handle::${member.name}(const ${pascal}Handler& handler) const {`);
871
+ lines.push(` if (m_owner == nullptr) return;`);
872
+ lines.push(` m_owner->set${pascal}CallHandler(handler);`);
873
+ lines.push(`}`);
874
+ lines.push("");
875
+ lines.push(`void ${widgetClassName}::set${pascal}CallHandler(const ${pascal}Handler& handler) {`);
876
+ lines.push(` m_${member.name}Handler = handler;`);
877
+ lines.push(`}`);
878
+ lines.push("");
879
+ }
880
+ }
881
+ lines.push(`const ${widgetClassName}::BridgeBindingRow ${widgetClassName}::kBridgeBindings[] = {`);
778
882
  for (const service of spec.services) {
779
883
  for (const member of service.members) {
780
884
  lines.push(` {"${service.name}", "${member.name}", "${member.kind}"},`);
@@ -782,7 +886,7 @@ function renderCppStub(spec, cppTypes) {
782
886
  }
783
887
  lines.push(`};`);
784
888
  lines.push("");
785
- lines.push(`${spec.widgetName}::${spec.widgetName}(QWidget* parent) : AnQstWebHostBase(parent) {`);
889
+ lines.push(`${widgetClassName}::${widgetClassName}(QWidget* parent) : AnQstWebHostBase(parent), handle(this) {`);
786
890
  lines.push(` static const bool kResourcesInitialized = []() {`);
787
891
  lines.push(` ::qInitResources_${spec.widgetName}();`);
788
892
  lines.push(` return true;`);
@@ -790,7 +894,46 @@ function renderCppStub(spec, cppTypes) {
790
894
  lines.push(` Q_UNUSED(kResourcesInitialized);`);
791
895
  lines.push(` registerGeneratedMetaTypes();`);
792
896
  lines.push(` installBridgeBindings();`);
793
- lines.push(` QObject::connect(this, &AnQstWebHostBase::onHostError, this, &${spec.widgetName}::diagnosticsForwarded);`);
897
+ for (const service of spec.services) {
898
+ for (const member of service.members) {
899
+ if (member.kind === "DropTarget" && member.payloadTypeText) {
900
+ const typeName = member.payloadTypeText.replace(/\s/g, "");
901
+ const mimeConst = `${spec.widgetName}::kDragDropMime_${typeName}`;
902
+ lines.push(` registerDropTarget(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), QString::fromUtf8(${mimeConst}));`);
903
+ }
904
+ else if (member.kind === "HoverTarget" && member.payloadTypeText) {
905
+ const typeName = member.payloadTypeText.replace(/\s/g, "");
906
+ const mimeConst = `${spec.widgetName}::kDragDropMime_${typeName}`;
907
+ lines.push(` registerHoverTarget(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), QString::fromUtf8(${mimeConst}), ${member.hoverThrottleMs});`);
908
+ }
909
+ }
910
+ }
911
+ for (const service of spec.services) {
912
+ for (const member of service.members) {
913
+ if (member.kind === "DropTarget" && member.payloadTypeText) {
914
+ const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
915
+ lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_dropReceived, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {`);
916
+ lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
917
+ lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
918
+ lines.push(` }`);
919
+ lines.push(` });`);
920
+ }
921
+ else if (member.kind === "HoverTarget" && member.payloadTypeText) {
922
+ const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
923
+ lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_hoverUpdated, this, [this](const QString& service, const QString& member, const QVariant& payload, double x, double y) {`);
924
+ lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
925
+ lines.push(` emit ${member.name}(payload.value<${cppType}>(), x, y);`);
926
+ lines.push(` }`);
927
+ lines.push(` });`);
928
+ lines.push(` QObject::connect(this, &AnQstWebHostBase::anQstBridge_hoverLeft, this, [this](const QString& service, const QString& member) {`);
929
+ lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
930
+ lines.push(` emit ${member.name}Left();`);
931
+ lines.push(` }`);
932
+ lines.push(` });`);
933
+ }
934
+ }
935
+ }
936
+ lines.push(` QObject::connect(this, &AnQstWebHostBase::onHostError, this, &${widgetClassName}::diagnosticsForwarded);`);
794
937
  lines.push(` const bool rootOk = setContentRoot(QString::fromUtf8(kBootstrapContentRoot));`);
795
938
  lines.push(` const bool bridgeOk = setBridgeObject(this, QString::fromUtf8(kBootstrapBridgeObject));`);
796
939
  lines.push(` const bool loadOk = rootOk && bridgeOk && loadEntryPoint(QString::fromUtf8(kBootstrapEntryPoint));`);
@@ -799,17 +942,74 @@ function renderCppStub(spec, cppTypes) {
799
942
  lines.push(` }`);
800
943
  lines.push("}");
801
944
  lines.push("");
802
- lines.push(`${spec.widgetName}::~${spec.widgetName}() = default;`);
945
+ lines.push(`${widgetClassName}::~${widgetClassName}() = default;`);
803
946
  lines.push("");
804
- lines.push(`bool ${spec.widgetName}::enableDebug() {`);
947
+ lines.push(`bool ${widgetClassName}::enableDebug() {`);
805
948
  lines.push(` return AnQstWebHostBase::enableDebug();`);
806
949
  lines.push("}");
807
950
  lines.push("");
808
- lines.push(`QString ${spec.widgetName}::makeBindingKey(const QString& service, const QString& member) {`);
951
+ lines.push(`QString ${widgetClassName}::makeBindingKey(const QString& service, const QString& member) {`);
809
952
  lines.push(` return service + QStringLiteral("::") + member;`);
810
953
  lines.push(`}`);
811
954
  lines.push("");
812
- lines.push(`void ${spec.widgetName}::installBridgeBindings() {`);
955
+ lines.push(`void ${widgetClassName}::removeQueuedCallById(const QString& queueKey, const QString& requestId) {`);
956
+ lines.push(` if (!m_queuedCalls.contains(queueKey)) return;`);
957
+ lines.push(` auto& queue = m_queuedCalls[queueKey];`);
958
+ lines.push(` for (int i = 0; i < queue.size(); ++i) {`);
959
+ lines.push(` if (queue[i].requestId == requestId) {`);
960
+ lines.push(` queue.removeAt(i);`);
961
+ lines.push(` break;`);
962
+ lines.push(` }`);
963
+ lines.push(` }`);
964
+ lines.push(`}`);
965
+ lines.push("");
966
+ lines.push(`QVariant ${widgetClassName}::waitForCallHandlerAndInvoke(`);
967
+ lines.push(` const QString& service,`);
968
+ lines.push(` const QString& member,`);
969
+ lines.push(` const QString& requestId,`);
970
+ lines.push(` int timeoutMs,`);
971
+ lines.push(` const std::function<QVariant()>& invokeNow) {`);
972
+ lines.push(` const QString queueKey = makeBindingKey(service, member);`);
973
+ lines.push(` QElapsedTimer timer;`);
974
+ lines.push(` timer.start();`);
975
+ lines.push(` QEventLoop loop;`);
976
+ lines.push(` QTimer tick;`);
977
+ lines.push(` tick.setSingleShot(true);`);
978
+ lines.push(` QObject::connect(&tick, &QTimer::timeout, &loop, &QEventLoop::quit);`);
979
+ lines.push(` while (true) {`);
980
+ lines.push(` if (m_queuedCalls.contains(queueKey) && !m_queuedCalls[queueKey].isEmpty() && m_queuedCalls[queueKey].head().requestId == requestId) {`);
981
+ lines.push(` m_queuedCalls[queueKey].dequeue();`);
982
+ lines.push(` return invokeNow();`);
983
+ lines.push(` }`);
984
+ lines.push(` if (timeoutMs > 0 && timer.elapsed() >= timeoutMs) {`);
985
+ lines.push(` removeQueuedCallById(queueKey, requestId);`);
986
+ lines.push(` return QVariantMap{`);
987
+ lines.push(` {QStringLiteral("code"), QStringLiteral("BridgeTimeoutError")},`);
988
+ lines.push(` {QStringLiteral("message"), QStringLiteral("Call timed out while waiting for callback registration.")},`);
989
+ lines.push(` {QStringLiteral("service"), service},`);
990
+ lines.push(` {QStringLiteral("member"), member},`);
991
+ lines.push(` {QStringLiteral("requestId"), requestId}`);
992
+ lines.push(` };`);
993
+ lines.push(` }`);
994
+ lines.push(` tick.start(10);`);
995
+ lines.push(` loop.exec();`);
996
+ lines.push(` }`);
997
+ lines.push(`}`);
998
+ lines.push("");
999
+ lines.push(`bool ${widgetClassName}::hasEmitterListeners(const QString& service, const QString& member) const {`);
1000
+ for (const service of spec.services) {
1001
+ for (const member of service.members) {
1002
+ if (member.kind !== "Emitter")
1003
+ continue;
1004
+ lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
1005
+ lines.push(` return isSignalConnected(QMetaMethod::fromSignal(&${widgetClassName}::${member.name}));`);
1006
+ lines.push(` }`);
1007
+ }
1008
+ }
1009
+ lines.push(` return false;`);
1010
+ lines.push(`}`);
1011
+ lines.push("");
1012
+ lines.push(`void ${widgetClassName}::installBridgeBindings() {`);
813
1013
  lines.push(` setCallHandler([this](const QString& service, const QString& member, const QVariantList& args) -> QVariant {`);
814
1014
  lines.push(` return handleGeneratedCall(service, member, args);`);
815
1015
  lines.push(` });`);
@@ -821,55 +1021,91 @@ function renderCppStub(spec, cppTypes) {
821
1021
  lines.push(` });`);
822
1022
  lines.push(`}`);
823
1023
  lines.push("");
824
- lines.push(`QVariant ${spec.widgetName}::handleGeneratedCall(const QString& service, const QString& member, const QVariantList& args) {`);
1024
+ lines.push(`QVariant ${widgetClassName}::handleGeneratedCall(const QString& service, const QString& member, const QVariantList& args) {`);
825
1025
  for (const service of spec.services) {
826
1026
  for (const member of service.members) {
827
1027
  if (member.kind !== "Call" || !member.payloadTypeText)
828
1028
  continue;
1029
+ const timeoutMs = member.timeoutMs;
829
1030
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
830
- const pascal = pascalCase(member.name);
831
1031
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
832
- lines.push(` if (!m_${member.name}Handler) {`);
833
- lines.push(` if (property("anqstDesignerContext").toBool()) {`);
834
- lines.push(` return ${cppToVariantExpression(cppType, designerPlaceholderCppExpression(cppType, member.name))};`);
835
- lines.push(` }`);
836
- lines.push(` return QVariant();`);
837
- lines.push(` }`);
838
1032
  for (let i = 0; i < member.parameters.length; i++) {
839
1033
  const p = member.parameters[i];
840
1034
  const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
841
1035
  lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
842
1036
  }
843
- const argNames = member.parameters.map((p) => p.name).join(", ");
844
- lines.push(` const ${cppType} result = m_${member.name}Handler(${argNames});`);
845
- lines.push(` return ${cppToVariantExpression(cppType, "result")};`);
1037
+ lines.push(` const QString requestId = QStringLiteral("call-%1").arg(++m_callRequestCounter);`);
1038
+ lines.push(` const QString queueKey = makeBindingKey(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"));`);
1039
+ lines.push(` auto invokeNow = [this, requestId${member.parameters.length > 0 ? `, ${member.parameters.map((p) => p.name).join(", ")}` : ""}]() -> QVariant {`);
1040
+ lines.push(` if (!m_${member.name}Handler) {`);
1041
+ lines.push(` return QVariantMap{`);
1042
+ lines.push(` {QStringLiteral("code"), QStringLiteral("HandlerNotRegisteredError")},`);
1043
+ lines.push(` {QStringLiteral("message"), QStringLiteral("No callback registered for Call endpoint.")},`);
1044
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1045
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1046
+ lines.push(` {QStringLiteral("requestId"), requestId}`);
1047
+ lines.push(` };`);
1048
+ lines.push(` }`);
1049
+ lines.push(` try {`);
1050
+ const callArgs = member.parameters.map((p) => p.name).join(", ");
1051
+ lines.push(` const ${cppType} result = m_${member.name}Handler(${callArgs});`);
1052
+ lines.push(` return ${cppToVariantExpression(cppType, "result")};`);
1053
+ lines.push(` } catch (const std::exception& ex) {`);
1054
+ lines.push(` return QVariantMap{`);
1055
+ lines.push(` {QStringLiteral("code"), QStringLiteral("CallHandlerError")},`);
1056
+ lines.push(` {QStringLiteral("message"), QString::fromUtf8(ex.what())},`);
1057
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1058
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1059
+ lines.push(` {QStringLiteral("requestId"), requestId}`);
1060
+ lines.push(` };`);
1061
+ lines.push(` } catch (...) {`);
1062
+ lines.push(` return QVariantMap{`);
1063
+ lines.push(` {QStringLiteral("code"), QStringLiteral("CallHandlerError")},`);
1064
+ lines.push(` {QStringLiteral("message"), QStringLiteral("Call handler threw unknown exception.")},`);
1065
+ lines.push(` {QStringLiteral("service"), QStringLiteral("${service.name}")},`);
1066
+ lines.push(` {QStringLiteral("member"), QStringLiteral("${member.name}")},`);
1067
+ lines.push(` {QStringLiteral("requestId"), requestId}`);
1068
+ lines.push(` };`);
1069
+ lines.push(` }`);
1070
+ lines.push(` };`);
1071
+ lines.push(` if (m_${member.name}Handler) {`);
1072
+ lines.push(` return invokeNow();`);
1073
+ lines.push(` }`);
1074
+ lines.push(` auto& queue = m_queuedCalls[queueKey];`);
1075
+ lines.push(` if (queue.size() >= kMaxQueuedCallsPerEndpoint) {`);
1076
+ lines.push(` queue.dequeue();`);
1077
+ lines.push(` }`);
1078
+ lines.push(` queue.enqueue(PendingCallInvocation{requestId, args, QDateTime::currentDateTimeUtc()});`);
1079
+ lines.push(` return waitForCallHandlerAndInvoke(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), requestId, ${timeoutMs}, invokeNow);`);
846
1080
  lines.push(` }`);
847
1081
  }
848
1082
  }
849
1083
  lines.push(` return QVariant();`);
850
1084
  lines.push(`}`);
851
1085
  lines.push("");
852
- lines.push(`void ${spec.widgetName}::handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args) {`);
1086
+ lines.push(`void ${widgetClassName}::handleGeneratedEmitter(const QString& service, const QString& member, const QVariantList& args) {`);
1087
+ lines.push(` if (!hasEmitterListeners(service, member)) {`);
1088
+ lines.push(` return;`);
1089
+ lines.push(` }`);
853
1090
  for (const service of spec.services) {
854
1091
  for (const member of service.members) {
855
1092
  if (member.kind !== "Emitter")
856
1093
  continue;
857
1094
  lines.push(` if (service == QStringLiteral("${service.name}") && member == QStringLiteral("${member.name}")) {`);
858
- lines.push(` if (!m_${member.name}Handler) return;`);
859
1095
  for (let i = 0; i < member.parameters.length; i++) {
860
1096
  const p = member.parameters[i];
861
1097
  const pType = cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name]);
862
1098
  lines.push(` const ${pType} ${p.name} = ${variantToCppExpression(pType, `args.value(${i})`)};`);
863
1099
  }
864
1100
  const argNames = member.parameters.map((p) => p.name).join(", ");
865
- lines.push(` m_${member.name}Handler(${argNames});`);
1101
+ lines.push(` emit ${member.name}(${argNames});`);
866
1102
  lines.push(` return;`);
867
1103
  lines.push(` }`);
868
1104
  }
869
1105
  }
870
1106
  lines.push(`}`);
871
1107
  lines.push("");
872
- lines.push(`void ${spec.widgetName}::handleGeneratedInput(const QString& service, const QString& member, const QVariant& value) {`);
1108
+ lines.push(`void ${widgetClassName}::handleGeneratedInput(const QString& service, const QString& member, const QVariant& value) {`);
873
1109
  for (const service of spec.services) {
874
1110
  for (const member of service.members) {
875
1111
  if (member.kind !== "Input" || !member.payloadTypeText)
@@ -888,20 +1124,8 @@ function renderCppStub(spec, cppTypes) {
888
1124
  for (const service of spec.services) {
889
1125
  for (const member of service.members) {
890
1126
  const memberPascal = pascalCase(member.name);
891
- if ((member.kind === "Call" || member.kind === "Input") && member.payloadTypeText) {
892
- lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
893
- lines.push(` m_${member.name}Handler = handler;`);
894
- lines.push("}");
895
- lines.push("");
896
- }
897
- else if (member.kind === "Emitter") {
898
- lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
899
- lines.push(` m_${member.name}Handler = handler;`);
900
- lines.push("}");
901
- lines.push("");
902
- }
903
- else if (member.kind === "Input" && member.payloadTypeText) {
904
- lines.push(`void ${spec.widgetName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
1127
+ if (member.kind === "Input" && member.payloadTypeText) {
1128
+ lines.push(`void ${widgetClassName}::set${memberPascal}Handler(const ${memberPascal}Handler& handler) {`);
905
1129
  lines.push(` m_${member.name}Handler = handler;`);
906
1130
  lines.push("}");
907
1131
  lines.push("");
@@ -909,8 +1133,7 @@ function renderCppStub(spec, cppTypes) {
909
1133
  if (member.kind === "Slot") {
910
1134
  const ret = member.payloadTypeText ? cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]) : "void";
911
1135
  const args = member.parameters.map((p) => `${cppTypes.mapTypeText(p.typeText, [service.name, member.name, p.name])} ${p.name}`).join(", ");
912
- const argsWithMeta = `${args}${args ? ", " : ""}bool* ok, QString* error`;
913
- lines.push(`${ret} ${spec.widgetName}::${member.name}(${argsWithMeta}) {`);
1136
+ lines.push(`${ret} ${widgetClassName}::slot_${member.name}(${args}) {`);
914
1137
  lines.push(` QVariantList invokeArgs;`);
915
1138
  for (const p of member.parameters) {
916
1139
  const pType = mapTsTypeToCpp(p.typeText);
@@ -919,14 +1142,18 @@ function renderCppStub(spec, cppTypes) {
919
1142
  lines.push(` QVariant result;`);
920
1143
  lines.push(` QString invokeError;`);
921
1144
  lines.push(` const bool success = invokeSlot(QStringLiteral("${service.name}"), QStringLiteral("${member.name}"), invokeArgs, &result, &invokeError);`);
922
- lines.push(` if (ok != nullptr) *ok = success;`);
923
- lines.push(` if (error != nullptr) *error = invokeError;`);
1145
+ lines.push(` if (!success) {`);
1146
+ lines.push(` if (invokeError == QStringLiteral("slot invocation timeout")) {`);
1147
+ lines.push(` const QString timeoutMsg = QStringLiteral("[Timeout] ${service.name}.${member.name}: The webapp inside the widget did not anwser within %1 ms.").arg(slotInvocationTimeoutMs());`);
1148
+ lines.push(` throw std::runtime_error(timeoutMsg.toStdString());`);
1149
+ lines.push(` }`);
1150
+ lines.push(` const QString requestFailed = QStringLiteral("[RequestFailed]: %1").arg(invokeError);`);
1151
+ lines.push(` throw std::runtime_error(requestFailed.toStdString());`);
1152
+ lines.push(` }`);
924
1153
  if (ret === "void") {
925
- lines.push(` if (!success) return;`);
926
1154
  lines.push(` return;`);
927
1155
  }
928
1156
  else {
929
- lines.push(` if (!success) return ${ret}{};`);
930
1157
  lines.push(` return ${variantToCppExpression(ret, "result")};`);
931
1158
  }
932
1159
  lines.push("}");
@@ -935,11 +1162,11 @@ function renderCppStub(spec, cppTypes) {
935
1162
  else if ((member.kind === "Input" || member.kind === "Output") && member.payloadTypeText) {
936
1163
  const cppType = cppTypes.mapTypeText(member.payloadTypeText, [service.name, member.name, "Payload"]);
937
1164
  const cap = member.name.charAt(0).toUpperCase() + member.name.slice(1);
938
- lines.push(`${cppType} ${spec.widgetName}::${member.name}() const {`);
1165
+ lines.push(`${cppType} ${widgetClassName}::${member.name}() const {`);
939
1166
  lines.push(` return m_${member.name};`);
940
1167
  lines.push("}");
941
1168
  lines.push("");
942
- lines.push(`void ${spec.widgetName}::set${cap}(const ${cppType}& value) {`);
1169
+ lines.push(`void ${widgetClassName}::set${cap}(const ${cppType}& value) {`);
943
1170
  lines.push(` if (m_${member.name} == value) return;`);
944
1171
  lines.push(` m_${member.name} = value;`);
945
1172
  if (member.kind === "Output") {
@@ -949,7 +1176,7 @@ function renderCppStub(spec, cppTypes) {
949
1176
  lines.push("}");
950
1177
  lines.push("");
951
1178
  if (member.kind === "Output") {
952
- lines.push(`void ${spec.widgetName}::publish${pascalCase(member.name)}(const ${cppType}& value) {`);
1179
+ lines.push(`void ${widgetClassName}::${member.name}Slot(const ${cppType}& value) {`);
953
1180
  lines.push(` set${cap}(value);`);
954
1181
  lines.push(`}`);
955
1182
  lines.push("");
@@ -957,7 +1184,16 @@ function renderCppStub(spec, cppTypes) {
957
1184
  }
958
1185
  }
959
1186
  }
960
- lines.push(`} // namespace ${spec.widgetName}`);
1187
+ lines.push(`void ${widgetClassName}::connectNotify(const QMetaMethod& signal) {`);
1188
+ lines.push(` AnQstWebHostBase::connectNotify(signal);`);
1189
+ lines.push(` Q_UNUSED(signal);`);
1190
+ lines.push(`}`);
1191
+ lines.push("");
1192
+ lines.push(`void ${widgetClassName}::disconnectNotify(const QMetaMethod& signal) {`);
1193
+ lines.push(` AnQstWebHostBase::disconnectNotify(signal);`);
1194
+ lines.push(` Q_UNUSED(signal);`);
1195
+ lines.push(`}`);
1196
+ lines.push("");
961
1197
  return lines.join("\n");
962
1198
  }
963
1199
  function renderCMake(spec) {
@@ -977,6 +1213,7 @@ add_library(${spec.widgetName}Widget
977
1213
  ${spec.widgetName}.cpp
978
1214
  ${spec.widgetName}.qrc
979
1215
  include/${spec.widgetName}.h
1216
+ include/${spec.widgetName}Widget.h
980
1217
  include/${spec.widgetName}Types.h
981
1218
  )
982
1219
  target_include_directories(${spec.widgetName}Widget
@@ -1114,6 +1351,12 @@ function renderLocalTypeImports(spec) {
1114
1351
  return "";
1115
1352
  return `import type { ${localTypeNames.join(", ")} } from "./types";`;
1116
1353
  }
1354
+ function slotHandlerReturnType(tsRet) {
1355
+ if (tsRet === "void") {
1356
+ return "void | Promise<void> | Error";
1357
+ }
1358
+ return `${tsRet} | Promise<${tsRet}> | Error`;
1359
+ }
1117
1360
  function renderTsService(spec, serviceName) {
1118
1361
  const members = spec.services.find((s) => s.name === serviceName)?.members ?? [];
1119
1362
  const fieldLines = [];
@@ -1137,7 +1380,7 @@ function renderTsService(spec, serviceName) {
1137
1380
  }
1138
1381
  if (m.kind === "Slot") {
1139
1382
  const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
1140
- onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${ret}): void => {`);
1383
+ onSlotMembers.push(` ${m.name}: (handler: (${args}) => ${slotHandlerReturnType(ret)}): void => {`);
1141
1384
  onSlotMembers.push(` this._bridge.registerSlot("${serviceName}", "${m.name}", handler as (...args: unknown[]) => unknown);`);
1142
1385
  onSlotMembers.push(" },");
1143
1386
  continue;
@@ -1156,6 +1399,19 @@ function renderTsService(spec, serviceName) {
1156
1399
  constructorBodyLines.push(` this._bridge.onOutput("${serviceName}", "${m.name}", (value) => this._${m.name}.set(value as ${tsType}));`);
1157
1400
  }
1158
1401
  }
1402
+ if (m.kind === "DropTarget" && m.payloadTypeText) {
1403
+ const tsType = mapTypeTextToTs(m.payloadTypeText);
1404
+ fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
1405
+ methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}(); }`);
1406
+ constructorBodyLines.push(` this._bridge.onDrop("${serviceName}", "${m.name}", (payload, x, y) => this._${m.name}.set({ payload: payload as ${tsType}, x, y }));`);
1407
+ }
1408
+ if (m.kind === "HoverTarget" && m.payloadTypeText) {
1409
+ const tsType = mapTypeTextToTs(m.payloadTypeText);
1410
+ fieldLines.push(` private readonly _${m.name} = signal<{ payload: ${tsType}; x: number; y: number } | null>(null);`);
1411
+ methodLines.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null { return this._${m.name}(); }`);
1412
+ constructorBodyLines.push(` this._bridge.onHover("${serviceName}", "${m.name}", (payload, x, y) => this._${m.name}.set({ payload: payload as ${tsType}, x, y }));`);
1413
+ constructorBodyLines.push(` this._bridge.onHoverLeft("${serviceName}", "${m.name}", () => this._${m.name}.set(null));`);
1414
+ }
1159
1415
  }
1160
1416
  const constructorLines = [
1161
1417
  " constructor() {",
@@ -1197,7 +1453,7 @@ function renderTsServiceDts(spec, serviceName) {
1197
1453
  }
1198
1454
  if (m.kind === "Slot") {
1199
1455
  const ret = mapTypeTextToTs(m.payloadTypeText ?? "void");
1200
- onSlotMembers.push(` ${m.name}(handler: (${args}) => ${ret}): void;`);
1456
+ onSlotMembers.push(` ${m.name}(handler: (${args}) => ${slotHandlerReturnType(ret)}): void;`);
1201
1457
  continue;
1202
1458
  }
1203
1459
  if ((m.kind === "Input" || m.kind === "Output") && m.payloadTypeText) {
@@ -1207,6 +1463,14 @@ function renderTsServiceDts(spec, serviceName) {
1207
1463
  setMembers.push(` ${m.name}(value: ${tsType}): void;`);
1208
1464
  }
1209
1465
  }
1466
+ if (m.kind === "DropTarget" && m.payloadTypeText) {
1467
+ const tsType = mapTypeTextToTs(m.payloadTypeText);
1468
+ classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
1469
+ }
1470
+ if (m.kind === "HoverTarget" && m.payloadTypeText) {
1471
+ const tsType = mapTypeTextToTs(m.payloadTypeText);
1472
+ classMembers.push(` ${m.name}(): { payload: ${tsType}; x: number; y: number } | null;`);
1473
+ }
1210
1474
  }
1211
1475
  const setInterfaceDecl = setMembers.length > 0
1212
1476
  ? `export interface ${setInterfaceName} {\n${setMembers.join("\n")}\n}`
@@ -1237,6 +1501,9 @@ type SlotHandler = (...args: unknown[]) => unknown;
1237
1501
  type OutputHandler = (value: unknown) => void;
1238
1502
  type SlotInvocationListener = (requestId: string, service: string, member: string, args: unknown[]) => void;
1239
1503
  type OutputListener = (service: string, member: string, value: unknown) => void;
1504
+ type DropListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
1505
+ type HoverListener = (service: string, member: string, payload: unknown, x: number, y: number) => void;
1506
+ type HoverLeftListener = (service: string, member: string) => void;
1240
1507
 
1241
1508
  interface HostBridgeApi {
1242
1509
  anQstBridge_call(service: string, member: string, args: unknown[], callback: (result: unknown) => void): void;
@@ -1248,6 +1515,9 @@ interface HostBridgeApi {
1248
1515
  anQstBridge_slotInvocationRequested: {
1249
1516
  connect: (cb: (requestId: string, service: string, member: string, args: unknown[]) => void) => void;
1250
1517
  };
1518
+ anQstBridge_dropReceived: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
1519
+ anQstBridge_hoverUpdated: { connect: (cb: (service: string, member: string, payload: unknown, x: number, y: number) => void) => void };
1520
+ anQstBridge_hoverLeft: { connect: (cb: (service: string, member: string) => void) => void };
1251
1521
  }
1252
1522
 
1253
1523
  interface QWebChannelCtor {
@@ -1265,6 +1535,27 @@ interface BridgeAdapter {
1265
1535
  resolveSlot(requestId: string, ok: boolean, payload: unknown, error: string): void;
1266
1536
  onOutput(handler: OutputListener): void;
1267
1537
  onSlotInvocation(handler: SlotInvocationListener): void;
1538
+ onDrop(handler: DropListener): void;
1539
+ onHover(handler: HoverListener): void;
1540
+ onHoverLeft(handler: HoverLeftListener): void;
1541
+ }
1542
+
1543
+ function isBridgeCallError(value: unknown): value is {
1544
+ code: unknown;
1545
+ message: unknown;
1546
+ service: unknown;
1547
+ member: unknown;
1548
+ requestId: unknown;
1549
+ } {
1550
+ if (value === null || typeof value !== "object") return false;
1551
+ const row = value as Record<string, unknown>;
1552
+ return (
1553
+ Object.prototype.hasOwnProperty.call(row, "code")
1554
+ && Object.prototype.hasOwnProperty.call(row, "message")
1555
+ && Object.prototype.hasOwnProperty.call(row, "service")
1556
+ && Object.prototype.hasOwnProperty.call(row, "member")
1557
+ && Object.prototype.hasOwnProperty.call(row, "requestId")
1558
+ );
1268
1559
  }
1269
1560
 
1270
1561
  class QtWebChannelAdapter implements BridgeAdapter {
@@ -1300,8 +1591,14 @@ class QtWebChannelAdapter implements BridgeAdapter {
1300
1591
  }
1301
1592
 
1302
1593
  async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
1303
- return new Promise<T>((resolve) => {
1304
- this.host.anQstBridge_call(service, member, args, (result) => resolve(result as T));
1594
+ return new Promise<T>((resolve, reject) => {
1595
+ this.host.anQstBridge_call(service, member, args, (result) => {
1596
+ if (isBridgeCallError(result)) {
1597
+ reject(result);
1598
+ return;
1599
+ }
1600
+ resolve(result as T);
1601
+ });
1305
1602
  });
1306
1603
  }
1307
1604
 
@@ -1328,12 +1625,33 @@ class QtWebChannelAdapter implements BridgeAdapter {
1328
1625
  onSlotInvocation(handler: SlotInvocationListener): void {
1329
1626
  this.host.anQstBridge_slotInvocationRequested.connect(handler);
1330
1627
  }
1628
+
1629
+ onDrop(handler: DropListener): void {
1630
+ this.host.anQstBridge_dropReceived.connect(handler);
1631
+ }
1632
+
1633
+ onHover(handler: HoverListener): void {
1634
+ this.host.anQstBridge_hoverUpdated.connect(handler);
1635
+ }
1636
+
1637
+ onHoverLeft(handler: HoverLeftListener): void {
1638
+ this.host.anQstBridge_hoverLeft.connect(handler);
1639
+ }
1331
1640
  }
1332
1641
 
1333
1642
  class WebSocketBridgeAdapter implements BridgeAdapter {
1334
- private readonly pending = new Map<string, (result: unknown) => void>();
1643
+ private readonly pending = new Map<string, {
1644
+ service: string;
1645
+ member: string;
1646
+ requestId: string;
1647
+ resolve: (result: unknown) => void;
1648
+ reject: (error: unknown) => void;
1649
+ }>();
1335
1650
  private readonly outputListeners: OutputListener[] = [];
1336
1651
  private readonly slotListeners: SlotInvocationListener[] = [];
1652
+ private readonly dropListeners: DropListener[] = [];
1653
+ private readonly hoverListeners: HoverListener[] = [];
1654
+ private readonly hoverLeftListeners: HoverLeftListener[] = [];
1337
1655
  private requestCounter = 0;
1338
1656
 
1339
1657
  private constructor(private readonly socket: WebSocket) {
@@ -1343,10 +1661,15 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1343
1661
  const type = String(message["type"] ?? "");
1344
1662
  if (type === "callResult") {
1345
1663
  const requestId = String(message["requestId"] ?? "");
1346
- const resolver = this.pending.get(requestId);
1347
- if (resolver) {
1664
+ const pending = this.pending.get(requestId);
1665
+ if (pending) {
1348
1666
  this.pending.delete(requestId);
1349
- resolver(message["result"]);
1667
+ const result = message["result"];
1668
+ if (isBridgeCallError(result)) {
1669
+ pending.reject(result);
1670
+ return;
1671
+ }
1672
+ pending.resolve(result);
1350
1673
  }
1351
1674
  return;
1352
1675
  }
@@ -1368,6 +1691,34 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1368
1691
  }
1369
1692
  return;
1370
1693
  }
1694
+ if (type === "dropReceived") {
1695
+ const service = String(message["service"] ?? "");
1696
+ const member = String(message["member"] ?? "");
1697
+ const x = Number(message["x"] ?? 0);
1698
+ const y = Number(message["y"] ?? 0);
1699
+ for (const listener of this.dropListeners) {
1700
+ listener(service, member, message["payload"], x, y);
1701
+ }
1702
+ return;
1703
+ }
1704
+ if (type === "hoverUpdated") {
1705
+ const service = String(message["service"] ?? "");
1706
+ const member = String(message["member"] ?? "");
1707
+ const x = Number(message["x"] ?? 0);
1708
+ const y = Number(message["y"] ?? 0);
1709
+ for (const listener of this.hoverListeners) {
1710
+ listener(service, member, message["payload"], x, y);
1711
+ }
1712
+ return;
1713
+ }
1714
+ if (type === "hoverLeft") {
1715
+ const service = String(message["service"] ?? "");
1716
+ const member = String(message["member"] ?? "");
1717
+ for (const listener of this.hoverLeftListeners) {
1718
+ listener(service, member);
1719
+ }
1720
+ return;
1721
+ }
1371
1722
  if (type === "hostError") {
1372
1723
  console.error("AnQst host error:", message["payload"]);
1373
1724
  return;
@@ -1377,6 +1728,18 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1377
1728
  this.socket.close();
1378
1729
  }
1379
1730
  });
1731
+ this.socket.addEventListener("close", () => {
1732
+ for (const pending of this.pending.values()) {
1733
+ pending.reject({
1734
+ code: "BridgeDisconnectedError",
1735
+ message: "Bridge disconnected before call completion.",
1736
+ service: pending.service,
1737
+ member: pending.member,
1738
+ requestId: pending.requestId
1739
+ });
1740
+ }
1741
+ this.pending.clear();
1742
+ });
1380
1743
  }
1381
1744
 
1382
1745
  static async create(): Promise<WebSocketBridgeAdapter> {
@@ -1408,8 +1771,14 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1408
1771
  async call<T>(service: string, member: string, args: unknown[]): Promise<T> {
1409
1772
  const requestId = \`req-\${++this.requestCounter}\`;
1410
1773
  const payload = { type: "call", requestId, service, member, args };
1411
- return await new Promise<T>((resolve) => {
1412
- this.pending.set(requestId, (value) => resolve(value as T));
1774
+ return await new Promise<T>((resolve, reject) => {
1775
+ this.pending.set(requestId, {
1776
+ service,
1777
+ member,
1778
+ requestId,
1779
+ resolve: (value) => resolve(value as T),
1780
+ reject
1781
+ });
1413
1782
  this.socket.send(JSON.stringify(payload));
1414
1783
  });
1415
1784
  }
@@ -1437,6 +1806,18 @@ class WebSocketBridgeAdapter implements BridgeAdapter {
1437
1806
  onSlotInvocation(handler: SlotInvocationListener): void {
1438
1807
  this.slotListeners.push(handler);
1439
1808
  }
1809
+
1810
+ onDrop(handler: DropListener): void {
1811
+ this.dropListeners.push(handler);
1812
+ }
1813
+
1814
+ onHover(handler: HoverListener): void {
1815
+ this.hoverListeners.push(handler);
1816
+ }
1817
+
1818
+ onHoverLeft(handler: HoverLeftListener): void {
1819
+ this.hoverLeftListeners.push(handler);
1820
+ }
1440
1821
  }
1441
1822
 
1442
1823
  @Injectable({ providedIn: "root" })
@@ -1444,6 +1825,9 @@ class AnQstBridgeRuntime {
1444
1825
  private adapter: BridgeAdapter | null = null;
1445
1826
  private readonly slotHandlers = new Map<string, SlotHandler>();
1446
1827
  private readonly outputHandlers = new Map<string, OutputHandler[]>();
1828
+ private readonly dropHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
1829
+ private readonly hoverHandlers = new Map<string, ((payload: unknown, x: number, y: number) => void)[]>();
1830
+ private readonly hoverLeftHandlers = new Map<string, (() => void)[]>();
1447
1831
  private readonly startup = this.init();
1448
1832
 
1449
1833
  async ready(): Promise<void> {
@@ -1494,6 +1878,27 @@ class AnQstBridgeRuntime {
1494
1878
  this.outputHandlers.set(key, existing);
1495
1879
  }
1496
1880
 
1881
+ onDrop(service: string, member: string, handler: (payload: unknown, x: number, y: number) => void): void {
1882
+ const key = this.key(service, member);
1883
+ const existing = this.dropHandlers.get(key) ?? [];
1884
+ existing.push(handler);
1885
+ this.dropHandlers.set(key, existing);
1886
+ }
1887
+
1888
+ onHover(service: string, member: string, handler: (payload: unknown, x: number, y: number) => void): void {
1889
+ const key = this.key(service, member);
1890
+ const existing = this.hoverHandlers.get(key) ?? [];
1891
+ existing.push(handler);
1892
+ this.hoverHandlers.set(key, existing);
1893
+ }
1894
+
1895
+ onHoverLeft(service: string, member: string, handler: () => void): void {
1896
+ const key = this.key(service, member);
1897
+ const existing = this.hoverLeftHandlers.get(key) ?? [];
1898
+ existing.push(handler);
1899
+ this.hoverLeftHandlers.set(key, existing);
1900
+ }
1901
+
1497
1902
  private requireAdapterSync(): BridgeAdapter {
1498
1903
  if (this.adapter === null) {
1499
1904
  throw new Error("AnQst bridge is not ready.");
@@ -1520,7 +1925,7 @@ class AnQstBridgeRuntime {
1520
1925
  outputHandler(value);
1521
1926
  }
1522
1927
  });
1523
- this.adapter.onSlotInvocation((requestId, service, member, args) => {
1928
+ this.adapter.onSlotInvocation(async (requestId, service, member, args) => {
1524
1929
  const key = this.key(service, member);
1525
1930
  const handler = this.slotHandlers.get(key);
1526
1931
  if (handler === undefined) {
@@ -1528,10 +1933,33 @@ class AnQstBridgeRuntime {
1528
1933
  return;
1529
1934
  }
1530
1935
  try {
1531
- const result = handler(...args);
1936
+ const result = await Promise.resolve(handler(...args));
1937
+ if (result instanceof Error) {
1938
+ this.adapter!.resolveSlot(requestId, false, undefined, result.message);
1939
+ return;
1940
+ }
1532
1941
  this.adapter!.resolveSlot(requestId, true, result, "");
1533
1942
  } catch (error) {
1534
- this.adapter!.resolveSlot(requestId, false, undefined, String(error));
1943
+ const message = error instanceof Error ? error.message : String(error);
1944
+ this.adapter!.resolveSlot(requestId, false, undefined, message);
1945
+ }
1946
+ });
1947
+ this.adapter.onDrop((service, member, payload, x, y) => {
1948
+ const key = this.key(service, member);
1949
+ for (const handler of this.dropHandlers.get(key) ?? []) {
1950
+ handler(payload, x, y);
1951
+ }
1952
+ });
1953
+ this.adapter.onHover((service, member, payload, x, y) => {
1954
+ const key = this.key(service, member);
1955
+ for (const handler of this.hoverHandlers.get(key) ?? []) {
1956
+ handler(payload, x, y);
1957
+ }
1958
+ });
1959
+ this.adapter.onHoverLeft((service, member) => {
1960
+ const key = this.key(service, member);
1961
+ for (const handler of this.hoverLeftHandlers.get(key) ?? []) {
1962
+ handler();
1535
1963
  }
1536
1964
  });
1537
1965
  for (const key of this.slotHandlers.keys()) {
@@ -1764,7 +2192,7 @@ function renderNodeExpressWsIndex(spec) {
1764
2192
  sendJson(session.socket, {
1765
2193
  type: "callResult",
1766
2194
  requestId,
1767
- result: { __anqstError: { code: "HandlerNotRegisteredError", message: err.message, service, member } }
2195
+ result: { code: "HandlerNotRegisteredError", message: err.message, service, member, requestId }
1768
2196
  });
1769
2197
  throw err;
1770
2198
  }
@@ -1786,7 +2214,7 @@ function renderNodeExpressWsIndex(spec) {
1786
2214
  sendJson(session.socket, {
1787
2215
  type: "callResult",
1788
2216
  requestId,
1789
- result: { __anqstError: { code: "CallHandlerError", message, service, member } }
2217
+ result: { code: "CallHandlerError", message, service, member, requestId }
1790
2218
  });
1791
2219
  });
1792
2220
  return;
@@ -2138,7 +2566,7 @@ export interface ${spec.widgetName}NodeBridge {
2138
2566
  export function create${spec.widgetName}NodeExpressWsBridge(options: ${spec.widgetName}NodeBridgeOptions): ${spec.widgetName}NodeBridge {
2139
2567
  const wsPath = options.wsPath ?? "/anqst-bridge";
2140
2568
  const devConfigPath = options.devConfigPath ?? "/anqst-dev-config.json";
2141
- const defaultSlotTimeoutMs = options.defaultSlotTimeoutMs ?? 120000;
2569
+ const defaultSlotTimeoutMs = options.defaultSlotTimeoutMs ?? 1000;
2142
2570
  const maxQueuedPerSlot = options.maxQueuedSlotInvocationsPerSlot ?? 1024;
2143
2571
  const sessions = new Map<WebSocket, ${spec.widgetName}NodeSession>();
2144
2572
  const diagnosticListeners = new Set<(diagnostic: AnQstDiagnostic) => void>();
@@ -2230,7 +2658,7 @@ ${callDispatch}
2230
2658
  sendJson(session.socket, {
2231
2659
  type: "callResult",
2232
2660
  requestId,
2233
- result: { __anqstError: { code: "HandlerNotRegisteredError", message: err.message, service, member } }
2661
+ result: { code: "HandlerNotRegisteredError", message: err.message, service, member, requestId }
2234
2662
  });
2235
2663
  throw err;
2236
2664
  }
@@ -2363,7 +2791,8 @@ function generateOutputs(spec, options = { emitQWidget: true, emitAngularService
2363
2791
  const cppTypes = buildCppTypeContext(spec);
2364
2792
  outputs[`${cppDir}/CMakeLists.txt`] = renderCMake(spec);
2365
2793
  outputs[`${cppDir}/${spec.widgetName}.qrc`] = renderEmbeddedQrc(spec.widgetName, []);
2366
- outputs[`${cppDir}/include/${spec.widgetName}.h`] = renderWidgetHeader(spec, cppTypes);
2794
+ outputs[`${cppDir}/include/${spec.widgetName}.h`] = renderWidgetUmbrellaHeader(spec);
2795
+ outputs[`${cppDir}/include/${spec.widgetName}Widget.h`] = renderWidgetHeader(spec, cppTypes);
2367
2796
  outputs[`${cppDir}/include/${spec.widgetName}Types.h`] = renderTypesHeader(spec, cppTypes);
2368
2797
  outputs[`${cppDir}/${spec.widgetName}.cpp`] = renderCppStub(spec, cppTypes);
2369
2798
  }
@@ -2572,6 +3001,7 @@ set(CMAKE_AUTOUIC ON)
2572
3001
  set(CMAKE_AUTORCC ON)
2573
3002
 
2574
3003
  find_program(ANQST_NPM_EXECUTABLE npm REQUIRED)
3004
+ find_program(ANQST_NPX_EXECUTABLE npx REQUIRED)
2575
3005
 
2576
3006
  add_custom_command(
2577
3007
  OUTPUT
@@ -2579,10 +3009,11 @@ add_custom_command(
2579
3009
  "\${${generatedRootVar}}/${widgetName}.qrc"
2580
3010
  "\${${generatedRootVar}}/${widgetName}.cpp"
2581
3011
  "\${${generatedIncludeVar}}/${widgetName}.h"
3012
+ "\${${generatedIncludeVar}}/${widgetName}Widget.h"
2582
3013
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
2583
3014
  "\${${generatedRootVar}}/webapp/index.html"
2584
3015
  COMMAND "\${ANQST_NPM_EXECUTABLE}" install
2585
- COMMAND "\${ANQST_NPM_EXECUTABLE}" run anqst:build
3016
+ COMMAND "\${ANQST_NPX_EXECUTABLE}" anqst build
2586
3017
  WORKING_DIRECTORY "\${${projectRootVar}}"
2587
3018
  COMMENT "Generating AnQst widget library (${widgetTarget}) from Angular project"
2588
3019
  VERBATIM
@@ -2594,6 +3025,7 @@ add_custom_target(${autogenTarget}
2594
3025
  "\${${generatedRootVar}}/${widgetName}.qrc"
2595
3026
  "\${${generatedRootVar}}/${widgetName}.cpp"
2596
3027
  "\${${generatedIncludeVar}}/${widgetName}.h"
3028
+ "\${${generatedIncludeVar}}/${widgetName}Widget.h"
2597
3029
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
2598
3030
  "\${${generatedRootVar}}/webapp/index.html"
2599
3031
  )
@@ -2602,6 +3034,7 @@ set_source_files_properties(
2602
3034
  "\${${generatedRootVar}}/${widgetName}.qrc"
2603
3035
  "\${${generatedRootVar}}/${widgetName}.cpp"
2604
3036
  "\${${generatedIncludeVar}}/${widgetName}.h"
3037
+ "\${${generatedIncludeVar}}/${widgetName}Widget.h"
2605
3038
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
2606
3039
  PROPERTIES GENERATED TRUE
2607
3040
  )
@@ -2610,6 +3043,7 @@ add_library(${widgetTarget}
2610
3043
  "\${${generatedRootVar}}/${widgetName}.qrc"
2611
3044
  "\${${generatedRootVar}}/${widgetName}.cpp"
2612
3045
  "\${${generatedIncludeVar}}/${widgetName}.h"
3046
+ "\${${generatedIncludeVar}}/${widgetName}Widget.h"
2613
3047
  "\${${generatedIncludeVar}}/${widgetName}Types.h"
2614
3048
  )
2615
3049
  add_dependencies(${widgetTarget} ${autogenTarget})
@@ -2797,7 +3231,7 @@ function installDesignerPluginIconAssets(cwd, pluginDir) {
2797
3231
  }
2798
3232
  function renderQtDesignerPluginCpp(widgetName, widgetCategory, hasIcon) {
2799
3233
  const pluginClass = `${widgetName}DesignerPlugin`;
2800
- const widgetClass = `${widgetName}::${widgetName}`;
3234
+ const widgetClass = `${widgetName}Widget`;
2801
3235
  const groupName = escapeCppStringLiteral(widgetCategory);
2802
3236
  const iconExpression = hasIcon
2803
3237
  ? 'QIcon(QStringLiteral(":/anqstdesignerplugin/plugin-icon.png"))'
@@ -2807,7 +3241,7 @@ function renderQtDesignerPluginCpp(widgetName, widgetCategory, hasIcon) {
2807
3241
  #include <QObject>
2808
3242
  #include <QString>
2809
3243
  #include <QWidget>
2810
- #include "include/${widgetName}.h"
3244
+ #include "${widgetName}.h"
2811
3245
 
2812
3246
  class ${pluginClass} final : public QObject, public QDesignerCustomWidgetInterface {
2813
3247
  Q_OBJECT
@@ -2823,9 +3257,10 @@ public:
2823
3257
  QString toolTip() const override { return QStringLiteral("${widgetName} generated by AnQst."); }
2824
3258
  QString whatsThis() const override { return QStringLiteral("${widgetName} generated by AnQst."); }
2825
3259
  bool isContainer() const override { return false; }
2826
- QString includeFile() const override { return QStringLiteral("include/${widgetName}.h"); }
3260
+ QString includeFile() const override { return QStringLiteral("${widgetName}.h"); }
2827
3261
  QWidget* createWidget(QWidget* parent) override {
2828
3262
  auto* widget = new ${widgetClass}(parent);
3263
+ widget->setMinimumHeight(128);
2829
3264
  widget->setProperty("anqstDesignerContext", true);
2830
3265
  return widget;
2831
3266
  }
@@ -2836,6 +3271,12 @@ public:
2836
3271
  return QStringLiteral(
2837
3272
  "<ui language=\\"c++\\">\\n"
2838
3273
  " <widget class=\\"${widgetClass}\\" name=\\"${widgetName.toLowerCase()}\\">\\n"
3274
+ " <property name=\\"minimumSize\\">\\n"
3275
+ " <size>\\n"
3276
+ " <width>256</width>\\n"
3277
+ " <height>128</height>\\n"
3278
+ " </size>\\n"
3279
+ " </property>\\n"
2839
3280
  " </widget>\\n"
2840
3281
  "</ui>\\n");
2841
3282
  }