diff --git a/src/backend/datasources/MQTTClient.cpp b/src/backend/datasources/MQTTClient.cpp index 9e351c308..0ae5fbc9c 100644 --- a/src/backend/datasources/MQTTClient.cpp +++ b/src/backend/datasources/MQTTClient.cpp @@ -1,1354 +1,1355 @@ /*************************************************************************** File : MQTTClient.cpp Project : LabPlot Description : Represents a MQTT Client -------------------------------------------------------------------- Copyright : (C) 2018 Kovacs Ferencz (kferike98@gmail.com) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ #include "backend/datasources/MQTTClient.h" #include "backend/datasources/MQTTSubscription.h" #include "backend/datasources/MQTTTopic.h" #include "backend/datasources/filters/AsciiFilter.h" #include "backend/datasources/filters/FITSFilter.h" #include "backend/datasources/filters/BinaryFilter.h" #include "backend/core/Project.h" #include "kdefrontend/spreadsheet/PlotDataDialog.h" #include "commonfrontend/spreadsheet/SpreadsheetView.h" #include "kdefrontend/datasources/MQTTErrorWidget.h" #include #include #include #include #include #include #include #include #include #include /*! \class MQTTClient \brief The MQTT Client connects to the broker set in ImportFileWidget. It manages the MQTTSubscriptions, and the MQTTTopics. \ingroup datasources */ MQTTClient::MQTTClient(const QString& name) : Folder(name, AspectType::MQTTClient), m_updateTimer(new QTimer(this)), m_client(new QMqttClient(this)), m_willTimer(new QTimer(this)) { connect(m_updateTimer, &QTimer::timeout, this, &MQTTClient::read); connect(m_client, &QMqttClient::connected, this, &MQTTClient::onMQTTConnect); connect(m_willTimer, &QTimer::timeout, this, &MQTTClient::updateWillMessage); connect(m_client, &QMqttClient::errorChanged, this, &MQTTClient::MQTTErrorChanged); } MQTTClient::~MQTTClient() { emit clientAboutToBeDeleted(m_client->hostname(), m_client->port()); //stop reading before deleting the objects pauseReading(); qDebug() << "Delete MQTTClient: " << m_client->hostname() << m_client->port(); delete m_filter; delete m_updateTimer; delete m_willTimer; m_client->disconnectFromHost(); delete m_client; } /*! * depending on the update type, periodically or on data changes, starts the timer. */ void MQTTClient::ready() { if (m_updateType == UpdateType::TimeInterval) m_updateTimer->start(m_updateInterval); } /*! * \brief Updates the MQTTTopics of the client */ void MQTTClient::updateNow() { m_updateTimer->stop(); read(); if ((m_updateType == UpdateType::TimeInterval) && !m_paused) m_updateTimer->start(m_updateInterval); } /*! * \brief Continue reading from messages after it was paused. */ void MQTTClient::continueReading() { m_paused = false; if (m_updateType == UpdateType::TimeInterval) m_updateTimer->start(m_updateInterval); } /*! * \brief Pause the reading from messages. */ void MQTTClient::pauseReading() { m_paused = true; if (m_updateType == UpdateType::TimeInterval) m_updateTimer->stop(); } /*! * \brief Sets the filter of the MQTTClient. * The ownership of the filter is passed to MQTTClient. * * \param f a pointer to the new filter */ void MQTTClient::setFilter(AsciiFilter* f) { delete m_filter; m_filter = f; } /*! * \brief Returns the filter of the MQTTClient. */ AsciiFilter* MQTTClient::filter() const { return m_filter; } /*! * \brief Sets the MQTTclient's update interval to \c interval * \param interval */ void MQTTClient::setUpdateInterval(int interval) { m_updateInterval = interval; if (!m_paused) m_updateTimer->start(m_updateInterval); } /*! * \brief Returns the MQTTClient's update interval to \c interval * \param interval */ int MQTTClient::updateInterval() const { return m_updateInterval; } /*! * \brief Sets how many values we should store * \param keepNValues */ void MQTTClient::setKeepNValues(int keepNValues) { m_keepNValues = keepNValues; } /*! * \brief Returns how many values we should store */ int MQTTClient::keepNValues() const { return m_keepNValues; } /*! * \brief Provides information about whether the reading is paused or not * * \return true if the reading is paused * \return false otherwise */ bool MQTTClient::isPaused() const { return m_paused; } /*! * \brief Sets the size rate to sampleSize * \param sampleSize */ void MQTTClient::setSampleSize(int sampleSize) { m_sampleSize = sampleSize; } /*! * \brief Returns the size rate */ int MQTTClient::sampleSize() const { return m_sampleSize; } /*! * \brief Sets the MQTTClient's reading type to readingType * \param readingType */ void MQTTClient::setReadingType(ReadingType readingType) { m_readingType = readingType; } /*! * \brief Returns the MQTTClient's reading type */ MQTTClient::ReadingType MQTTClient::readingType() const { return m_readingType; } /*! * \brief Sets the MQTTClient's update type to updatetype and handles this change * \param updatetype */ void MQTTClient::setUpdateType(UpdateType updateType) { if (updateType == UpdateType::NewData) m_updateTimer->stop(); m_updateType = updateType; } /*! * \brief Returns the MQTTClient's update type */ MQTTClient::UpdateType MQTTClient::updateType() const { return m_updateType; } /*! * \brief Returns the MQTTClient's icon */ QIcon MQTTClient::icon() const { return QIcon::fromTheme("network-server-database"); } /*! * \brief Sets the host and port for the client. * * \param host the hostname of the broker we want to connect to * \param port the port used by the broker */ void MQTTClient::setMQTTClientHostPort(const QString& host, quint16 port) { m_client->setHostname(host); m_client->setPort(port); } /*! * \brief Returns hostname of the broker the client is connected to. */ QString MQTTClient::clientHostName() const { return m_client->hostname(); } /*! * \brief Returns the port used by the broker. */ quint16 MQTTClient::clientPort() const { return m_client->port(); } /*! * \brief Sets the flag on the given value. * If set true it means that the broker requires authentication, otherwise it doesn't. * * \param use */ void MQTTClient::setMQTTUseAuthentication(bool use) { m_MQTTUseAuthentication = use; } /*! * \brief Returns whether the broker requires authentication or not. */ bool MQTTClient::MQTTUseAuthentication() const { return m_MQTTUseAuthentication; } /*! * \brief Sets the username and password for the client. * * \param username the username used for authentication * \param password the password used for authentication */ void MQTTClient::setMQTTClientAuthentication(const QString& username, const QString& password) { m_client->setUsername(username); m_client->setPassword(password); } /*! * \brief Returns the username used for authentication. */ QString MQTTClient::clientUserName() const { return m_client->username(); } /*! * \brief Returns the password used for authentication. */ QString MQTTClient::clientPassword() const { return m_client->password(); } /*! * \brief Sets the flag on the given value. * If set true it means that user wants to set the client ID, otherwise it's not the case. * * \param use */ void MQTTClient::setMQTTUseID(bool use) { m_MQTTUseID = use; } /*! * \brief Returns whether the user wants to set the client ID or not. */ bool MQTTClient::MQTTUseID() const { return m_MQTTUseID; } /*! * \brief Sets the ID of the client * * \param id */ void MQTTClient::setMQTTClientId(const QString& clientId) { m_client->setClientId(clientId); } /*! * \brief Returns the ID of the client */ QString MQTTClient::clientID () const { return m_client->clientId(); } /*! * \brief Sets the flag on the given value. * If retain is true we interpret retain messages, otherwise we do not * * \param retain */ void MQTTClient::setMQTTRetain(bool retain) { m_MQTTRetain = retain; } /*! * \brief Returns the flag, which set to true means that interpret retain messages, otherwise we do not */ bool MQTTClient::MQTTRetain() const { return m_MQTTRetain; } /*! * \brief Returns the name of every MQTTTopics which already received a message, and is child of the MQTTClient */ QVector MQTTClient::topicNames() const { return m_topicNames; } /*! * \brief Adds the initial subscriptions that were set in ImportFileWidget * * \param filter the name of the subscribed topic * \param qos the qos level of the subscription */ void MQTTClient::addInitialMQTTSubscriptions(const QMqttTopicFilter& filter, quint8 qos) { m_subscribedTopicNameQoS[filter] = qos; } /*! * \brief Returns the name of every MQTTSubscription of the MQTTClient */ QVector MQTTClient::MQTTSubscriptions() const { return m_subscriptions; } /*! * \brief Adds a new MQTTSubscription to the MQTTClient * * \param topic, the name of the topic * \param QoS */ void MQTTClient::addMQTTSubscription(const QString& topicName, quint8 QoS) { //Check whether the subscription already exists, if it doesn't, we can add it if (!m_subscriptions.contains(topicName)) { const QMqttTopicFilter filter {topicName}; QMqttSubscription* temp = m_client->subscribe(filter, QoS); if (temp) { - qDebug()<<"Subscribe to: "<< temp->topic() << " " << temp->qos(); +// qDebug()<<"Subscribe to: "<< temp->topic() << " " << temp->qos(); m_subscriptions.push_back(temp->topic().filter()); m_subscribedTopicNameQoS[temp->topic().filter()] = temp->qos(); MQTTSubscription* newSubscription = new MQTTSubscription(temp->topic().filter()); newSubscription->setMQTTClient(this); addChildFast(newSubscription); m_MQTTSubscriptions.push_back(newSubscription); //Search for inferior subscriptions, that the new subscription contains bool found = false; QVector inferiorSubscriptions; for (auto* subscription : m_MQTTSubscriptions) { if (checkTopicContains(topicName, subscription->subscriptionName()) && topicName != subscription->subscriptionName()) { found = true; inferiorSubscriptions.push_back(subscription); } } //If there are some inferior subscriptions, we have to deal with them if (found) { for (auto* inferiorSubscription : inferiorSubscriptions) { - qDebug()<<"Reparent topics of inferior subscription: "<< inferiorSubscription->subscriptionName(); +// qDebug()<<"Reparent topics of inferior subscription: "<< inferiorSubscription->subscriptionName(); //We have to reparent every topic of the inferior subscription, so no data is lost QVector topics = inferiorSubscription->topics(); for (auto* topic : topics) { topic->reparent(newSubscription); } //Then remove the subscription and every connected information QMqttTopicFilter unsubscribeFilter {inferiorSubscription->subscriptionName()}; m_client->unsubscribe(unsubscribeFilter); for (int j = 0; j < m_MQTTSubscriptions.size(); ++j) { if (m_MQTTSubscriptions[j]->subscriptionName() == inferiorSubscription->subscriptionName()) { m_MQTTSubscriptions.remove(j); } } m_subscriptions.removeAll(inferiorSubscription->subscriptionName()); m_subscribedTopicNameQoS.remove(inferiorSubscription->subscriptionName()); removeChild(inferiorSubscription); } } connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::MQTTSubscriptionMessageReceived); emit MQTTTopicsChanged(); } } } /*! * \brief Removes a MQTTSubscription from the MQTTClient * * \param name, the name of the subscription to remove */ void MQTTClient::removeMQTTSubscription(const QString& subscriptionName) { //We can only remove the subscription if it exists if (m_subscriptions.contains(subscriptionName)) { //unsubscribe from the topic const QMqttTopicFilter filter{subscriptionName}; m_client->unsubscribe(filter); - qDebug()<<"Unsubscribe from: " << subscriptionName; +// qDebug()<<"Unsubscribe from: " << subscriptionName; //Remove every connected information m_subscriptions.removeAll(subscriptionName); for (int i = 0; i < m_MQTTSubscriptions.size(); ++i) { if (m_MQTTSubscriptions[i]->subscriptionName() == subscriptionName) { MQTTSubscription* removeSubscription = m_MQTTSubscriptions[i]; m_MQTTSubscriptions.remove(i); //Remove every topic of the subscription as well QVector topics = removeSubscription->topics(); for (int j = 0; j < topics.size(); ++j) { m_topicNames.removeAll(topics[j]->topicName()); } //Remove the MQTTSubscription removeChild(removeSubscription); break; } } QMapIterator j(m_subscribedTopicNameQoS); while (j.hasNext()) { j.next(); if (j.key().filter() == subscriptionName) { m_subscribedTopicNameQoS.remove(j.key()); break; } } emit MQTTTopicsChanged(); } } /*! * \brief Adds a MQTTSubscription to the MQTTClient *Used when the user unsubscribes from a topic of a MQTTSubscription * * \param topic, the name of the topic * \param QoS */ void MQTTClient::addBeforeRemoveSubscription(const QString& topicName, quint8 QoS) { //We can't add the subscription if it already exists if (!m_subscriptions.contains(topicName)) { //Subscribe to the topic QMqttTopicFilter filter {topicName}; QMqttSubscription* temp = m_client->subscribe(filter, QoS); if (temp) { //Add the MQTTSubscription and other connected data - qDebug()<<"Add subscription before remove: " << temp->topic() << " " << temp->qos(); +// qDebug()<<"Add subscription before remove: " << temp->topic() << " " << temp->qos(); m_subscriptions.push_back(temp->topic().filter()); m_subscribedTopicNameQoS[temp->topic().filter()] = temp->qos(); MQTTSubscription* newSubscription = new MQTTSubscription(temp->topic().filter()); newSubscription->setMQTTClient(this); addChildFast(newSubscription); m_MQTTSubscriptions.push_back(newSubscription); //Search for the subscription the topic belonged to bool found = false; MQTTSubscription* superiorSubscription = nullptr; for (auto* subscription : m_MQTTSubscriptions) { if (checkTopicContains(subscription->subscriptionName(), topicName) && topicName != subscription->subscriptionName()) { found = true; superiorSubscription = subscription; break; } } if (found) { //Search for topics belonging to the superior(old) subscription //which are also contained by the new subscription QVector topics = superiorSubscription->topics(); - qDebug()<< topics.size(); +// qDebug()<< topics.size(); QVector inferiorTopics; for (auto* topic : topics) { if (checkTopicContains(topicName, topic->topicName())) { inferiorTopics.push_back(topic); } } //Reparent these topics, in order to avoid data loss for (auto* inferiorTopic : inferiorTopics) { inferiorTopic->reparent(newSubscription); } } connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::MQTTSubscriptionMessageReceived); } } } /*! * \brief Reparents the given MQTTTopic to the given MQTTSubscription * * \param topic, the name of the MQTTTopic * \param parent, the name of the MQTTSubscription */ void MQTTClient::reparentTopic(const QString& topicName, const QString& parentTopicName) { //We can only reparent if the parent containd the topic if (m_subscriptions.contains(parentTopicName) && m_topicNames.contains(topicName)) { - qDebug() << "Reparent " << topicName << " to " << parentTopicName; +// qDebug() << "Reparent " << topicName << " to " << parentTopicName; //search for the parent MQTTSubscription bool found = false; MQTTSubscription* superiorSubscription = nullptr; for (auto* subscription : m_MQTTSubscriptions) { if (subscription->subscriptionName() == parentTopicName) { found = true; superiorSubscription = subscription; break; } } if (found) { //get every topic of the MQTTClient QVector topics = children(AbstractAspect::ChildIndexFlag::Recursive); //Search for the given topic among the MQTTTopics for (auto* topic : topics) { if (topicName == topic->topicName()) { //if found, it is reparented to the parent MQTTSubscription topic->reparent(superiorSubscription); break; } } } } } /*! *\brief Checks if a topic contains another one * * \param superior the name of a topic * \param inferior the name of a topic * \return true if superior is equal to or contains(if superior contains wildcards) inferior, * false otherwise */ bool MQTTClient::checkTopicContains(const QString& superior, const QString& inferior) { if (superior == inferior) return true; else { if (superior.contains(QLatin1String("/"))) { QStringList superiorList = superior.split('/', QString::SkipEmptyParts); QStringList inferiorList = inferior.split('/', QString::SkipEmptyParts); //a longer topic can't contain a shorter one if (superiorList.size() > inferiorList.size()) return false; bool ok = true; for (int i = 0; i < superiorList.size(); ++i) { if (superiorList.at(i) != inferiorList.at(i)) { if ((superiorList.at(i) != '+') && !(superiorList.at(i) == '#' && i == superiorList.size() - 1)) { //if the two topics differ, and the superior's current level isn't + or #(which can be only in the last position) //then superior can't contain inferior ok = false; break; } else if (i == superiorList.size() - 1 && (superiorList.at(i) == '+' && inferiorList.at(i) == '#') ) { //if the two topics differ at the last level //and the superior's current level is + while the inferior's is #(which can be only in the last position) //then superior can't contain inferior ok = false; break; } } } return ok; } return false; } } /*! *\brief Returns the '+' wildcard containing topic name, which includes the given topic names * * \param first the name of a topic * \param second the name of a topic * \return The name of the common topic, if it exists, otherwise an empty string */ QString MQTTClient::checkCommonLevel(const QString& first, const QString& second) { QStringList firstList = first.split('/', QString::SkipEmptyParts); QStringList secondtList = second.split('/', QString::SkipEmptyParts); QString commonTopic; if (!firstList.isEmpty()) { //the two topics have to be the same size and can't be identic if ((firstList.size() == secondtList.size()) && (first != second)) { //the index where they differ int differIndex = -1; for (int i = 0; i < firstList.size(); ++i) { if (firstList.at(i) != secondtList.at(i)) { differIndex = i; break; } } //they can differ at only one level and that can't be the first bool differ = false; if (differIndex > 0) { for (int j = differIndex +1; j < firstList.size(); ++j) { if (firstList.at(j) != secondtList.at(j)) { differ = true; break; } } } else differ = true; if (!differ) { for (int i = 0; i < firstList.size(); ++i) { if (i != differIndex) { commonTopic.append(firstList.at(i)); } else { //we put '+' wildcard at the level where they differ commonTopic.append('+'); } if (i != firstList.size() - 1) commonTopic.append("/"); } } } } - qDebug() << first << " " << second << " common topic: "<stop(); } /*! * \brief Returns whether the user wants to use will message or not */ bool MQTTClient::MQTTWillUse() const { return m_MQTTWill.enabled; } /*! * \brief Sets the will topic of the client * * \param topic */ void MQTTClient::setWillTopic(const QString& topic) { - qDebug() << "Set will topic:" << topic; +// qDebug() << "Set will topic:" << topic; m_MQTTWill.willTopic = topic; } /*! * \brief Returns the will topic of the client */ QString MQTTClient::willTopic() const { return m_MQTTWill.willTopic; } /*! * \brief Sets the retain flag of the client's will message * * \param retain */ void MQTTClient::setWillRetain(bool retain) { m_MQTTWill.willRetain = retain; } /*! * \brief Returns the retain flag of the client's will message */ bool MQTTClient::willRetain() const { return m_MQTTWill.willRetain; } /*! * \brief Sets the QoS level of the client's will message * * \param QoS */ void MQTTClient::setWillQoS(quint8 QoS) { m_MQTTWill.willQoS = QoS; } /*! * \brief Returns the QoS level of the client's will message */ quint8 MQTTClient::willQoS() const { return m_MQTTWill.willQoS; } /*! * \brief Sets the will message type of the client * * \param messageType */ void MQTTClient::setWillMessageType(WillMessageType messageType) { m_MQTTWill.willMessageType = messageType; } /*! * \brief Returns the will message type of the client */ MQTTClient::WillMessageType MQTTClient::willMessageType() const { return m_MQTTWill.willMessageType; } /*! * \brief Sets the own will message of the user * * \param ownMessage */ void MQTTClient::setWillOwnMessage(const QString& ownMessage) { m_MQTTWill.willOwnMessage = ownMessage; } /*! * \brief Returns the own will message of the user */ QString MQTTClient::willOwnMessage() const { return m_MQTTWill.willOwnMessage; } /*! * \brief Updates the will message of the client */ void MQTTClient::updateWillMessage() { QVector topics = children(AbstractAspect::ChildIndexFlag::Recursive); const MQTTTopic* willTopic = nullptr; //Search for the will topic for (const auto* topic : topics) { if (topic->topicName() == m_MQTTWill.willTopic) { willTopic = topic; break; } } //if the will topic is found we can update the will message if (willTopic != nullptr) { //To update the will message we have to disconnect first, then after setting everything connect again if (m_MQTTWill.enabled && (m_client->state() == QMqttClient::ClientState::Connected) ) { //Disconnect only once (disconnecting may take a while) if (!m_disconnectForWill) { - qDebug() << "Disconnecting from host in order to update will message"; +// qDebug() << "Disconnecting from host in order to update will message"; m_client->disconnectFromHost(); m_disconnectForWill = true; } //Try to update again updateWillMessage(); } //If client is disconnected we can update the settings else if (m_MQTTWill.enabled && (m_client->state() == QMqttClient::ClientState::Disconnected) && m_disconnectForWill) { m_client->setWillQoS(m_MQTTWill.willQoS); - qDebug()<<"Will QoS" << m_MQTTWill.willQoS; +// qDebug()<<"Will QoS" << m_MQTTWill.willQoS; m_client->setWillRetain(m_MQTTWill.willRetain); - qDebug()<<"Will retain" << m_MQTTWill.willRetain; +// qDebug()<<"Will retain" << m_MQTTWill.willRetain; m_client->setWillTopic(m_MQTTWill.willTopic); - qDebug()<<"Will Topic" << m_MQTTWill.willTopic; +// qDebug()<<"Will Topic" << m_MQTTWill.willTopic; //Set the will message according to m_willMessageType switch (m_MQTTWill.willMessageType) { case WillMessageType::OwnMessage: m_client->setWillMessage(m_MQTTWill.willOwnMessage.toUtf8()); - qDebug()<<"Will own message" << m_MQTTWill.willOwnMessage; +// qDebug()<<"Will own message" << m_MQTTWill.willOwnMessage; break; case WillMessageType::Statistics: { const auto asciiFilter = willTopic->filter(); //If the topic's asciiFilter was found, get the needed statistics if (asciiFilter != nullptr) { //Statistics is only possible if the data stored in the MQTTTopic is of type integer or numeric if ((asciiFilter->MQTTColumnMode() == AbstractColumn::ColumnMode::Integer) || (asciiFilter->MQTTColumnMode() == AbstractColumn::ColumnMode::Numeric)) { m_client->setWillMessage(asciiFilter->MQTTColumnStatistics(willTopic).toUtf8()); } //Otherwise set empty message else { m_client->setWillMessage(QByteArray()); } - qDebug() << "Will statistics message: "<< QString(m_client->willMessage()); +// qDebug() << "Will statistics message: "<< QString(m_client->willMessage()); } break; } case WillMessageType::LastMessage: m_client->setWillMessage(m_MQTTWill.willLastMessage.toUtf8()); - qDebug()<<"Will last message:\n" << m_MQTTWill.willLastMessage; +// qDebug()<<"Will last message:\n" << m_MQTTWill.willLastMessage; break; default: break; } m_disconnectForWill = false; //Reconnect with the updated message m_client->connectToHost(); - qDebug()<< "Reconnect to host after updating will message"; +// qDebug()<< "Reconnect to host after updating will message"; } } } /*! * \brief Returns the MQTTClient's will update type */ MQTTClient::WillUpdateType MQTTClient::willUpdateType() const { return m_MQTTWill.willUpdateType; } /*! * \brief Sets the MQTTClient's will update type * * \param willUpdateType */ void MQTTClient::setWillUpdateType(WillUpdateType willUpdateType) { m_MQTTWill.willUpdateType = willUpdateType; } /*! * \brief Returns the time interval of updating the MQTTClient's will message */ int MQTTClient::willTimeInterval() const { return m_MQTTWill.willTimeInterval; } /*! * \brief Sets the time interval of updating the MQTTClient's will message, if update type is TimePeriod * * \param interval */ void MQTTClient::setWillTimeInterval(int interval) { m_MQTTWill.willTimeInterval = interval; } /*! * \brief Clear the lastly received message by the will topic * Called when the will topic is changed */ void MQTTClient::clearLastMessage() { m_MQTTWill.willLastMessage.clear(); } /*! * \brief Sets true the corresponding flag of the statistic type, * what means that the given statistic type will be added to the will message * * \param statistics */ void MQTTClient::addWillStatistics(WillStatisticsType statistic) { m_MQTTWill.willStatistics[static_cast(statistic)] = true; } /*! * \brief Sets false the corresponding flag of the statistic type, * what means that the given statistic will no longer be added to the will message * * \param statistics */ void MQTTClient::removeWillStatistics(WillStatisticsType statistic) { m_MQTTWill.willStatistics[static_cast(statistic)] = false; } /*! * \brief Returns a bool vector, meaning which statistic types are included in the will message * If the corresponding value is true, the statistic type is included, otherwise it isn't */ QVector MQTTClient::willStatistics() const { return m_MQTTWill.willStatistics; } /*! * \brief Starts the will timer, which will update the will message */ void MQTTClient::startWillTimer() const { if (m_MQTTWill.willUpdateType == WillUpdateType::TimePeriod) m_willTimer->start(m_MQTTWill.willTimeInterval); } /*! * \brief Stops the will timer */ void MQTTClient::stopWillTimer() const { m_willTimer->stop(); } //############################################################################## //################################# SLOTS #################################### //############################################################################## /*! *\brief called periodically when update type is TimeInterval */ void MQTTClient::read() { if (!m_filter) return; if (!m_prepared) { - qDebug()<<"Connect"; +// qDebug()<<"Connect"; //connect to the broker m_client->connectToHost(); m_prepared = true; } if ((m_client->state() == QMqttClient::ClientState::Connected) && m_MQTTFirstConnectEstablished) { // qDebug()<<"Read"; //Signal for every MQTTTopic that they can read emit readFromTopics(); } } /*! *\brief called when the client successfully connected to the broker */ void MQTTClient::onMQTTConnect() { if (m_client->error() == QMqttClient::NoError) { //if this is the first connection (after setting the options in ImportFileWidget or loading saved project) if (!m_MQTTFirstConnectEstablished) { - qDebug()<<"connection made in MQTTClient"; +// qDebug()<<"connection made in MQTTClient"; //Subscribe to initial or loaded topics QMapIterator i(m_subscribedTopicNameQoS); while (i.hasNext()) { i.next(); - qDebug()<subscribe(i.key(), i.value()); if (temp) { //If we didn't load the MQTTClient from xml we have to add the MQTTSubscriptions if (!m_loaded) { m_subscriptions.push_back(temp->topic().filter()); MQTTSubscription* newSubscription = new MQTTSubscription(temp->topic().filter()); newSubscription->setMQTTClient(this); addChildFast(newSubscription); m_MQTTSubscriptions.push_back(newSubscription); } connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::MQTTSubscriptionMessageReceived); } } m_MQTTFirstConnectEstablished = true; //Signal that the initial subscriptions were made emit MQTTSubscribed(); } //if there was already a connection made(happens after updating will message) else { - qDebug() << "Start resubscribing after will message update"; +// qDebug() << "Start resubscribing after will message update"; //Only the client has to make the subscriptions again, every other connected data is still available QMapIterator i(m_subscribedTopicNameQoS); while (i.hasNext()) { i.next(); QMqttSubscription* temp = m_client->subscribe(i.key(), i.value()); if (temp) { - qDebug()<topic()<<" "<qos(); +// qDebug()<topic()<<" "<qos(); connect(temp, &QMqttSubscription::messageReceived, this, &MQTTClient::MQTTSubscriptionMessageReceived); - } else - qDebug()<<"Couldn't subscribe after will update"; + } +// else +// qDebug()<<"Couldn't subscribe after will update"; } } } } /*! *\brief called when a message is received by a topic belonging to one of subscriptions of the client. * It passes the message to the appropriate MQTTSubscription which will pass it to the appropriate MQTTTopic */ void MQTTClient::MQTTSubscriptionMessageReceived(const QMqttMessage& msg) { //Decide to interpret retain message or not if (!msg.retain() || m_MQTTRetain) { //If this is the first message from the topic, save its name if (!m_topicNames.contains(msg.topic().name())) m_topicNames.push_back(msg.topic().name()); //Pass the message and the topic name to the MQTTSubscription which contains the topic for (auto* subscription : m_MQTTSubscriptions) { if (checkTopicContains(subscription->subscriptionName(), msg.topic().name())) { subscription->messageArrived(msg.payload(), msg.topic().name()); break; } } //if the message was received by the will topic, update the last message received by it if (msg.topic().name() == m_MQTTWill.willTopic) { m_MQTTWill.willLastMessage = QString(msg.payload()); emit MQTTTopicsChanged(); } } } /*! *\brief Handles some of the possible errors of the client, using MQTTErrorWidget */ void MQTTClient::MQTTErrorChanged(QMqttClient::ClientError clientError) { if (clientError != QMqttClient::ClientError::NoError) { MQTTErrorWidget* errorWidget = new MQTTErrorWidget(clientError, this); errorWidget->show(); } } /*! *\brief Called when a subscription is loaded. * Checks whether every saved subscription was loaded or not. * If everything is loaded, it makes the connection and starts the reading * * \param name, the name of the subscription */ void MQTTClient::subscriptionLoaded(const QString &name) { if (!name.isEmpty()) { - qDebug() << "Finished loading: " << name; +// qDebug() << "Finished loading: " << name; //Save information about the subscription m_subscriptionsLoaded++; m_subscriptions.push_back(name); QMqttTopicFilter filter {name}; m_subscribedTopicNameQoS[filter] = 0; //Save the topics belonging to the subscription for (const auto* subscription : m_MQTTSubscriptions) { if (subscription->subscriptionName() == name) { const auto& topics = subscription->topics(); for (auto* topic : topics) { m_topicNames.push_back(topic->topicName()); } break; } } //Check whether every subscription was loaded or not if (m_subscriptionsLoaded == m_subscriptionCountToLoad) { //if everything was loaded we can start reading m_loaded = true; read(); } } } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## /*! Saves as XML. */ void MQTTClient::save(QXmlStreamWriter* writer) const { writer->writeStartElement("MQTTClient"); writeBasicAttributes(writer); writeCommentElement(writer); //general writer->writeStartElement("general"); writer->writeAttribute("subscriptionCount", QString::number(m_MQTTSubscriptions.size())); writer->writeAttribute("updateType", QString::number(static_cast(m_updateType))); writer->writeAttribute("readingType", QString::number(static_cast(m_readingType))); writer->writeAttribute("keepValues", QString::number(m_keepNValues)); if (m_updateType == UpdateType::TimeInterval) writer->writeAttribute("updateInterval", QString::number(m_updateInterval)); if (m_readingType != ReadingType::TillEnd) writer->writeAttribute("sampleSize", QString::number(m_sampleSize)); writer->writeAttribute("host", m_client->hostname()); writer->writeAttribute("port", QString::number(m_client->port())); writer->writeAttribute("username", m_client->username()); writer->writeAttribute("password", m_client->password()); writer->writeAttribute("clientId", m_client->clientId()); writer->writeAttribute("useRetain", QString::number(m_MQTTRetain)); writer->writeAttribute("useWill", QString::number(m_MQTTWill.enabled)); writer->writeAttribute("willTopic", m_MQTTWill.willTopic); writer->writeAttribute("willOwnMessage", m_MQTTWill.willOwnMessage); writer->writeAttribute("willQoS", QString::number(m_MQTTWill.willQoS)); writer->writeAttribute("willRetain", QString::number(m_MQTTWill.willRetain)); writer->writeAttribute("willMessageType", QString::number(static_cast(m_MQTTWill.willMessageType))); writer->writeAttribute("willUpdateType", QString::number(static_cast(m_MQTTWill.willUpdateType))); writer->writeAttribute("willTimeInterval", QString::number(m_MQTTWill.willTimeInterval)); for (int i = 0; i < m_MQTTWill.willStatistics.count(); ++i) writer->writeAttribute("willStatistics"+QString::number(i), QString::number(m_MQTTWill.willStatistics[i])); writer->writeAttribute("useID", QString::number(m_MQTTUseID)); writer->writeAttribute("useAuthentication", QString::number(m_MQTTUseAuthentication)); writer->writeEndElement(); //filter m_filter->save(writer); //MQTTSubscription for (auto* sub : children(AbstractAspect::ChildIndexFlag::IncludeHidden)) sub->save(writer); writer->writeEndElement(); // "MQTTClient" } /*! Loads from XML. */ bool MQTTClient::load(XmlStreamReader* reader, bool preview) { if (!readBasicAttributes(reader)) return false; QString attributeWarning = i18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs; QString str; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "MQTTClient") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (reader->name() == "general") { attribs = reader->attributes(); str = attribs.value("subscriptionCount").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'subscriptionCount'")); else m_subscriptionCountToLoad = str.toInt(); str = attribs.value("keepValues").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'keepValues'")); else m_keepNValues = str.toInt(); str = attribs.value("updateType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'updateType'")); else m_updateType = static_cast(str.toInt()); str = attribs.value("readingType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'readingType'")); else m_readingType = static_cast(str.toInt()); if (m_updateType == UpdateType::TimeInterval) { str = attribs.value("updateInterval").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'updateInterval'")); else m_updateInterval = str.toInt(); } if (m_readingType != ReadingType::TillEnd) { str = attribs.value("sampleSize").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'sampleSize'")); else m_sampleSize = str.toInt(); } str = attribs.value("host").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'host'")); else m_client->setHostname(str); str = attribs.value("port").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'port'")); else m_client->setPort(str.toUInt()); str = attribs.value("useAuthentication").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'useAuthentication'")); else m_MQTTUseAuthentication = str.toInt(); if (m_MQTTUseAuthentication) { str = attribs.value("username").toString(); if (!str.isEmpty()) m_client->setUsername(str); str = attribs.value("password").toString(); if (!str.isEmpty()) m_client->setPassword(str); } str = attribs.value("useID").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'useID'")); else m_MQTTUseID = str.toInt(); if (m_MQTTUseID) { str = attribs.value("clientId").toString(); if (!str.isEmpty()) m_client->setClientId(str); } str = attribs.value("useRetain").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'useRetain'")); else m_MQTTRetain = str.toInt(); str = attribs.value("useWill").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'useWill'")); else m_MQTTWill.enabled = str.toInt(); if (m_MQTTWill.enabled) { str = attribs.value("willTopic").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willTopic'")); else m_MQTTWill.willTopic = str; str = attribs.value("willOwnMessage").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willOwnMessage'")); else m_MQTTWill.willOwnMessage = str; str = attribs.value("willQoS").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willQoS'")); else m_MQTTWill.willQoS = str.toUInt(); str = attribs.value("willRetain").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willRetain'")); else m_MQTTWill.willRetain = str.toInt(); str = attribs.value("willMessageType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willMessageType'")); else m_MQTTWill.willMessageType = static_cast(str.toInt()); str = attribs.value("willUpdateType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willUpdateType'")); else m_MQTTWill.willUpdateType = static_cast(str.toInt()); str = attribs.value("willTimeInterval").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willTimeInterval'")); else m_MQTTWill.willTimeInterval = str.toInt(); for (int i = 0; i < m_MQTTWill.willStatistics.count(); ++i) { str = attribs.value("willStatistics"+QString::number(i)).toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.arg("'willTimeInterval'")); else m_MQTTWill.willStatistics[i] = str.toInt(); } } } else if (reader->name() == "asciiFilter") { setFilter(new AsciiFilter); if (!m_filter->load(reader)) return false; } else if (reader->name() == "MQTTSubscription") { MQTTSubscription* subscription = new MQTTSubscription(QString()); subscription->setMQTTClient(this); m_MQTTSubscriptions.push_back(subscription); connect(subscription, &MQTTSubscription::loaded, this, &MQTTClient::subscriptionLoaded); if (!subscription->load(reader, preview)) { delete subscription; return false; } addChildFast(subscription); } else {// unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } return !reader->hasError(); } diff --git a/src/backend/worksheet/plots/cartesian/Histogram.cpp b/src/backend/worksheet/plots/cartesian/Histogram.cpp index d72cd0d7c..c545b6838 100644 --- a/src/backend/worksheet/plots/cartesian/Histogram.cpp +++ b/src/backend/worksheet/plots/cartesian/Histogram.cpp @@ -1,1867 +1,1875 @@ /*************************************************************************** File : Histogram.cpp Project : LabPlot Description : Histogram -------------------------------------------------------------------- Copyright : (C) 2016 Anu Mittal (anu22mittal@gmail.com) Copyright : (C) 2016-2018 by Alexander Semke (alexander.semke@web.de) Copyright : (C) 2017-2018 by Garvit Khatri (garvitdelhi@gmail.com) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ /*! \class Histogram \brief A 2D-curve, provides an interface for editing many properties of the curve. \ingroup worksheet */ #include "Histogram.h" #include "HistogramPrivate.h" #include "backend/core/column/Column.h" #include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/lib/commandtemplates.h" #include "backend/worksheet/Worksheet.h" #include "backend/lib/XmlStreamReader.h" #include "tools/ImageTools.h" #include "backend/lib/trace.h" #include #include #include #include #include #include #include extern "C" { #include #include #include } Histogram::Histogram(const QString &name) : WorksheetElement(name, AspectType::Histogram), d_ptr(new HistogramPrivate(this)) { init(); } Histogram::Histogram(const QString &name, HistogramPrivate *dd) : WorksheetElement(name, AspectType::Histogram), d_ptr(dd) { init(); } void Histogram::init() { Q_D(Histogram); KConfig config; KConfigGroup group = config.group("Histogram"); d->dataColumn = nullptr; d->type = (Histogram::HistogramType) group.readEntry("Type", (int)Histogram::Ordinary); d->orientation = (Histogram::HistogramOrientation) group.readEntry("Orientation", (int)Histogram::Vertical); d->binningMethod = (Histogram::BinningMethod) group.readEntry("BinningMethod", (int)Histogram::SquareRoot); d->binCount = group.readEntry("BinCount", 10); d->binWidth = group.readEntry("BinWidth", 1.0f); d->autoBinRanges = group.readEntry("AutoBinRanges", true); d->binRangesMin = 0.0; d->binRangesMax = 1.0; d->lineType = (Histogram::LineType) group.readEntry("LineType", (int)Histogram::Bars); d->linePen.setStyle( (Qt::PenStyle) group.readEntry("LineStyle", (int)Qt::SolidLine) ); d->linePen.setColor( group.readEntry("LineColor", QColor(Qt::black)) ); d->linePen.setWidthF( group.readEntry("LineWidth", Worksheet::convertToSceneUnits(1.0, Worksheet::Unit::Point)) ); d->lineOpacity = group.readEntry("LineOpacity", 1.0); d->symbolsStyle = (Symbol::Style)group.readEntry("SymbolStyle", (int)Symbol::NoSymbols); d->symbolsSize = group.readEntry("SymbolSize", Worksheet::convertToSceneUnits(5, Worksheet::Unit::Point)); d->symbolsRotationAngle = group.readEntry("SymbolRotation", 0.0); d->symbolsOpacity = group.readEntry("SymbolOpacity", 1.0); d->symbolsBrush.setStyle( (Qt::BrushStyle)group.readEntry("SymbolFillingStyle", (int)Qt::SolidPattern) ); d->symbolsBrush.setColor( group.readEntry("SymbolFillingColor", QColor(Qt::black)) ); d->symbolsPen.setStyle( (Qt::PenStyle)group.readEntry("SymbolBorderStyle", (int)Qt::SolidLine) ); d->symbolsPen.setColor( group.readEntry("SymbolBorderColor", QColor(Qt::black)) ); d->symbolsPen.setWidthF( group.readEntry("SymbolBorderWidth", Worksheet::convertToSceneUnits(0.0, Worksheet::Unit::Point)) ); d->valuesType = (Histogram::ValuesType) group.readEntry("ValuesType", (int)Histogram::NoValues); d->valuesColumn = nullptr; d->valuesPosition = (Histogram::ValuesPosition) group.readEntry("ValuesPosition", (int)Histogram::ValuesAbove); d->valuesDistance = group.readEntry("ValuesDistance", Worksheet::convertToSceneUnits(5, Worksheet::Unit::Point)); d->valuesRotationAngle = group.readEntry("ValuesRotation", 0.0); d->valuesOpacity = group.readEntry("ValuesOpacity", 1.0); d->valuesNumericFormat = group.readEntry("ValuesNumericFormat", "f").at(0).toLatin1(); d->valuesPrecision = group.readEntry("ValuesNumericFormat", 2); d->valuesDateTimeFormat = group.readEntry("ValuesDateTimeFormat", "yyyy-MM-dd"); d->valuesPrefix = group.readEntry("ValuesPrefix", ""); d->valuesSuffix = group.readEntry("ValuesSuffix", ""); d->valuesFont = group.readEntry("ValuesFont", QFont()); d->valuesFont.setPixelSize( Worksheet::convertToSceneUnits( 8, Worksheet::Unit::Point ) ); d->valuesColor = group.readEntry("ValuesColor", QColor(Qt::black)); d->fillingEnabled = group.readEntry("FillingEnabled", true); d->fillingType = (PlotArea::BackgroundType) group.readEntry("FillingType", (int)PlotArea::Color); d->fillingColorStyle = (PlotArea::BackgroundColorStyle) group.readEntry("FillingColorStyle", (int) PlotArea::SingleColor); d->fillingImageStyle = (PlotArea::BackgroundImageStyle) group.readEntry("FillingImageStyle", (int) PlotArea::Scaled); d->fillingBrushStyle = (Qt::BrushStyle) group.readEntry("FillingBrushStyle", (int) Qt::SolidPattern); d->fillingFileName = group.readEntry("FillingFileName", QString()); d->fillingFirstColor = group.readEntry("FillingFirstColor", QColor(Qt::white)); d->fillingSecondColor = group.readEntry("FillingSecondColor", QColor(Qt::black)); d->fillingOpacity = group.readEntry("FillingOpacity", 1.0); d->errorType = (Histogram::ErrorType) group.readEntry("ErrorType", (int)Histogram::NoError); d->errorBarsType = (XYCurve::ErrorBarsType) group.readEntry("ErrorBarsType", (int)XYCurve::ErrorBarsSimple); d->errorBarsCapSize = group.readEntry( "ErrorBarsCapSize", Worksheet::convertToSceneUnits(10, Worksheet::Unit::Point) ); d->errorBarsPen.setStyle( (Qt::PenStyle)group.readEntry("ErrorBarsStyle", (int)Qt::SolidLine) ); d->errorBarsPen.setColor( group.readEntry("ErrorBarsColor", QColor(Qt::black)) ); d->errorBarsPen.setWidthF( group.readEntry("ErrorBarsWidth", Worksheet::convertToSceneUnits(1.0, Worksheet::Unit::Point)) ); d->errorBarsOpacity = group.readEntry("ErrorBarsOpacity", 1.0); this->initActions(); } void Histogram::initActions() { visibilityAction = new QAction(QIcon::fromTheme("view-visible"), i18n("Visible"), this); visibilityAction->setCheckable(true); connect(visibilityAction, &QAction::triggered, this, &Histogram::visibilityChangedSlot); } QMenu* Histogram::createContextMenu() { QMenu *menu = WorksheetElement::createContextMenu(); QAction* firstAction = menu->actions().at(1); //skip the first action because of the "title-action" visibilityAction->setChecked(isVisible()); menu->insertAction(firstAction, visibilityAction); return menu; } /*! Returns an icon to be used in the project explorer. */ QIcon Histogram::icon() const { return QIcon::fromTheme("view-object-histogram-linear"); } QGraphicsItem* Histogram::graphicsItem() const { return d_ptr; } STD_SWAP_METHOD_SETTER_CMD_IMPL(Histogram, SetVisible, bool, swapVisible) void Histogram::setVisible(bool on) { Q_D(Histogram); exec(new HistogramSetVisibleCmd(d, on, on ? ki18n("%1: set visible") : ki18n("%1: set invisible"))); } bool Histogram::isVisible() const { Q_D(const Histogram); return d->isVisible(); } void Histogram::setPrinting(bool on) { Q_D(Histogram); d->m_printing = on; } //############################################################################## //########################## getter methods ################################## //############################################################################## //general BASIC_SHARED_D_READER_IMPL(Histogram, Histogram::HistogramType, type, type) BASIC_SHARED_D_READER_IMPL(Histogram, Histogram::HistogramOrientation, orientation, orientation) BASIC_SHARED_D_READER_IMPL(Histogram, Histogram::BinningMethod, binningMethod, binningMethod) BASIC_SHARED_D_READER_IMPL(Histogram, int, binCount, binCount) BASIC_SHARED_D_READER_IMPL(Histogram, float, binWidth, binWidth) BASIC_SHARED_D_READER_IMPL(Histogram, bool, autoBinRanges, autoBinRanges) BASIC_SHARED_D_READER_IMPL(Histogram, double, binRangesMin, binRangesMin) BASIC_SHARED_D_READER_IMPL(Histogram, double, binRangesMax, binRangesMax) BASIC_SHARED_D_READER_IMPL(Histogram, const AbstractColumn*, dataColumn, dataColumn) QString& Histogram::dataColumnPath() const { return d_ptr->dataColumnPath; } //line BASIC_SHARED_D_READER_IMPL(Histogram, Histogram::LineType, lineType, lineType) CLASS_SHARED_D_READER_IMPL(Histogram, QPen, linePen, linePen) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, lineOpacity, lineOpacity) //symbols BASIC_SHARED_D_READER_IMPL(Histogram, Symbol::Style, symbolsStyle, symbolsStyle) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, symbolsOpacity, symbolsOpacity) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, symbolsRotationAngle, symbolsRotationAngle) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, symbolsSize, symbolsSize) CLASS_SHARED_D_READER_IMPL(Histogram, QBrush, symbolsBrush, symbolsBrush) CLASS_SHARED_D_READER_IMPL(Histogram, QPen, symbolsPen, symbolsPen) //values BASIC_SHARED_D_READER_IMPL(Histogram, Histogram::ValuesType, valuesType, valuesType) BASIC_SHARED_D_READER_IMPL(Histogram, const AbstractColumn *, valuesColumn, valuesColumn) QString& Histogram::valuesColumnPath() const { return d_ptr->valuesColumnPath; } BASIC_SHARED_D_READER_IMPL(Histogram, Histogram::ValuesPosition, valuesPosition, valuesPosition) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, valuesDistance, valuesDistance) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, valuesRotationAngle, valuesRotationAngle) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, valuesOpacity, valuesOpacity) CLASS_SHARED_D_READER_IMPL(Histogram, char, valuesNumericFormat, valuesNumericFormat) BASIC_SHARED_D_READER_IMPL(Histogram, int, valuesPrecision, valuesPrecision) CLASS_SHARED_D_READER_IMPL(Histogram, QString, valuesDateTimeFormat, valuesDateTimeFormat) CLASS_SHARED_D_READER_IMPL(Histogram, QString, valuesPrefix, valuesPrefix) CLASS_SHARED_D_READER_IMPL(Histogram, QString, valuesSuffix, valuesSuffix) CLASS_SHARED_D_READER_IMPL(Histogram, QColor, valuesColor, valuesColor) CLASS_SHARED_D_READER_IMPL(Histogram, QFont, valuesFont, valuesFont) //filling BASIC_SHARED_D_READER_IMPL(Histogram, bool, fillingEnabled, fillingEnabled) BASIC_SHARED_D_READER_IMPL(Histogram, PlotArea::BackgroundType, fillingType, fillingType) BASIC_SHARED_D_READER_IMPL(Histogram, PlotArea::BackgroundColorStyle, fillingColorStyle, fillingColorStyle) BASIC_SHARED_D_READER_IMPL(Histogram, PlotArea::BackgroundImageStyle, fillingImageStyle, fillingImageStyle) CLASS_SHARED_D_READER_IMPL(Histogram, Qt::BrushStyle, fillingBrushStyle, fillingBrushStyle) CLASS_SHARED_D_READER_IMPL(Histogram, QColor, fillingFirstColor, fillingFirstColor) CLASS_SHARED_D_READER_IMPL(Histogram, QColor, fillingSecondColor, fillingSecondColor) CLASS_SHARED_D_READER_IMPL(Histogram, QString, fillingFileName, fillingFileName) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, fillingOpacity, fillingOpacity) //error bars BASIC_SHARED_D_READER_IMPL(Histogram, Histogram::ErrorType, errorType, errorType) BASIC_SHARED_D_READER_IMPL(Histogram, XYCurve::ErrorBarsType, errorBarsType, errorBarsType) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, errorBarsCapSize, errorBarsCapSize) CLASS_SHARED_D_READER_IMPL(Histogram, QPen, errorBarsPen, errorBarsPen) BASIC_SHARED_D_READER_IMPL(Histogram, qreal, errorBarsOpacity, errorBarsOpacity) double Histogram::getYMaximum() const { return d_ptr->getYMaximum(); } double Histogram::getYMinimum() const { return d_ptr->getYMinimum(); } double Histogram::getXMaximum() const { return d_ptr->getXMaximum(); } double Histogram::getXMinimum() const { return d_ptr->getXMinimum(); } //############################################################################## //################# setter methods and undo commands ########################## //############################################################################## //General STD_SETTER_CMD_IMPL_F_S(Histogram, SetDataColumn, const AbstractColumn*, dataColumn, recalcHistogram) void Histogram::setDataColumn(const AbstractColumn* column) { Q_D(Histogram); if (column != d->dataColumn) { exec(new HistogramSetDataColumnCmd(d, column, ki18n("%1: set data column"))); if (column) { connect(column, &AbstractColumn::dataChanged, this, &Histogram::dataChanged); //update the curve itself on changes connect(column, &AbstractColumn::dataChanged, this, &Histogram::recalcHistogram); connect(column->parentAspect(), &AbstractAspect::aspectAboutToBeRemoved, this, &Histogram::dataColumnAboutToBeRemoved); //TODO: add disconnect in the undo-function } } } STD_SETTER_CMD_IMPL_F_S(Histogram, SetHistogramType, Histogram::HistogramType, type, updateType) void Histogram::setType(Histogram::HistogramType type) { Q_D(Histogram); if (type != d->type) exec(new HistogramSetHistogramTypeCmd(d, type, ki18n("%1: set histogram type"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetHistogramOrientation, Histogram::HistogramOrientation, orientation, updateOrientation) void Histogram::setOrientation(Histogram::HistogramOrientation orientation) { Q_D(Histogram); if (orientation != d->orientation) exec(new HistogramSetHistogramOrientationCmd(d, orientation, ki18n("%1: set histogram orientation"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetBinningMethod, Histogram::BinningMethod, binningMethod, recalcHistogram) void Histogram::setBinningMethod(Histogram::BinningMethod method) { Q_D(Histogram); if (method != d->binningMethod) exec(new HistogramSetBinningMethodCmd(d, method, ki18n("%1: set binning method"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetBinCount, int, binCount, recalcHistogram) void Histogram::setBinCount(int count) { Q_D(Histogram); if (count != d->binCount) exec(new HistogramSetBinCountCmd(d, count, ki18n("%1: set bin count"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetBinWidth, float, binWidth, recalcHistogram) void Histogram::setBinWidth(float width) { Q_D(Histogram); if (width != d->binWidth) exec(new HistogramSetBinWidthCmd(d, width, ki18n("%1: set bin width"))); } class HistogramSetAutoBinRangesCmd : public QUndoCommand { public: HistogramSetAutoBinRangesCmd(HistogramPrivate* private_obj, bool autoBinRanges) : m_private(private_obj), m_autoBinRanges(autoBinRanges) { setText(i18n("%1: change auto bin ranges", m_private->name())); }; void redo() override { m_autoBinRangesOld = m_private->autoBinRanges; if (m_autoBinRanges) { m_binRangesMinOld = m_private->binRangesMin; m_binRangesMaxOld = m_private->binRangesMax; m_private->q->recalcHistogram(); } m_private->autoBinRanges = m_autoBinRanges; emit m_private->q->autoBinRangesChanged(m_autoBinRanges); }; void undo() override { if (!m_autoBinRangesOld) { m_private->binRangesMin = m_binRangesMinOld; m_private->binRangesMax = m_binRangesMaxOld; m_private->recalcHistogram(); } m_private->autoBinRanges = m_autoBinRangesOld; emit m_private->q->autoBinRangesChanged(m_autoBinRangesOld); } private: HistogramPrivate* m_private; bool m_autoBinRanges; bool m_autoBinRangesOld{false}; double m_binRangesMinOld{0.0}; double m_binRangesMaxOld{0.0}; }; void Histogram::setAutoBinRanges(bool autoBinRanges) { Q_D(Histogram); if (autoBinRanges != d->autoBinRanges) exec(new HistogramSetAutoBinRangesCmd(d, autoBinRanges)); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetBinRangesMin, double, binRangesMin, recalcHistogram) void Histogram::setBinRangesMin(double binRangesMin) { Q_D(Histogram); if (binRangesMin != d->binRangesMin) exec(new HistogramSetBinRangesMinCmd(d, binRangesMin, ki18n("%1: set bin ranges start"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetBinRangesMax, double, binRangesMax, recalcHistogram) void Histogram::setBinRangesMax(double binRangesMax) { Q_D(Histogram); if (binRangesMax != d->binRangesMax) exec(new HistogramSetBinRangesMaxCmd(d, binRangesMax, ki18n("%1: set bin ranges end"))); } //Line STD_SETTER_CMD_IMPL_F_S(Histogram, SetLineType, Histogram::LineType, lineType, updateLines) void Histogram::setLineType(LineType type) { Q_D(Histogram); if (type != d->lineType) exec(new HistogramSetLineTypeCmd(d, type, ki18n("%1: line type changed"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetLinePen, QPen, linePen, recalcShapeAndBoundingRect) void Histogram::setLinePen(const QPen &pen) { Q_D(Histogram); if (pen != d->linePen) exec(new HistogramSetLinePenCmd(d, pen, ki18n("%1: set line style"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetLineOpacity, qreal, lineOpacity, updatePixmap); void Histogram::setLineOpacity(qreal opacity) { Q_D(Histogram); if (opacity != d->lineOpacity) exec(new HistogramSetLineOpacityCmd(d, opacity, ki18n("%1: set line opacity"))); } // Symbols STD_SETTER_CMD_IMPL_F_S(Histogram, SetSymbolsStyle, Symbol::Style, symbolsStyle, updateSymbols) void Histogram::setSymbolsStyle(Symbol::Style style) { Q_D(Histogram); if (style != d->symbolsStyle) exec(new HistogramSetSymbolsStyleCmd(d, style, ki18n("%1: set symbol style"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetSymbolsSize, qreal, symbolsSize, updateSymbols) void Histogram::setSymbolsSize(qreal size) { Q_D(Histogram); if (!qFuzzyCompare(1 + size, 1 + d->symbolsSize)) exec(new HistogramSetSymbolsSizeCmd(d, size, ki18n("%1: set symbol size"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetSymbolsRotationAngle, qreal, symbolsRotationAngle, updateSymbols) void Histogram::setSymbolsRotationAngle(qreal angle) { Q_D(Histogram); if (!qFuzzyCompare(1 + angle, 1 + d->symbolsRotationAngle)) exec(new HistogramSetSymbolsRotationAngleCmd(d, angle, ki18n("%1: rotate symbols"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetSymbolsBrush, QBrush, symbolsBrush, updatePixmap) void Histogram::setSymbolsBrush(const QBrush &brush) { Q_D(Histogram); if (brush != d->symbolsBrush) exec(new HistogramSetSymbolsBrushCmd(d, brush, ki18n("%1: set symbol filling"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetSymbolsPen, QPen, symbolsPen, updateSymbols) void Histogram::setSymbolsPen(const QPen &pen) { Q_D(Histogram); if (pen != d->symbolsPen) exec(new HistogramSetSymbolsPenCmd(d, pen, ki18n("%1: set symbol outline style"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetSymbolsOpacity, qreal, symbolsOpacity, updatePixmap) void Histogram::setSymbolsOpacity(qreal opacity) { Q_D(Histogram); if (opacity != d->symbolsOpacity) exec(new HistogramSetSymbolsOpacityCmd(d, opacity, ki18n("%1: set symbols opacity"))); } //Values STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesType, Histogram::ValuesType, valuesType, updateValues) void Histogram::setValuesType(Histogram::ValuesType type) { Q_D(Histogram); if (type != d->valuesType) exec(new HistogramSetValuesTypeCmd(d, type, ki18n("%1: set values type"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesColumn, const AbstractColumn*, valuesColumn, updateValues) void Histogram::setValuesColumn(const AbstractColumn* column) { Q_D(Histogram); if (column != d->valuesColumn) { exec(new HistogramSetValuesColumnCmd(d, column, ki18n("%1: set values column"))); if (column) { connect(column, &AbstractColumn::dataChanged, this, &Histogram::updateValues); connect(column->parentAspect(), &AbstractAspect::aspectAboutToBeRemoved, this, &Histogram::valuesColumnAboutToBeRemoved); } } } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesPosition, Histogram::ValuesPosition, valuesPosition, updateValues) void Histogram::setValuesPosition(ValuesPosition position) { Q_D(Histogram); if (position != d->valuesPosition) exec(new HistogramSetValuesPositionCmd(d, position, ki18n("%1: set values position"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesDistance, qreal, valuesDistance, updateValues) void Histogram::setValuesDistance(qreal distance) { Q_D(Histogram); if (distance != d->valuesDistance) exec(new HistogramSetValuesDistanceCmd(d, distance, ki18n("%1: set values distance"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesRotationAngle, qreal, valuesRotationAngle, updateValues) void Histogram::setValuesRotationAngle(qreal angle) { Q_D(Histogram); if (!qFuzzyCompare(1 + angle, 1 + d->valuesRotationAngle)) exec(new HistogramSetValuesRotationAngleCmd(d, angle, ki18n("%1: rotate values"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesOpacity, qreal, valuesOpacity, updatePixmap) void Histogram::setValuesOpacity(qreal opacity) { Q_D(Histogram); if (opacity != d->valuesOpacity) exec(new HistogramSetValuesOpacityCmd(d, opacity, ki18n("%1: set values opacity"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesNumericFormat, char, valuesNumericFormat, updateValues) void Histogram::setValuesNumericFormat(char format) { Q_D(Histogram); if (format != d->valuesNumericFormat) exec(new HistogramSetValuesNumericFormatCmd(d, format, ki18n("%1: set values numeric format"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesPrecision, int, valuesPrecision, updateValues) void Histogram::setValuesPrecision(int precision) { Q_D(Histogram); if (precision != d->valuesPrecision) exec(new HistogramSetValuesPrecisionCmd(d, precision, ki18n("%1: set values precision"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesDateTimeFormat, QString, valuesDateTimeFormat, updateValues) void Histogram::setValuesDateTimeFormat(const QString& format) { Q_D(Histogram); if (format != d->valuesDateTimeFormat) exec(new HistogramSetValuesDateTimeFormatCmd(d, format, ki18n("%1: set values datetime format"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesPrefix, QString, valuesPrefix, updateValues) void Histogram::setValuesPrefix(const QString& prefix) { Q_D(Histogram); if (prefix!= d->valuesPrefix) exec(new HistogramSetValuesPrefixCmd(d, prefix, ki18n("%1: set values prefix"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesSuffix, QString, valuesSuffix, updateValues) void Histogram::setValuesSuffix(const QString& suffix) { Q_D(Histogram); if (suffix!= d->valuesSuffix) exec(new HistogramSetValuesSuffixCmd(d, suffix, ki18n("%1: set values suffix"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesFont, QFont, valuesFont, updateValues) void Histogram::setValuesFont(const QFont& font) { Q_D(Histogram); if (font!= d->valuesFont) exec(new HistogramSetValuesFontCmd(d, font, ki18n("%1: set values font"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetValuesColor, QColor, valuesColor, updatePixmap) void Histogram::setValuesColor(const QColor& color) { Q_D(Histogram); if (color != d->valuesColor) exec(new HistogramSetValuesColorCmd(d, color, ki18n("%1: set values color"))); } //Filling STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingEnabled, bool, fillingEnabled, updateFilling) void Histogram::setFillingEnabled(bool enabled) { Q_D(Histogram); if (enabled != d->fillingEnabled) exec(new HistogramSetFillingEnabledCmd(d, enabled, ki18n("%1: filling changed"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingType, PlotArea::BackgroundType, fillingType, updatePixmap) void Histogram::setFillingType(PlotArea::BackgroundType type) { Q_D(Histogram); if (type != d->fillingType) exec(new HistogramSetFillingTypeCmd(d, type, ki18n("%1: filling type changed"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingColorStyle, PlotArea::BackgroundColorStyle, fillingColorStyle, updatePixmap) void Histogram::setFillingColorStyle(PlotArea::BackgroundColorStyle style) { Q_D(Histogram); if (style != d->fillingColorStyle) exec(new HistogramSetFillingColorStyleCmd(d, style, ki18n("%1: filling color style changed"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingImageStyle, PlotArea::BackgroundImageStyle, fillingImageStyle, updatePixmap) void Histogram::setFillingImageStyle(PlotArea::BackgroundImageStyle style) { Q_D(Histogram); if (style != d->fillingImageStyle) exec(new HistogramSetFillingImageStyleCmd(d, style, ki18n("%1: filling image style changed"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingBrushStyle, Qt::BrushStyle, fillingBrushStyle, updatePixmap) void Histogram::setFillingBrushStyle(Qt::BrushStyle style) { Q_D(Histogram); if (style != d->fillingBrushStyle) exec(new HistogramSetFillingBrushStyleCmd(d, style, ki18n("%1: filling brush style changed"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingFirstColor, QColor, fillingFirstColor, updatePixmap) void Histogram::setFillingFirstColor(const QColor& color) { Q_D(Histogram); if (color!= d->fillingFirstColor) exec(new HistogramSetFillingFirstColorCmd(d, color, ki18n("%1: set filling first color"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingSecondColor, QColor, fillingSecondColor, updatePixmap) void Histogram::setFillingSecondColor(const QColor& color) { Q_D(Histogram); if (color!= d->fillingSecondColor) exec(new HistogramSetFillingSecondColorCmd(d, color, ki18n("%1: set filling second color"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingFileName, QString, fillingFileName, updatePixmap) void Histogram::setFillingFileName(const QString& fileName) { Q_D(Histogram); if (fileName!= d->fillingFileName) exec(new HistogramSetFillingFileNameCmd(d, fileName, ki18n("%1: set filling image"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetFillingOpacity, qreal, fillingOpacity, updatePixmap) void Histogram::setFillingOpacity(qreal opacity) { Q_D(Histogram); if (opacity != d->fillingOpacity) exec(new HistogramSetFillingOpacityCmd(d, opacity, ki18n("%1: set filling opacity"))); } //Error bars STD_SETTER_CMD_IMPL_F_S(Histogram, SetErrorType, Histogram::ErrorType, errorType, updateErrorBars) void Histogram::setErrorType(ErrorType type) { Q_D(Histogram); if (type != d->errorType) exec(new HistogramSetErrorTypeCmd(d, type, ki18n("%1: x-error type changed"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetErrorBarsCapSize, qreal, errorBarsCapSize, updateErrorBars) void Histogram::setErrorBarsCapSize(qreal size) { Q_D(Histogram); if (size != d->errorBarsCapSize) exec(new HistogramSetErrorBarsCapSizeCmd(d, size, ki18n("%1: set error bar cap size"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetErrorBarsType, XYCurve::ErrorBarsType, errorBarsType, updateErrorBars) void Histogram::setErrorBarsType(XYCurve::ErrorBarsType type) { Q_D(Histogram); if (type != d->errorBarsType) exec(new HistogramSetErrorBarsTypeCmd(d, type, ki18n("%1: error bar type changed"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetErrorBarsPen, QPen, errorBarsPen, recalcShapeAndBoundingRect) void Histogram::setErrorBarsPen(const QPen& pen) { Q_D(Histogram); if (pen != d->errorBarsPen) exec(new HistogramSetErrorBarsPenCmd(d, pen, ki18n("%1: set error bar style"))); } STD_SETTER_CMD_IMPL_F_S(Histogram, SetErrorBarsOpacity, qreal, errorBarsOpacity, updatePixmap) void Histogram::setErrorBarsOpacity(qreal opacity) { Q_D(Histogram); if (opacity != d->errorBarsOpacity) exec(new HistogramSetErrorBarsOpacityCmd(d, opacity, ki18n("%1: set error bar opacity"))); } //############################################################################## //################################# SLOTS #################################### //############################################################################## void Histogram::retransform() { d_ptr->retransform(); } void Histogram::recalcHistogram() { d_ptr->recalcHistogram(); } //TODO void Histogram::handleResize(double horizontalRatio, double verticalRatio, bool pageResize) { Q_UNUSED(pageResize); Q_UNUSED(verticalRatio); Q_D(const Histogram); //setValuesDistance(d->distance*); QFont font = d->valuesFont; font.setPointSizeF(font.pointSizeF()*horizontalRatio); setValuesFont(font); retransform(); } void Histogram::updateValues() { d_ptr->updateValues(); } void Histogram::dataColumnAboutToBeRemoved(const AbstractAspect* aspect) { Q_D(Histogram); if (aspect == d->dataColumn) { d->dataColumn = nullptr; d->retransform(); } } void Histogram::valuesColumnAboutToBeRemoved(const AbstractAspect* aspect) { Q_D(Histogram); if (aspect == d->valuesColumn) { d->valuesColumn = nullptr; d->updateValues(); } } //############################################################################## //###### SLOTs for changes triggered via QActions in the context menu ######## //############################################################################## void Histogram::visibilityChangedSlot() { Q_D(const Histogram); this->setVisible(!d->isVisible()); } //############################################################################## //######################### Private implementation ############################# //############################################################################## HistogramPrivate::HistogramPrivate(Histogram *owner) : q(owner) { setFlag(QGraphicsItem::ItemIsSelectable, true); setAcceptHoverEvents(true); } HistogramPrivate::~HistogramPrivate() { if (m_histogram) gsl_histogram_free(m_histogram); } QString HistogramPrivate::name() const { return q->name(); } QRectF HistogramPrivate::boundingRect() const { return boundingRectangle; } double HistogramPrivate::getMaximumOccuranceofHistogram() { if (m_histogram) { double yMaxRange = -INFINITY; switch (type) { case Histogram::Ordinary: { size_t maxYAddes = gsl_histogram_max_bin(m_histogram); yMaxRange = gsl_histogram_get(m_histogram, maxYAddes); break; } case Histogram::Cumulative: { size_t maxYAddes = gsl_histogram_max_bin(m_histogram); yMaxRange = gsl_histogram_get(m_histogram, maxYAddes); double point = 0.0; for (size_t i = 0; i < m_bins; ++i) { point+= gsl_histogram_get(m_histogram,i); if (point > yMaxRange) { yMaxRange = point; } } //yMaxRange = dataColumn->rowCount(); break; } case Histogram::AvgShift: { //TODO } } return yMaxRange; } return -INFINITY; } double HistogramPrivate::getXMinimum() { switch (orientation) { case Histogram::Vertical: return autoBinRanges ? dataColumn->minimum() : binRangesMin; case Histogram::Horizontal: return 0; } return INFINITY; } double HistogramPrivate::getXMaximum() { switch (orientation) { case Histogram::Vertical: return autoBinRanges ? dataColumn->maximum() : binRangesMax; case Histogram::Horizontal: return getMaximumOccuranceofHistogram(); } return -INFINITY; } double HistogramPrivate::getYMinimum() { switch (orientation) { case Histogram::Vertical: return 0; case Histogram::Horizontal: return autoBinRanges ? dataColumn->minimum() : binRangesMin; } return INFINITY; } double HistogramPrivate::getYMaximum() { switch (orientation) { case Histogram::Vertical: return getMaximumOccuranceofHistogram(); case Histogram::Horizontal: return autoBinRanges ? dataColumn->maximum() : binRangesMax; } return INFINITY; } /*! Returns the shape of the Histogram as a QPainterPath in local coordinates */ QPainterPath HistogramPrivate::shape() const { return curveShape; } void HistogramPrivate::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { q->createContextMenu()->exec(event->screenPos()); } bool HistogramPrivate::swapVisible(bool on) { bool oldValue = isVisible(); setVisible(on); emit q->visibilityChanged(on); return oldValue; } /*! called when the size of the plot or its data ranges (manual changes, zooming, etc.) were changed. recalculates the position of the scene points to be drawn. triggers the update of lines, drop lines, symbols etc. */ void HistogramPrivate::retransform() { if (m_suppressRetransform) return; if (!isVisible()) return; PERFTRACE(name().toLatin1() + ", HistogramPrivate::retransform()"); if (!dataColumn) { linePath = QPainterPath(); symbolsPath = QPainterPath(); valuesPath = QPainterPath(); curveShape = QPainterPath(); lines.clear(); pointsLogical.clear(); pointsScene.clear(); visiblePoints.clear(); valuesPoints.clear(); valuesStrings.clear(); fillPolygon.clear(); recalcShapeAndBoundingRect(); return; } m_suppressRecalc = true; updateLines(); updateSymbols(); updateValues(); m_suppressRecalc = false; } /*! * called when the data was changed. recalculates the histogram. */ void HistogramPrivate::recalcHistogram() { PERFTRACE(name().toLatin1() + ", HistogramPrivate::recalcHistogram()"); if (m_histogram) { gsl_histogram_free(m_histogram); m_histogram = nullptr; } if (!dataColumn) return; //calculate the number of valid data points int count = 0; for (int row = 0; row < dataColumn->rowCount(); ++row) { if (dataColumn->isValid(row) && !dataColumn->isMasked(row)) ++count; } //calculate the number of bins if (count > 0) { if (autoBinRanges) { if (binRangesMin != dataColumn->minimum()) { binRangesMin = dataColumn->minimum(); emit q->binRangesMinChanged(binRangesMin); } if (binRangesMax != dataColumn->maximum()) { binRangesMax = dataColumn->maximum(); emit q->binRangesMaxChanged(binRangesMax); } } else { if (binRangesMin >= binRangesMax) { emit q->dataChanged(); return; } } switch (binningMethod) { case Histogram::ByNumber: m_bins = (size_t)binCount; break; case Histogram::ByWidth: m_bins = (size_t) (binRangesMax-binRangesMin)/binWidth; break; case Histogram::SquareRoot: m_bins = (size_t)sqrt(count); break; case Histogram::Rice: m_bins = (size_t)2*cbrt(count); break; case Histogram::Sturges: m_bins = (size_t) 1 + log2(count); break; case Histogram::Doane: { const double skewness = static_cast(dataColumn)->statistics().skewness; m_bins = (size_t)( 1 + log2(count) + log2(1 + abs(skewness)/sqrt((double)6*(count-2)/(count+1)/(count+3))) ); break; } case Histogram::Scott: { const double sigma = static_cast(dataColumn)->statistics().standardDeviation; const double width = 3.5*sigma/cbrt(count); DEBUG("blablub " << sigma << " " << width << " " <<(binRangesMax - binRangesMin)/width); m_bins = (size_t)(binRangesMax - binRangesMin)/width; break; } } DEBUG("min " << binRangesMin) DEBUG("max " << binRangesMax) DEBUG("number of bins " << m_bins) //calculate the histogram if (m_bins > 0) { m_histogram = gsl_histogram_alloc (m_bins); gsl_histogram_set_ranges_uniform (m_histogram, binRangesMin, binRangesMax); for (int row = 0; row < dataColumn->rowCount(); ++row) { if ( dataColumn->isValid(row) && !dataColumn->isMasked(row) ) gsl_histogram_increment(m_histogram, dataColumn->valueAt(row)); } } else DEBUG("Number of bins must be positiv integer") } //histogram changed because of the actual data changes or because of new bin settings, //emit dataChanged() in order to recalculate everything with the new size/shape of the histogram emit q->dataChanged(); } void HistogramPrivate::updateType() { //type (ordinary or cumulative) changed, //emit dataChanged() in order to recalculate everything with the new size/shape of the histogram emit q->dataChanged(); } void HistogramPrivate::updateOrientation() { //orientation (horizontal or vertical) changed //emit dataChanged() in order to recalculate everything with the new size/shape of the histogram emit q->dataChanged(); } /*! recalculates the painter path for the lines connecting the data points. Called each time when the type of this connection is changed. */ void HistogramPrivate::updateLines() { PERFTRACE(name().toLatin1() + ", HistogramPrivate::updateLines()"); linePath = QPainterPath(); lines.clear(); pointsLogical.clear(); pointsScene.clear(); if (orientation == Histogram::Vertical) verticalHistogram(); else horizontalHistogram(); //map the lines and the symbol positions to the scene coordinates const auto* plot = static_cast(q->parentAspect()); const auto* cSystem = static_cast(plot->coordinateSystem()); lines = cSystem->mapLogicalToScene(lines); visiblePoints = std::vector(pointsLogical.count(), false); cSystem->mapLogicalToScene(pointsLogical, pointsScene, visiblePoints); //new line path for (const auto& line : lines) { linePath.moveTo(line.p1()); linePath.lineTo(line.p2()); } updateFilling(); recalcShapeAndBoundingRect(); } void HistogramPrivate::verticalHistogram() { if (!m_histogram) return; const double width = (binRangesMax - binRangesMin)/m_bins; double value = 0.; if (lineType == Histogram::Bars) { for (size_t i = 0; i < m_bins; ++i) { if (type == Histogram::Ordinary) value = gsl_histogram_get(m_histogram, i); else value += gsl_histogram_get(m_histogram, i); const double x = binRangesMin + i*width; lines.append(QLineF(x, 0., x, value)); lines.append(QLineF(x, value, x + width, value)); lines.append(QLineF(x + width, value, x + width, 0.)); pointsLogical.append(QPointF(x+width/2, value)); } } else if (lineType == Histogram::NoLine || lineType == Histogram::Envelope) { double prevValue = 0.; for (size_t i = 0; i < m_bins; ++i) { if (type == Histogram::Ordinary) value = gsl_histogram_get(m_histogram, i); else value += gsl_histogram_get(m_histogram, i); const double x = binRangesMin + i*width; lines.append(QLineF(x, prevValue, x, value)); lines.append(QLineF(x, value, x + width, value)); pointsLogical.append(QPointF(x+width/2, value)); if (i == m_bins - 1) lines.append(QLineF(x + width, value, x + width, 0.)); prevValue = value; } } else { //drop lines for (size_t i = 0; i < m_bins; ++i) { if (type == Histogram::Ordinary) value = gsl_histogram_get(m_histogram, i); else value += gsl_histogram_get(m_histogram, i); const double x = binRangesMin + i*width - width/2; lines.append(QLineF(x, 0., x, value)); pointsLogical.append(QPointF(x, value)); } } if (lineType != Histogram::DropLines) lines.append(QLineF(binRangesMin, 0., binRangesMax, 0.)); } void HistogramPrivate::horizontalHistogram() { if (!m_histogram) return; const double width = (binRangesMax - binRangesMin)/m_bins; double value = 0.; if (lineType == Histogram::Bars) { for (size_t i = 0; i < m_bins; ++i) { if (type == Histogram::Ordinary) value = gsl_histogram_get(m_histogram,i); else value += gsl_histogram_get(m_histogram,i); const double y = binRangesMin + i*width; lines.append(QLineF(0., y, value, y)); lines.append(QLineF(value, y, value, y + width)); lines.append(QLineF(value, y + width, 0., y + width)); pointsLogical.append(QPointF(value, y+width/2)); } } else if (lineType == Histogram::NoLine || lineType == Histogram::Envelope) { double prevValue = 0.; for (size_t i = 0; i < m_bins; ++i) { if (type == Histogram::Ordinary) value = gsl_histogram_get(m_histogram, i); else value += gsl_histogram_get(m_histogram, i); const double y = binRangesMin + i*width; lines.append(QLineF(prevValue, y, value, y)); lines.append(QLineF(value, y, value, y + width)); pointsLogical.append(QPointF(value, y+width/2)); if (i == m_bins - 1) lines.append(QLineF(value, y + width, 0., y + width)); prevValue = value; } } else { //drop lines for (size_t i = 0; i < m_bins; ++i) { if (type == Histogram::Ordinary) value = gsl_histogram_get(m_histogram, i); else value += gsl_histogram_get(m_histogram, i); const double y = binRangesMin + i*width - width/2; lines.append(QLineF(0., y, value, y)); pointsLogical.append(QPointF(value, y)); } } if (lineType != Histogram::DropLines) lines.append(QLineF(0., binRangesMin, 0., binRangesMax)); } void HistogramPrivate::updateSymbols() { symbolsPath = QPainterPath(); if (symbolsStyle != Symbol::NoSymbols) { QPainterPath path = Symbol::pathFromStyle(symbolsStyle); QTransform trafo; trafo.scale(symbolsSize, symbolsSize); path = trafo.map(path); trafo.reset(); if (symbolsRotationAngle != 0) { trafo.rotate(symbolsRotationAngle); path = trafo.map(path); } for (const auto& point : pointsScene) { trafo.reset(); trafo.translate(point.x(), point.y()); symbolsPath.addPath(trafo.map(path)); } } recalcShapeAndBoundingRect(); } /*! recreates the value strings to be shown and recalculates their draw position. */ void HistogramPrivate::updateValues() { valuesPath = QPainterPath(); valuesPoints.clear(); valuesStrings.clear(); if (valuesType == Histogram::NoValues || !m_histogram) { recalcShapeAndBoundingRect(); return; } //determine the value string for all points that are currently visible in the plot if (valuesType == Histogram::ValuesBinEntries) { switch (type) { case Histogram::Ordinary: for (size_t i = 0; i < m_bins; ++i) { if (!visiblePoints[i]) continue; valuesStrings << valuesPrefix + QString::number(gsl_histogram_get(m_histogram, i)) + valuesSuffix; } break; case Histogram::Cumulative: { value = 0; for (size_t i = 0; i < m_bins; ++i) { if (!visiblePoints[i]) continue; value += gsl_histogram_get(m_histogram, i); valuesStrings << valuesPrefix + QString::number(value) + valuesSuffix; } break; } case Histogram::AvgShift: break; } } else if (valuesType == Histogram::ValuesCustomColumn) { if (!valuesColumn) { recalcShapeAndBoundingRect(); return; } const int endRow = qMin(pointsLogical.size(), valuesColumn->rowCount()); const auto xColMode = valuesColumn->columnMode(); for (int i = 0; i < endRow; ++i) { if (!visiblePoints[i]) continue; if ( !valuesColumn->isValid(i) || valuesColumn->isMasked(i) ) continue; switch (xColMode) { case AbstractColumn::ColumnMode::Numeric: valuesStrings << valuesPrefix + QString::number(valuesColumn->valueAt(i), valuesNumericFormat, valuesPrecision) + valuesSuffix; break; case AbstractColumn::ColumnMode::Integer: case AbstractColumn::ColumnMode::BigInt: valuesStrings << valuesPrefix + QString::number(valuesColumn->valueAt(i)) + valuesSuffix; break; case AbstractColumn::ColumnMode::Text: valuesStrings << valuesPrefix + valuesColumn->textAt(i) + valuesSuffix; break; case AbstractColumn::ColumnMode::DateTime: case AbstractColumn::ColumnMode::Month: case AbstractColumn::ColumnMode::Day: valuesStrings << valuesPrefix + valuesColumn->dateTimeAt(i).toString(valuesDateTimeFormat) + valuesSuffix; break; } } } //Calculate the coordinates where to paint the value strings. //The coordinates depend on the actual size of the string. QPointF tempPoint; QFontMetrics fm(valuesFont); qreal w; const qreal h = fm.ascent(); switch (valuesPosition) { case Histogram::ValuesAbove: for (int i = 0; i < valuesStrings.size(); i++) { w = fm.boundingRect(valuesStrings.at(i)).width(); tempPoint.setX( pointsScene.at(i).x() -w/2); tempPoint.setY( pointsScene.at(i).y() - valuesDistance ); valuesPoints.append(tempPoint); } break; case Histogram::ValuesUnder: for (int i = 0; i < valuesStrings.size(); i++) { w = fm.boundingRect(valuesStrings.at(i)).width(); tempPoint.setX( pointsScene.at(i).x() -w/2); tempPoint.setY( pointsScene.at(i).y() + valuesDistance + h/2); valuesPoints.append(tempPoint); } break; case Histogram::ValuesLeft: for (int i = 0; i < valuesStrings.size(); i++) { w = fm.boundingRect(valuesStrings.at(i)).width(); tempPoint.setX( pointsScene.at(i).x() - valuesDistance - w - 1); tempPoint.setY( pointsScene.at(i).y()); valuesPoints.append(tempPoint); } break; case Histogram::ValuesRight: for (int i = 0; i < valuesStrings.size(); i++) { tempPoint.setX( pointsScene.at(i).x() + valuesDistance - 1); tempPoint.setY( pointsScene.at(i).y() ); valuesPoints.append(tempPoint); } break; } QTransform trafo; QPainterPath path; for (int i = 0; i < valuesPoints.size(); i++) { path = QPainterPath(); path.addText( QPoint(0,0), valuesFont, valuesStrings.at(i) ); trafo.reset(); trafo.translate( valuesPoints.at(i).x(), valuesPoints.at(i).y() ); if (valuesRotationAngle!=0) trafo.rotate( -valuesRotationAngle ); valuesPath.addPath(trafo.map(path)); } recalcShapeAndBoundingRect(); } void HistogramPrivate::updateFilling() { fillPolygon.clear(); if (!fillingEnabled || lineType == Histogram::DropLines) { recalcShapeAndBoundingRect(); return; } QVector fillLines; const auto* plot = static_cast(q->parentAspect()); const AbstractCoordinateSystem* cSystem = plot->coordinateSystem(); //if there're no interpolation lines available (Histogram::NoLine selected), create line-interpolation, //use already available lines otherwise. if (lines.size()) fillLines = lines; else { for (int i = 0; i < pointsLogical.count()-1; i++) fillLines.append(QLineF(pointsLogical.at(i), pointsLogical.at(i+1))); fillLines = cSystem->mapLogicalToScene(fillLines); } //no lines available (no points), nothing to do if (!fillLines.size()) return; QPointF p1, p2; for (int i = 0; i < fillLines.size(); ++i) { const QLineF& line = fillLines.at(i); p1 = line.p1(); p2 = line.p2(); if (i != 0 && p1 != fillLines.at(i-1).p2()) fillPolygon << fillLines.at(i-1).p2() << p1; fillPolygon << p1 << p2; } //close the last polygon float yEnd = cSystem->mapLogicalToScene(QPointF(plot->xMin(), plot->yMin()>0 ? plot->yMin() : 0)).y(); fillPolygon << QPointF(fillLines.last().p2().x(), yEnd); fillPolygon << QPointF(fillLines.first().p1().x(), yEnd); recalcShapeAndBoundingRect(); } void HistogramPrivate::updateErrorBars() { } /*! recalculates the outer bounds and the shape of the curve. */ void HistogramPrivate::recalcShapeAndBoundingRect() { //if (m_suppressRecalc) // return; prepareGeometryChange(); curveShape = QPainterPath(); if (lineType != Histogram::NoLine) curveShape.addPath(WorksheetElement::shapeFromPath(linePath, linePen)); if (symbolsStyle != Symbol::NoSymbols) curveShape.addPath(symbolsPath); if (valuesType != Histogram::NoValues) curveShape.addPath(valuesPath); boundingRectangle = curveShape.boundingRect(); boundingRectangle = boundingRectangle.united(fillPolygon.boundingRect()); //TODO: when the selection is painted, line intersections are visible. //simplified() removes those artifacts but is horrible slow for curves with large number of points. //search for an alternative. //curveShape = curveShape.simplified(); updatePixmap(); } void HistogramPrivate::draw(QPainter* painter) { PERFTRACE(name().toLatin1() + ", HistogramPrivate::draw()"); //drawing line if (lineType != Histogram::NoLine) { painter->setOpacity(lineOpacity); painter->setPen(linePen); painter->setBrush(Qt::NoBrush); painter->drawPath(linePath); } //draw filling if (fillingEnabled) { painter->setOpacity(fillingOpacity); painter->setPen(Qt::SolidLine); drawFilling(painter); } //draw symbols if (symbolsStyle != Symbol::NoSymbols) { painter->setOpacity(symbolsOpacity); painter->setPen(symbolsPen); painter->setBrush(symbolsBrush); drawSymbols(painter); } //draw values if (valuesType != Histogram::NoValues) { painter->setOpacity(valuesOpacity); painter->setPen(valuesColor); painter->setBrush(Qt::SolidPattern); drawValues(painter); } } void HistogramPrivate::updatePixmap() { QPixmap pixmap(boundingRectangle.width(), boundingRectangle.height()); if (boundingRectangle.width() == 0 || boundingRectangle.height() == 0) { m_pixmap = pixmap; m_hoverEffectImageIsDirty = true; m_selectionEffectImageIsDirty = true; return; } pixmap.fill(Qt::transparent); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing, true); painter.translate(-boundingRectangle.topLeft()); draw(&painter); painter.end(); m_pixmap = pixmap; m_hoverEffectImageIsDirty = true; m_selectionEffectImageIsDirty = true; } /*! Reimplementation of QGraphicsItem::paint(). This function does the actual painting of the curve. \sa QGraphicsItem::paint(). */ void HistogramPrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { Q_UNUSED(option); Q_UNUSED(widget); if (!isVisible()) return; painter->setPen(Qt::NoPen); painter->setBrush(Qt::NoBrush); painter->setRenderHint(QPainter::SmoothPixmapTransform, true); if ( KSharedConfig::openConfig()->group("Settings_Worksheet").readEntry("DoubleBuffering", true) ) painter->drawPixmap(boundingRectangle.topLeft(), m_pixmap); //draw the cached pixmap (fast) else draw(painter); //draw directly again (slow) if (m_hovered && !isSelected() && !m_printing) { if (m_hoverEffectImageIsDirty) { QPixmap pix = m_pixmap; QPainter p(&pix); p.setCompositionMode(QPainter::CompositionMode_SourceIn); // source (shadow) pixels merged with the alpha channel of the destination (m_pixmap) p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Shadow)); p.end(); m_hoverEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5); m_hoverEffectImageIsDirty = false; } painter->drawImage(boundingRectangle.topLeft(), m_hoverEffectImage, m_pixmap.rect()); return; } if (isSelected() && !m_printing) { if (m_selectionEffectImageIsDirty) { QPixmap pix = m_pixmap; QPainter p(&pix); p.setCompositionMode(QPainter::CompositionMode_SourceIn); p.fillRect(pix.rect(), QApplication::palette().color(QPalette::Highlight)); p.end(); m_selectionEffectImage = ImageTools::blurred(pix.toImage(), m_pixmap.rect(), 5); m_selectionEffectImageIsDirty = false; } painter->drawImage(boundingRectangle.topLeft(), m_selectionEffectImage, m_pixmap.rect()); return; } } void HistogramPrivate::drawSymbols(QPainter* painter) { QPainterPath path = Symbol::pathFromStyle(symbolsStyle); QTransform trafo; trafo.scale(symbolsSize, symbolsSize); path = trafo.map(path); trafo.reset(); if (symbolsRotationAngle != 0) { trafo.rotate(-symbolsRotationAngle); path = trafo.map(path); } for (const auto& point : pointsScene) { trafo.reset(); trafo.translate(point.x(), point.y()); painter->drawPath(trafo.map(path)); } } void HistogramPrivate::drawValues(QPainter* painter) { QTransform trafo; QPainterPath path; for (int i = 0; i < valuesPoints.size(); i++) { path = QPainterPath(); path.addText( QPoint(0,0), valuesFont, valuesStrings.at(i) ); trafo.reset(); trafo.translate( valuesPoints.at(i).x(), valuesPoints.at(i).y() ); if (valuesRotationAngle!=0) trafo.rotate(-valuesRotationAngle ); painter->drawPath(trafo.map(path)); } } void HistogramPrivate::drawFilling(QPainter* painter) { const QRectF& rect = fillPolygon.boundingRect(); if (fillingType == PlotArea::Color) { switch (fillingColorStyle) { case PlotArea::SingleColor: { painter->setBrush(QBrush(fillingFirstColor)); break; } case PlotArea::HorizontalLinearGradient: { QLinearGradient linearGrad(rect.topLeft(), rect.topRight()); linearGrad.setColorAt(0, fillingFirstColor); linearGrad.setColorAt(1, fillingSecondColor); painter->setBrush(QBrush(linearGrad)); break; } case PlotArea::VerticalLinearGradient: { QLinearGradient linearGrad(rect.topLeft(), rect.bottomLeft()); linearGrad.setColorAt(0, fillingFirstColor); linearGrad.setColorAt(1, fillingSecondColor); painter->setBrush(QBrush(linearGrad)); break; } case PlotArea::TopLeftDiagonalLinearGradient: { QLinearGradient linearGrad(rect.topLeft(), rect.bottomRight()); linearGrad.setColorAt(0, fillingFirstColor); linearGrad.setColorAt(1, fillingSecondColor); painter->setBrush(QBrush(linearGrad)); break; } case PlotArea::BottomLeftDiagonalLinearGradient: { QLinearGradient linearGrad(rect.bottomLeft(), rect.topRight()); linearGrad.setColorAt(0, fillingFirstColor); linearGrad.setColorAt(1, fillingSecondColor); painter->setBrush(QBrush(linearGrad)); break; } case PlotArea::RadialGradient: { QRadialGradient radialGrad(rect.center(), rect.width()/2); radialGrad.setColorAt(0, fillingFirstColor); radialGrad.setColorAt(1, fillingSecondColor); painter->setBrush(QBrush(radialGrad)); break; } } } else if (fillingType == PlotArea::Image) { if ( !fillingFileName.trimmed().isEmpty() ) { QPixmap pix(fillingFileName); switch (fillingImageStyle) { case PlotArea::ScaledCropped: pix = pix.scaled(rect.size().toSize(),Qt::KeepAspectRatioByExpanding,Qt::SmoothTransformation); painter->setBrush(QBrush(pix)); painter->setBrushOrigin(pix.size().width()/2,pix.size().height()/2); break; case PlotArea::Scaled: pix = pix.scaled(rect.size().toSize(),Qt::IgnoreAspectRatio,Qt::SmoothTransformation); painter->setBrush(QBrush(pix)); painter->setBrushOrigin(pix.size().width()/2,pix.size().height()/2); break; case PlotArea::ScaledAspectRatio: pix = pix.scaled(rect.size().toSize(),Qt::KeepAspectRatio,Qt::SmoothTransformation); painter->setBrush(QBrush(pix)); painter->setBrushOrigin(pix.size().width()/2,pix.size().height()/2); break; case PlotArea::Centered: { QPixmap backpix(rect.size().toSize()); backpix.fill(); QPainter p(&backpix); p.drawPixmap(QPointF(0,0),pix); p.end(); painter->setBrush(QBrush(backpix)); painter->setBrushOrigin(-pix.size().width()/2,-pix.size().height()/2); break; } case PlotArea::Tiled: painter->setBrush(QBrush(pix)); break; case PlotArea::CenterTiled: painter->setBrush(QBrush(pix)); painter->setBrushOrigin(pix.size().width()/2,pix.size().height()/2); } } } else if (fillingType == PlotArea::Pattern) painter->setBrush(QBrush(fillingFirstColor,fillingBrushStyle)); painter->drawPolygon(fillPolygon); } void HistogramPrivate::hoverEnterEvent(QGraphicsSceneHoverEvent*) { const auto* plot = static_cast(q->parentAspect()); if (plot->mouseMode() == CartesianPlot::SelectionMode && !isSelected()) { m_hovered = true; emit q->hovered(); update(); } } void HistogramPrivate::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { const auto* plot = static_cast(q->parentAspect()); if (plot->mouseMode() == CartesianPlot::SelectionMode && m_hovered) { m_hovered = false; emit q->unhovered(); update(); } } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## //! Save as XML void Histogram::save(QXmlStreamWriter* writer) const { Q_D(const Histogram); writer->writeStartElement("Histogram"); writeBasicAttributes(writer); writeCommentElement(writer); //general writer->writeStartElement("general"); WRITE_COLUMN(d->dataColumn, dataColumn); writer->writeAttribute( "type", QString::number(d->type) ); writer->writeAttribute( "orientation", QString::number(d->orientation) ); writer->writeAttribute( "binningMethod", QString::number(d->binningMethod) ); writer->writeAttribute( "binCount", QString::number(d->binCount)); writer->writeAttribute( "binWidth", QString::number(d->binWidth)); writer->writeAttribute( "autoBinRanges", QString::number(d->autoBinRanges) ); writer->writeAttribute( "binRangesMin", QString::number(d->binRangesMin) ); writer->writeAttribute( "binRangesMax", QString::number(d->binRangesMax) ); writer->writeAttribute( "visible", QString::number(d->isVisible()) ); writer->writeEndElement(); //Line writer->writeStartElement("line"); writer->writeAttribute( "type", QString::number(d->lineType) ); WRITE_QPEN(d->linePen); writer->writeAttribute( "opacity", QString::number(d->lineOpacity) ); writer->writeEndElement(); //Symbols writer->writeStartElement( "symbols" ); writer->writeAttribute( "symbolsStyle", QString::number(d->symbolsStyle) ); writer->writeAttribute( "opacity", QString::number(d->symbolsOpacity) ); writer->writeAttribute( "rotation", QString::number(d->symbolsRotationAngle) ); writer->writeAttribute( "size", QString::number(d->symbolsSize) ); WRITE_QBRUSH(d->symbolsBrush); WRITE_QPEN(d->symbolsPen); writer->writeEndElement(); //Values writer->writeStartElement("values"); writer->writeAttribute( "type", QString::number(d->valuesType) ); WRITE_COLUMN(d->valuesColumn, valuesColumn); writer->writeAttribute( "position", QString::number(d->valuesPosition) ); writer->writeAttribute( "distance", QString::number(d->valuesDistance) ); writer->writeAttribute( "rotation", QString::number(d->valuesRotationAngle) ); writer->writeAttribute( "opacity", QString::number(d->valuesOpacity) ); //TODO values format and precision writer->writeAttribute( "prefix", d->valuesPrefix ); writer->writeAttribute( "suffix", d->valuesSuffix ); WRITE_QCOLOR(d->valuesColor); WRITE_QFONT(d->valuesFont); writer->writeEndElement(); //Filling writer->writeStartElement("filling"); writer->writeAttribute( "enalbed", QString::number(d->fillingEnabled) ); writer->writeAttribute( "type", QString::number(d->fillingType) ); writer->writeAttribute( "colorStyle", QString::number(d->fillingColorStyle) ); writer->writeAttribute( "imageStyle", QString::number(d->fillingImageStyle) ); writer->writeAttribute( "brushStyle", QString::number(d->fillingBrushStyle) ); writer->writeAttribute( "firstColor_r", QString::number(d->fillingFirstColor.red()) ); writer->writeAttribute( "firstColor_g", QString::number(d->fillingFirstColor.green()) ); writer->writeAttribute( "firstColor_b", QString::number(d->fillingFirstColor.blue()) ); writer->writeAttribute( "secondColor_r", QString::number(d->fillingSecondColor.red()) ); writer->writeAttribute( "secondColor_g", QString::number(d->fillingSecondColor.green()) ); writer->writeAttribute( "secondColor_b", QString::number(d->fillingSecondColor.blue()) ); writer->writeAttribute( "fileName", d->fillingFileName ); writer->writeAttribute( "opacity", QString::number(d->fillingOpacity) ); writer->writeEndElement(); writer->writeEndElement(); //close "Histogram" section } //! Load from XML bool Histogram::load(XmlStreamReader* reader, bool preview) { Q_D(Histogram); if (!readBasicAttributes(reader)) return false; KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs; QString str; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "Histogram") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (!preview && reader->name() == "general") { attribs = reader->attributes(); READ_COLUMN(dataColumn); READ_INT_VALUE("type", type, Histogram::HistogramType); READ_INT_VALUE("orientation", orientation, Histogram::HistogramOrientation); READ_INT_VALUE("binningMethod", binningMethod, Histogram::BinningMethod); READ_INT_VALUE("binCount", binCount, int); READ_DOUBLE_VALUE("binWidth", binWidth); READ_INT_VALUE("autoBinRanges", autoBinRanges, bool); READ_DOUBLE_VALUE("binRangesMin", binRangesMin); READ_DOUBLE_VALUE("binRangesMax", binRangesMax); str = attribs.value("visible").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("visible").toString()); else d->setVisible(str.toInt()); } else if (!preview && reader->name() == "line") { attribs = reader->attributes(); READ_INT_VALUE("type", lineType, Histogram::LineType); READ_QPEN(d->linePen); READ_DOUBLE_VALUE("opacity", lineOpacity); } else if (!preview && reader->name() == "symbols") { attribs = reader->attributes(); READ_INT_VALUE("symbolsStyle", symbolsStyle, Symbol::Style); READ_DOUBLE_VALUE("opacity", symbolsOpacity); READ_DOUBLE_VALUE("rotation", symbolsRotationAngle); READ_DOUBLE_VALUE("size", symbolsSize); READ_QBRUSH(d->symbolsBrush); READ_QPEN(d->symbolsPen); } else if (!preview && reader->name() == "values") { attribs = reader->attributes(); READ_INT_VALUE("type", valuesType, Histogram::ValuesType); READ_COLUMN(valuesColumn); READ_INT_VALUE("position", valuesPosition, Histogram::ValuesPosition); READ_DOUBLE_VALUE("distance", valuesRotationAngle); READ_DOUBLE_VALUE("rotation", valuesRotationAngle); READ_DOUBLE_VALUE("opacity", valuesOpacity); //don't produce any warning if no prefix or suffix is set (empty string is allowed here in xml) d->valuesPrefix = attribs.value("prefix").toString(); d->valuesSuffix = attribs.value("suffix").toString(); READ_QCOLOR(d->valuesColor); READ_QFONT(d->valuesFont); } else if (!preview && reader->name() == "filling") { attribs = reader->attributes(); READ_INT_VALUE("enabled", fillingEnabled, bool); READ_INT_VALUE("type", fillingType, PlotArea::BackgroundType); READ_INT_VALUE("colorStyle", fillingColorStyle, PlotArea::BackgroundColorStyle); READ_INT_VALUE("imageStyle", fillingImageStyle, PlotArea::BackgroundImageStyle); READ_INT_VALUE("brushStyle", fillingBrushStyle, Qt::BrushStyle); str = attribs.value("firstColor_r").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("firstColor_r").toString()); else d->fillingFirstColor.setRed(str.toInt()); str = attribs.value("firstColor_g").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("firstColor_g").toString()); else d->fillingFirstColor.setGreen(str.toInt()); str = attribs.value("firstColor_b").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("firstColor_b").toString()); else d->fillingFirstColor.setBlue(str.toInt()); str = attribs.value("secondColor_r").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("secondColor_r").toString()); else d->fillingSecondColor.setRed(str.toInt()); str = attribs.value("secondColor_g").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("secondColor_g").toString()); else d->fillingSecondColor.setGreen(str.toInt()); str = attribs.value("secondColor_b").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("secondColor_b").toString()); else d->fillingSecondColor.setBlue(str.toInt()); d->fillingFileName = attribs.value("fileName").toString(); READ_DOUBLE_VALUE("opacity", fillingOpacity); } } return true; } //############################################################################## //######################### Theme management ################################## //############################################################################## void Histogram::loadThemeConfig(const KConfig& config) { - KConfigGroup group = config.group("XYCurve"); + KConfigGroup group; + if (config.hasGroup(QLatin1String("Theme"))) + group = config.group("XYCurve"); //when loading from the theme config, use the same properties as for XYCurve + else + group = config.group("Histogram"); int index = parentAspect()->indexOfChild(this); const auto* plot = static_cast(parentAspect()); QColor themeColor; if (indexthemeColorPalette().size()) themeColor = plot->themeColorPalette().at(index); else { if (plot->themeColorPalette().size()) themeColor = plot->themeColorPalette().last(); } QPen p; Q_D(Histogram); d->m_suppressRecalc = true; //Line - p.setStyle((Qt::PenStyle)group.readEntry("LineStyle", (int)this->linePen().style())); + p.setStyle((Qt::PenStyle) group.readEntry("LineStyle", (int)Qt::SolidLine)); + p.setWidthF(group.readEntry("LineWidth", Worksheet::convertToSceneUnits(1.0, Worksheet::Unit::Point))); p.setWidthF(group.readEntry("LineWidth", this->linePen().widthF())); p.setColor(themeColor); this->setLinePen(p); - this->setLineOpacity(group.readEntry("LineOpacity", this->lineOpacity())); + this->setLineOpacity(group.readEntry("LineOpacity", 1.0)); //Symbol - this->setSymbolsOpacity(group.readEntry("SymbolOpacity", this->symbolsOpacity())); - QBrush brush = symbolsBrush(); + this->setSymbolsOpacity(group.readEntry("SymbolOpacity", 1.0)); + + QBrush brush; + brush.setStyle((Qt::BrushStyle)group.readEntry("SymbolFillingStyle", (int)Qt::SolidPattern)); brush.setColor(themeColor); this->setSymbolsBrush(brush); - p = symbolsPen(); + p.setStyle((Qt::PenStyle)group.readEntry("SymbolBorderStyle", (int)Qt::SolidLine)); p.setColor(themeColor); + p.setWidthF(group.readEntry("SymbolBorderWidth", Worksheet::convertToSceneUnits(0.0, Worksheet::Unit::Point))); this->setSymbolsPen(p); //Values - this->setValuesOpacity(group.readEntry("ValuesOpacity", this->valuesOpacity())); - this->setValuesColor(group.readEntry("ValuesColor", this->valuesColor())); + this->setValuesOpacity(group.readEntry("ValuesOpacity", 1.0)); + this->setValuesColor(group.readEntry("ValuesColor", themeColor)); //Filling - this->setFillingBrushStyle((Qt::BrushStyle)group.readEntry("FillingBrushStyle",(int) this->fillingBrushStyle())); - this->setFillingColorStyle((PlotArea::BackgroundColorStyle)group.readEntry("FillingColorStyle",(int) this->fillingColorStyle())); - this->setFillingOpacity(group.readEntry("FillingOpacity", this->fillingOpacity())); - this->setFillingSecondColor(group.readEntry("FillingSecondColor",(QColor) this->fillingSecondColor())); + this->setFillingBrushStyle((Qt::BrushStyle)group.readEntry("FillingBrushStyle", (int)Qt::SolidPattern)); + this->setFillingColorStyle((PlotArea::BackgroundColorStyle)group.readEntry("FillingColorStyle", (int)PlotArea::SingleColor)); + this->setFillingOpacity(group.readEntry("FillingOpacity", 1.0)); this->setFillingFirstColor(themeColor); - this->setFillingType((PlotArea::BackgroundType)group.readEntry("FillingType",(int) this->fillingType())); + this->setFillingSecondColor(group.readEntry("FillingSecondColor", QColor(Qt::black))); + this->setFillingType((PlotArea::BackgroundType)group.readEntry("FillingType", (int)PlotArea::Color)); //Error Bars //TODO: // p.setStyle((Qt::PenStyle)group.readEntry("ErrorBarsStyle",(int) this->errorBarsPen().style())); // p.setWidthF(group.readEntry("ErrorBarsWidth", this->errorBarsPen().widthF())); // p.setColor(themeColor); // this->setErrorBarsPen(p); // this->setErrorBarsOpacity(group.readEntry("ErrorBarsOpacity",this->errorBarsOpacity())); d->m_suppressRecalc = false; d->recalcShapeAndBoundingRect(); } void Histogram::saveThemeConfig(const KConfig& config) { KConfigGroup group = config.group("Histogram"); //Line group.writeEntry("LineOpacity", this->lineOpacity()); group.writeEntry("LineStyle",(int) this->linePen().style()); group.writeEntry("LineWidth", this->linePen().widthF()); //Error Bars // group.writeEntry("ErrorBarsCapSize",this->errorBarsCapSize()); // group.writeEntry("ErrorBarsOpacity",this->errorBarsOpacity()); // group.writeEntry("ErrorBarsColor",(QColor) this->errorBarsPen().color()); // group.writeEntry("ErrorBarsStyle",(int) this->errorBarsPen().style()); // group.writeEntry("ErrorBarsWidth", this->errorBarsPen().widthF()); //Filling group.writeEntry("FillingBrushStyle",(int) this->fillingBrushStyle()); group.writeEntry("FillingColorStyle",(int) this->fillingColorStyle()); group.writeEntry("FillingOpacity", this->fillingOpacity()); group.writeEntry("FillingSecondColor",(QColor) this->fillingSecondColor()); group.writeEntry("FillingType",(int) this->fillingType()); //Symbol group.writeEntry("SymbolOpacity", this->symbolsOpacity()); //Values group.writeEntry("ValuesOpacity", this->valuesOpacity()); group.writeEntry("ValuesColor", (QColor) this->valuesColor()); group.writeEntry("ValuesFont", this->valuesFont()); int index = parentAspect()->indexOfChild(this); if (index < 5) { KConfigGroup themeGroup = config.group("Theme"); for (int i = index; i < 5; i++) { QString s = "ThemePaletteColor" + QString::number(i+1); themeGroup.writeEntry(s,(QColor) this->linePen().color()); } } } diff --git a/src/kdefrontend/dockwidgets/LiveDataDock.cpp b/src/kdefrontend/dockwidgets/LiveDataDock.cpp index 2066a10c3..2d524c047 100644 --- a/src/kdefrontend/dockwidgets/LiveDataDock.cpp +++ b/src/kdefrontend/dockwidgets/LiveDataDock.cpp @@ -1,949 +1,960 @@ /*************************************************************************** File : LiveDataDock.cpp Project : LabPlot Description : Dock widget for live data properties -------------------------------------------------------------------- Copyright : (C) 2017 by Fabian Kristof (fkristofszabolcs@gmail.com) Copyright : (C) 2018-2019 Kovacs Ferencz (kferike98@gmail.com) Copyright : (C) 2018 by Stefan Gerlach (stefan.gerlach@uni.kn) Copyright : (C) 2017-2019 Alexander Semke (alexander.semke@web.de) ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, * * Boston, MA 02110-1301 USA * * * ***************************************************************************/ #include "LiveDataDock.h" #include #include #include #include #include #include #ifdef HAVE_MQTT #include "kdefrontend/widgets/MQTTWillSettingsWidget.h" #include "kdefrontend/datasources/MQTTSubscriptionWidget.h" #include #include #include #endif LiveDataDock::LiveDataDock(QWidget* parent) : BaseDock(parent) #ifdef HAVE_MQTT , m_subscriptionWidget(new MQTTSubscriptionWidget(this)) #endif { ui.setupUi(this); m_leName = ui.leName; //leComment = // not available ui.bUpdateNow->setIcon(QIcon::fromTheme(QLatin1String("view-refresh"))); connect(ui.leName, &QLineEdit::textChanged, this, &LiveDataDock::nameChanged); connect(ui.bPausePlayReading, &QPushButton::clicked, this, &LiveDataDock::pauseContinueReading); connect(ui.bUpdateNow, &QPushButton::clicked, this, &LiveDataDock::updateNow); connect(ui.sbUpdateInterval, static_cast(&QSpinBox::valueChanged), this, &LiveDataDock::updateIntervalChanged); connect(ui.sbKeepNValues, static_cast(&QSpinBox::valueChanged), this, &LiveDataDock::keepNValuesChanged); connect(ui.sbSampleSize, static_cast(&QSpinBox::valueChanged), this, &LiveDataDock::sampleSizeChanged); connect(ui.cbUpdateType, static_cast(&QComboBox::currentIndexChanged), this, &LiveDataDock::updateTypeChanged); connect(ui.cbReadingType, static_cast(&QComboBox::currentIndexChanged), this, &LiveDataDock::readingTypeChanged); #ifdef HAVE_MQTT connect(ui.bWillUpdateNow, &QPushButton::clicked, this, &LiveDataDock::willUpdateNow); connect(ui.bLWT, &QPushButton::clicked, this, &LiveDataDock::showWillSettings); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::enableWill, this, &LiveDataDock::enableWill); ui.swSubscriptions->addWidget(m_subscriptionWidget); ui.swSubscriptions->setCurrentWidget(m_subscriptionWidget); ui.bLWT->setToolTip(i18n("Manage MQTT connection's will settings")); ui.bLWT->setIcon(ui.bLWT->style()->standardIcon(QStyle::SP_FileDialogDetailedView)); QString info = i18n("Specify the 'Last Will and Testament' message (LWT). At least one topic has to be subscribed."); ui.lLWT->setToolTip(info); ui.bLWT->setToolTip(info); ui.bLWT->setEnabled(false); ui.bLWT->setIcon(ui.bLWT->style()->standardIcon(QStyle::SP_FileDialogDetailedView)); #endif } #ifdef HAVE_MQTT LiveDataDock::~LiveDataDock() { for (auto & host : m_hosts) delete host.client; delete m_subscriptionWidget; } #else LiveDataDock::~LiveDataDock() = default; #endif #ifdef HAVE_MQTT /*! * \brief Sets the MQTTClient of this dock widget * \param clients */ void LiveDataDock::setMQTTClient(MQTTClient* const client) { m_liveDataSource = nullptr; // prevent updates due to changes to input widgets if (m_mqttClient == client) return; auto oldclient = m_mqttClient; m_mqttClient = nullptr; // prevent updates due to changes to input widgets ui.leName->setText(client->name()); const QPair id(client->clientHostName(), client->clientPort()); ui.leSourceInfo->setText(QStringLiteral("%1:%2").arg(id.first).arg(id.second)); ui.sbUpdateInterval->setValue(client->updateInterval()); ui.cbUpdateType->setCurrentIndex(static_cast(client->updateType())); ui.cbReadingType->setCurrentIndex(static_cast(client->readingType())); if (client->updateType() == MQTTClient::UpdateType::NewData) { ui.lUpdateInterval->hide(); ui.sbUpdateInterval->hide(); } if (client->isPaused()) { ui.bPausePlayReading->setText(i18n("Continue reading")); ui.bPausePlayReading->setIcon(QIcon::fromTheme(QLatin1String("media-record"))); } else { ui.bPausePlayReading->setText(i18n("Pause reading")); ui.bPausePlayReading->setIcon(QIcon::fromTheme(QLatin1String("media-playback-pause"))); } ui.sbKeepNValues->setValue(client->keepNValues()); ui.sbKeepNValues->setEnabled(true); if (client->readingType() == MQTTClient::ReadingType::TillEnd) { ui.lSampleSize->hide(); ui.sbSampleSize->hide(); } else ui.sbSampleSize->setValue(client->sampleSize()); // disable "whole file" option const QStandardItemModel* model = qobject_cast(ui.cbReadingType->model()); QStandardItem* item = model->item(static_cast(LiveDataSource::ReadingType::WholeFile)); item->setFlags(item->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled)); if (ui.cbReadingType->currentIndex() == static_cast(LiveDataSource::ReadingType::WholeFile)) ui.cbReadingType->setCurrentIndex(static_cast(LiveDataSource::ReadingType::TillEnd)); m_mqttClient = client; // updates may be applied from now on //show MQTT connected options ui.lTopics->show(); ui.swSubscriptions->setVisible(true); m_subscriptionWidget->setVisible(true); m_subscriptionWidget->makeVisible(true); ui.lLWT->show(); ui.bLWT->show(); m_previousHost = m_currentHost; //if there isn't a client with this hostname we instantiate a new one auto it = m_hosts.find(id); if (it == m_hosts.end()) { m_currentHost = &m_hosts[id]; m_currentHost->count = 1; m_currentHost->client = new QMqttClient; connect(client, &MQTTClient::clientAboutToBeDeleted, this, &LiveDataDock::removeClient); connect(m_currentHost->client, &QMqttClient::connected, this, &LiveDataDock::onMQTTConnect); connect(m_currentHost->client, &QMqttClient::messageReceived, this, &LiveDataDock::mqttMessageReceived); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::reparentTopic, client, &MQTTClient::reparentTopic); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::addBeforeRemoveSubscription, client, &MQTTClient::addBeforeRemoveSubscription); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::removeMQTTSubscription, client, &MQTTClient::removeMQTTSubscription); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::makeSubscription, client, &MQTTClient::addMQTTSubscription); m_currentHost->client->setHostname(id.first); m_currentHost->client->setPort(id.second); if (client->MQTTUseAuthentication()) { m_currentHost->client->setUsername(client->clientUserName()); m_currentHost->client->setPassword(client->clientPassword()); } if (client->MQTTUseID()) m_currentHost->client->setClientId(client->clientID()); m_currentHost->client->connectToHost(); } else { m_currentHost = &it.value(); ++m_currentHost->count; } if (m_previousMQTTClient == nullptr) { m_updateSubscriptionConn = connect(client, &MQTTClient::MQTTSubscribed, [this]() { emit updateSubscriptionTree(m_mqttClient->MQTTSubscriptions()); }); //Fill the subscription tree(useful if the MQTTClient was loaded) QVector topics = client->topicNames(); for (const auto& topic : topics) addTopicToTree(topic); emit updateSubscriptionTree(m_mqttClient->MQTTSubscriptions()); } //if the previous MQTTClient's host name was different from the current one we have to disconnect some slots //and clear the tree widgets else if (m_previousMQTTClient->clientHostName() != client->clientHostName()) { disconnect(m_updateSubscriptionConn); disconnect(m_previousHost->client, &QMqttClient::messageReceived, this, &LiveDataDock::mqttMessageReceived); connect(m_previousHost->client, &QMqttClient::messageReceived, this, &LiveDataDock::mqttMessageReceivedInBackground); disconnect(m_currentHost->client, &QMqttClient::messageReceived, this, &LiveDataDock::mqttMessageReceivedInBackground); disconnect(m_subscriptionWidget, &MQTTSubscriptionWidget::reparentTopic, m_previousMQTTClient, &MQTTClient::reparentTopic); disconnect(m_subscriptionWidget, &MQTTSubscriptionWidget::addBeforeRemoveSubscription, m_previousMQTTClient, &MQTTClient::addBeforeRemoveSubscription); disconnect(m_subscriptionWidget, &MQTTSubscriptionWidget::removeMQTTSubscription, m_previousMQTTClient, &MQTTClient::removeMQTTSubscription); disconnect(m_subscriptionWidget, &MQTTSubscriptionWidget::makeSubscription, m_previousMQTTClient, &MQTTClient::addMQTTSubscription); m_previousHost->topicList = m_subscriptionWidget->getTopicList(); m_subscriptionWidget->setTopicList(m_currentHost->topicList); emit MQTTClearTopics(); //repopulating the tree widget with the already known topics of the client for (int i = 0; i < m_currentHost->addedTopics.size(); ++i) addTopicToTree(m_currentHost->addedTopics.at(i)); //fill subscriptions tree widget emit updateSubscriptionTree(m_mqttClient->MQTTSubscriptions()); m_updateSubscriptionConn = connect(client, &MQTTClient::MQTTSubscribed, [this]() { emit updateSubscriptionTree(m_mqttClient->MQTTSubscriptions()); }); connect(m_currentHost->client, &QMqttClient::messageReceived, this, &LiveDataDock::mqttMessageReceived); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::reparentTopic, client, &MQTTClient::reparentTopic); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::addBeforeRemoveSubscription, client, &MQTTClient::addBeforeRemoveSubscription); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::removeMQTTSubscription, client, &MQTTClient::removeMQTTSubscription); connect(m_subscriptionWidget, &MQTTSubscriptionWidget::makeSubscription, client, &MQTTClient::addMQTTSubscription); } if (client->willUpdateType() == MQTTClient::WillUpdateType::OnClick && client->MQTTWillUse()) ui.bWillUpdateNow->show(); m_previousMQTTClient = oldclient; } #endif /*! * \brief Sets the live data source of this dock widget * \param source */ void LiveDataDock::setLiveDataSource(LiveDataSource* const source) { #ifdef HAVE_MQTT m_mqttClient = nullptr; #endif // if (m_liveDataSource == source) // return; m_liveDataSource = nullptr; // prevent updates due to changes to input widgets ui.leName->setText(source->name()); ui.leName->setStyleSheet(""); ui.leName->setToolTip(""); const LiveDataSource::SourceType sourceType = source->sourceType(); const LiveDataSource::ReadingType readingType = source->readingType(); const LiveDataSource::UpdateType updateType = source->updateType(); const AbstractFileFilter::FileType fileType = source->fileType(); ui.sbUpdateInterval->setValue(source->updateInterval()); ui.cbUpdateType->setCurrentIndex(static_cast(updateType)); ui.cbReadingType->setCurrentIndex(static_cast(readingType)); switch (sourceType) { case LiveDataSource::SourceType::FileOrPipe: ui.leSourceInfo->setText(source->fileName()); if (QFile::exists(source->fileName())) ui.leSourceInfo->setStyleSheet(QString()); else ui.leSourceInfo->setStyleSheet("QLineEdit{background:red;}"); break; case LiveDataSource::SourceType::NetworkTcpSocket: case LiveDataSource::SourceType::NetworkUdpSocket: ui.leSourceInfo->setText(QStringLiteral("%1:%2").arg(source->host()).arg(source->port())); break; case LiveDataSource::SourceType::LocalSocket: ui.leSourceInfo->setText(source->localSocketName()); break; case LiveDataSource::SourceType::SerialPort: ui.leSourceInfo->setText(source->serialPortName()); break; case LiveDataSource::SourceType::MQTT: break; } if (updateType == LiveDataSource::UpdateType::NewData) { ui.lUpdateInterval->hide(); ui.sbUpdateInterval->hide(); } if (source->isPaused()) { ui.bPausePlayReading->setText(i18n("Continue Reading")); ui.bPausePlayReading->setIcon(QIcon::fromTheme(QLatin1String("media-record"))); } else { ui.bPausePlayReading->setText(i18n("Pause Reading")); ui.bPausePlayReading->setIcon(QIcon::fromTheme(QLatin1String("media-playback-pause"))); } ui.sbKeepNValues->setValue(source->keepNValues()); // disable "whole file" when having no file (i.e. socket or port) auto* model = qobject_cast(ui.cbReadingType->model()); QStandardItem* item = model->item(static_cast(LiveDataSource::ReadingType::WholeFile)); if (sourceType == LiveDataSource::SourceType::FileOrPipe) { item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); //for file types other than ASCII and binary we support re-reading the whole file only //select "read whole file" and deactivate the combobox if (fileType != AbstractFileFilter::Ascii && fileType != AbstractFileFilter::Binary) { ui.cbReadingType->setCurrentIndex(static_cast(LiveDataSource::ReadingType::WholeFile)); ui.cbReadingType->setEnabled(false); } else ui.cbReadingType->setEnabled(true); } else { if (ui.cbReadingType->currentIndex() == static_cast(LiveDataSource::ReadingType::WholeFile)) ui.cbReadingType->setCurrentIndex(static_cast(LiveDataSource::ReadingType::TillEnd)); item->setFlags(item->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled)); } if (((sourceType == LiveDataSource::SourceType::FileOrPipe || sourceType == LiveDataSource::SourceType::NetworkUdpSocket) && (readingType == LiveDataSource::ReadingType::ContinuousFixed || readingType == LiveDataSource::ReadingType::FromEnd))) ui.sbSampleSize->setValue(source->sampleSize()); else { ui.lSampleSize->hide(); ui.sbSampleSize->hide(); } // disable "on new data"-option if not available model = qobject_cast(ui.cbUpdateType->model()); item = model->item(static_cast(LiveDataSource::UpdateType::NewData)); if (sourceType == LiveDataSource::SourceType::NetworkTcpSocket || sourceType == LiveDataSource::SourceType::NetworkUdpSocket || sourceType == LiveDataSource::SourceType::SerialPort) item->setFlags(item->flags() & ~(Qt::ItemIsSelectable | Qt::ItemIsEnabled)); else item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); ui.lTopics->hide(); ui.bLWT->hide(); ui.lLWT->hide(); ui.bWillUpdateNow->hide(); ui.swSubscriptions->hide(); #ifdef HAVE_MQTT m_subscriptionWidget->hide(); m_subscriptionWidget->hide(); #endif m_liveDataSource = source; // updates may be applied from now on } /*! * \brief Modifies the sample size of the live data source or MQTTClient object * \param sampleSize */ void LiveDataDock::sampleSizeChanged(int sampleSize) { if (m_liveDataSource) m_liveDataSource->setSampleSize(sampleSize); #ifdef HAVE_MQTT else if (m_mqttClient) m_mqttClient->setSampleSize(sampleSize); #endif } /*! * \brief Updates the live data source now */ void LiveDataDock::updateNow() { if (m_liveDataSource) m_liveDataSource->updateNow(); #ifdef HAVE_MQTT else if (m_mqttClient) m_mqttClient->updateNow(); #endif } void LiveDataDock::nameChanged(const QString& name) { if (m_liveDataSource) { if (!m_liveDataSource->setName(name, false)) { ui.leName->setStyleSheet("background:red;"); ui.leName->setToolTip(i18n("Please choose another name, because this is already in use.")); return; } } #ifdef HAVE_MQTT else if (m_mqttClient) { if (!m_mqttClient->setName(name, false)) { ui.leName->setStyleSheet("background:red;"); ui.leName->setToolTip(i18n("Please choose another name, because this is already in use.")); return; } } #endif ui.leName->setStyleSheet(""); ui.leName->setToolTip(""); } /*! * \brief LiveDataDock::updateTypeChanged * \param idx */ void LiveDataDock::updateTypeChanged(int idx) { if (m_liveDataSource) { DEBUG("LiveDataDock::updateTypeChanged()"); const auto updateType = static_cast(idx); switch (updateType) { case LiveDataSource::UpdateType::TimeInterval: { ui.lUpdateInterval->show(); ui.sbUpdateInterval->show(); const auto s = m_liveDataSource->sourceType(); const auto r = m_liveDataSource->readingType(); const bool showSampleSize = ((s == LiveDataSource::SourceType::FileOrPipe || s == LiveDataSource::SourceType::NetworkUdpSocket) && (r == LiveDataSource::ReadingType::ContinuousFixed || r == LiveDataSource::ReadingType::FromEnd)); ui.lSampleSize->setVisible(showSampleSize); ui.sbSampleSize->setVisible(showSampleSize); m_liveDataSource->setUpdateType(updateType); m_liveDataSource->setUpdateInterval(ui.sbUpdateInterval->value()); break; } case LiveDataSource::UpdateType::NewData: ui.lUpdateInterval->hide(); ui.sbUpdateInterval->hide(); ui.lSampleSize->hide(); ui.sbSampleSize->hide(); m_liveDataSource->setUpdateType(updateType); } } #ifdef HAVE_MQTT else if (m_mqttClient) { DEBUG("LiveDataDock::updateTypeChanged()"); const auto type = static_cast(idx); if (type == MQTTClient::UpdateType::TimeInterval) { ui.lUpdateInterval->show(); ui.sbUpdateInterval->show(); m_mqttClient->setUpdateType(type); m_mqttClient->setUpdateInterval(ui.sbUpdateInterval->value()); } else if (type == MQTTClient::UpdateType::NewData) { ui.lUpdateInterval->hide(); ui.sbUpdateInterval->hide(); m_mqttClient->setUpdateType(type); } } #endif } /*! * \brief Handles the change of the reading type in the dock widget * \param idx */ void LiveDataDock::readingTypeChanged(int idx) { if (m_liveDataSource) { const auto type = static_cast(idx); const auto sourceType = m_liveDataSource->sourceType(); const auto updateType = m_liveDataSource->updateType(); if (sourceType == LiveDataSource::SourceType::NetworkTcpSocket || sourceType == LiveDataSource::SourceType::LocalSocket || sourceType == LiveDataSource::SourceType::SerialPort || type == LiveDataSource::ReadingType::TillEnd || type == LiveDataSource::ReadingType::WholeFile || updateType == LiveDataSource::UpdateType::NewData) { ui.lSampleSize->hide(); ui.sbSampleSize->hide(); } else { ui.lSampleSize->show(); ui.sbSampleSize->show(); } m_liveDataSource->setReadingType(type); } #ifdef HAVE_MQTT else if (m_mqttClient) { MQTTClient::ReadingType type = static_cast(idx); if (type == MQTTClient::ReadingType::TillEnd) { ui.lSampleSize->hide(); ui.sbSampleSize->hide(); } else { ui.lSampleSize->show(); ui.sbSampleSize->show(); } m_mqttClient->setReadingType(type); } #endif } /*! * \brief Modifies the update interval of the live data source * \param updateInterval */ void LiveDataDock::updateIntervalChanged(int updateInterval) { if (m_liveDataSource) m_liveDataSource->setUpdateInterval(updateInterval); #ifdef HAVE_MQTT else if (m_mqttClient) m_mqttClient->setUpdateInterval(updateInterval); #endif } /*! * \brief Modifies the number of samples to keep in each of the live data source * \param keepNValues */ void LiveDataDock::keepNValuesChanged(const int keepNValues) { if (m_liveDataSource) m_liveDataSource->setKeepNValues(keepNValues); #ifdef HAVE_MQTT else if (m_mqttClient) m_mqttClient->setKeepNValues(keepNValues); #endif } /*! * \brief Pauses the reading of the live data source */ void LiveDataDock::pauseReading() { if (m_liveDataSource) m_liveDataSource->pauseReading(); #ifdef HAVE_MQTT else if (m_mqttClient) m_mqttClient->pauseReading(); #endif } /*! * \brief Continues the reading of the live data source */ void LiveDataDock::continueReading() { if (m_liveDataSource) m_liveDataSource->continueReading(); #ifdef HAVE_MQTT else if (m_mqttClient) m_mqttClient->continueReading(); #endif } /*! * \brief Handles the pausing/continuing of reading of the live data source */ void LiveDataDock::pauseContinueReading() { m_paused = !m_paused; if (m_paused) { pauseReading(); ui.bPausePlayReading->setText(i18n("Continue Reading")); ui.bPausePlayReading->setIcon(QIcon::fromTheme(QLatin1String("media-record"))); } else { continueReading(); ui.bPausePlayReading->setText(i18n("Pause Reading")); ui.bPausePlayReading->setIcon(QIcon::fromTheme(QLatin1String("media-playback-pause"))); } } #ifdef HAVE_MQTT /*! *\brief called when use will message checkbox's state is changed in the will settings widget, * Sets the mqttUseWill according to state for the m_mqttClient * * \param state the state of the checbox */ void LiveDataDock::useWillMessage(bool use) { qDebug()<<"Use will message: " << use; if (use) { m_mqttClient->setMQTTWillUse(true); if (m_mqttClient->willUpdateType() == MQTTClient::WillUpdateType::OnClick) ui.bWillUpdateNow->show(); } else { m_mqttClient->setMQTTWillUse(false); ui.bWillUpdateNow->hide(); } } /*! *\brief called when will message's QoS is changed in the will settings widget * sets the will QoS level for the m_mqttClient * * \param QoS the QoS level of the will message */ void LiveDataDock::willQoSChanged(int QoS) { m_mqttClient->setWillQoS(QoS); } /*! *\brief called when will message's retain flag is changed in the will settings widget * sets the retain flag for the will message in in m_mqttClient * * \param state the state of the will retain chechbox */ void LiveDataDock::willRetainChanged(bool useWillRetainMessages) { if (useWillRetainMessages) m_mqttClient->setWillRetain(true); else m_mqttClient->setWillRetain(false); } /*! *\brief called when will topic combobox's current item is changed in the will settings widget * sets the will topic for the m_mqttClient * * \param topic the current text of cbWillTopic */ void LiveDataDock::willTopicChanged(const QString& topic) { if (m_mqttClient->willTopic() != topic) m_mqttClient->clearLastMessage(); m_mqttClient->setWillTopic(topic); } /*! *\brief called when the selected will message type is changed in the will settings widget * sets the will message type for the m_mqttClient * * \param type the selected will message type */ void LiveDataDock::willMessageTypeChanged(MQTTClient::WillMessageType willMessageType) { m_mqttClient->setWillMessageType(willMessageType); } /*! *\brief called when the will own message is changed in the will settings widget * sets the will own message for the m_mqttClient * * \param message the will message given by the user */ void LiveDataDock::willOwnMessageChanged(const QString& message) { m_mqttClient->setWillOwnMessage(message); } /*! *\brief called when the selected update type for the will message is changed in the will settings widget * sets the will update type for the m_mqttClient * * \param type the selected will update type */ void LiveDataDock::willUpdateTypeChanged(int updateType) { m_mqttClient->setWillUpdateType(static_cast(updateType)); +<<<<<<< HEAD if (updateType == static_cast(MQTTClient::WillUpdateType::TimePeriod)) { ui.bWillUpdateNow->hide(); m_mqttClient->startWillTimer(); } else if (updateType == static_cast(MQTTClient::WillUpdateType::OnClick)) { +======= + if (static_cast(updateType) == MQTTClient::WillUpdateType::TimePeriod) { + ui.bWillUpdateNow->hide(); + m_mqttClient->startWillTimer(); + } else if (static_cast(updateType) == MQTTClient::WillUpdateType::OnClick) { +>>>>>>> 89e8ac3aa142df66580c3d747a1832c200e304dd ui.bWillUpdateNow->show(); //if update type is on click we stop the will timer m_mqttClient->stopWillTimer(); } } /*! *\brief called when the will update now button is pressed * updates the will message of m_mqttClient */ void LiveDataDock::willUpdateNow() { m_mqttClient->updateWillMessage(); } /*! *\brief called when the update interval for will message is changed in the will settings widget * sets the will update interval for the m_mqttClient, then starts the will timer for each one * * \param interval the new will update interval */ void LiveDataDock::willUpdateIntervalChanged(int interval) { m_mqttClient->setWillTimeInterval(interval); m_mqttClient->startWillTimer(); } /*! *\brief called when the will statistics are changed in the will settings widget * adds or removes the statistic represented by the index from m_mqttClient */ void LiveDataDock::statisticsChanged(MQTTClient::WillStatisticsType willStatisticsType) { +<<<<<<< HEAD if (willStatisticsType != MQTTClient::WillStatisticsType::NoStatistics) { +======= + if (static_cast(willStatisticsType) >= 0) { +>>>>>>> 89e8ac3aa142df66580c3d747a1832c200e304dd //if it's not already added and it's checked we add it if (!m_mqttClient->willStatistics().at(static_cast(willStatisticsType))) m_mqttClient->addWillStatistics(willStatisticsType); else //otherwise remove it m_mqttClient->removeWillStatistics(willStatisticsType); } } /*! *\brief called when the client connects to the broker successfully, it subscribes to every topic (# wildcard) * in order to later list every available topic */ void LiveDataDock::onMQTTConnect() { if (!m_currentHost || !m_currentHost->client || !m_currentHost->client->subscribe(QMqttTopicFilter(QLatin1String("#")), 1)) QMessageBox::critical(this, i18n("Couldn't subscribe"), i18n("Couldn't subscribe to all available topics. Something went wrong")); } /*! *\brief called when the client receives a message * if the message arrived from a new topic, the topic is put in twTopics */ void LiveDataDock::mqttMessageReceived(const QByteArray& message, const QMqttTopicName& topic) { Q_UNUSED(message) if (!m_currentHost->addedTopics.contains(topic.name())) { m_currentHost->addedTopics.push_back(topic.name()); addTopicToTree(topic.name()); } } /*! *\brief Adds topicName to twTopics * * \param topicName the name of the topic, which will be added to the tree widget */ void LiveDataDock::addTopicToTree(const QString &topicName) { QStringList name; QChar sep = '/'; QString rootName; if (topicName.contains(sep)) { QStringList list = topicName.split(sep, QString::SkipEmptyParts); if (!list.isEmpty()) { rootName = list.at(0); name.append(list.at(0)); QTreeWidgetItem* currentItem; //check whether the first level of the topic can be found in twTopics int topItemIdx = -1; for (int i = 0; i < m_subscriptionWidget->topicCount(); ++i) { if (m_subscriptionWidget->topLevelTopic(i)->text(0) == list.at(0)) { topItemIdx = i; break; } } //if not we simply add every level of the topic to the tree if ( topItemIdx < 0) { currentItem = new QTreeWidgetItem(name); m_subscriptionWidget->addTopic(currentItem); for (int i = 1; i < list.size(); ++i) { name.clear(); name.append(list.at(i)); currentItem->addChild(new QTreeWidgetItem(name)); currentItem = currentItem->child(0); } } //otherwise we search for the first level that isn't part of the tree, //then add every level of the topic to the tree from that certain level else { currentItem = m_subscriptionWidget->topLevelTopic(topItemIdx); int listIdx = 1; for (; listIdx < list.size(); ++listIdx) { QTreeWidgetItem* childItem = nullptr; bool found = false; for (int j = 0; j < currentItem->childCount(); ++j) { childItem = currentItem->child(j); if (childItem->text(0) == list.at(listIdx)) { found = true; currentItem = childItem; break; } } if (!found) { //this is the level that isn't present in the tree break; } } //add every level to the tree starting with the first level that isn't part of the tree for (; listIdx < list.size(); ++listIdx) { name.clear(); name.append(list.at(listIdx)); currentItem->addChild(new QTreeWidgetItem(name)); currentItem = currentItem->child(currentItem->childCount() - 1); } } } } else { rootName = topicName; name.append(topicName); m_subscriptionWidget->addTopic(new QTreeWidgetItem(name)); } //if a subscribed topic contains the new topic, we have to update twSubscriptions for (int i = 0; i < m_subscriptionWidget->subscriptionCount(); ++i) { QStringList subscriptionName = m_subscriptionWidget->topLevelSubscription(i)->text(0).split('/', QString::SkipEmptyParts); if (rootName == subscriptionName[0]) { emit updateSubscriptionTree(m_mqttClient->MQTTSubscriptions()); break; } } //signals that a newTopic was added, in order to fill the completer of leTopics //we have to pass the whole topic name, not just the root name, for testing purposes emit newTopic(topicName); } /*! *\brief called when a client receives a message, if the clients hostname isn't identic with the host name of MQTTClient * if the message arrived from a new topic, the topic is added to the host data */ void LiveDataDock::mqttMessageReceivedInBackground(const QByteArray& message, const QMqttTopicName& topic) { Q_UNUSED(message) if (!m_currentHost->addedTopics.contains(topic.name())) m_currentHost->addedTopics.push_back(topic.name()); } /*! *\brief called when an MQTTClient is about to be deleted * removes every data connected to the MQTTClient, and disconnects the corresponding client from the host * * \param hostname the host name of the MQTTClient that will be deleted * \param name the host name of the MQTTClient that will be deleted */ void LiveDataDock::removeClient(const QString& hostname, quint16 port) { auto it = m_hosts.find(qMakePair(hostname, port)); if (it == m_hosts.end()) return; MQTTHost & host = it.value(); if (host.count > 1) { --host.count; return; } host.client->disconnectFromHost(); if (m_previousMQTTClient != nullptr && m_previousMQTTClient->clientHostName() == hostname) { disconnect(m_previousHost->client, &QMqttClient::messageReceived, this, &LiveDataDock::mqttMessageReceivedInBackground); m_previousMQTTClient = nullptr; } if (m_mqttClient->clientHostName() == hostname) { emit MQTTClearTopics(); m_mqttClient = nullptr; } delete host.client; m_hosts.erase(it); } /*! * \brief Used for testing the MQTT related features * \param topic */ bool LiveDataDock::testSubscribe(const QString& topic) { QStringList topicList = topic.split('/', QString::SkipEmptyParts); QTreeWidgetItem* currentItem = nullptr; for (int i = 0; i < m_subscriptionWidget->topicCount(); ++i) { if (m_subscriptionWidget->topLevelTopic(i)->text(0) == topicList[0]) { currentItem = m_subscriptionWidget->topLevelTopic(i); break; } } if (currentItem) { for (int i = 1 ; i < topicList.size(); ++i) { if (topicList[i] == '#') break; for (int j = 0; j < currentItem->childCount(); ++j) { if (currentItem->child(j)->text(0) == topicList[i]) { currentItem = currentItem->child(j); break; } else if (j == currentItem->childCount() - 1) return false; } } } else return false; m_subscriptionWidget->testSubscribe(currentItem); return true; } /*! * \brief Used for testing the MQTT related features * \param topic */ bool LiveDataDock::testUnsubscribe(const QString& topic) { QTreeWidgetItem* currentItem = nullptr; for (int i = 0; i < m_subscriptionWidget->subscriptionCount(); ++i) { if (MQTTSubscriptionWidget::checkTopicContains(m_subscriptionWidget->topLevelSubscription(i)->text(0), topic)) { currentItem = m_subscriptionWidget->topLevelSubscription(i); break; } } if (currentItem) { do { if (topic == currentItem->text(0)) { m_subscriptionWidget->testUnsubscribe(currentItem); return true; } else { for (int i = 0; i < currentItem->childCount(); ++i) { qDebug()<child(i)->text(0)<<" "<child(i)->text(0), topic)) { currentItem = currentItem->child(i); break; } else if (i == currentItem->childCount() - 1) return false; } } } while (currentItem); } else return false; return false; } void LiveDataDock::showWillSettings() { QMenu menu; const QVector& topics = m_mqttClient->topicNames(); MQTTWillSettingsWidget willSettingsWidget(&menu, m_mqttClient->willSettings(), topics); connect(&willSettingsWidget, &MQTTWillSettingsWidget::applyClicked, [this, &menu, &willSettingsWidget]() { this->useWillMessage(willSettingsWidget.will().enabled); this->willMessageTypeChanged(willSettingsWidget.will().willMessageType); this->updateTypeChanged(static_cast(willSettingsWidget.will().willUpdateType)); this->willRetainChanged(willSettingsWidget.will().willRetain); this->willUpdateIntervalChanged(willSettingsWidget.will().willTimeInterval); this->willOwnMessageChanged(willSettingsWidget.will().willOwnMessage); this->willTopicChanged(willSettingsWidget.will().willTopic); this->statisticsChanged(willSettingsWidget.statisticsType()); menu.close(); }); QWidgetAction* widgetAction = new QWidgetAction(this); widgetAction->setDefaultWidget(&willSettingsWidget); menu.addAction(widgetAction); QPoint pos(ui.bLWT->sizeHint().width(), ui.bLWT->sizeHint().height()); menu.exec(ui.bLWT->mapToGlobal(pos)); } void LiveDataDock::enableWill(bool enable) { if(enable) { if(!ui.bLWT->isEnabled()) ui.bLWT->setEnabled(enable); } else ui.bLWT->setEnabled(enable); } #endif diff --git a/src/kdefrontend/ui/spreadsheet/dropvalueswidget.ui b/src/kdefrontend/ui/spreadsheet/dropvalueswidget.ui index 7a1ebb158..a5a558790 100644 --- a/src/kdefrontend/ui/spreadsheet/dropvalueswidget.ui +++ b/src/kdefrontend/ui/spreadsheet/dropvalueswidget.ui @@ -1,55 +1,71 @@ DropValuesWidget 0 0 453 51 Drop values: min. and max. + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 30 + 20 + + + + diff --git a/src/kdefrontend/ui/spreadsheet/randomvalueswidget.ui b/src/kdefrontend/ui/spreadsheet/randomvalueswidget.ui index dba833ed8..4f7afa47f 100644 --- a/src/kdefrontend/ui/spreadsheet/randomvalueswidget.ui +++ b/src/kdefrontend/ui/spreadsheet/randomvalueswidget.ui @@ -1,125 +1,138 @@ RandomValuesWidget 0 0 469 301 Distribution: - + + + + 0 + 0 + + + 75 true p(x) Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 0 + 0 + + 75 true 1: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 75 true 2: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 75 true 3: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::Vertical 20 45