#include "PipeWireGraphModel.h" #include "PipeWireGraphModel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { QVariant nodeStyleVariant(const Potato::NodeInfo &info) { QtNodes::NodeStyle style = QtNodes::StyleCollection::nodeStyle(); QColor base; switch (info.type) { case Potato::NodeType::Hardware: base = QColor(72, 94, 118); break; case Potato::NodeType::Virtual: base = QColor(62, 122, 104); break; case Potato::NodeType::Application: base = QColor(138, 104, 72); break; case Potato::NodeType::Bus: base = QColor(86, 92, 128); break; default: base = QColor(86, 94, 108); break; } style.GradientColor0 = base.lighter(120); style.GradientColor1 = base.lighter(108); style.GradientColor2 = base.darker(105); style.GradientColor3 = base.darker(120); style.NormalBoundaryColor = base.lighter(135); style.SelectedBoundaryColor = QColor(255, 165, 0); style.FontColor = QColor(236, 240, 246); style.FontColorFaded = QColor(160, 168, 182); style.ConnectionPointColor = QColor(200, 208, 220); style.FilledConnectionPointColor = QColor(255, 165, 0); style.WarningColor = QColor(230, 180, 70); style.ErrorColor = QColor(220, 70, 70); style.PenWidth = 1.3f; style.HoveredPenWidth = 2.4f; style.ConnectionPointDiameter = 10.0f; style.Opacity = 1.0f; return style.toJson().toVariantMap(); } int nodeWidthFor(const Potato::NodeInfo &info) { QFont captionFont; captionFont.setBold(true); QFontMetrics captionMetrics(captionFont); int maxTextWidth = captionMetrics.horizontalAdvance(info.name); QFont portFont; QFontMetrics portMetrics(portFont); for (const auto &port : info.inputPorts) { maxTextWidth = std::max(maxTextWidth, portMetrics.horizontalAdvance(port.name)); } for (const auto &port : info.outputPorts) { maxTextWidth = std::max(maxTextWidth, portMetrics.horizontalAdvance(port.name)); } const int widthPadding = 120; const int minWidth = 200; const int maxWidth = 600; const int width = maxTextWidth + widthPadding; return std::max(minWidth, std::min(maxWidth, width)); } QString elideLabel(const QString &text, int width, const QFont &font) { QFontMetrics metrics(font); const int available = std::max(60, width); return metrics.elidedText(text, Qt::ElideRight, available); } } PipeWireGraphModel::PipeWireGraphModel(Potato::PipeWireController *controller, QObject *parent) : QtNodes::AbstractGraphModel() , m_controller(controller) { if (parent) { setParent(parent); } } QWidget *PipeWireGraphModel::nodeWidget(QtNodes::NodeId nodeId) const { auto it = m_nodeWidgets.find(nodeId); if (it != m_nodeWidgets.end()) { return it->second; } uint32_t pipewireId = 0; auto nodeIt = m_nodes.find(nodeId); if (nodeIt != m_nodes.end()) { pipewireId = nodeIt->second.id; } auto *widget = new QWidget(); auto *layout = new QHBoxLayout(widget); layout->setContentsMargins(6, 2, 6, 2); layout->setSpacing(6); auto *muteButton = new QToolButton(widget); muteButton->setText("M"); muteButton->setCheckable(true); muteButton->setFixedSize(20, 20); muteButton->setToolTip(QString("Mute")); auto *slider = new QSlider(Qt::Horizontal, widget); slider->setRange(0, 100); slider->setValue(100); slider->setFixedHeight(18); slider->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); slider->setToolTip(QString("Volume")); const auto applyVolume = [this, pipewireId, slider, muteButton]() { if (!m_controller || pipewireId == 0) { return; } const float volume = static_cast(slider->value()) / 100.0f; m_controller->setNodeVolume(pipewireId, volume, muteButton->isChecked()); }; QObject::connect(slider, &QSlider::valueChanged, widget, [applyVolume](int) { applyVolume(); }); QObject::connect(muteButton, &QToolButton::toggled, widget, [applyVolume](bool) { applyVolume(); }); layout->addWidget(muteButton); layout->addWidget(slider); widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); widget->setFixedHeight(26); widget->setStyleSheet( "QToolButton { background: #2b2f38; color: #dbe2ee; border: 1px solid #39404c; border-radius: 4px; font-weight: 700; }" "QToolButton:checked { background: #3c4350; color: #ffcf7a; }" "QSlider::groove:horizontal { height: 4px; background: #2b2f38; border-radius: 2px; }" "QSlider::sub-page:horizontal { background: #5fcf8d; border-radius: 2px; }" "QSlider::add-page:horizontal { background: #2b2f38; border-radius: 2px; }" "QSlider::handle:horizontal { width: 10px; margin: -6px 0; background: #dbe2ee; border-radius: 5px; }" ); widget->adjustSize(); m_nodeWidgets.emplace(nodeId, widget); return widget; } QtNodes::NodeId PipeWireGraphModel::newNodeId() { return m_nextNodeId++; } std::unordered_set PipeWireGraphModel::allNodeIds() const { std::unordered_set ids; ids.reserve(m_nodes.size()); for (const auto &entry : m_nodes) { ids.insert(entry.first); } return ids; } std::unordered_set PipeWireGraphModel::allConnectionIds(QtNodes::NodeId const nodeId) const { std::unordered_set result; for (const auto &conn : m_connections) { if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) { result.insert(conn); } } return result; } std::unordered_set PipeWireGraphModel::connections(QtNodes::NodeId nodeId, QtNodes::PortType portType, QtNodes::PortIndex portIndex) const { std::unordered_set result; for (const auto &conn : m_connections) { if (portType == QtNodes::PortType::Out) { if (conn.outNodeId == nodeId && conn.outPortIndex == portIndex) { result.insert(conn); } } else if (portType == QtNodes::PortType::In) { if (conn.inNodeId == nodeId && conn.inPortIndex == portIndex) { result.insert(conn); } } } return result; } bool PipeWireGraphModel::connectionExists(QtNodes::ConnectionId const connectionId) const { return m_connections.find(connectionId) != m_connections.end(); } QtNodes::NodeId PipeWireGraphModel::addNode(QString const nodeType) { Q_UNUSED(nodeType) const QtNodes::NodeId nodeId = newNodeId(); Potato::NodeInfo info; info.id = 0; info.name = QString("Node %1").arg(static_cast(nodeId)); info.description = info.name; info.stableId = info.name; m_nodes.emplace(nodeId, info); QPointF position = nextPosition(); if (!info.stableId.isEmpty() && m_layoutByStableId.contains(info.stableId)) { position = m_layoutByStableId.value(info.stableId); } m_positions.emplace(nodeId, position); updateLayoutForNode(nodeId, position); Q_EMIT nodeCreated(nodeId); Q_EMIT nodeUpdated(nodeId); return nodeId; } bool PipeWireGraphModel::connectionPossible(QtNodes::ConnectionId const connectionId) const { if (!nodeExists(connectionId.outNodeId) || !nodeExists(connectionId.inNodeId)) { return false; } if (connectionExists(connectionId)) { return false; } const auto outIt = m_nodes.find(connectionId.outNodeId); const auto inIt = m_nodes.find(connectionId.inNodeId); if (outIt == m_nodes.end() || inIt == m_nodes.end()) { return false; } const auto &outInfo = outIt->second; const auto &inInfo = inIt->second; if (connectionId.outPortIndex >= static_cast(outInfo.outputPorts.size())) { return false; } if (connectionId.inPortIndex >= static_cast(inInfo.inputPorts.size())) { return false; } const auto &outPort = outInfo.outputPorts.at(connectionId.outPortIndex); const auto &inPort = inInfo.inputPorts.at(connectionId.inPortIndex); if (outPort.direction != SPA_DIRECTION_OUTPUT || inPort.direction != SPA_DIRECTION_INPUT) { return false; } const auto isAudioClass = [](Potato::MediaClass mediaClass) { return mediaClass == Potato::MediaClass::AudioSink || mediaClass == Potato::MediaClass::AudioSource || mediaClass == Potato::MediaClass::AudioDuplex || mediaClass == Potato::MediaClass::Stream; }; if (!isAudioClass(outInfo.mediaClass) || !isAudioClass(inInfo.mediaClass)) { return false; } return true; } void PipeWireGraphModel::addConnection(QtNodes::ConnectionId const connectionId) { if (!connectionPossible(connectionId)) { return; } m_connections.insert(connectionId); Q_EMIT connectionCreated(connectionId); } bool PipeWireGraphModel::nodeExists(QtNodes::NodeId const nodeId) const { return m_nodes.find(nodeId) != m_nodes.end(); } QVariant PipeWireGraphModel::nodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole role) const { auto it = m_nodes.find(nodeId); if (it == m_nodes.end()) { return QVariant(); } const auto &info = it->second; switch (role) { case QtNodes::NodeRole::Caption: { QFont captionFont; captionFont.setBold(true); const int width = nodeWidthFor(info) - 50; const QString title = info.description.isEmpty() ? info.name : info.description; return elideLabel(title, width, captionFont); } case QtNodes::NodeRole::CaptionVisible: return true; case QtNodes::NodeRole::Position: { auto posIt = m_positions.find(nodeId); if (posIt != m_positions.end()) { return posIt->second; } return QPointF(0, 0); } case QtNodes::NodeRole::Size: { auto sizeIt = m_nodeSizes.find(nodeId); if (sizeIt != m_nodeSizes.end()) { return sizeIt->second; } const int maxPorts = std::max(info.inputPorts.size(), info.outputPorts.size()); const int baseHeight = 50; const int perPortHeight = 28; const int height = std::max(80, baseHeight + (maxPorts * perPortHeight)); const int width = nodeWidthFor(info); return QSize(width, height); } case QtNodes::NodeRole::InPortCount: return static_cast(info.inputPorts.size()); case QtNodes::NodeRole::OutPortCount: return static_cast(info.outputPorts.size()); case QtNodes::NodeRole::Type: return QString("PipeWire"); case QtNodes::NodeRole::Style: return nodeStyleVariant(info); case QtNodes::NodeRole::Widget: return QVariant::fromValue(nodeWidget(nodeId)); default: return QVariant(); } } bool PipeWireGraphModel::setNodeData(QtNodes::NodeId nodeId, QtNodes::NodeRole role, QVariant value) { if (!nodeExists(nodeId)) { return false; } if (role == QtNodes::NodeRole::Position) { const QPointF position = value.toPointF(); m_positions[nodeId] = position; updateLayoutForNode(nodeId, position); Q_EMIT nodePositionUpdated(nodeId); return true; } if (role == QtNodes::NodeRole::Size) { m_nodeSizes[nodeId] = value.toSize(); return true; } return false; } QVariant PipeWireGraphModel::portData(QtNodes::NodeId nodeId, QtNodes::PortType portType, QtNodes::PortIndex portIndex, QtNodes::PortRole role) const { auto it = m_nodes.find(nodeId); if (it == m_nodes.end()) { return QVariant(); } const auto &info = it->second; if (role == QtNodes::PortRole::DataType) { return QString("audio"); } if (role == QtNodes::PortRole::CaptionVisible) { return true; } if (role == QtNodes::PortRole::Caption) { if (portType == QtNodes::PortType::In) { if (portIndex < static_cast(info.inputPorts.size())) { QFont font; const int width = nodeWidthFor(info) - 100; return elideLabel(info.inputPorts.at(portIndex).name, width, font); } } else if (portType == QtNodes::PortType::Out) { if (portIndex < static_cast(info.outputPorts.size())) { QFont font; const int width = nodeWidthFor(info) - 100; return elideLabel(info.outputPorts.at(portIndex).name, width, font); } } } if (role == QtNodes::PortRole::ConnectionPolicyRole) { return QVariant::fromValue(QtNodes::ConnectionPolicy::Many); } return QVariant(); } bool PipeWireGraphModel::setPortData(QtNodes::NodeId, QtNodes::PortType, QtNodes::PortIndex, QVariant const &, QtNodes::PortRole) { return false; } bool PipeWireGraphModel::deleteConnection(QtNodes::ConnectionId const connectionId) { auto it = m_connections.find(connectionId); if (it == m_connections.end()) { return false; } m_connections.erase(it); Q_EMIT connectionDeleted(connectionId); return true; } bool PipeWireGraphModel::deleteNode(QtNodes::NodeId const nodeId) { if (!nodeExists(nodeId)) { return false; } std::vector toRemove; for (const auto &conn : m_connections) { if (conn.outNodeId == nodeId || conn.inNodeId == nodeId) { toRemove.push_back(conn); } } for (const auto &conn : toRemove) { deleteConnection(conn); } m_nodes.erase(nodeId); m_positions.erase(nodeId); m_nodeSizes.erase(nodeId); m_nodeWidgets.erase(nodeId); Q_EMIT nodeDeleted(nodeId); return true; } QJsonObject PipeWireGraphModel::saveNode(QtNodes::NodeId const) const { return QJsonObject(); } void PipeWireGraphModel::loadNode(QJsonObject const &) { } QtNodes::NodeId PipeWireGraphModel::addPipeWireNode(const Potato::NodeInfo &node) { if (m_pwToNode.find(node.id) != m_pwToNode.end()) { return m_pwToNode.at(node.id); } const QtNodes::NodeId nodeId = newNodeId(); m_nodes.emplace(nodeId, node); m_pwToNode.emplace(node.id, nodeId); QPointF position = nextPosition(); if (!node.stableId.isEmpty() && m_layoutByStableId.contains(node.stableId)) { position = m_layoutByStableId.value(node.stableId); } m_positions.emplace(nodeId, position); updateLayoutForNode(nodeId, position); Q_EMIT nodeCreated(nodeId); Q_EMIT nodeUpdated(nodeId); return nodeId; } void PipeWireGraphModel::removePipeWireNode(uint32_t nodeId) { auto it = m_pwToNode.find(nodeId); if (it == m_pwToNode.end()) { return; } deleteNode(it->second); m_pwToNode.erase(it); } bool PipeWireGraphModel::addPipeWireConnection(const Potato::LinkInfo &link, QtNodes::ConnectionId *connectionId) { bool ok = false; QtNodes::ConnectionId localId = connectionFromPipeWire(link, &ok); if (!ok) { return false; } if (connectionExists(localId)) { return false; } m_connections.insert(localId); m_linkIdToConnection.emplace(link.id, localId); if (connectionId) { *connectionId = localId; } Q_EMIT connectionCreated(localId); return true; } void PipeWireGraphModel::removePipeWireConnection(uint32_t linkId) { auto it = m_linkIdToConnection.find(linkId); if (it == m_linkIdToConnection.end()) { return; } deleteConnection(it->second); m_linkIdToConnection.erase(it); } bool PipeWireGraphModel::findConnectionForLink(uint32_t linkId, QtNodes::ConnectionId &connectionId) const { auto it = m_linkIdToConnection.find(linkId); if (it == m_linkIdToConnection.end()) { return false; } connectionId = it->second; return true; } bool PipeWireGraphModel::updatePipeWireNode(const Potato::NodeInfo &node) { auto it = m_pwToNode.find(node.id); if (it == m_pwToNode.end()) { return false; } const QtNodes::NodeId nodeId = it->second; m_nodes[nodeId] = node; m_nodeSizes.erase(nodeId); Q_EMIT nodeUpdated(nodeId); return true; } const Potato::NodeInfo *PipeWireGraphModel::nodeInfo(QtNodes::NodeId nodeId) const { auto it = m_nodes.find(nodeId); if (it == m_nodes.end()) { return nullptr; } return &it->second; } bool PipeWireGraphModel::connectionIdForLink(const Potato::LinkInfo &link, QtNodes::ConnectionId &connectionId) const { bool ok = false; connectionId = connectionFromPipeWire(link, &ok); return ok; } void PipeWireGraphModel::reset() { m_connections.clear(); m_linkIdToConnection.clear(); m_nodes.clear(); m_pwToNode.clear(); m_positions.clear(); m_nodeSizes.clear(); m_nodeWidgets.clear(); m_nextNodeId = 1; Q_EMIT modelReset(); } void PipeWireGraphModel::autoArrange() { std::vector ids; ids.reserve(m_nodes.size()); for (const auto &entry : m_nodes) { ids.push_back(entry.first); } std::sort(ids.begin(), ids.end(), [this](QtNodes::NodeId a, QtNodes::NodeId b) { const QString &left = m_nodes.at(a).stableId; const QString &right = m_nodes.at(b).stableId; return left < right; }); int maxWidth = 0; int maxHeight = 0; for (const auto &entry : m_nodes) { const Potato::NodeInfo &info = entry.second; maxWidth = std::max(maxWidth, nodeWidthFor(info)); const int ports = std::max(info.inputPorts.size(), info.outputPorts.size()); const int height = std::max(110, 70 + (ports * 28)); maxHeight = std::max(maxHeight, height); } const int columns = 3; const qreal spacingX = static_cast(maxWidth + 160); const qreal spacingY = static_cast(maxHeight + 120); for (int i = 0; i < static_cast(ids.size()); ++i) { const int row = i / columns; const int col = i % columns; const QPointF position(col * spacingX, row * spacingY); m_positions[ids[i]] = position; updateLayoutForNode(ids[i], position); Q_EMIT nodePositionUpdated(ids[i]); } } void PipeWireGraphModel::loadLayout() { m_layoutByStableId.clear(); m_hasViewState = false; m_hasSplitterSizes = false; m_splitterSizes.clear(); const QString path = layoutFilePath(); if (path.isEmpty()) { return; } QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return; } const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (doc.isArray()) { applyLayoutData(doc.array()); return; } if (doc.isObject()) { const QJsonObject root = doc.object(); const QJsonArray nodes = root.value("nodes").toArray(); applyLayoutData(nodes); const QJsonObject view = root.value("view").toObject(); if (!view.isEmpty()) { m_viewScale = view.value("scale").toDouble(1.0); const double x = view.value("center_x").toDouble(0.0); const double y = view.value("center_y").toDouble(0.0); m_viewCenter = QPointF(x, y); m_hasViewState = true; } const QJsonArray splitter = root.value("splitter").toArray(); if (!splitter.isEmpty()) { QList sizes; sizes.reserve(splitter.size()); for (const auto &value : splitter) { sizes.append(value.toInt()); } if (!sizes.isEmpty()) { m_splitterSizes = sizes; m_hasSplitterSizes = true; } } } } void PipeWireGraphModel::saveLayout() const { const QString path = layoutFilePath(); if (path.isEmpty()) { return; } writeLayoutToFile(path); } void PipeWireGraphModel::saveLayoutAs(const QString &path) const { if (path.isEmpty()) { return; } writeLayoutToFile(path); } void PipeWireGraphModel::resetLayout() { m_layoutByStableId.clear(); autoArrange(); saveLayout(); } QString PipeWireGraphModel::defaultLayoutPath() const { return layoutFilePath(); } QtNodes::ConnectionId PipeWireGraphModel::connectionFromPipeWire(const Potato::LinkInfo &link, bool *ok) const { auto outIt = m_pwToNode.find(link.outputNodeId); auto inIt = m_pwToNode.find(link.inputNodeId); if (outIt == m_pwToNode.end() || inIt == m_pwToNode.end()) { if (ok) { *ok = false; } return QtNodes::ConnectionId{QtNodes::InvalidNodeId, 0, QtNodes::InvalidNodeId, 0}; } const auto &outInfo = m_nodes.at(outIt->second); const auto &inInfo = m_nodes.at(inIt->second); QtNodes::PortIndex outIndex = 0; QtNodes::PortIndex inIndex = 0; if (!findPortIndex(outInfo, link.outputPortId, QtNodes::PortType::Out, outIndex)) { if (ok) { *ok = false; } return QtNodes::ConnectionId{QtNodes::InvalidNodeId, 0, QtNodes::InvalidNodeId, 0}; } if (!findPortIndex(inInfo, link.inputPortId, QtNodes::PortType::In, inIndex)) { if (ok) { *ok = false; } return QtNodes::ConnectionId{QtNodes::InvalidNodeId, 0, QtNodes::InvalidNodeId, 0}; } if (ok) { *ok = true; } return QtNodes::ConnectionId{outIt->second, outIndex, inIt->second, inIndex}; } bool PipeWireGraphModel::findPortIndex(const Potato::NodeInfo &node, uint32_t portId, QtNodes::PortType type, QtNodes::PortIndex &index) const { if (type == QtNodes::PortType::In) { for (int i = 0; i < node.inputPorts.size(); ++i) { if (node.inputPorts.at(i).id == portId) { index = static_cast(i); return true; } } return false; } for (int i = 0; i < node.outputPorts.size(); ++i) { if (node.outputPorts.at(i).id == portId) { index = static_cast(i); return true; } } return false; } QString PipeWireGraphModel::portLabel(const Potato::PortInfo &port) const { return port.name; } QPointF PipeWireGraphModel::nextPosition() const { const int index = static_cast(m_positions.size()); int maxWidth = 0; int maxHeight = 0; for (const auto &entry : m_nodes) { const Potato::NodeInfo &info = entry.second; maxWidth = std::max(maxWidth, nodeWidthFor(info)); const int ports = std::max(info.inputPorts.size(), info.outputPorts.size()); const int height = std::max(110, 70 + (ports * 28)); maxHeight = std::max(maxHeight, height); } const int columns = 3; const qreal spacingX = static_cast(maxWidth + 160); const qreal spacingY = static_cast(maxHeight + 120); const int row = index / columns; const int col = index % columns; return QPointF(col * spacingX, row * spacingY); } bool PipeWireGraphModel::hasOverlaps() const { struct NodeBox { QtNodes::NodeId id; QRectF rect; }; std::vector boxes; boxes.reserve(m_nodes.size()); for (const auto &entry : m_nodes) { const QtNodes::NodeId nodeId = entry.first; const auto posIt = m_positions.find(nodeId); if (posIt == m_positions.end()) { continue; } const Potato::NodeInfo &info = entry.second; const int width = nodeWidthFor(info); const int ports = std::max(info.inputPorts.size(), info.outputPorts.size()); const int height = std::max(110, 70 + (ports * 28)); QRectF rect(posIt->second, QSizeF(width, height)); boxes.push_back({nodeId, rect}); } for (size_t i = 0; i < boxes.size(); ++i) { for (size_t j = i + 1; j < boxes.size(); ++j) { if (boxes[i].rect.intersects(boxes[j].rect)) { return true; } } } return false; } QString PipeWireGraphModel::layoutFilePath() const { const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (baseDir.isEmpty()) { return QString(); } return baseDir + QString("/layout.json"); } void PipeWireGraphModel::updateLayoutForNode(QtNodes::NodeId nodeId, QPointF position) { auto it = m_nodes.find(nodeId); if (it == m_nodes.end()) { return; } const QString stableId = it->second.stableId; if (!stableId.isEmpty()) { m_layoutByStableId.insert(stableId, position); } } void PipeWireGraphModel::writeLayoutToFile(const QString &path) const { QJsonArray nodes; for (const auto &entry : m_nodes) { const auto &info = entry.second; if (info.stableId.isEmpty()) { continue; } auto posIt = m_positions.find(entry.first); if (posIt == m_positions.end()) { continue; } const QPointF pos = posIt->second; QJsonObject obj; obj["id"] = info.stableId; obj["x"] = pos.x(); obj["y"] = pos.y(); nodes.append(obj); } QJsonObject root; root["nodes"] = nodes; QJsonObject view; view["scale"] = m_viewScale; view["center_x"] = m_viewCenter.x(); view["center_y"] = m_viewCenter.y(); root["view"] = view; if (m_hasSplitterSizes && !m_splitterSizes.isEmpty()) { QJsonArray splitter; for (const auto size : m_splitterSizes) { splitter.append(size); } root["splitter"] = splitter; } QFile file(path); QDir().mkpath(QFileInfo(path).absolutePath()); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return; } file.write(QJsonDocument(root).toJson(QJsonDocument::Compact)); } void PipeWireGraphModel::applyLayoutData(const QJsonArray &nodes) { for (const auto &entry : nodes) { const QJsonObject obj = entry.toObject(); const QString id = obj.value("id").toString(); const double x = obj.value("x").toDouble(); const double y = obj.value("y").toDouble(); if (!id.isEmpty()) { m_layoutByStableId.insert(id, QPointF(x, y)); } } } void PipeWireGraphModel::setViewState(double scale, const QPointF ¢er) { m_viewScale = scale; m_viewCenter = center; m_hasViewState = true; } bool PipeWireGraphModel::viewState(double &scale, QPointF ¢er) const { if (!m_hasViewState) { return false; } scale = m_viewScale; center = m_viewCenter; return true; } void PipeWireGraphModel::setSplitterSizes(const QList &sizes) { m_splitterSizes = sizes; m_hasSplitterSizes = !sizes.isEmpty(); } bool PipeWireGraphModel::splitterSizes(QList &sizes) const { if (!m_hasSplitterSizes) { return false; } sizes = m_splitterSizes; return !sizes.isEmpty(); } #include