diff --git a/src/backend/cantorWorksheet/CantorWorksheet.cpp b/src/backend/cantorWorksheet/CantorWorksheet.cpp index dd50c5e6a..9a91f40dc 100644 --- a/src/backend/cantorWorksheet/CantorWorksheet.cpp +++ b/src/backend/cantorWorksheet/CantorWorksheet.cpp @@ -1,326 +1,326 @@ /*************************************************************************** File : CantorWorksheet.cpp Project : LabPlot Description : Aspect providing a Cantor Worksheets for Multiple backends -------------------------------------------------------------------- Copyright : (C) 2015 Garvit Khatri (garvitdelhi@gmail.com) Copyright : (C) 2016 by 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 "CantorWorksheet.h" #include "VariableParser.h" #include "backend/core/column/Column.h" #include "backend/core/column/ColumnPrivate.h" #include "backend/core/Project.h" #include "commonfrontend/cantorWorksheet/CantorWorksheetView.h" #include #include #include #include #include #include "cantor/cantor_part.h" #include #include #include CantorWorksheet::CantorWorksheet(const QString &name, bool loading) : AbstractPart(name, AspectType::CantorWorksheet), m_backendName(name) { if (!loading) init(); } /*! initializes Cantor's part and plugins */ bool CantorWorksheet::init(QByteArray* content) { KPluginLoader loader(QLatin1String("cantorpart")); KPluginFactory* factory = loader.factory(); if (!factory) { //we can only get to this here if we open a project having Cantor content and Cantor plugins were not found. //return false here, a proper error message will be created in load() and propagated further. WARN("Failed to load Cantor plugin:") WARN("Cantor Part file name: " << STDSTRING(loader.fileName())) WARN(" " << STDSTRING(loader.errorString())) return false; } else { m_part = factory->create(this, QVariantList() << m_backendName << QLatin1String("--noprogress")); if (!m_part) { DEBUG("Could not create the Cantor Part.") return false; } m_worksheetAccess = m_part->findChild(Cantor::WorksheetAccessInterface::Name); //load worksheet content if available if (content) m_worksheetAccess->loadWorksheetFromByteArray(content); connect(m_worksheetAccess, SIGNAL(modified()), this, SLOT(modified())); //Cantor's session m_session = m_worksheetAccess->session(); connect(m_session, SIGNAL(statusChanged(Cantor::Session::Status)), this, SIGNAL(statusChanged(Cantor::Session::Status))); //variable model #ifndef OLD_CANTORLIBS_VERSION m_variableModel = m_session->variableDataModel(); #else m_variableModel = m_session->variableModel(); #endif connect(m_variableModel, &QAbstractItemModel::dataChanged, this, &CantorWorksheet::dataChanged); connect(m_variableModel, &QAbstractItemModel::rowsInserted, this, &CantorWorksheet::rowsInserted); connect(m_variableModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &CantorWorksheet::rowsAboutToBeRemoved); connect(m_variableModel, &QAbstractItemModel::modelReset, this, &CantorWorksheet::modelReset); //available plugins auto* handler = m_part->findChild(QLatin1String("PanelPluginHandler")); if (!handler) { KMessageBox::error(nullptr, i18n("No PanelPluginHandle found for the Cantor Part.")); return false; } m_plugins = handler->plugins(); } return true; } //SLots void CantorWorksheet::dataChanged(const QModelIndex& index) { const QString& name = m_variableModel->data(m_variableModel->index(index.row(), 0)).toString(); Column* col = child(name); if (col) { // Cantor::DefaultVariableModel::DataRole == 257 QVariant dataValue = m_variableModel->data(m_variableModel->index(index.row(), 1), 257); if (dataValue.isNull()) dataValue = m_variableModel->data(m_variableModel->index(index.row(), 1)); const QString& value = dataValue.toString(); VariableParser parser(m_backendName, value); if (parser.isParsed()) col->replaceValues(0, parser.values()); } } void CantorWorksheet::rowsInserted(const QModelIndex& parent, int first, int last) { Q_UNUSED(parent) for (int i = first; i <= last; ++i) { const QString& name = m_variableModel->data(m_variableModel->index(i, 0)).toString(); QVariant dataValue = m_variableModel->data(m_variableModel->index(i, 1), 257); if (dataValue.isNull()) dataValue = m_variableModel->data(m_variableModel->index(i, 1)); const QString& value = dataValue.toString(); VariableParser parser(m_backendName, value); if (parser.isParsed()) { Column* col = child(name); if (col) { col->replaceValues(0, parser.values()); } else { col = new Column(name, parser.values()); col->setUndoAware(false); addChild(col); //TODO: Cantor currently ignores the order of variables in the worksheets //and adds new variables at the last position in the model. //Fix this in Cantor and switch to insertChildBefore here later. //insertChildBefore(col, child(i)); } } else { //the already existing variable doesn't contain any numerical values -> remove it Column* col = child(name); if (col) removeChild(col); } } project()->setChanged(true); } void CantorWorksheet::modified() { project()->setChanged(true); } void CantorWorksheet::modelReset() { for (int i = 0; i < childCount(); ++i) child(i)->remove(); } void CantorWorksheet::rowsAboutToBeRemoved(const QModelIndex & parent, int first, int last) { Q_UNUSED(parent); #ifndef OLD_CANTORLIBS_VERSION for (int i = first; i <= last; ++i) { const QString& name = m_variableModel->data(m_variableModel->index(first, 0)).toString(); Column* column = child(name); if (column) column->remove(); } #else Q_UNUSED(first); Q_UNUSED(last); //TODO: Old Cantor removes rows from the model even when the variable was changed only. //We don't want this behaviour since this removes the columns from the datasource in the curve. return; #endif } QList CantorWorksheet::getPlugins() { return m_plugins; } KParts::ReadWritePart* CantorWorksheet::part() { return m_part; } QIcon CantorWorksheet::icon() const { if (m_session) return QIcon::fromTheme(m_session->backend()->icon()); return QIcon(); } QWidget* CantorWorksheet::view() const { if (!m_partView) { m_view = new CantorWorksheetView(const_cast(this)); m_view->setBaseSize(1500, 1500); m_partView = m_view; // connect(m_view, SIGNAL(statusInfo(QString)), this, SIGNAL(statusInfo(QString))); } return m_partView; } //! Return a new context menu. /** * The caller takes ownership of the menu. */ QMenu* CantorWorksheet::createContextMenu() { QMenu* menu = AbstractPart::createContextMenu(); Q_ASSERT(menu); emit requestProjectContextMenu(menu); return menu; } QString CantorWorksheet::backendName() { return this->m_backendName; } //TODO bool CantorWorksheet::exportView() const { return false; } bool CantorWorksheet::printView() { m_part->action("file_print")->trigger(); return true; } bool CantorWorksheet::printPreview() const { m_part->action("file_print_preview")->trigger(); return true; } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## //! Save as XML void CantorWorksheet::save(QXmlStreamWriter* writer) const{ writer->writeStartElement("cantorWorksheet"); writeBasicAttributes(writer); writeCommentElement(writer); //general writer->writeStartElement( "general" ); writer->writeAttribute( "backend_name", m_backendName); //TODO: save worksheet settings writer->writeEndElement(); //save the content of Cantor's worksheet QByteArray content = m_worksheetAccess->saveWorksheetToByteArray(); writer->writeStartElement("worksheet"); writer->writeAttribute("content", content.toBase64()); writer->writeEndElement(); //save columns(variables) - for (auto* col : children(IncludeHidden)) + for (auto* col : children(ChildIndexFlag::IncludeHidden)) col->save(writer); writer->writeEndElement(); // close "cantorWorksheet" section } //! Load from XML bool CantorWorksheet::load(XmlStreamReader* reader, bool preview) { if (!readBasicAttributes(reader)) return false; KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs; bool rc = false; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "cantorWorksheet") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (!preview && reader->name() == "general") { attribs = reader->attributes(); m_backendName = attribs.value("backend_name").toString().trimmed(); if (m_backendName.isEmpty()) reader->raiseWarning(attributeWarning.subs("backend_name").toString()); } else if (!preview && reader->name() == "worksheet") { attribs = reader->attributes(); QString str = attribs.value("content").toString().trimmed(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("content").toString()); QByteArray content = QByteArray::fromBase64(str.toLatin1()); rc = init(&content); if (!rc) { QString msg = i18n("This project has Cantor content but no Cantor plugins were found. Please check your installation. The project will be closed."); reader->raiseError(msg); return false; } } else if (!preview && reader->name() == "column") { Column* column = new Column(QString()); column->setUndoAware(false); if (!column->load(reader, preview)) { delete column; return false; } addChild(column); } else { // unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } return true; } diff --git a/src/backend/core/AbstractAspect.cpp b/src/backend/core/AbstractAspect.cpp index 53a3a1aa2..d31a5c5af 100644 --- a/src/backend/core/AbstractAspect.cpp +++ b/src/backend/core/AbstractAspect.cpp @@ -1,901 +1,901 @@ /*************************************************************************** File : AbstractAspect.cpp Project : LabPlot -------------------------------------------------------------------- Copyright : (C) 2007-2009 by Tilman Benkert (thzs@gmx.net) Copyright : (C) 2007-2010 by Knut Franke (knut.franke@gmx.de) Copyright : (C) 2011-2016 by Alexander Semke (alexander.semke@web.de) Description : Base class for all objects in a Project. ***************************************************************************/ /*************************************************************************** * * * 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/core/AbstractAspect.h" #include "backend/core/AspectPrivate.h" #include "backend/core/aspectcommands.h" #include "backend/core/Project.h" #include "backend/datapicker/DatapickerCurve.h" #include "backend/datasources/LiveDataSource.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/lib/XmlStreamReader.h" #include "backend/lib/SignallingUndoCommand.h" #include "backend/lib/PropertyChangeCommand.h" #ifdef HAVE_MQTT #include "backend/datasources/MQTTSubscription.h" #include "backend/datasources/MQTTTopic.h" #endif #include /** * \class AbstractAspect * \brief Base class of all persistent objects in a Project. * * Before going into the details, it's useful to understand the ideas behind the * \ref aspect "Aspect Framework". * * Aspects organize themselves into trees, where a parent takes ownership of its children. Usually, * though not necessarily, a Project instance will sit at the root of the tree (without a Project * ancestor, project() will return 0 and undo does not work). Children are organized using * addChild(), removeChild(), child(), indexOfChild() and childCount() on the parent's side as well * as the equivalent convenience methods index() and remove() on the child's side. * In contrast to the similar feature of QObject, Aspect trees are fully undo/redo aware and provide * signals around object adding/removal. * * AbstractAspect manages for every Aspect the properties #name, #comment, #captionSpec and * #creationTime. All of these translate into the caption() as described in the documentation * of setCaptionSpec(). * * If an undoStack() can be found (usually it is managed by Project), changes to the properties * as well as adding/removing children support multi-level undo/redo. In order to support undo/redo * for problem-specific data in derived classes, make sure that all changes to your data are done * by handing appropriate commands to exec(). */ /** * \enum AbstractAspect::ChildIndexFlag * \brief Flags which control numbering scheme of children. */ /** * \var AbstractAspect::IncludeHidden * \brief Include aspects marked as "hidden" in numbering or listing children. */ /** * \var AbstractAspect::Recursive * \brief Recursively handle all descendents, not just immediate children. */ /** * \var AbstractAspect::Compress * \brief Remove all null pointers from the result list. */ //////////////////////////////////////////////////////////////////////////////////////////////////// // documentation of template and inline methods //////////////////////////////////////////////////////////////////////////////////////////////////// /** * \fn template < class T > T *AbstractAspect::ancestor() const * \brief Return the closest ancestor of class T (or NULL if none found). */ /** * \fn template < class T > QVector AbstractAspect::children(const ChildIndexFlags &flags=0) const * \brief Return list of children inheriting from class T. * * Use AbstractAspect for T in order to get all children. */ /** * \fn template < class T > T *AbstractAspect::child(int index, const ChildIndexFlags &flags=0) const * \brief Return child identified by (0 based) index and class. * * Identifying objects by an index is inherently error-prone and confusing, * given that the index can be based on different criteria (viz, counting * only instances of specific classes and including/excluding hidden * aspects). Therefore, it is recommended to avoid indices wherever possible * and instead refer to aspects using AbstractAspect pointers. */ /** * \fn template < class T > T *AbstractAspect::child(const QString &name) const * \brief Get child by name and class. */ /** * \fn template < class T > int AbstractAspect::childCount(const ChildIndexFlags &flags=0) const * \brief Return the number of child Aspects inheriting from given class. */ /** * \fn template < class T > int AbstractAspect::indexOfChild(const AbstractAspect * child, const ChildIndexFlags &flags=0) const * \brief Return (0 based) index of child in the list of children inheriting from class T. */ /** * \fn void AbstractAspect::aspectDescriptionAboutToChange(const AbstractAspect *aspect) * \brief Emitted before the name, comment or caption spec is changed */ /** * \fn void AbstractAspect::aspectDescriptionChanged(const AbstractAspect *aspect) * \brief Emitted after the name, comment or caption spec have changed */ /** * \fn void AbstractAspect::aspectAboutToBeAdded(const AbstractAspect *parent, const AbstractAspect *before, const AbstractAspect * child) * \brief Emitted before a new child is inserted */ /** * \fn void AbstractAspect::aspectAdded(const AbstractAspect *aspect) * \brief Emitted after a new Aspect has been added to the tree */ /** * \fn void AbstractAspect::aspectAboutToBeRemoved(const AbstractAspect *aspect) * \brief Emitted before an aspect is removed from its parent */ /** * \fn void AbstractAspect::aspectRemoved(const AbstractAspect *parent, const AbstractAspect * before, const AbstractAspect * child) * \brief Emitted from the parent after removing a child */ /** * \fn void AbstractAspect::aspectHiddenAboutToChange(const AbstractAspect *aspect) * \brief Emitted before the hidden attribute is changed */ /** * \fn void AbstractAspect::aspectHiddenChanged(const AbstractAspect *aspect) * \brief Emitted after the hidden attribute has changed */ /** * \fn void AbstractAspect::statusInfo(const QString &text) * \brief Emitted whenever some aspect in the tree wants to give status information to the user * \sa info(const QString&) */ /** * \fn protected void AbstractAspect::info(const QString &text) * \brief Implementations should call this whenever status information should be given to the user. * * This will cause statusInfo() to be emitted. Typically, this will cause the specified string * to be displayed in a status bar, a log window or some similar non-blocking way so as not to * disturb the workflow. */ /** * \fn protected virtual void childSelected(const AbstractAspect*) {} * \brief called when a child's child aspect was selected in the model */ /** * \fn protected virtual void childDeselected() * \brief called when a child aspect was deselected in the model */ /** * \fn protected virtual void childDeselected(const AbstractAspect*) * \brief called when a child's child aspect was deselected in the model */ //////////////////////////////////////////////////////////////////////////////////////////////////// // start of AbstractAspect implementation //////////////////////////////////////////////////////////////////////////////////////////////////// AbstractAspect::AbstractAspect(const QString &name, AspectType type) : m_type(type), d(new AbstractAspectPrivate(this, name)) { } AbstractAspect::~AbstractAspect() { delete d; } QString AbstractAspect::name() const { return d->m_name; } /*! * \brief AbstractAspect::setName * sets the name of the abstract aspect * \param value * \param autoUnique * \return returns, if the new name is valid or not */ bool AbstractAspect::setName(const QString &value, bool autoUnique) { if (value.isEmpty()) return setName(QLatin1String("1"), autoUnique); if (value == d->m_name) return true; // name not changed, but the name is valid QString new_name; if (d->m_parent) { new_name = d->m_parent->uniqueNameFor(value); if (!autoUnique && new_name.compare(value) != 0) // value is not unique, so don't change name return false; // this value is used in the dock to check if the name is valid if (new_name != value) info(i18n("Intended name \"%1\" was changed to \"%2\" in order to avoid name collision.", value, new_name)); } else new_name = value; exec(new PropertyChangeCommand(i18n("%1: rename to %2", d->m_name, new_name), &d->m_name, new_name), "aspectDescriptionAboutToChange", "aspectDescriptionChanged", Q_ARG(const AbstractAspect*,this)); return true; } QString AbstractAspect::comment() const { return d->m_comment; } void AbstractAspect::setComment(const QString& value) { if (value == d->m_comment) return; exec(new PropertyChangeCommand(i18n("%1: change comment", d->m_name), &d->m_comment, value), "aspectDescriptionAboutToChange", "aspectDescriptionChanged", Q_ARG(const AbstractAspect*,this)); } void AbstractAspect::setCreationTime(const QDateTime& time) { d->m_creation_time = time; } QDateTime AbstractAspect::creationTime() const { return d->m_creation_time; } bool AbstractAspect::hidden() const { return d->m_hidden; } /** * \brief Set "hidden" property, i.e. whether to exclude this aspect from being shown in the explorer. */ void AbstractAspect::setHidden(bool value) { if (value == d->m_hidden) return; d->m_hidden = value; } void AbstractAspect::setIsLoading(bool load) { d->m_isLoading = load; } bool AbstractAspect::isLoading() const { return d->m_isLoading; } /** * \brief Return an icon to be used for decorating my views. */ QIcon AbstractAspect::icon() const { return QIcon(); } /** * \brief Return a new context menu. * * The caller takes ownership of the menu. */ QMenu* AbstractAspect::createContextMenu() { QMenu* menu = new QMenu(); menu->addSection(this->name()); //TODO: activate this again when the functionality is implemented // menu->addAction( KStandardAction::cut(this) ); // menu->addAction(KStandardAction::copy(this)); // menu->addAction(KStandardAction::paste(this)); // menu->addSeparator(); //don't allow to rename and delete // - data spreadsheets of datapicker curves // - columns in data spreadsheets of datapicker curves // - columns in live-data source // - Mqtt subscriptions // - Mqtt topics // - Columns in Mqtt topics bool enabled = !(dynamic_cast(this) && dynamic_cast(this->parentAspect())) && !(dynamic_cast(this) && this->parentAspect()->parentAspect() && dynamic_cast(this->parentAspect()->parentAspect())) && !(dynamic_cast(this) && dynamic_cast(this->parentAspect())) #ifdef HAVE_MQTT && !dynamic_cast(this) && !dynamic_cast(this) && !(dynamic_cast(this) && dynamic_cast(this->parentAspect())) #endif ; if(enabled) { menu->addAction(QIcon::fromTheme(QLatin1String("edit-rename")), i18n("Rename"), this, SIGNAL(renameRequested())); if (type() != AspectType::Project) menu->addAction(QIcon::fromTheme(QLatin1String("edit-delete")), i18n("Delete"), this, SLOT(remove())); } return menu; } AspectType AbstractAspect::type() const { return m_type; } bool AbstractAspect::inherits(AspectType type) const { return (static_cast(m_type) & static_cast(type)) == static_cast(type); } /** * \brief In the parent-child hierarchy, return the first parent of type \param type or null pointer if there is none. */ AbstractAspect* AbstractAspect::parent(AspectType type) const { AbstractAspect* parent = parentAspect(); if (!parent) return nullptr; if (parent->inherits(type)) return parent; return parent->parent(type); } /** * \brief Return my parent Aspect or 0 if I currently don't have one. */ AbstractAspect* AbstractAspect::parentAspect() const { return d->m_parent; } void AbstractAspect::setParentAspect(AbstractAspect* parent) { d->m_parent = parent; } /** * \brief Return the folder the Aspect is contained in or 0 if there is none. * * The returned folder may be the aspect itself if it inherits Folder. */ Folder* AbstractAspect::folder() { if (inherits(AspectType::Folder)) return static_cast(this); AbstractAspect* parent_aspect = parentAspect(); while (parent_aspect && !parent_aspect->inherits(AspectType::Folder)) parent_aspect = parent_aspect->parentAspect(); return static_cast(parent_aspect); } /** * \brief Return whether the there is a path upwards to the given aspect * * This also returns true if other==this. */ bool AbstractAspect::isDescendantOf(AbstractAspect* other) { if (other == this) return true; AbstractAspect* parent_aspect = parentAspect(); while (parent_aspect) { if (parent_aspect == other) return true; parent_aspect = parent_aspect->parentAspect(); } return false; } /** * \brief Return the Project this Aspect belongs to, or 0 if it is currently not part of one. */ Project* AbstractAspect::project() { return parentAspect() ? parentAspect()->project() : nullptr; } /** * \brief Return the path that leads from the top-most Aspect (usually a Project) to me. */ QString AbstractAspect::path() const { return parentAspect() ? parentAspect()->path() + QLatin1Char('/') + name() : QString(); } /** * \brief Add the given Aspect to my list of children. */ void AbstractAspect::addChild(AbstractAspect* child) { Q_CHECK_PTR(child); QString new_name = uniqueNameFor(child->name()); beginMacro(i18n("%1: add %2", name(), new_name)); if (new_name != child->name()) { info(i18n("Renaming \"%1\" to \"%2\" in order to avoid name collision.", child->name(), new_name)); child->setName(new_name); } exec(new AspectChildAddCmd(d, child, d->m_children.count())); child->finalizeAdd(); endMacro(); } /** * \brief Add the given Aspect to my list of children without any checks and without putting this step onto the undo-stack */ void AbstractAspect::addChildFast(AbstractAspect* child) { emit aspectAboutToBeAdded(this, nullptr, child); //TODO: before-pointer is 0 here, also in the commands classes. why? d->insertChild(d->m_children.count(), child); child->finalizeAdd(); emit aspectAdded(child); } /** * \brief Insert the given Aspect at a specific position in my list of children. */ void AbstractAspect::insertChildBefore(AbstractAspect* child, AbstractAspect* before) { Q_CHECK_PTR(child); QString new_name = uniqueNameFor(child->name()); beginMacro(before ? i18n("%1: insert %2 before %3", name(), new_name, before->name()) : i18n("%1: insert %2 before end", name(), new_name)); if (new_name != child->name()) { info(i18n("Renaming \"%1\" to \"%2\" in order to avoid name collision.", child->name(), new_name)); child->setName(new_name); } int index = d->indexOfChild(before); if (index == -1) index = d->m_children.count(); exec(new AspectChildAddCmd(d, child, index)); endMacro(); } /** * \brief Insert the given Aspect at a specific position in my list of children.without any checks and without putting this step onto the undo-stack */ void AbstractAspect::insertChildBeforeFast(AbstractAspect* child, AbstractAspect* before) { connect(child, &AbstractAspect::selected, this, &AbstractAspect::childSelected); connect(child, &AbstractAspect::deselected, this, &AbstractAspect::childDeselected); int index = d->indexOfChild(before); if (index == -1) index = d->m_children.count(); emit aspectAboutToBeAdded(this, nullptr, child); d->insertChild(index, child); emit aspectAdded(child); } /** * \brief Remove the given Aspect from my list of children. * * The ownership of the child is transferred to the undo command, * i.e., the aspect is deleted by the undo command. * \sa reparent() */ void AbstractAspect::removeChild(AbstractAspect* child) { Q_ASSERT(child->parentAspect() == this); beginMacro(i18n("%1: remove %2", name(), child->name())); exec(new AspectChildRemoveCmd(d, child)); endMacro(); } /** * \brief Remove all child Aspects. */ void AbstractAspect::removeAllChildren() { beginMacro(i18n("%1: remove all children", name())); QVector children_list = children(); QVector::const_iterator i = children_list.constBegin(); AbstractAspect *current = nullptr, *nextSibling = nullptr; if (i != children_list.constEnd()) { current = *i; if (++i != children_list.constEnd()) nextSibling = *i; } while (current) { emit aspectAboutToBeRemoved(current); exec(new AspectChildRemoveCmd(d, current)); emit aspectRemoved(this, nextSibling, current); current = nextSibling; if (i != children_list.constEnd() && ++i != children_list.constEnd()) nextSibling = *i; else nextSibling = nullptr; } endMacro(); } /** * \brief Move a child to another parent aspect and transfer ownership. */ void AbstractAspect::reparent(AbstractAspect* newParent, int newIndex) { Q_ASSERT(parentAspect() != nullptr); Q_ASSERT(newParent != nullptr); - int max_index = newParent->childCount(IncludeHidden); + int max_index = newParent->childCount(ChildIndexFlag::IncludeHidden); if (newIndex == -1) newIndex = max_index; Q_ASSERT(newIndex >= 0 && newIndex <= max_index); // AbstractAspect* old_parent = parentAspect(); // int old_index = old_parent->indexOfChild(this, IncludeHidden); // auto* old_sibling = old_parent->child(old_index+1, IncludeHidden); // auto* new_sibling = newParent->child(newIndex, IncludeHidden); // emit newParent->aspectAboutToBeAdded(newParent, new_sibling, this); exec(new AspectChildReparentCmd(parentAspect()->d, newParent->d, this, newIndex)); // emit old_parent->aspectRemoved(old_parent, old_sibling, this); } QVector AbstractAspect::children(AspectType type, ChildIndexFlags flags) const { QVector result; for (auto* child : children()) { - if (flags & IncludeHidden || !child->hidden()) { - if (child->inherits(type) || !(flags & Compress)) { + if (flags & ChildIndexFlag::IncludeHidden || !child->hidden()) { + if (child->inherits(type) || !(flags & ChildIndexFlag::Compress)) { result << child; - if (flags & Recursive) { + if (flags & ChildIndexFlag::Recursive) { result << child->children(type, flags); } } } } return result; } const QVector& AbstractAspect::children() const { return d->m_children; } /** * \brief Remove me from my parent's list of children. */ void AbstractAspect::remove() { if (parentAspect()) parentAspect()->removeChild(this); } /*! * returns the list of all parent aspects (folders and sub-folders) */ QVector AbstractAspect::dependsOn() const { QVector aspects; if (parentAspect()) aspects << parentAspect() << parentAspect()->dependsOn(); return aspects; } bool AbstractAspect::isDraggable() const { return false; } QVector AbstractAspect::dropableOn() const { return QVector(); } //////////////////////////////////////////////////////////////////////////////////////////////////// //! \name serialize/deserialize //@{ //////////////////////////////////////////////////////////////////////////////////////////////////// /** * \fn virtual void AbstractAspect::save(QXmlStreamWriter *) const * \brief Save as XML */ /** * \fn virtual bool AbstractAspect::load(XmlStreamReader *) * \brief Load from XML * * XmlStreamReader supports errors as well as warnings. If only * warnings (non-critical errors) occur, this function must return * the reader at the end element corresponding to the current * element at the time the function was called. * * This function is normally intended to be called directly * after the ctor. If you want to call load on an aspect that * has been altered, you must make sure beforehand that * it is in the same state as after creation, e.g., remove * all its child aspects. * * \return false on error */ /** * \brief Save the comment to XML */ void AbstractAspect::writeCommentElement(QXmlStreamWriter * writer) const{ writer->writeStartElement(QLatin1String("comment")); writer->writeCharacters(comment()); writer->writeEndElement(); } /** * \brief Load comment from an XML element */ bool AbstractAspect::readCommentElement(XmlStreamReader * reader) { setComment(reader->readElementText()); return true; } /** * \brief Save name and creation time to XML */ void AbstractAspect::writeBasicAttributes(QXmlStreamWriter* writer) const { writer->writeAttribute(QLatin1String("creation_time") , creationTime().toString(QLatin1String("yyyy-dd-MM hh:mm:ss:zzz"))); writer->writeAttribute(QLatin1String("name"), name()); } /** * \brief Load name and creation time from XML * * \return false on error */ bool AbstractAspect::readBasicAttributes(XmlStreamReader* reader) { const QXmlStreamAttributes& attribs = reader->attributes(); // name QString str = attribs.value(QLatin1String("name")).toString(); if (str.isEmpty()) reader->raiseWarning(i18n("Attribute 'name' is missing or empty.")); d->m_name = str; // creation time str = attribs.value(QLatin1String("creation_time")).toString(); if (str.isEmpty()) { reader->raiseWarning(i18n("Invalid creation time for '%1'. Using current time.", name())); d->m_creation_time = QDateTime::currentDateTime(); } else { QDateTime creation_time = QDateTime::fromString(str, QLatin1String("yyyy-dd-MM hh:mm:ss:zzz")); if (creation_time.isValid()) d->m_creation_time = creation_time; else d->m_creation_time = QDateTime::currentDateTime(); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// //@} //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// //! \name undo related //@{ //////////////////////////////////////////////////////////////////////////////////////////////////// void AbstractAspect::setUndoAware(bool b) { d->m_undoAware = b; } /** * \brief Return the undo stack of the Project, or 0 if this Aspect is not part of a Project. * * It's also possible to construct undo-enabled Aspect trees without Project. * The only requirement is that the root Aspect reimplements undoStack() to get the * undo stack from somewhere (the default implementation just delegates to parentAspect()). */ QUndoStack* AbstractAspect::undoStack() const { return parentAspect() ? parentAspect()->undoStack() : nullptr; } /** * \brief Execute the given command, pushing it on the undoStack() if available. */ void AbstractAspect::exec(QUndoCommand* cmd) { Q_CHECK_PTR(cmd); if (d->m_undoAware) { QUndoStack *stack = undoStack(); if (stack) stack->push(cmd); else { cmd->redo(); delete cmd; } if (project()) project()->setChanged(true); } else { cmd->redo(); delete cmd; } } /** * \brief Execute command and arrange for signals to be sent before/after it is redone or undone. * * \arg \c command The command to be executed. * \arg \c preChangeSignal The name of the signal to be triggered before re-/undoing the command. * \arg \c postChangeSignal The name of the signal to be triggered after re-/undoing the command. * \arg val0,val1,val2,val3 Arguments to the signals; to be given using Q_ARG(). * * Signal arguments are given using the macro Q_ARG(typename, const value&). Since * the variable given as "value" will likely be out of scope when the signals are emitted, a copy * needs to be created. This uses QMetaType, which means that (non-trivial) argument types need to * be registered using qRegisterMetaType() before giving them to exec() (in particular, this also * goes for pointers to custom data types). * * \sa SignallingUndoCommand */ void AbstractAspect::exec(QUndoCommand* command, const char* preChangeSignal, const char* postChangeSignal, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3) { beginMacro(command->text()); exec(new SignallingUndoCommand(QLatin1String("change signal"), this, preChangeSignal, postChangeSignal, val0, val1, val2, val3)); exec(command); exec(new SignallingUndoCommand(QLatin1String("change signal"), this, postChangeSignal, preChangeSignal, val0, val1, val2, val3)); endMacro(); } /** * \brief Begin an undo stack macro (series of commands) */ void AbstractAspect::beginMacro(const QString& text) { if (!d->m_undoAware) return; QUndoStack* stack = undoStack(); if (stack) stack->beginMacro(text); } /** * \brief End the current undo stack macro */ void AbstractAspect::endMacro() { if (!d->m_undoAware) return; QUndoStack* stack = undoStack(); if (stack) stack->endMacro(); } //////////////////////////////////////////////////////////////////////////////////////////////////// //@} //////////////////////////////////////////////////////////////////////////////////////////////////// /*! * this function is called when the selection in ProjectExplorer was changed. * forwards the selection/deselection to the parent aspect via emitting a signal. */ void AbstractAspect::setSelected(bool s) { if (s) emit selected(this); else emit deselected(this); } void AbstractAspect::childSelected(const AbstractAspect* aspect) { //forward the signal to the highest possible level in the parent-child hierarchy //e.g. axis of a plot was selected. Don't include parent aspects here that do not //need to react on the selection of children: //* Folder //* XYFitCurve with the child column for calculated residuals //* XYSmouthCurve with the child column for calculated rough values //* CantorWorksheet with the child columns for CAS variables if (aspect->parentAspect() && !aspect->parentAspect()->inherits(AspectType::Folder) && !aspect->parentAspect()->inherits(AspectType::XYFitCurve) && !aspect->parentAspect()->inherits(AspectType::XYSmoothCurve) && !aspect->parentAspect()->inherits(AspectType::CantorWorksheet)) emit aspect->parentAspect()->selected(aspect); } void AbstractAspect::childDeselected(const AbstractAspect* aspect) { //forward the signal to the highest possible level in the parent-child hierarchy //e.g. axis of a plot was selected. Don't include parent aspects here that do not //need to react on the deselection of children: //* Folder //* XYFitCurve with the child column for calculated residuals //* XYSmouthCurve with the child column for calculated rough values //* CantorWorksheet with the child columns for CAS variables if (aspect->parentAspect() && !aspect->parentAspect()->inherits(AspectType::Folder) && !aspect->parentAspect()->inherits(AspectType::XYFitCurve) && !aspect->parentAspect()->inherits(AspectType::XYSmoothCurve) && !aspect->parentAspect()->inherits(AspectType::CantorWorksheet)) emit aspect->parentAspect()->deselected(aspect); } /** * \brief Make the specified name unique among my children by incrementing a trailing number. */ QString AbstractAspect::uniqueNameFor(const QString& current_name) const { QStringList child_names; for (auto* child : children()) child_names << child->name(); if (!child_names.contains(current_name)) return current_name; QString base = current_name; int last_non_digit; for (last_non_digit = base.size() - 1; last_non_digit >= 0 && base[last_non_digit].category() == QChar::Number_DecimalDigit; --last_non_digit) base.chop(1); if (last_non_digit >=0 && base[last_non_digit].category() != QChar::Separator_Space) base.append(" "); int new_nr = current_name.rightRef(current_name.size() - base.size()).toInt(); QString new_name; do new_name = base + QString::number(++new_nr); while (child_names.contains(new_name)); return new_name; } void AbstractAspect::connectChild(AbstractAspect* child) { connect(child, &AbstractAspect::aspectDescriptionAboutToChange, this, &AbstractAspect::aspectDescriptionAboutToChange); connect(child, &AbstractAspect::aspectDescriptionChanged, this, &AbstractAspect::aspectDescriptionChanged); connect(child, &AbstractAspect::aspectAboutToBeAdded, this, &AbstractAspect::aspectAboutToBeAdded); connect(child, &AbstractAspect::aspectAdded, this, &AbstractAspect::aspectAdded); connect(child, &AbstractAspect::aspectAboutToBeRemoved, this, &AbstractAspect::aspectAboutToBeRemoved); connect(child, &AbstractAspect::aspectRemoved, this, &AbstractAspect::aspectRemoved); connect(child, &AbstractAspect::aspectHiddenAboutToChange, this, &AbstractAspect::aspectHiddenAboutToChange); connect(child, &AbstractAspect::aspectHiddenChanged, this, &AbstractAspect::aspectHiddenChanged); connect(child, &AbstractAspect::statusInfo, this, &AbstractAspect::statusInfo); connect(child, &AbstractAspect::selected, this, &AbstractAspect::childSelected); connect(child, &AbstractAspect::deselected, this, &AbstractAspect::childDeselected); } //############################################################################## //###################### Private implementation ############################### //############################################################################## AbstractAspectPrivate::AbstractAspectPrivate(AbstractAspect* owner, const QString& name) : m_name(name.isEmpty() ? QLatin1String("1") : name), q(owner) { m_creation_time = QDateTime::currentDateTime(); } AbstractAspectPrivate::~AbstractAspectPrivate() { for (auto* child : m_children) delete child; } void AbstractAspectPrivate::insertChild(int index, AbstractAspect* child) { m_children.insert(index, child); // Always remove from any previous parent before adding to a new one! // Can't handle this case here since two undo commands have to be created. Q_ASSERT(child->parentAspect() == nullptr); child->setParentAspect(q); q->connectChild(child); } int AbstractAspectPrivate::indexOfChild(const AbstractAspect* child) const { for (int i = 0; i < m_children.size(); ++i) if (m_children.at(i) == child) return i; return -1; } int AbstractAspectPrivate::removeChild(AbstractAspect* child) { int index = indexOfChild(child); Q_ASSERT(index != -1); m_children.removeAll(child); QObject::disconnect(child, nullptr, q, nullptr); child->setParentAspect(nullptr); return index; } diff --git a/src/backend/core/AbstractAspect.h b/src/backend/core/AbstractAspect.h index 5e1695740..69c7008f2 100644 --- a/src/backend/core/AbstractAspect.h +++ b/src/backend/core/AbstractAspect.h @@ -1,300 +1,300 @@ /*************************************************************************** File : AbstractAspect.h Project : LabPlot -------------------------------------------------------------------- Copyright : (C) 2007-2009 by Tilman Benkert (thzs@gmx.net) Copyright : (C) 2007-2010 by Knut Franke (knut.franke@gmx.de) Copyright : (C) 2011-2015 by Alexander Semke (alexander.semke@web.de) Description : Base class for all objects in a Project. ***************************************************************************/ /*************************************************************************** * * * 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 * * * ***************************************************************************/ #ifndef ABSTRACT_ASPECT_H #define ABSTRACT_ASPECT_H #include #include class AbstractAspectPrivate; class Folder; class Project; class XmlStreamReader; class QDateTime; class QDropEvent; class QIcon; class QMenu; class QUndoCommand; class QUndoStack; class QXmlStreamWriter; /// Information about class inheritance /// enum values are chosen such that @verbatim inherits(base)@endverbatim /// returns true iff the class inherits from @verbatim base@endverbatim. /// /// AspectType is used in GuiObserver to select the correct dock widget. enum class AspectType : quint64 { AbstractAspect = 0, // classes without inheriters AbstractFilter = 0x0100001, DatapickerCurve = 0x0100002, DatapickerPoint = 0x0100004, WorksheetElement = 0x0200000, Axis = 0x0210001, CartesianPlotLegend = 0x0210002, CustomPoint = 0x0210004, Histogram = 0x0210008, PlotArea = 0x0210010, TextLabel = 0x0210020, Image = 0x0210030, ReferenceLine = 0x0210040, WorksheetElementContainer = 0x0220000, AbstractPlot = 0x0221000, CartesianPlot = 0x0221001, WorksheetElementGroup = 0x0222000, XYCurve = 0x0240000, XYEquationCurve = 0x0240001, XYAnalysisCurve = 0x0280000, XYConvolution = 0x0280001, XYCorrelationCurve = 0x0280002, XYDataReductionCurve = 0x0280004, XYDifferentiationCurve = 0x0280008, XYFitCurve = 0x0280010, XYFourierFilterCurve = 0x0280020, XYFourierTransformCurve = 0x0280040, XYInterpolationCurve = 0x0280080, XYIntegrationCurve = 0x0280100, XYSmoothCurve = 0x0280200, AbstractPart = 0x0400000, AbstractDataSource = 0x0410000, Matrix = 0x0411000, Spreadsheet = 0x0412000, LiveDataSource = 0x0412001, MQTTTopic = 0x0412002, CantorWorksheet = 0x0420001, Datapicker = 0x0420002, DatapickerImage = 0x0420004, Note = 0x0420008, Workbook = 0x0420010, Worksheet = 0x0420020, AbstractColumn = 0x1000000, Column = 0x1000001, SimpleFilterColumn = 0x1000002, ColumnStringIO = 0x1000004, Folder = 0x2000000, Project = 0x2000001, MQTTClient = 0x2000002, MQTTSubscription = 0x2000004, }; class AbstractAspect : public QObject { Q_OBJECT public: - enum ChildIndexFlag { + enum class ChildIndexFlag { IncludeHidden = 0x01, Recursive = 0x02, Compress = 0x04 }; Q_DECLARE_FLAGS(ChildIndexFlags, ChildIndexFlag) friend class AspectChildAddCmd; friend class AspectChildRemoveCmd; friend class AbstractAspectPrivate; AbstractAspect(const QString& name, AspectType type); ~AbstractAspect() override; QString name() const; QString comment() const; void setCreationTime(const QDateTime&); QDateTime creationTime() const; virtual Project* project(); virtual QString path() const; void setHidden(bool); bool hidden() const; void setSelected(bool); void setIsLoading(bool); bool isLoading() const; virtual QIcon icon() const; virtual QMenu* createContextMenu(); AspectType type() const; bool inherits(AspectType type) const; //functions related to the handling of the tree-like project structure AbstractAspect* parentAspect() const; AbstractAspect* parent(AspectType type) const; void setParentAspect(AbstractAspect*); Folder* folder(); bool isDescendantOf(AbstractAspect* other); void addChild(AbstractAspect*); void addChildFast(AbstractAspect*); virtual void finalizeAdd() {}; QVector children(AspectType type, ChildIndexFlags flags=nullptr) const; void insertChildBefore(AbstractAspect* child, AbstractAspect* before); void insertChildBeforeFast(AbstractAspect* child, AbstractAspect* before); void reparent(AbstractAspect* newParent, int newIndex = -1); void removeChild(AbstractAspect*); void removeAllChildren(); virtual QVector dependsOn() const; virtual bool isDraggable() const; virtual QVector dropableOn() const; virtual void processDropEvent(QDropEvent*) {}; template T* ancestor() const { AbstractAspect* parent = parentAspect(); while (parent) { T* ancestorAspect = dynamic_cast(parent); if (ancestorAspect) return ancestorAspect; parent = parent->parentAspect(); } return nullptr; } template QVector children(ChildIndexFlags flags = nullptr) const { QVector result; for (auto* child: children()) { - if (flags & IncludeHidden || !child->hidden()) { + if (flags & ChildIndexFlag::IncludeHidden || !child->hidden()) { T* i = dynamic_cast(child); if (i) result << i; - if (child && flags & Recursive) + if (child && flags & ChildIndexFlag::Recursive) result << child->template children(flags); } } return result; } - template T* child(int index, ChildIndexFlags flags=nullptr) const { + template T* child(int index, ChildIndexFlags flags = nullptr) const { int i = 0; for (auto* child: children()) { T* c = dynamic_cast(child); - if (c && (flags & IncludeHidden || !child->hidden()) && index == i++) + if (c && (flags & ChildIndexFlag::IncludeHidden || !child->hidden()) && index == i++) return c; } return nullptr; } template T* child(const QString& name) const { for (auto* child: children()) { T* c = dynamic_cast(child); if (c && child->name() == name) return c; } return nullptr; } template int childCount(ChildIndexFlags flags = nullptr) const { int result = 0; for (auto* child: children()) { T* i = dynamic_cast(child); - if (i && (flags & IncludeHidden || !child->hidden())) + if (i && (flags & ChildIndexFlag::IncludeHidden || !child->hidden())) result++; } return result; } template int indexOfChild(const AbstractAspect* child, ChildIndexFlags flags = nullptr) const { int index = 0; for (auto* c: children()) { if (child == c) return index; T* i = dynamic_cast(c); - if (i && (flags & IncludeHidden || !c->hidden())) + if (i && (flags & ChildIndexFlag::IncludeHidden || !c->hidden())) index++; } return -1; } //undo/redo related functions void setUndoAware(bool); virtual QUndoStack* undoStack() const; void exec(QUndoCommand*); void exec(QUndoCommand* command, const char* preChangeSignal, const char* postChangeSignal, QGenericArgument val0 = QGenericArgument(), QGenericArgument val1 = QGenericArgument(), QGenericArgument val2 = QGenericArgument(), QGenericArgument val3 = QGenericArgument()); void beginMacro(const QString& text); void endMacro(); //save/load virtual void save(QXmlStreamWriter*) const = 0; virtual bool load(XmlStreamReader*, bool preview) = 0; protected: void info(const QString& text) { emit statusInfo(text); } //serialization/deserialization bool readBasicAttributes(XmlStreamReader*); void writeBasicAttributes(QXmlStreamWriter*) const; void writeCommentElement(QXmlStreamWriter*) const; bool readCommentElement(XmlStreamReader*); const AspectType m_type; private: AbstractAspectPrivate* d; QString uniqueNameFor(const QString&) const; const QVector& children() const; void connectChild(AbstractAspect*); public slots: bool setName(const QString&, bool autoUnique = true); void setComment(const QString&); void remove(); protected slots: virtual void childSelected(const AbstractAspect*); virtual void childDeselected(const AbstractAspect*); signals: void aspectDescriptionAboutToChange(const AbstractAspect*); void aspectDescriptionChanged(const AbstractAspect*); void aspectAboutToBeAdded(const AbstractAspect* parent, const AbstractAspect* before, const AbstractAspect* child); void aspectAdded(const AbstractAspect*); void aspectAboutToBeRemoved(const AbstractAspect*); void aspectRemoved(const AbstractAspect* parent, const AbstractAspect* before, const AbstractAspect* child); void aspectHiddenAboutToChange(const AbstractAspect*); void aspectHiddenChanged(const AbstractAspect*); void statusInfo(const QString&); void renameRequested(); //selection/deselection in model (project explorer) void selected(const AbstractAspect*); void deselected(const AbstractAspect*); //selection/deselection in view void childAspectSelectedInView(const AbstractAspect*); void childAspectDeselectedInView(const AbstractAspect*); }; Q_DECLARE_OPERATORS_FOR_FLAGS(AbstractAspect::ChildIndexFlags) #endif // ifndef ABSTRACT_ASPECT_H diff --git a/src/backend/core/AspectTreeModel.cpp b/src/backend/core/AspectTreeModel.cpp index 63eeb23bd..a81dc683a 100644 --- a/src/backend/core/AspectTreeModel.cpp +++ b/src/backend/core/AspectTreeModel.cpp @@ -1,473 +1,473 @@ /*************************************************************************** File : AspectTreeModel.h Project : LabPlot Description : Represents a tree of AbstractAspect objects as a Qt item model. -------------------------------------------------------------------- Copyright : (C) 2007-2009 by Knut Franke (knut.franke@gmx.de) Copyright : (C) 2007-2009 by Tilman Benkert (thzs@gmx.net) Copyright : (C) 2011-2016 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 "backend/core/AbstractAspect.h" #include "backend/core/column/Column.h" #include "backend/worksheet/WorksheetElement.h" #include "backend/core/AspectTreeModel.h" #include #include #include #include #include #include /** * \class AspectTreeModel * \brief Represents a tree of AbstractAspect objects as a Qt item model. * * This class is an adapter between an AbstractAspect hierarchy and Qt's view classes. * * It represents children of an Aspect as rows in the model, with the fixed columns * Name (AbstractAspect::name()), Type (the class name), Created (AbstractAspect::creationTime()) * and Comment (AbstractAspect::comment()). Name is decorated using AbstractAspect::icon(). * The tooltip for all columns is generated from AbstractAspect::caption(). * * Name and Comment are editable. * * For views which support this (currently ProjectExplorer), the menu created by * AbstractAspect::createContextMenu() is made available via the custom role ContextMenuRole. */ /** * \enum AspectTreeModel::CustomDataRole * \brief Custom data roles used in addition to Qt::ItemDataRole */ /** * \var AspectTreeModel::ContextMenuRole * \brief pointer to a new context menu for an Aspect */ /** * \fn QModelIndex AspectTreeModel::modelIndexOfAspect(const AbstractAspect *aspect, int column=0) const * \brief Convenience wrapper around QAbstractItemModel::createIndex(). */ AspectTreeModel::AspectTreeModel(AbstractAspect* root, QObject* parent) : QAbstractItemModel(parent), m_root(root) { connect(m_root, &AbstractAspect::aspectDescriptionChanged, this, &AspectTreeModel::aspectDescriptionChanged); connect(m_root, &AbstractAspect::aspectAboutToBeAdded, this, &AspectTreeModel::aspectAboutToBeAdded); connect(m_root, &AbstractAspect::aspectAboutToBeRemoved, this, &AspectTreeModel::aspectAboutToBeRemoved); connect(m_root, &AbstractAspect::aspectAdded, this, &AspectTreeModel::aspectAdded); connect(m_root, &AbstractAspect::aspectRemoved, this, &AspectTreeModel::aspectRemoved); connect(m_root, &AbstractAspect::aspectHiddenAboutToChange, this, &AspectTreeModel::aspectHiddenAboutToChange); connect(m_root, &AbstractAspect::aspectHiddenChanged, this, &AspectTreeModel::aspectHiddenChanged); } /*! \c list contains the class names of the aspects, that can be selected in the corresponding model view. */ void AspectTreeModel::setSelectableAspects(const QList& list) { m_selectableAspects = list; } void AspectTreeModel::setReadOnly(bool readOnly) { m_readOnly = readOnly; } void AspectTreeModel::enablePlottableColumnsOnly(bool value) { m_plottableColumnsOnly = value; } void AspectTreeModel::enableNumericColumnsOnly(bool value) { m_numericColumnsOnly = value; } void AspectTreeModel::enableNonEmptyNumericColumnsOnly(bool value) { m_nonEmptyNumericColumnsOnly = value; } void AspectTreeModel::enableShowPlotDesignation(bool value) { m_showPlotDesignation = value; } QModelIndex AspectTreeModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return QModelIndex{}; if (!parent.isValid()) { if (row != 0) return QModelIndex{}; return createIndex(row, column, m_root); } auto* parent_aspect = static_cast(parent.internalPointer()); auto* child_aspect = parent_aspect->child(row); if (!child_aspect) return QModelIndex{}; return createIndex(row, column, child_aspect); } QModelIndex AspectTreeModel::parent(const QModelIndex &index) const { if (!index.isValid()) return QModelIndex{}; auto* parent_aspect = static_cast(index.internalPointer())->parentAspect(); if (!parent_aspect) return QModelIndex{}; return modelIndexOfAspect(parent_aspect); } int AspectTreeModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) return 1; auto* parent_aspect = static_cast(parent.internalPointer()); return parent_aspect->childCount(); } int AspectTreeModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 4; } QVariant AspectTreeModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal) return QVariant(); switch (role) { case Qt::DisplayRole: switch (section) { case 0: return i18n("Name"); case 1: return i18n("Type"); case 2: return i18n("Created"); case 3: return i18n("Comment"); default: return QVariant(); } default: return QVariant(); } } QVariant AspectTreeModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); auto* aspect = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: case Qt::EditRole: switch (index.column()) { case 0: { const auto* column = dynamic_cast(aspect); if (column) { QString name = aspect->name(); if (m_plottableColumnsOnly && !column->isPlottable()) name = i18n("%1 (non-plottable data)", name); else if (m_numericColumnsOnly && !column->isNumeric()) name = i18n("%1 (non-numeric data)", name); else if (m_nonEmptyNumericColumnsOnly && !column->hasValues()) name = i18n("%1 (no values)", name); if (m_showPlotDesignation) name += QLatin1Char('\t') + column->plotDesignationString(); return name; } else return aspect->name(); } case 1: if (aspect->metaObject()->className() != QLatin1String("CantorWorksheet")) return aspect->metaObject()->className(); else return QLatin1String("CAS Worksheet"); case 2: return aspect->creationTime().toString(); case 3: return aspect->comment().replace('\n', ' ').simplified(); default: return QVariant(); } case Qt::ToolTipRole: if (aspect->comment().isEmpty()) return QLatin1String("") + aspect->name() + QLatin1String(""); else return QLatin1String("") + aspect->name() + QLatin1String("

") + aspect->comment().replace(QLatin1Char('\n'), QLatin1String("
")); case Qt::DecorationRole: return index.column() == 0 ? aspect->icon() : QIcon(); case Qt::ForegroundRole: { const WorksheetElement* we = dynamic_cast(aspect); if (we) { if (!we->isVisible()) return QVariant( QApplication::palette().color(QPalette::Disabled,QPalette::Text ) ); } return QVariant( QApplication::palette().color(QPalette::Active,QPalette::Text ) ); } default: return QVariant(); } } Qt::ItemFlags AspectTreeModel::flags(const QModelIndex &index) const { if (!index.isValid()) return nullptr; Qt::ItemFlags result; auto* aspect = static_cast(index.internalPointer()); if (!m_selectableAspects.isEmpty()) { for (AspectType type : m_selectableAspects) { if (aspect->inherits(type)) { result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; if (index != this->index(0,0,QModelIndex()) && !m_filterString.isEmpty()) { if (this->containsFilterString(aspect)) result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; else result &= ~Qt::ItemIsEnabled; } break; } else result &= ~Qt::ItemIsEnabled; } } else { //default case: the list for the selectable aspects is empty and all aspects are selectable. // Apply filter, if available. Indices, that don't match the filter are not selectable. //Don't apply any filter to the very first index in the model - this top index corresponds to the project item. if (index != this->index(0,0,QModelIndex()) && !m_filterString.isEmpty()) { if (this->containsFilterString(aspect)) result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; else result = Qt::ItemIsSelectable; } else result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; } //the columns "name" and "description" are editable if (!m_readOnly) { if (index.column() == 0 || index.column() == 3) result |= Qt::ItemIsEditable; } const auto* column = dynamic_cast(aspect); if (column) { //allow to drag and drop columns for the faster creation of curves in the plots. //TODO: allow drag&drop later for other objects too, once we implement copy and paste in the project explorer result = result |Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; if (m_plottableColumnsOnly && !column->isPlottable()) result &= ~Qt::ItemIsEnabled; if (m_numericColumnsOnly && !column->isNumeric()) result &= ~Qt::ItemIsEnabled; if (m_nonEmptyNumericColumnsOnly && !(column->isNumeric() && column->hasValues())) result &= ~Qt::ItemIsEnabled; } return result; } void AspectTreeModel::aspectDescriptionChanged(const AbstractAspect* aspect) { emit dataChanged(modelIndexOfAspect(aspect), modelIndexOfAspect(aspect, 3)); } void AspectTreeModel::aspectAboutToBeAdded(const AbstractAspect* parent, const AbstractAspect* before, const AbstractAspect* child) { Q_UNUSED(child); int index = parent->indexOfChild(before); if (index == -1) index = parent->childCount(); beginInsertRows(modelIndexOfAspect(parent), index, index); } void AspectTreeModel::aspectAdded(const AbstractAspect* aspect) { endInsertRows(); AbstractAspect* parent = aspect->parentAspect(); emit dataChanged(modelIndexOfAspect(parent), modelIndexOfAspect(parent, 3)); connect(aspect, &AbstractAspect::renameRequested, this, &AspectTreeModel::renameRequestedSlot); connect(aspect, &AbstractAspect::childAspectSelectedInView, this, &AspectTreeModel::aspectSelectedInView); connect(aspect, &AbstractAspect::childAspectDeselectedInView, this, &AspectTreeModel::aspectDeselectedInView); //add signal-slot connects for all children, too - for (const auto* child : aspect->children(AbstractAspect::Recursive)) { + for (const auto* child : aspect->children(AbstractAspect::ChildIndexFlag::Recursive)) { connect(child, &AbstractAspect::renameRequested, this, &AspectTreeModel::renameRequestedSlot); connect(child, &AbstractAspect::childAspectSelectedInView, this, &AspectTreeModel::aspectSelectedInView); connect(child, &AbstractAspect::childAspectDeselectedInView, this, &AspectTreeModel::aspectDeselectedInView); } } void AspectTreeModel::aspectAboutToBeRemoved(const AbstractAspect* aspect) { AbstractAspect* parent = aspect->parentAspect(); int index = parent->indexOfChild(aspect); beginRemoveRows(modelIndexOfAspect(parent), index, index); } void AspectTreeModel::aspectRemoved() { endRemoveRows(); } void AspectTreeModel::aspectHiddenAboutToChange(const AbstractAspect* aspect) { for (AbstractAspect* i = aspect->parentAspect(); i; i = i->parentAspect()) if (i->hidden()) return; if (aspect->hidden()) aspectAboutToBeAdded(aspect->parentAspect(), aspect, aspect); else aspectAboutToBeRemoved(aspect); } void AspectTreeModel::aspectHiddenChanged(const AbstractAspect* aspect) { for (AbstractAspect* i = aspect->parentAspect(); i; i = i->parentAspect()) if (i->hidden()) return; if (aspect->hidden()) aspectRemoved(); else aspectAdded(aspect); } bool AspectTreeModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid() || role != Qt::EditRole) return false; auto* aspect = static_cast(index.internalPointer()); switch (index.column()) { case 0: { if (!aspect->setName(value.toString(), false)) { emit statusInfo(i18n("The name \"%1\" is already in use. Choose another name.", value.toString())); return false; } break; } case 3: aspect->setComment(value.toString()); break; default: return false; } emit dataChanged(index, index); return true; } QModelIndex AspectTreeModel::modelIndexOfAspect(const AbstractAspect* aspect, int column) const { AbstractAspect* parent = aspect->parentAspect(); return createIndex(parent ? parent->indexOfChild(aspect) : 0, column, const_cast(aspect)); } /*! returns the model index of an aspect defined via its path. */ QModelIndex AspectTreeModel::modelIndexOfAspect(const QString& path, int column) const { //determine the aspect out of aspect path AbstractAspect* aspect = nullptr; - auto children = m_root->children(AspectType::AbstractAspect, AbstractAspect::Recursive); + auto children = m_root->children(AspectType::AbstractAspect, AbstractAspect::ChildIndexFlag::Recursive); for (auto* child: children) { if (child->path() == path) { aspect = child; break; } } //return the model index of the aspect if (aspect) return modelIndexOfAspect(aspect, column); return QModelIndex{}; } void AspectTreeModel::setFilterString(const QString& s) { m_filterString = s; QModelIndex topLeft = this->index(0, 0, QModelIndex()); QModelIndex bottomRight = this->index(this->rowCount() - 1, 3, QModelIndex()); emit dataChanged(topLeft, bottomRight); } void AspectTreeModel::setFilterCaseSensitivity(Qt::CaseSensitivity cs) { m_filterCaseSensitivity = cs; } void AspectTreeModel::setFilterMatchCompleteWord(bool b) { m_matchCompleteWord = b; } bool AspectTreeModel::containsFilterString(const AbstractAspect* aspect) const { if (m_matchCompleteWord) { if (aspect->name().compare(m_filterString, m_filterCaseSensitivity) == 0) return true; } else { if (aspect->name().contains(m_filterString, m_filterCaseSensitivity)) return true; } //check for the occurrence of the filter string in the names of the parents if ( aspect->parentAspect() ) return this->containsFilterString(aspect->parentAspect()); else return false; //TODO make this optional // //check for the occurrence of the filter string in the names of the children // foreach(const AbstractAspect * child, aspect->children()) { // if ( this->containsFilterString(child) ) // return true; // } } //############################################################################## //################################# SLOTS #################################### //############################################################################## void AspectTreeModel::renameRequestedSlot() { auto* aspect = dynamic_cast(QObject::sender()); if (aspect) emit renameRequested(modelIndexOfAspect(aspect)); } void AspectTreeModel::aspectSelectedInView(const AbstractAspect* aspect) { if (aspect->hidden()) { //a hidden aspect was selected in the view (e.g. plot title in WorksheetView) //select the parent aspect first, if available AbstractAspect* parent = aspect->parentAspect(); if (parent) emit indexSelected(modelIndexOfAspect(parent)); //emit also this signal, so the GUI can handle this selection. emit hiddenAspectSelected(aspect); } else emit indexSelected(modelIndexOfAspect(aspect)); //deselect the root item when one of the children was selected in the view //in order to avoid multiple selection with the project item (if selected) in the project explorer emit indexDeselected(modelIndexOfAspect(m_root)); } void AspectTreeModel::aspectDeselectedInView(const AbstractAspect* aspect) { if (aspect->hidden()) { AbstractAspect* parent = aspect->parentAspect(); if (parent) emit indexDeselected(modelIndexOfAspect(parent)); } else emit indexDeselected(modelIndexOfAspect(aspect)); } diff --git a/src/backend/core/Folder.cpp b/src/backend/core/Folder.cpp index 7445afd87..9cd68dc31 100644 --- a/src/backend/core/Folder.cpp +++ b/src/backend/core/Folder.cpp @@ -1,326 +1,326 @@ /*************************************************************************** File : Folder.cpp Project : LabPlot Description : Folder in a project -------------------------------------------------------------------- Copyright : (C) 2009-2015 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2007 Tilman Benkert (thzs@gmx.net) Copyright : (C) 2007 Knut Franke (knut.franke@gmx.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 "backend/core/Folder.h" #include "backend/datapicker/Datapicker.h" #include "backend/core/Project.h" #include "backend/core/Workbook.h" #include "backend/core/column/Column.h" #include "backend/datasources/LiveDataSource.h" #include "backend/matrix/Matrix.h" #include "backend/note/Note.h" #include "backend/spreadsheet/Spreadsheet.h" #ifdef HAVE_CANTOR_LIBS #include "backend/cantorWorksheet/CantorWorksheet.h" #endif #ifdef HAVE_MQTT #include "backend/datasources/MQTTClient.h" #include "backend/datasources/MQTTSubscription.h" #endif #include "backend/worksheet/Worksheet.h" #include #include #include #include /** * \class Folder * \brief Folder in a project */ Folder::Folder(const QString &name, AspectType type) : AbstractAspect(name, type) { //when the child being removed is a LiveDataSource, stop reading from the source connect(this, &AbstractAspect::aspectAboutToBeRemoved, this, [=](const AbstractAspect* aspect) { const auto* lds = dynamic_cast(aspect); if (lds) const_cast(lds)->pauseReading(); } ); } QIcon Folder::icon() const { return QIcon::fromTheme("folder"); } /** * \brief Return a new context menu. * * The caller takes ownership of the menu. */ QMenu* Folder::createContextMenu() { if (project() #ifdef HAVE_MQTT && !dynamic_cast(this) #endif ) return project()->createFolderContextMenu(this); return nullptr; } bool Folder::isDraggable() const { if (dynamic_cast(this)) return false; else return true; } QVector Folder::dropableOn() const { return QVector{AspectType::Folder, AspectType::Project}; } void Folder::processDropEvent(QDropEvent* event) { const QMimeData* mimeData = event->mimeData(); if (!mimeData) return; //deserialize the mime data to the vector of aspect pointers QByteArray data = mimeData->data(QLatin1String("labplot-dnd")); QVector vec; QDataStream stream(&data, QIODevice::ReadOnly); stream >> vec; //reparent AbstractPart and Folder objects only AbstractAspect* lastMovedAspect{nullptr}; for (auto a : vec) { auto* aspect = (AbstractAspect*)a; auto* part = dynamic_cast(aspect); if (part) { part->reparent(this); lastMovedAspect = part; } else { auto* folder = dynamic_cast(aspect); if (folder && folder != this) { folder->reparent(this); lastMovedAspect = folder; } } } //select the last moved aspect in the project explorer if (lastMovedAspect) lastMovedAspect->setSelected(true); } /** * \brief Save as XML */ void Folder::save(QXmlStreamWriter* writer) const { writer->writeStartElement(QLatin1String("folder")); writeBasicAttributes(writer); writeCommentElement(writer); - for (auto* child : children(IncludeHidden)) { + for (auto* child : children(ChildIndexFlag::IncludeHidden)) { writer->writeStartElement(QLatin1String("child_aspect")); child->save(writer); writer->writeEndElement(); // "child_aspect" } writer->writeEndElement(); // "folder" } /** * \brief Load from XML */ bool Folder::load(XmlStreamReader* reader, bool preview) { if (!readBasicAttributes(reader)) return false; // read child elements while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement()) break; if (reader->isStartElement()) { if (reader->name() == QLatin1String("comment")) { if (!readCommentElement(reader)) return false; } else if (reader->name() == QLatin1String("child_aspect")) { if (!readChildAspectElement(reader, preview)) return false; } else {// unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } } return !reader->hasError(); } void Folder::setPathesToLoad(const QStringList& pathes) { m_pathesToLoad = pathes; } const QStringList& Folder::pathesToLoad() const { return m_pathesToLoad; } /** * \brief Read child aspect from XML */ bool Folder::readChildAspectElement(XmlStreamReader* reader, bool preview) { if (!reader->skipToNextTag()) return false; if (reader->isEndElement() && reader->name() == QLatin1String("child_aspect")) return true; // empty element tag //check whether we need to skip the loading of the current child aspect if (!m_pathesToLoad.isEmpty()) { const QString& name = reader->attributes().value("name").toString(); //name of the current child aspect const QString childPath = path() + '/' + name; //child's path is not available yet (child not added yet) -> construct it manually //skip the current child aspect it is not in the list of aspects to be loaded if (m_pathesToLoad.indexOf(childPath) == -1) { //skip to the end of the current element if (!reader->skipToEndElement()) return false; //skip to the end of the "child_asspect" element if (!reader->skipToEndElement()) return false; return true; } } QString element_name = reader->name().toString(); if (element_name == QLatin1String("folder")) { Folder* folder = new Folder(QString()); if (!m_pathesToLoad.isEmpty()) { //a child folder to be read -> provide the list of aspects to be loaded to the child folder, too. //since the child folder and all its children are not added yet (path() returns empty string), //we need to remove the path of the current child folder from the full pathes provided in m_pathesToLoad. //E.g. we want to import the path "Project/Folder/Spreadsheet" in the following project // Project // \Spreadsheet // \Folder // \Spreadsheet // //Here, we remove the part "Project/Folder/" and proceed for this child folder with "Spreadsheet" only. //With this the logic above where it is determined whether to import the child aspect or not works out. //manually construct the path of the child folder to be read const QString& curFolderPath = path() + '/' + reader->attributes().value("name").toString(); //remove the path of the current child folder QStringList pathesToLoadNew; for (auto path : m_pathesToLoad) { if (path.startsWith(curFolderPath)) pathesToLoadNew << path.right(path.length() - curFolderPath.length()); } folder->setPathesToLoad(pathesToLoadNew); } if (!folder->load(reader, preview)) { delete folder; return false; } addChildFast(folder); } else if (element_name == QLatin1String("workbook")) { Workbook* workbook = new Workbook(QString()); if (!workbook->load(reader, preview)) { delete workbook; return false; } addChildFast(workbook); } else if (element_name == QLatin1String("spreadsheet")) { Spreadsheet* spreadsheet = new Spreadsheet(QString(), true); if (!spreadsheet->load(reader, preview)) { delete spreadsheet; return false; } addChildFast(spreadsheet); } else if (element_name == QLatin1String("matrix")) { Matrix* matrix = new Matrix(QString(), true); if (!matrix->load(reader, preview)) { delete matrix; return false; } addChildFast(matrix); } else if (element_name == QLatin1String("worksheet")) { Worksheet* worksheet = new Worksheet(QString()); worksheet->setIsLoading(true); if (!worksheet->load(reader, preview)) { delete worksheet; return false; } addChildFast(worksheet); worksheet->setIsLoading(false); #ifdef HAVE_CANTOR_LIBS } else if (element_name == QLatin1String("cantorWorksheet")) { CantorWorksheet* cantorWorksheet = new CantorWorksheet(QLatin1String("null"), true); if (!cantorWorksheet->load(reader, preview)) { delete cantorWorksheet; return false; } addChildFast(cantorWorksheet); #endif #ifdef HAVE_MQTT } else if (element_name == QLatin1String("MQTTClient")) { MQTTClient* client = new MQTTClient(QString()); if (!client->load(reader, preview)) { delete client; return false; } addChildFast(client); #endif } else if (element_name == QLatin1String("liveDataSource") || element_name == QLatin1String("LiveDataSource")) { //TODO: remove "LiveDataSources" in couple of releases LiveDataSource* liveDataSource = new LiveDataSource(QString(), true); if (!liveDataSource->load(reader, preview)) { delete liveDataSource; return false; } addChildFast(liveDataSource); } else if (element_name == QLatin1String("datapicker")) { Datapicker* datapicker = new Datapicker(QString(), true); if (!datapicker->load(reader, preview)) { delete datapicker; return false; } addChildFast(datapicker); } else if (element_name == QLatin1String("note")) { Note* note = new Note(QString()); if (!note->load(reader, preview)) { delete note; return false; } addChildFast(note); } else { reader->raiseWarning(i18n("unknown element '%1' found", element_name)); if (!reader->skipToEndElement()) return false; } if (!reader->skipToNextTag()) return false; return !reader->hasError(); } diff --git a/src/backend/core/Project.cpp b/src/backend/core/Project.cpp index ad070e5fa..3b303b530 100644 --- a/src/backend/core/Project.cpp +++ b/src/backend/core/Project.cpp @@ -1,678 +1,678 @@ /*************************************************************************** File : Project.cpp Project : LabPlot Description : Represents a LabPlot project. -------------------------------------------------------------------- Copyright : (C) 2011-2019 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2007-2008 Tilman Benkert (thzs@gmx.net) Copyright : (C) 2007 Knut Franke (knut.franke@gmx.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 "backend/core/Project.h" #include "backend/lib/XmlStreamReader.h" #include "backend/datasources/LiveDataSource.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/worksheet/Worksheet.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/worksheet/plots/cartesian/Histogram.h" #include "backend/worksheet/plots/cartesian/XYEquationCurve.h" #include "backend/worksheet/plots/cartesian/XYFitCurve.h" #include "backend/worksheet/plots/cartesian/Axis.h" #include "backend/datapicker/DatapickerCurve.h" #ifdef HAVE_MQTT #include "backend/datasources/MQTTClient.h" #endif #include #include #include #include #include #include #include #include #include #include #include /** * \class Project * \ingroup core * \brief Represents a project. * * Project represents the root node of all objects created during the runtime of the program. * Manages also the undo stack. */ /** * \enum Project::MdiWindowVisibility * \brief MDI subwindow visibility setting */ /** * \var Project::folderOnly * \brief only show MDI windows corresponding to Parts in the current folder */ /** * \var Project::foldAndSubfolders * \brief show MDI windows corresponding to Parts in the current folder and its subfolders */ /** * \var Project::allMdiWindows * \brief show MDI windows for all Parts in the project simultaneously */ class Project::Private { public: Private() : version(LVERSION), author(QString(qgetenv("USER"))), modificationTime(QDateTime::currentDateTime()) { } QUndoStack undo_stack; - MdiWindowVisibility mdiWindowVisibility{Project::folderOnly}; + MdiWindowVisibility mdiWindowVisibility{Project::MdiWindowVisibility::folderOnly}; QString fileName; QString version; QString author; QDateTime modificationTime; bool changed{false}; bool aspectAddedSignalSuppressed{false}; }; Project::Project() : Folder(i18n("Project"), AspectType::Project), d(new Private()) { //load default values for name, comment and author from config KConfig config; KConfigGroup group = config.group("Project"); d->author = group.readEntry("Author", QString()); //we don't have direct access to the members name and comment //->temporary disable the undo stack and call the setters setUndoAware(false); setIsLoading(true); setName(group.readEntry("Name", i18n("Project"))); setComment(group.readEntry("Comment", QString())); setUndoAware(true); setIsLoading(false); d->changed = false; connect(this, &Project::aspectDescriptionChanged,this, &Project::descriptionChanged); connect(this, &Project::aspectAdded,this, &Project::aspectAddedSlot); } Project::~Project() { //if the project is being closed and the live data sources still continue reading the data, //the dependent objects (columns, etc.), which are already deleted maybe here, are still being notified about the changes. //->stop reading the live data sources prior to deleting all objects. for (auto* lds : children()) lds->pauseReading(); #ifdef HAVE_MQTT for (auto* client : children()) client->pauseReading(); #endif //if the project is being closed, in Worksheet the scene items are being removed and the selection in the view can change. //don't react on these changes since this can lead crashes (worksheet object is already in the destructor). //->notify all worksheets about the project being closed. - for (auto* w : children(AbstractAspect::Recursive)) + for (auto* w : children(ChildIndexFlag::Recursive)) w->setIsClosing(); d->undo_stack.clear(); delete d; } QUndoStack* Project::undoStack() const { return &d->undo_stack; } QMenu* Project::createContextMenu() { QMenu* menu = AbstractAspect::createContextMenu(); //add close action menu->addSeparator(); menu->addAction(QIcon::fromTheme(QLatin1String("document-close")), i18n("Close"), this, SIGNAL(closeRequested())); //add the actions from MainWin emit requestProjectContextMenu(menu); return menu; } QMenu* Project::createFolderContextMenu(const Folder* folder) { QMenu* menu = const_cast(folder)->AbstractAspect::createContextMenu(); emit requestFolderContextMenu(folder, menu); return menu; } void Project::setMdiWindowVisibility(MdiWindowVisibility visibility) { d->mdiWindowVisibility = visibility; emit mdiWindowVisibilityChanged(); } Project::MdiWindowVisibility Project::mdiWindowVisibility() const { return d->mdiWindowVisibility; } CLASS_D_ACCESSOR_IMPL(Project, QString, fileName, FileName, fileName) BASIC_D_ACCESSOR_IMPL(Project, QString, version, Version, version) CLASS_D_ACCESSOR_IMPL(Project, QString, author, Author, author) CLASS_D_ACCESSOR_IMPL(Project, QDateTime, modificationTime, ModificationTime, modificationTime) void Project::setChanged(const bool value) { if (isLoading()) return; if (value) emit changed(); d->changed = value; } void Project::setSuppressAspectAddedSignal(bool value) { d->aspectAddedSignalSuppressed = value; } bool Project::aspectAddedSignalSuppressed() const { return d->aspectAddedSignalSuppressed; } bool Project::hasChanged() const { return d->changed ; } /*! * \brief Project::descriptionChanged * This function is called, when an object changes its name. When a column changed its name and wasn't connected before to the curve/column(formula) then * this is done in this function * \param aspect */ void Project::descriptionChanged(const AbstractAspect* aspect) { if (isLoading()) return; if (this != aspect) { const auto* column = dynamic_cast(aspect); if (!column) return; // When the column is created, it gets a random name and is eventually not connected to any curve. // When changing the name it can match a curve and should than be connected to the curve. - const QVector& curves = children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector& curves = children(ChildIndexFlag::Recursive); QString columnPath = column->path(); // setXColumnPath must not be set, because if curve->column matches column, there already exist a // signal/slot connection between the curve and the column to update this. If they are not same, // xColumnPath is set in setXColumn. Same for the yColumn. for (auto* curve : curves) { curve->setUndoAware(false); auto* analysisCurve = dynamic_cast(curve); if (analysisCurve) { if (analysisCurve->xDataColumnPath() == columnPath) analysisCurve->setXDataColumn(column); if (analysisCurve->yDataColumnPath() == columnPath) analysisCurve->setYDataColumn(column); if (analysisCurve->y2DataColumnPath() == columnPath) analysisCurve->setY2DataColumn(column); auto* fitCurve = dynamic_cast(curve); if (fitCurve) { if (fitCurve->xErrorColumnPath() == columnPath) fitCurve->setXErrorColumn(column); if (fitCurve->yErrorColumnPath() == columnPath) fitCurve->setYErrorColumn(column); } } else { if (curve->xColumnPath() == columnPath) curve->setXColumn(column); if (curve->yColumnPath() == columnPath) curve->setYColumn(column); if (curve->valuesColumnPath() == columnPath) curve->setValuesColumn(column); if (curve->xErrorPlusColumnPath() == columnPath) curve->setXErrorPlusColumn(column); if (curve->xErrorMinusColumnPath() == columnPath) curve->setXErrorMinusColumn(column); if (curve->yErrorPlusColumnPath() == columnPath) curve->setYErrorPlusColumn(column); if (curve->yErrorMinusColumnPath() == columnPath) curve->setYErrorMinusColumn(column); } curve->setUndoAware(true); } - const QVector& columns = children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector& columns = children(ChildIndexFlag::Recursive); for (auto* tempColumn : columns) { const QStringList& formulaVariableColumnsPath = tempColumn->formulaVariableColumnPaths(); for (int i = 0; i < formulaVariableColumnsPath.count(); i++) { if (formulaVariableColumnsPath.at(i) == columnPath) tempColumn->setformulVariableColumn(i, const_cast(static_cast(column))); } } return; } d->changed = true; emit changed(); } /*! * \brief Project::aspectAddedSlot * When adding new columns, these should be connected to the corresponding curves * \param aspect */ void Project::aspectAddedSlot(const AbstractAspect* aspect) { const QVector& _children = aspect->children(AspectType::Column, ChildIndexFlag::Recursive); QVector columns; for (auto child : _children) columns.append(static_cast(child)); const auto* column = dynamic_cast(aspect); if (column) columns.append(column); if (columns.isEmpty()) return; for (auto column : columns) { - const QVector& curves = children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector& curves = children(ChildIndexFlag::Recursive); QString columnPath = column->path(); for (auto* curve : curves) { curve->setUndoAware(false); auto* analysisCurve = dynamic_cast(curve); if (analysisCurve) { if (analysisCurve->xDataColumnPath() == columnPath) analysisCurve->setXDataColumn(column); if (analysisCurve->yDataColumnPath() == columnPath) analysisCurve->setYDataColumn(column); if (analysisCurve->y2DataColumnPath() == columnPath) analysisCurve->setY2DataColumn(column); auto* fitCurve = dynamic_cast(curve); if (fitCurve) { if (fitCurve->xErrorColumnPath() == columnPath) fitCurve->setXErrorColumn(column); if (fitCurve->yErrorColumnPath() == columnPath) fitCurve->setYErrorColumn(column); } } else { if (curve->xColumnPath() == columnPath) curve->setXColumn(column); if (curve->yColumnPath() == columnPath) curve->setYColumn(column); if (curve->valuesColumnPath() == columnPath) curve->setValuesColumn(column); if (curve->xErrorPlusColumnPath() == columnPath) curve->setXErrorPlusColumn(column); if (curve->xErrorMinusColumnPath() == columnPath) curve->setXErrorMinusColumn(column); if (curve->yErrorPlusColumnPath() == columnPath) curve->setYErrorPlusColumn(column); if (curve->yErrorMinusColumnPath() == columnPath) curve->setYErrorMinusColumn(column); } curve->setUndoAware(true); } - const QVector& columns = children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector& columns = children(ChildIndexFlag::Recursive); for (auto* tempColumn : columns) { const QStringList& formulaVariableColumnPaths = tempColumn->formulaVariableColumnPaths(); for (int i = 0; i < formulaVariableColumnPaths.count(); i++) { if (formulaVariableColumnPaths.at(i) == column->path()) tempColumn->setformulVariableColumn(i, const_cast(static_cast(column))); } } } } void Project::navigateTo(const QString& path) { emit requestNavigateTo(path); } bool Project::isLabPlotProject(const QString& fileName) { return fileName.endsWith(QStringLiteral(".lml"), Qt::CaseInsensitive) || fileName.endsWith(QStringLiteral(".lml.gz"), Qt::CaseInsensitive) || fileName.endsWith(QStringLiteral(".lml.bz2"), Qt::CaseInsensitive) || fileName.endsWith(QStringLiteral(".lml.xz"), Qt::CaseInsensitive); } QString Project::supportedExtensions() { static const QString extensions = "*.lml *.lml.gz *.lml.bz2 *.lml.xz *.LML *.LML.GZ *.LML.BZ2 *.LML.XZ"; return extensions; } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## void Project::save(const QPixmap& thumbnail, QXmlStreamWriter* writer) const { //set the version and the modification time to the current values d->version = LVERSION; d->modificationTime = QDateTime::currentDateTime(); writer->setAutoFormatting(true); writer->writeStartDocument(); writer->writeDTD(""); writer->writeStartElement("project"); writer->writeAttribute("version", version()); writer->writeAttribute("fileName", fileName()); writer->writeAttribute("modificationTime", modificationTime().toString("yyyy-dd-MM hh:mm:ss:zzz")); writer->writeAttribute("author", author()); QByteArray bArray; QBuffer buffer(&bArray); buffer.open(QIODevice::WriteOnly); QPixmap scaledThumbnail = thumbnail.scaled(512,512, Qt::KeepAspectRatio); scaledThumbnail.save(&buffer, "JPEG"); QString image = QString::fromLatin1(bArray.toBase64().data()); writer->writeAttribute("thumbnail", image); writeBasicAttributes(writer); writeCommentElement(writer); save(writer); } /** * \brief Save as XML */ void Project::save(QXmlStreamWriter* writer) const { //save all children - for (auto* child : children(IncludeHidden)) { + for (auto* child : children(ChildIndexFlag::IncludeHidden)) { writer->writeStartElement("child_aspect"); child->save(writer); writer->writeEndElement(); } //save the state of the views (visible, maximized/minimized/geometry) //and the state of the project explorer (expanded items, currently selected item) emit requestSaveState(writer); writer->writeEndElement(); writer->writeEndDocument(); } bool Project::load(const QString& filename, bool preview) { QIODevice* file; // first try gzip compression, because projects can be gzipped and end with .lml if (filename.endsWith(QLatin1String(".lml"), Qt::CaseInsensitive)) file = new KCompressionDevice(filename,KFilterDev::compressionTypeForMimeType("application/x-gzip")); else // opens filename using file ending file = new KFilterDev(filename); if (!file) file = new QFile(filename); if (!file->open(QIODevice::ReadOnly)) { KMessageBox::error(nullptr, i18n("Sorry. Could not open file for reading.")); return false; } char c; bool rc = file->getChar(&c); if (!rc) { KMessageBox::error(nullptr, i18n("The project file is empty."), i18n("Error opening project")); file->close(); delete file; return false; } file->seek(0); //parse XML XmlStreamReader reader(file); setIsLoading(true); rc = this->load(&reader, preview); setIsLoading(false); if (rc == false) { RESET_CURSOR; QString msg = reader.errorString(); if (msg.isEmpty()) msg = i18n("Unknown error when opening the project %1.", filename); KMessageBox::error(nullptr, msg, i18n("Error when opening the project")); return false; } if (reader.hasWarnings()) { qWarning("The following problems occurred when loading the project file:"); const QStringList& warnings = reader.warningStrings(); for (const auto& str : warnings) qWarning() << qUtf8Printable(str); //TODO: show warnings in a kind of "log window" but not in message box // KMessageBox::error(this, msg, i18n("Project loading partly failed")); } file->close(); delete file; return true; } /** * \brief Load from XML */ bool Project::load(XmlStreamReader* reader, bool preview) { while (!(reader->isStartDocument() || reader->atEnd())) reader->readNext(); if (!(reader->atEnd())) { if (!reader->skipToNextTag()) return false; if (reader->name() == "project") { QString version = reader->attributes().value("version").toString(); if (version.isEmpty()) reader->raiseWarning(i18n("Attribute 'version' is missing.")); else d->version = version; if (!readBasicAttributes(reader)) return false; if (!readProjectAttributes(reader)) return false; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement()) break; if (reader->isStartElement()) { if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (reader->name() == "child_aspect") { if (!readChildAspectElement(reader, preview)) return false; } else if (reader->name() == "state") { //load the state of the views (visible, maximized/minimized/geometry) //and the state of the project explorer (expanded items, currently selected item) emit requestLoadState(reader); } else { reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } } } else // no project element reader->raiseError(i18n("no project element found")); } else // no start document reader->raiseError(i18n("no valid XML document found")); if (!preview) { //wait until all columns are decoded from base64-encoded data QThreadPool::globalInstance()->waitForDone(); //LiveDataSource: //call finalizeLoad() to replace relative with absolute paths if required //and to create columns during the initial read - auto sources = children(AbstractAspect::Recursive); + auto sources = children(ChildIndexFlag::Recursive); for (auto* source : sources) { if (!source) continue; source->finalizeLoad(); } //everything is read now. //restore the pointer to the data sets (columns) in xy-curves etc. - auto columns = children(AbstractAspect::Recursive); + auto columns = children(ChildIndexFlag::Recursive); //xy-curves // cannot be removed by the column observer, because it does not react // on curve changes - auto curves = children(AbstractAspect::Recursive); + auto curves = children(ChildIndexFlag::Recursive); for (auto* curve : curves) { if (!curve) continue; curve->suppressRetransform(true); auto* equationCurve = dynamic_cast(curve); auto* analysisCurve = dynamic_cast(curve); if (equationCurve) { //curves defined by a mathematical equations recalculate their own columns on load again. if (!preview) equationCurve->recalculate(); } else if (analysisCurve) { RESTORE_COLUMN_POINTER(analysisCurve, xDataColumn, XDataColumn); RESTORE_COLUMN_POINTER(analysisCurve, yDataColumn, YDataColumn); RESTORE_COLUMN_POINTER(analysisCurve, y2DataColumn, Y2DataColumn); auto* fitCurve = dynamic_cast(curve); if (fitCurve) { RESTORE_COLUMN_POINTER(fitCurve, xErrorColumn, XErrorColumn); RESTORE_COLUMN_POINTER(fitCurve, yErrorColumn, YErrorColumn); } } else { RESTORE_COLUMN_POINTER(curve, xColumn, XColumn); RESTORE_COLUMN_POINTER(curve, yColumn, YColumn); RESTORE_COLUMN_POINTER(curve, valuesColumn, ValuesColumn); RESTORE_COLUMN_POINTER(curve, xErrorPlusColumn, XErrorPlusColumn); RESTORE_COLUMN_POINTER(curve, xErrorMinusColumn, XErrorMinusColumn); RESTORE_COLUMN_POINTER(curve, yErrorPlusColumn, YErrorPlusColumn); RESTORE_COLUMN_POINTER(curve, yErrorMinusColumn, YErrorMinusColumn); } if (dynamic_cast(curve)) RESTORE_POINTER(dynamic_cast(curve), dataSourceCurve, DataSourceCurve, XYCurve, curves); curve->suppressRetransform(false); } //axes - auto axes = children(AbstractAspect::Recursive); + auto axes = children(ChildIndexFlag::Recursive); for (auto* axis : axes) { if (!axis) continue; RESTORE_COLUMN_POINTER(axis, majorTicksColumn, MajorTicksColumn); RESTORE_COLUMN_POINTER(axis, minorTicksColumn, MinorTicksColumn); } //histograms - auto hists = children(AbstractAspect::Recursive); + auto hists = children(ChildIndexFlag::Recursive); for (auto* hist : hists) { if (!hist) continue; RESTORE_COLUMN_POINTER(hist, dataColumn, DataColumn); } //data picker curves - auto dataPickerCurves = children(AbstractAspect::Recursive); + auto dataPickerCurves = children(ChildIndexFlag::Recursive); for (auto* dataPickerCurve : dataPickerCurves) { if (!dataPickerCurve) continue; RESTORE_COLUMN_POINTER(dataPickerCurve, posXColumn, PosXColumn); RESTORE_COLUMN_POINTER(dataPickerCurve, posYColumn, PosYColumn); RESTORE_COLUMN_POINTER(dataPickerCurve, plusDeltaXColumn, PlusDeltaXColumn); RESTORE_COLUMN_POINTER(dataPickerCurve, minusDeltaXColumn, MinusDeltaXColumn); RESTORE_COLUMN_POINTER(dataPickerCurve, plusDeltaYColumn, PlusDeltaYColumn); RESTORE_COLUMN_POINTER(dataPickerCurve, minusDeltaYColumn, MinusDeltaYColumn); } //if a column was calculated via a formula, restore the pointers to the variable columns defining the formula for (auto* col : columns) { if (!col->formulaVariableColumnPaths().isEmpty()) { auto& formulaVariableColumns = const_cast&>(col->formulaVariableColumns()); formulaVariableColumns.resize(col->formulaVariableColumnPaths().length()); for (int i = 0; i < col->formulaVariableColumnPaths().length(); i++) { auto path = col->formulaVariableColumnPaths()[i]; for (Column* c : columns) { if (!c) continue; if (c->path() == path) { formulaVariableColumns[i] = c; col->finalizeLoad(); break; } } } } } //all data was read in spreadsheets: //call CartesianPlot::retransform() to retransform the plots - for (auto* plot : children(AbstractAspect::Recursive)) { + for (auto* plot : children(ChildIndexFlag::Recursive)) { plot->setIsLoading(false); plot->retransform(); } //all data was read in live-data sources: //call CartesianPlot::dataChanged() to notify affected plots about the new data. //this needs to be done here since in LiveDataSource::finalizeImport() called above //where the data is read the column pointers are not restored yes in curves. QVector plots; for (auto* source : sources) { for (int n = 0; n < source->columnCount(); ++n) { Column* column = source->column(n); //determine the plots where the column is consumed for (const auto* curve : curves) { if (curve->xColumn() == column || curve->yColumn() == column) { auto* plot = static_cast(curve->parentAspect()); if (plots.indexOf(plot) == -1) { plots << plot; plot->setSuppressDataChangedSignal(true); } } } column->setChanged(); } } //loop over all affected plots and retransform them for (auto* plot : plots) { plot->setSuppressDataChangedSignal(false); plot->dataChanged(); } } emit loaded(); return !reader->hasError(); } bool Project::readProjectAttributes(XmlStreamReader* reader) { QXmlStreamAttributes attribs = reader->attributes(); QString str = attribs.value(reader->namespaceUri().toString(), "modificationTime").toString(); QDateTime modificationTime = QDateTime::fromString(str, "yyyy-dd-MM hh:mm:ss:zzz"); if (str.isEmpty() || !modificationTime.isValid()) { reader->raiseWarning(i18n("Invalid project modification time. Using current time.")); d->modificationTime = QDateTime::currentDateTime(); } else d->modificationTime = modificationTime; d->author = attribs.value(reader->namespaceUri().toString(), "author").toString(); return true; } diff --git a/src/backend/core/Project.h b/src/backend/core/Project.h index fc2b4d853..193633b1b 100644 --- a/src/backend/core/Project.h +++ b/src/backend/core/Project.h @@ -1,108 +1,108 @@ /*************************************************************************** File : Project.h Project : LabPlot Description : Represents a LabPlot project. -------------------------------------------------------------------- Copyright : (C) 2011-2014 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2007-2008 Tilman Benkert (thzs@gmx.net) Copyright : (C) 2007 Knut Franke (knut.franke@gmx.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 * * * ***************************************************************************/ #ifndef PROJECT_H #define PROJECT_H #include "backend/core/Folder.h" #include "backend/lib/macros.h" class QString; class Project : public Folder { Q_OBJECT public: - enum MdiWindowVisibility { + enum class MdiWindowVisibility { folderOnly, folderAndSubfolders, allMdiWindows }; public: Project(); ~Project() override; virtual const Project* project() const { return this; } Project* project() override { return this; } QUndoStack* undoStack() const override; QString path() const override { return name(); } QMenu* createContextMenu() override; virtual QMenu* createFolderContextMenu(const Folder*); void setMdiWindowVisibility(MdiWindowVisibility visibility); MdiWindowVisibility mdiWindowVisibility() const; CLASS_D_ACCESSOR_DECL(QString, fileName, FileName) BASIC_D_ACCESSOR_DECL(QString, version, Version) CLASS_D_ACCESSOR_DECL(QString, author, Author) CLASS_D_ACCESSOR_DECL(QDateTime, modificationTime, ModificationTime) void setChanged(const bool value=true); bool hasChanged() const; void navigateTo(const QString& path); void setSuppressAspectAddedSignal(bool); bool aspectAddedSignalSuppressed() const; void save(const QPixmap&, QXmlStreamWriter*) const; bool load(XmlStreamReader*, bool preview) override; bool load(const QString&, bool preview = false); static bool isLabPlotProject(const QString& fileName); static QString supportedExtensions(); public slots: void descriptionChanged(const AbstractAspect*); void aspectAddedSlot(const AbstractAspect*); signals: void requestSaveState(QXmlStreamWriter*) const; void requestLoadState(XmlStreamReader*); void requestProjectContextMenu(QMenu*); void requestFolderContextMenu(const Folder*, QMenu*); void mdiWindowVisibilityChanged(); void changed(); void requestNavigateTo(const QString& path); void loaded(); void closeRequested(); private: class Private; Private* d; bool readProjectAttributes(XmlStreamReader*); void save(QXmlStreamWriter*) const override; }; #endif // ifndef PROJECT_H diff --git a/src/backend/core/column/Column.cpp b/src/backend/core/column/Column.cpp index 8075c9bcb..566c85a68 100644 --- a/src/backend/core/column/Column.cpp +++ b/src/backend/core/column/Column.cpp @@ -1,2219 +1,2219 @@ /*************************************************************************** File : Column.cpp Project : LabPlot Description : Aspect that manages a column -------------------------------------------------------------------- Copyright : (C) 2007-2009 Tilman Benkert (thzs@gmx.net) Copyright : (C) 2013-2017 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2017 Stefan Gerlach (stefan.gerlach@uni.kn) ***************************************************************************/ /*************************************************************************** * * * 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/core/column/Column.h" #include "backend/core/column/ColumnPrivate.h" #include "backend/core/column/ColumnStringIO.h" #include "backend/core/column/columncommands.h" #include "backend/core/Project.h" #include "backend/lib/XmlStreamReader.h" #include "backend/core/datatypes/String2DateTimeFilter.h" #include "backend/core/datatypes/DateTime2StringFilter.h" #include "backend/core/datatypes/Double2StringFilter.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/worksheet/plots/cartesian/Histogram.h" #include "backend/worksheet/plots/cartesian/XYCurve.h" #include "backend/worksheet/plots/cartesian/XYAnalysisCurve.h" extern "C" { #include #include #include } #include #include #include #include #include #include #include #include /** * \class Column * \brief Aspect that manages a column * * This class represents a column, i.e., (mathematically) a 1D vector of * values with a header. It provides a public reading and (undo aware) writing * interface as defined in AbstractColumn. A column * can have one of currently three data types: double, QString, or * QDateTime. The string representation of the values can differ depending * on the mode of the column. * * Column inherits from AbstractAspect and is intended to be a child * of the corresponding Spreadsheet in the aspect hierarchy. Columns don't * have a view as they are intended to be displayed inside a spreadsheet. */ Column::Column(const QString& name, ColumnMode mode) : AbstractColumn(name, AspectType::Column), d(new ColumnPrivate(this, mode)) { init(); } /** * \brief Common part of ctors */ void Column::init() { m_string_io = new ColumnStringIO(this); d->inputFilter()->input(0, m_string_io); d->outputFilter()->input(0, this); d->inputFilter()->setHidden(true); d->outputFilter()->setHidden(true); addChildFast(d->inputFilter()); addChildFast(d->outputFilter()); m_suppressDataChangedSignal = false; m_copyDataAction = new QAction(QIcon::fromTheme("edit-copy"), i18n("Copy Data"), this); connect(m_copyDataAction, &QAction::triggered, this, &Column::copyData); m_usedInActionGroup = new QActionGroup(this); connect(m_usedInActionGroup, &QActionGroup::triggered, this, &Column::navigateTo); connect(this, &AbstractColumn::maskingChanged, this, [=]{d->propertiesAvailable = false;}); } Column::~Column() { delete m_string_io; delete d; } QMenu* Column::createContextMenu() { QMenu* menu = AbstractAspect::createContextMenu(); QAction* firstAction{nullptr}; //insert after "rename" and "delete" actions, if available. //MQTTTopic columns don't have these actions if (menu->actions().size() > 1) firstAction = menu->actions().at(1); //add actions available in SpreadsheetView //TODO: we don't need to add anything from the view for MQTTTopic columns. //at the moment it's ok to check to the null pointer for firstAction here. //later, once we have some actions in the menu also for MQTT topics we'll //need to explicitly to dynamic_cast for MQTTTopic if (firstAction) emit requestProjectContextMenu(menu); //"Used in" menu containing all curves where the column is used QMenu* usedInMenu = new QMenu(i18n("Used in")); usedInMenu->setIcon(QIcon::fromTheme("go-next-view")); //remove previously added actions for (auto* action : m_usedInActionGroup->actions()) m_usedInActionGroup->removeAction(action); Project* project = this->project(); //add curves where the column is currently in use usedInMenu->addSection(i18n("XY-Curves")); - auto curves = project->children(AbstractAspect::Recursive); + auto curves = project->children(AbstractAspect::ChildIndexFlag::Recursive); for (const auto* curve : curves) { bool used = false; const auto* analysisCurve = dynamic_cast(curve); if (analysisCurve) { if (analysisCurve->dataSourceType() == XYAnalysisCurve::DataSourceSpreadsheet && (analysisCurve->xDataColumn() == this || analysisCurve->yDataColumn() == this || analysisCurve->y2DataColumn() == this) ) used = true; } else { if (curve->xColumn() == this || curve->yColumn() == this) used = true; } if (used) { QAction* action = new QAction(curve->icon(), curve->name(), m_usedInActionGroup); action->setData(curve->path()); usedInMenu->addAction(action); } } //add histograms where the column is used usedInMenu->addSection(i18n("Histograms")); - auto hists = project->children(AbstractAspect::Recursive); + auto hists = project->children(AbstractAspect::ChildIndexFlag::Recursive); for (const auto* hist : hists) { bool used = (hist->dataColumn() == this); if (used) { QAction* action = new QAction(hist->icon(), hist->name(), m_usedInActionGroup); action->setData(hist->path()); usedInMenu->addAction(action); } } //add calculated columns where the column is used in formula variables usedInMenu->addSection(i18n("Calculated Columns")); - QVector columns = project->children(AbstractAspect::Recursive); + QVector columns = project->children(AbstractAspect::ChildIndexFlag::Recursive); const QString& path = this->path(); for (const auto* column : columns) { auto paths = column->formulaVariableColumnPaths(); if (paths.indexOf(path) != -1) { QAction* action = new QAction(column->icon(), column->name(), m_usedInActionGroup); action->setData(column->path()); usedInMenu->addAction(action); } } if (firstAction) menu->insertSeparator(firstAction); menu->insertMenu(firstAction, usedInMenu); menu->insertSeparator(firstAction); menu->insertAction(firstAction, m_copyDataAction); menu->insertSeparator(firstAction); return menu; } void Column::navigateTo(QAction* action) { project()->navigateTo(action->data().toString()); } /*! * copies the values of the column to the clipboard */ void Column::copyData() { QLocale locale; QString output; int rows = rowCount(); if (columnMode() == AbstractColumn::Numeric) { const Double2StringFilter* filter = static_cast(outputFilter()); char format = filter->numericFormat(); for (int r = 0; r < rows; r++) { output += locale.toString(valueAt(r), format, 16); // copy with max. precision if (r < rows-1) output += '\n'; } } else if (columnMode() == AbstractColumn::Integer || columnMode() == AbstractColumn::BigInt) { for (int r = 0; r < rowCount(); r++) { output += QString::number(valueAt(r)); if (r < rows-1) output += '\n'; } } else { for (int r = 0; r < rowCount(); r++) { output += asStringColumn()->textAt(r); if (r < rows-1) output += '\n'; } } QApplication::clipboard()->setText(output); } /*! * */ void Column::setSuppressDataChangedSignal(bool b) { m_suppressDataChangedSignal = b; } void Column::addUsedInPlots(QVector& plots) { const Project* project = this->project(); //when executing tests we don't create any project, //add a null-pointer check for tests here. if (!project) return; - auto curves = project->children(AbstractAspect::Recursive); + auto curves = project->children(AbstractAspect::ChildIndexFlag::Recursive); //determine the plots where the column is consumed for (const auto* curve : curves) { if (curve->xColumn() == this || curve->yColumn() == this || (curve->xErrorType() == XYCurve::SymmetricError && curve->xErrorPlusColumn() == this) || (curve->xErrorType() == XYCurve::AsymmetricError && (curve->xErrorPlusColumn() == this ||curve->xErrorMinusColumn() == this)) || (curve->yErrorType() == XYCurve::SymmetricError && curve->yErrorPlusColumn() == this) || (curve->yErrorType() == XYCurve::AsymmetricError && (curve->yErrorPlusColumn() == this ||curve->yErrorMinusColumn() == this)) ) { auto* plot = static_cast(curve->parentAspect()); if (plots.indexOf(plot) == -1) plots << plot; } } - auto hists = project->children(AbstractAspect::Recursive); + auto hists = project->children(AbstractAspect::ChildIndexFlag::Recursive); for (const auto* hist : hists) { if (hist->dataColumn() == this ) { auto* plot = static_cast(hist->parentAspect()); if (plots.indexOf(plot) == -1) plots << plot; } } } /** * \brief Set the column mode * * This sets the column mode and, if * necessary, converts it to another datatype. */ void Column::setColumnMode(AbstractColumn::ColumnMode mode) { if (mode == columnMode()) return; DEBUG("Column::setColumnMode()"); beginMacro(i18n("%1: change column type", name())); auto* old_input_filter = d->inputFilter(); auto* old_output_filter = d->outputFilter(); exec(new ColumnSetModeCmd(d, mode)); if (d->inputFilter() != old_input_filter) { removeChild(old_input_filter); addChild(d->inputFilter()); d->inputFilter()->input(0, m_string_io); } if (d->outputFilter() != old_output_filter) { removeChild(old_output_filter); addChild(d->outputFilter()); d->outputFilter()->input(0, this); } endMacro(); DEBUG("Column::setColumnMode() DONE"); } void Column::setColumnModeFast(AbstractColumn::ColumnMode mode) { if (mode == columnMode()) return; auto* old_input_filter = d->inputFilter(); auto* old_output_filter = d->outputFilter(); exec(new ColumnSetModeCmd(d, mode)); if (d->inputFilter() != old_input_filter) { removeChild(old_input_filter); addChildFast(d->inputFilter()); d->inputFilter()->input(0, m_string_io); } if (d->outputFilter() != old_output_filter) { removeChild(old_output_filter); addChildFast(d->outputFilter()); d->outputFilter()->input(0, this); } } bool Column::isDraggable() const { return true; } QVector Column::dropableOn() const { return QVector{AspectType::CartesianPlot}; } /** * \brief Copy another column of the same type * * This function will return false if the data type * of 'other' is not the same as the type of 'this'. * Use a filter to convert a column to another type. */ bool Column::copy(const AbstractColumn* other) { Q_CHECK_PTR(other); if (other->columnMode() != columnMode()) return false; exec(new ColumnFullCopyCmd(d, other)); return true; } /** * \brief Copies a part of another column of the same type * * This function will return false if the data type * of 'other' is not the same as the type of 'this'. * \param source pointer to the column to copy * \param source_start first row to copy in the column to copy * \param dest_start first row to copy in * \param num_rows the number of rows to copy */ bool Column::copy(const AbstractColumn* source, int source_start, int dest_start, int num_rows) { Q_CHECK_PTR(source); if (source->columnMode() != columnMode()) return false; exec(new ColumnPartialCopyCmd(d, source, source_start, dest_start, num_rows)); return true; } /** * \brief Insert some empty (or initialized with zero) rows */ void Column::handleRowInsertion(int before, int count) { AbstractColumn::handleRowInsertion(before, count); exec(new ColumnInsertRowsCmd(d, before, count)); if (!m_suppressDataChangedSignal) emit dataChanged(this); d->statisticsAvailable = false; d->hasValuesAvailable = false; d->propertiesAvailable = false; } /** * \brief Remove 'count' rows starting from row 'first' */ void Column::handleRowRemoval(int first, int count) { AbstractColumn::handleRowRemoval(first, count); exec(new ColumnRemoveRowsCmd(d, first, count)); if (!m_suppressDataChangedSignal) emit dataChanged(this); d->statisticsAvailable = false; d->hasValuesAvailable = false; d->propertiesAvailable = false; } /** * \brief Set the column plot designation */ void Column::setPlotDesignation(AbstractColumn::PlotDesignation pd) { if (pd != plotDesignation()) exec(new ColumnSetPlotDesignationCmd(d, pd)); } /** * \brief Get width */ int Column::width() const { return d->width(); } /** * \brief Set width */ void Column::setWidth(int value) { d->setWidth(value); } /** * \brief Clear the whole column */ void Column::clear() { exec(new ColumnClearCmd(d)); } //////////////////////////////////////////////////////////////////////////////// //@} //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //! \name Formula related functions //@{ //////////////////////////////////////////////////////////////////////////////// /** * \brief Returns the formula used to generate column values */ QString Column:: formula() const { return d->formula(); } const QStringList& Column::formulaVariableNames() const { return d->formulaVariableNames(); } const QVector& Column::formulaVariableColumns() const { return d->formulaVariableColumns(); } const QStringList& Column::formulaVariableColumnPaths() const { return d->formulaVariableColumnPaths(); } void Column::setformulVariableColumnsPath(int index, const QString& path) { d->setformulVariableColumnsPath(index, path); } void Column::setformulVariableColumn(int index, Column* column) { d->setformulVariableColumn(index, column); } bool Column::formulaAutoUpdate() const { return d->formulaAutoUpdate(); } /** * \brief Sets the formula used to generate column values */ void Column::setFormula(const QString& formula, const QStringList& variableNames, const QVector& columns, bool autoUpdate) { exec(new ColumnSetGlobalFormulaCmd(d, formula, variableNames, columns, autoUpdate)); } /*! * in case the cell values are calculated via a global column formula, * updates the values on data changes in all the dependent changes in the * "variable columns". */ void Column::updateFormula() { d->statisticsAvailable = false; d->hasValuesAvailable = false; d->propertiesAvailable = false; d->updateFormula(); } /** * \brief Set a formula string for an interval of rows */ void Column::setFormula(const Interval& i, const QString& formula) { exec(new ColumnSetFormulaCmd(d, i, formula)); } /** * \brief Overloaded function for convenience */ void Column::setFormula(int row, const QString& formula) { setFormula(Interval(row, row), formula); } /** * \brief Clear all formulas */ void Column::clearFormulas() { exec(new ColumnClearFormulasCmd(d)); } //////////////////////////////////////////////////////////////////////////////// //@} //////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// //! \name type specific functions //@{ //////////////////////////////////////////////////////////////////////////////// /** * \brief Set the content of row 'row' * * Use this only when columnMode() is Text */ void Column::setTextAt(int row, const QString& new_value) { DEBUG("Column::setTextAt()"); exec(new ColumnSetTextCmd(d, row, new_value)); } /** * \brief Replace a range of values * * Use this only when columnMode() is Text */ void Column::replaceTexts(int first, const QVector& new_values) { DEBUG("Column::replaceTexts()"); if (!new_values.isEmpty()) //TODO: do we really need this check? exec(new ColumnReplaceTextsCmd(d, first, new_values)); } /** * \brief Set the content of row 'row' * * Use this only when columnMode() is DateTime, Month or Day */ void Column::setDateAt(int row, QDate new_value) { setDateTimeAt(row, QDateTime(new_value, timeAt(row))); } /** * \brief Set the content of row 'row' * * Use this only when columnMode() is DateTime, Month or Day */ void Column::setTimeAt(int row, QTime new_value) { setDateTimeAt(row, QDateTime(dateAt(row), new_value)); } /** * \brief Set the content of row 'row' * * Use this only when columnMode() is DateTime, Month or Day */ void Column::setDateTimeAt(int row, const QDateTime& new_value) { exec(new ColumnSetDateTimeCmd(d, row, new_value)); } /** * \brief Replace a range of values * * Use this only when columnMode() is DateTime, Month or Day */ void Column::replaceDateTimes(int first, const QVector& new_values) { if (!new_values.isEmpty()) exec(new ColumnReplaceDateTimesCmd(d, first, new_values)); } /** * \brief Set the content of row 'row' * * Use this only when columnMode() is Numeric */ void Column::setValueAt(int row, const double new_value) { DEBUG("Column::setValueAt()") DEBUG("setvalue") exec(new ColumnSetValueCmd(d, row, new_value)); } /** * \brief Replace a range of values * * Use this only when columnMode() is Numeric */ void Column::replaceValues(int first, const QVector& new_values) { DEBUG("Column::replaceValues()") if (!new_values.isEmpty()) exec(new ColumnReplaceValuesCmd(d, first, new_values)); } /** * \brief Set the content of row 'row' * * Use this only when columnMode() is Integer */ void Column::setIntegerAt(int row, const int new_value) { DEBUG("Column::setIntegerAt()") exec(new ColumnSetIntegerCmd(d, row, new_value)); } /** * \brief Replace a range of values * * Use this only when columnMode() is Integer */ void Column::replaceInteger(int first, const QVector& new_values) { DEBUG("Column::replaceInteger()") if (!new_values.isEmpty()) exec(new ColumnReplaceIntegerCmd(d, first, new_values)); } /** * \brief Set the content of row 'row' * * Use this only when columnMode() is BigInt */ void Column::setBigIntAt(int row, const qint64 new_value) { DEBUG("Column::setBigIntAt()") d->statisticsAvailable = false; d->hasValuesAvailable = false; d->propertiesAvailable = false; exec(new ColumnSetBigIntCmd(d, row, new_value)); } /** * \brief Replace a range of values * * Use this only when columnMode() is BigInt */ void Column::replaceBigInt(int first, const QVector& new_values) { DEBUG("Column::replaceInteger()") if (!new_values.isEmpty()) exec(new ColumnReplaceBigIntCmd(d, first, new_values)); } /*! * \brief Column::properties * Returns the column properties of this curve (monoton increasing, monoton decreasing, ... ) * \see AbstractColumn::properties */ AbstractColumn::Properties Column::properties() const { if (!d->propertiesAvailable) d->updateProperties(); return d->properties; } const Column::ColumnStatistics& Column::statistics() const { if (!d->statisticsAvailable) calculateStatistics(); return d->statistics; } void Column::calculateStatistics() const { if ( (columnMode() != AbstractColumn::Numeric) && (columnMode() != AbstractColumn::Integer) && (columnMode() != AbstractColumn::BigInt) ) return; d->statistics = ColumnStatistics(); ColumnStatistics& statistics = d->statistics; int rowValuesSize = 0; int notNanCount = 0; double val; double columnSum = 0.0; double columnProduct = 1.0; double columnSumNeg = 0.0; double columnSumSquare = 0.0; statistics.minimum = INFINITY; statistics.maximum = -INFINITY; QMap frequencyOfValues; QVector rowData; if (columnMode() == AbstractColumn::Numeric) { auto* rowValues = reinterpret_cast*>(data()); rowValuesSize = rowValues->size(); rowData.reserve(rowValuesSize); for (int row = 0; row < rowValuesSize; ++row) { val = rowValues->value(row); if (std::isnan(val) || isMasked(row)) continue; if (val < statistics.minimum) statistics.minimum = val; if (val > statistics.maximum) statistics.maximum = val; columnSum += val; columnSumNeg += (1.0 / val); columnSumSquare += val*val; columnProduct *= val; if (frequencyOfValues.contains(val)) frequencyOfValues.operator [](val)++; else frequencyOfValues.insert(val, 1); ++notNanCount; rowData.push_back(val); } } else if (columnMode() == AbstractColumn::Integer) { //TODO: code duplication because of the reinterpret_cast... auto* rowValues = reinterpret_cast*>(data()); rowValuesSize = rowValues->size(); rowData.reserve(rowValuesSize); for (int row = 0; row < rowValuesSize; ++row) { val = rowValues->value(row); if (std::isnan(val) || isMasked(row)) continue; if (val < statistics.minimum) statistics.minimum = val; if (val > statistics.maximum) statistics.maximum = val; columnSum += val; columnSumNeg += (1.0 / val); columnSumSquare += val*val; columnProduct *= val; if (frequencyOfValues.contains(val)) frequencyOfValues.operator [](val)++; else frequencyOfValues.insert(val, 1); ++notNanCount; rowData.push_back(val); } } else if (columnMode() == AbstractColumn::BigInt) { //TODO: code duplication because of the reinterpret_cast... auto* rowValues = reinterpret_cast*>(data()); rowValuesSize = rowValues->size(); rowData.reserve(rowValuesSize); for (int row = 0; row < rowValuesSize; ++row) { val = rowValues->value(row); if (std::isnan(val) || isMasked(row)) continue; if (val < statistics.minimum) statistics.minimum = val; if (val > statistics.maximum) statistics.maximum = val; columnSum += val; columnSumNeg += (1.0 / val); columnSumSquare += val*val; columnProduct *= val; if (frequencyOfValues.contains(val)) frequencyOfValues.operator [](val)++; else frequencyOfValues.insert(val, 1); ++notNanCount; rowData.push_back(val); } } if (notNanCount == 0) { d->statisticsAvailable = true; return; } if (rowData.size() < rowValuesSize) rowData.squeeze(); statistics.size = notNanCount; statistics.arithmeticMean = columnSum / notNanCount; statistics.geometricMean = pow(columnProduct, 1.0 / notNanCount); statistics.harmonicMean = notNanCount / columnSumNeg; statistics.contraharmonicMean = columnSumSquare / columnSum; //calculate the mode, the most frequent value in the data set int maxFreq = 0; double mode = NAN; QMap::const_iterator it = frequencyOfValues.constBegin(); while (it != frequencyOfValues.constEnd()) { if (it.value() > maxFreq) { maxFreq = it.value(); mode = it.key(); } ++it; } //check how many times the max frequency occurs in the data set. //if more than once, we have a multi-modal distribution and don't show any mode it = frequencyOfValues.constBegin(); int maxFreqOccurance = 0; while (it != frequencyOfValues.constEnd()) { if (it.value() == maxFreq) ++maxFreqOccurance; if (maxFreqOccurance > 1) { mode = NAN; break; } ++it; } statistics.mode = mode; double columnSumVariance = 0; double columnSumMeanDeviation = 0.0; double columnSumMedianDeviation = 0.0; double sumForCentralMoment_r3 = 0.0; double sumForCentralMoment_r4 = 0.0; //sort the data to calculate the percentiles gsl_sort(rowData.data(), 1, notNanCount); // statistics.median = (notNanCount%2) ? rowData.at((int)((notNanCount-1)/2)) : // (rowData.at((int)((notNanCount-1)/2)) + rowData.at((int)(notNanCount/2)))/2.0; statistics.firstQuartile = gsl_stats_quantile_from_sorted_data(rowData.data(), 1, notNanCount, 0.25); statistics.median = gsl_stats_quantile_from_sorted_data(rowData.data(), 1, notNanCount, 0.50); statistics.thirdQuartile = gsl_stats_quantile_from_sorted_data(rowData.data(), 1, notNanCount, 0.75); statistics.iqr = statistics.thirdQuartile - statistics.firstQuartile; statistics.trimean = (statistics.firstQuartile + 2*statistics.median + statistics.thirdQuartile) / 4; QVector absoluteMedianList; absoluteMedianList.reserve((int)notNanCount); absoluteMedianList.resize((int)notNanCount); for (int row = 0; row < notNanCount; ++row) { val = rowData.value(row); columnSumVariance += gsl_pow_2(val - statistics.arithmeticMean); sumForCentralMoment_r3 += gsl_pow_3(val - statistics.arithmeticMean); sumForCentralMoment_r4 += gsl_pow_4(val - statistics.arithmeticMean); columnSumMeanDeviation += fabs(val - statistics.arithmeticMean); absoluteMedianList[row] = fabs(val - statistics.median); columnSumMedianDeviation += absoluteMedianList[row]; } statistics.meanDeviationAroundMedian = columnSumMedianDeviation / notNanCount; //sort the data to calculate the median gsl_sort(absoluteMedianList.data(), 1, notNanCount); statistics.medianDeviation = (notNanCount%2) ? absoluteMedianList.at((int)((notNanCount-1)/2)) : (absoluteMedianList.at((int)((notNanCount-1)/2)) + absoluteMedianList.at((int)(notNanCount/2)))/2.0; const double centralMoment_r3 = sumForCentralMoment_r3 / notNanCount; const double centralMoment_r4 = sumForCentralMoment_r4 / notNanCount; statistics.variance = columnSumVariance / notNanCount; statistics.standardDeviation = sqrt(statistics.variance * notNanCount / (notNanCount - 1)); statistics.skewness = centralMoment_r3 / gsl_pow_3(statistics.standardDeviation); statistics.kurtosis = (centralMoment_r4 / gsl_pow_4(statistics.standardDeviation)) - 3.0; statistics.meanDeviation = columnSumMeanDeviation / notNanCount; double entropy = 0.0; for (const auto& v : frequencyOfValues) { const double frequencyNorm = static_cast(v) / notNanCount; entropy += (frequencyNorm * log2(frequencyNorm)); } statistics.entropy = -entropy; d->statisticsAvailable = true; } ////////////////////////////////////////////////////////////////////////////////////////////// void* Column::data() const { return d->data(); } /*! * return \c true if the column has numeric values, \c false otherwise. */ bool Column::hasValues() const { if (d->hasValuesAvailable) return d->hasValues; bool foundValues = false; switch (columnMode()) { case AbstractColumn::Numeric: { for (int row = 0; row < rowCount(); ++row) { if (!std::isnan(valueAt(row))) { foundValues = true; break; } } break; } case AbstractColumn::Text: { for (int row = 0; row < rowCount(); ++row) { if (!textAt(row).isEmpty()) { foundValues = true; break; } } break; } case AbstractColumn::Integer: case AbstractColumn::BigInt: //integer column has always valid values foundValues = true; break; case AbstractColumn::DateTime: case AbstractColumn::Month: case AbstractColumn::Day: { for (int row = 0; row < rowCount(); ++row) { if (dateTimeAt(row).isValid()) { foundValues = true; break; } } break; } } d->hasValues = foundValues; d->hasValuesAvailable = true; return d->hasValues; } /** * \brief Return the content of row 'row'. * * Use this only when columnMode() is Text */ QString Column::textAt(int row) const { return d->textAt(row); } /** * \brief Return the date part of row 'row' * * Use this only when columnMode() is DateTime, Month or Day */ QDate Column::dateAt(int row) const { return d->dateAt(row); } /** * \brief Return the time part of row 'row' * * Use this only when columnMode() is DateTime, Month or Day */ QTime Column::timeAt(int row) const { return d->timeAt(row); } /** * \brief Return the QDateTime in row 'row' * * Use this only when columnMode() is DateTime, Month or Day */ QDateTime Column::dateTimeAt(int row) const { return d->dateTimeAt(row); } /** * \brief Return the double value in row 'row' */ double Column::valueAt(int row) const { return d->valueAt(row); } /** * \brief Return the int value in row 'row' */ int Column::integerAt(int row) const { return d->integerAt(row); } /** * \brief Return the bigint value in row 'row' */ qint64 Column::bigIntAt(int row) const { return d->bigIntAt(row); } /* * call this function if the data of the column was changed directly via the data()-pointer * and not via the setValueAt() in order to emit the dataChanged-signal. * This is used e.g. in \c XYFitCurvePrivate::recalculate() */ void Column::setChanged() { d->propertiesAvailable = false; if (!m_suppressDataChangedSignal) emit dataChanged(this); d->statisticsAvailable = false; d->hasValuesAvailable = false; } //////////////////////////////////////////////////////////////////////////////// //@} //////////////////////////////////////////////////////////////////////////////// /** * \brief Return an icon to be used for decorating the views and spreadsheet column headers */ QIcon Column::icon() const { return iconForMode(columnMode()); } //////////////////////////////////////////////////////////////////////////////////////////////////// //! \name serialize/deserialize //@{ //////////////////////////////////////////////////////////////////////////////////////////////////// /** * \brief Save the column as XML */ void Column::save(QXmlStreamWriter* writer) const { writer->writeStartElement("column"); writeBasicAttributes(writer); writer->writeAttribute("rows", QString::number(rowCount())); writer->writeAttribute("designation", QString::number(plotDesignation())); writer->writeAttribute("mode", QString::number(columnMode())); writer->writeAttribute("width", QString::number(width())); //save the formula used to generate column values, if available if (!formula().isEmpty() ) { writer->writeStartElement("formula"); writer->writeAttribute("autoUpdate", QString::number(d->formulaAutoUpdate())); writer->writeTextElement("text", formula()); writer->writeStartElement("variableNames"); for (const auto& name : formulaVariableNames()) writer->writeTextElement("name", name); writer->writeEndElement(); writer->writeStartElement("columnPathes"); for (const auto& path : formulaVariableColumnPaths()) writer->writeTextElement("path", path); writer->writeEndElement(); writer->writeEndElement(); } writeCommentElement(writer); writer->writeStartElement("input_filter"); d->inputFilter()->save(writer); writer->writeEndElement(); writer->writeStartElement("output_filter"); d->outputFilter()->save(writer); writer->writeEndElement(); XmlWriteMask(writer); //TODO: formula in cells is not implemented yet // QVector< Interval > formulas = formulaIntervals(); // foreach(const Interval& interval, formulas) { // writer->writeStartElement("formula"); // writer->writeAttribute("start_row", QString::number(interval.start())); // writer->writeAttribute("end_row", QString::number(interval.end())); // writer->writeCharacters(formula(interval.start())); // writer->writeEndElement(); // } int i; switch (columnMode()) { case AbstractColumn::Numeric: { const char* data = reinterpret_cast(static_cast< QVector* >(d->data())->constData()); size_t size = d->rowCount() * sizeof(double); writer->writeCharacters(QByteArray::fromRawData(data, (int)size).toBase64()); break; } case AbstractColumn::Integer: { const char* data = reinterpret_cast(static_cast< QVector* >(d->data())->constData()); size_t size = d->rowCount() * sizeof(int); writer->writeCharacters(QByteArray::fromRawData(data, (int)size).toBase64()); break; } case AbstractColumn::BigInt: { const char* data = reinterpret_cast(static_cast< QVector* >(d->data())->constData()); size_t size = d->rowCount() * sizeof(qint64); writer->writeCharacters(QByteArray::fromRawData(data, (int)size).toBase64()); break; } case AbstractColumn::Text: for (i = 0; i < rowCount(); ++i) { writer->writeStartElement("row"); writer->writeAttribute("index", QString::number(i)); writer->writeCharacters(textAt(i)); writer->writeEndElement(); } break; case AbstractColumn::DateTime: case AbstractColumn::Month: case AbstractColumn::Day: for (i = 0; i < rowCount(); ++i) { writer->writeStartElement("row"); writer->writeAttribute("index", QString::number(i)); writer->writeCharacters(dateTimeAt(i).toString("yyyy-dd-MM hh:mm:ss:zzz")); writer->writeEndElement(); } break; } writer->writeEndElement(); // "column" } //TODO: extra header class DecodeColumnTask : public QRunnable { public: DecodeColumnTask(ColumnPrivate* priv, const QString& content) { m_private = priv; m_content = content; }; void run() override { QByteArray bytes = QByteArray::fromBase64(m_content.toLatin1()); if (m_private->columnMode() == AbstractColumn::Numeric) { auto* data = new QVector(bytes.size()/(int)sizeof(double)); memcpy(data->data(), bytes.data(), bytes.size()); m_private->replaceData(data); } else if (m_private->columnMode() == AbstractColumn::BigInt) { auto* data = new QVector(bytes.size()/(int)sizeof(qint64)); memcpy(data->data(), bytes.data(), bytes.size()); m_private->replaceData(data); } else { auto* data = new QVector(bytes.size()/(int)sizeof(int)); memcpy(data->data(), bytes.data(), bytes.size()); m_private->replaceData(data); } } private: ColumnPrivate* m_private; QString m_content; }; /** * \brief Load the column from XML */ bool Column::load(XmlStreamReader* reader, bool preview) { if (!readBasicAttributes(reader)) return false; KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs = reader->attributes(); QString str = attribs.value("rows").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("rows").toString()); else d->resizeTo(str.toInt()); str = attribs.value("designation").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("designation").toString()); else d->setPlotDesignation( AbstractColumn::PlotDesignation(str.toInt()) ); str = attribs.value("mode").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("mode").toString()); else setColumnModeFast( AbstractColumn::ColumnMode(str.toInt()) ); str = attribs.value("width").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("width").toString()); else d->setWidth(str.toInt()); // read child elements while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement()) break; if (reader->isStartElement()) { bool ret_val = true; if (reader->name() == "comment") ret_val = readCommentElement(reader); else if (reader->name() == "input_filter") ret_val = XmlReadInputFilter(reader); else if (reader->name() == "output_filter") ret_val = XmlReadOutputFilter(reader); else if (reader->name() == "mask") ret_val = XmlReadMask(reader); else if (reader->name() == "formula") ret_val = XmlReadFormula(reader); else if (reader->name() == "row") ret_val = XmlReadRow(reader); else { // unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } if (!ret_val) return false; } if (!preview) { QString content = reader->text().toString().trimmed(); if (!content.isEmpty() && ( columnMode() == AbstractColumn::Numeric || columnMode() == AbstractColumn::Integer || columnMode() == AbstractColumn::BigInt)) { auto* task = new DecodeColumnTask(d, content); QThreadPool::globalInstance()->start(task); } } } return !reader->error(); } void Column::finalizeLoad() { d->finalizeLoad(); } /** * \brief Read XML input filter element */ bool Column::XmlReadInputFilter(XmlStreamReader* reader) { Q_ASSERT(reader->isStartElement() == true && reader->name() == "input_filter"); if (!reader->skipToNextTag()) return false; if (!d->inputFilter()->load(reader, false)) return false; if (!reader->skipToNextTag()) return false; Q_ASSERT(reader->isEndElement() == true && reader->name() == "input_filter"); return true; } /** * \brief Read XML output filter element */ bool Column::XmlReadOutputFilter(XmlStreamReader* reader) { Q_ASSERT(reader->isStartElement() == true && reader->name() == "output_filter"); if (!reader->skipToNextTag()) return false; if (!d->outputFilter()->load(reader, false)) return false; if (!reader->skipToNextTag()) return false; Q_ASSERT(reader->isEndElement() == true && reader->name() == "output_filter"); return true; } /** * \brief Read XML formula element */ bool Column::XmlReadFormula(XmlStreamReader* reader) { QString formula; QStringList variableNames; QStringList columnPathes; bool autoUpdate = reader->attributes().value("autoUpdate").toInt(); while (reader->readNext()) { if (reader->isEndElement()) break; if (reader->name() == "text") formula = reader->readElementText(); else if (reader->name() == "variableNames") { while (reader->readNext()) { if (reader->name() == "variableNames" && reader->isEndElement()) break; if (reader->isStartElement()) variableNames << reader->readElementText(); } } else if (reader->name() == "columnPathes") { while (reader->readNext()) { if (reader->name() == "columnPathes" && reader->isEndElement()) break; if (reader->isStartElement()) columnPathes << reader->readElementText(); } } } d->setFormula(formula, variableNames, columnPathes, autoUpdate); return true; } //TODO: read cell formula, not implemented yet // bool Column::XmlReadFormula(XmlStreamReader* reader) // { // Q_ASSERT(reader->isStartElement() && reader->name() == "formula"); // // bool ok1, ok2; // int start, end; // start = reader->readAttributeInt("start_row", &ok1); // end = reader->readAttributeInt("end_row", &ok2); // if (!ok1 || !ok2) // { // reader->raiseError(i18n("invalid or missing start or end row")); // return false; // } // setFormula(Interval(start,end), reader->readElementText()); // // return true; // } /** * \brief Read XML row element */ bool Column::XmlReadRow(XmlStreamReader* reader) { Q_ASSERT(reader->isStartElement() == true && reader->name() == "row"); // QXmlStreamAttributes attribs = reader->attributes(); bool ok; int index = reader->readAttributeInt("index", &ok); if (!ok) { reader->raiseError(i18n("invalid or missing row index")); return false; } QString str = reader->readElementText(); switch (columnMode()) { case AbstractColumn::Numeric: { double value = str.toDouble(&ok); if (!ok) { reader->raiseError(i18n("invalid row value")); return false; } setValueAt(index, value); break; } case AbstractColumn::Integer: { int value = str.toInt(&ok); if (!ok) { reader->raiseError(i18n("invalid row value")); return false; } setIntegerAt(index, value); break; } case AbstractColumn::BigInt: { qint64 value = str.toLongLong(&ok); if (!ok) { reader->raiseError(i18n("invalid row value")); return false; } setBigIntAt(index, value); break; } case AbstractColumn::Text: setTextAt(index, str); break; case AbstractColumn::DateTime: case AbstractColumn::Month: case AbstractColumn::Day: QDateTime date_time = QDateTime::fromString(str,"yyyy-dd-MM hh:mm:ss:zzz"); setDateTimeAt(index, date_time); break; } return true; } //////////////////////////////////////////////////////////////////////////////// //@} //////////////////////////////////////////////////////////////////////////////// /** * \brief Return whether the object is read-only */ bool Column::isReadOnly() const { return false; } /** * \brief Return the column mode * * This function is mostly used by spreadsheets but can also be used * by plots. The column mode specifies how to interpret * the values in the column additional to the data type. */ AbstractColumn::ColumnMode Column::columnMode() const { return d->columnMode(); } /** * \brief Return the data vector size * * This returns the number of rows that actually contain data. * Rows beyond this can be masked etc. but should be ignored by filters, * plots etc. */ int Column::rowCount() const { return d->rowCount(); } /** * \brief Return the column plot designation */ AbstractColumn::PlotDesignation Column::plotDesignation() const { return d->plotDesignation(); } QString Column::plotDesignationString() const { switch (plotDesignation()) { case AbstractColumn::NoDesignation: return QString(""); case AbstractColumn::X: return QLatin1String("[X]"); case AbstractColumn::Y: return QLatin1String("[Y]"); case AbstractColumn::Z: return QLatin1String("[Z]"); case AbstractColumn::XError: return QLatin1String("[") + i18n("X-error") + QLatin1Char(']'); case AbstractColumn::XErrorPlus: return QLatin1String("[") + i18n("X-error +") + QLatin1Char(']'); case AbstractColumn::XErrorMinus: return QLatin1String("[") + i18n("X-error -") + QLatin1Char(']'); case AbstractColumn::YError: return QLatin1String("[") + i18n("Y-error") + QLatin1Char(']'); case AbstractColumn::YErrorPlus: return QLatin1String("[") + i18n("Y-error +") + QLatin1Char(']'); case AbstractColumn::YErrorMinus: return QLatin1String("[") + i18n("Y-error -") + QLatin1Char(']'); } return QString(""); } AbstractSimpleFilter* Column::outputFilter() const { return d->outputFilter(); } /** * \brief Return a wrapper column object used for String I/O. */ ColumnStringIO* Column::asStringColumn() const { return m_string_io; } //////////////////////////////////////////////////////////////////////////////// //! \name IntervalAttribute related functions //@{ //////////////////////////////////////////////////////////////////////////////// /** * \brief Return the formula associated with row 'row' */ QString Column::formula(int row) const { return d->formula(row); } /** * \brief Return the intervals that have associated formulas * * This can be used to make a list of formulas with their intervals. * Here is some example code: * * \code * QStringList list; * QVector< Interval > intervals = my_column.formulaIntervals(); * foreach(Interval interval, intervals) * list << QString(interval.toString() + ": " + my_column.formula(interval.start())); * \endcode */ QVector< Interval > Column::formulaIntervals() const { return d->formulaIntervals(); } void Column::handleFormatChange() { DEBUG("Column::handleFormatChange() mode = " << ENUM_TO_STRING(AbstractColumn, ColumnMode, columnMode())); if (columnMode() == AbstractColumn::DateTime) { auto* input_filter = static_cast(d->inputFilter()); auto* output_filter = static_cast(d->outputFilter()); DEBUG("change format " << STDSTRING(input_filter->format()) << " to " << STDSTRING(output_filter->format())); input_filter->setFormat(output_filter->format()); } emit aspectDescriptionChanged(this); // the icon for the type changed if (!m_suppressDataChangedSignal) emit formatChanged(this); // all cells must be repainted d->statisticsAvailable = false; d->hasValuesAvailable = false; d->propertiesAvailable = false; DEBUG("Column::handleFormatChange() DONE"); } /*! * calculates the minimal value in the column. * for \c count = 0, the minimum of all elements is returned. * for \c count > 0, the minimum of the first \p count elements is returned. * for \c count < 0, the minimum of the last \p count elements is returned. */ double Column::minimum(int count) const { double min = INFINITY; if (count == 0 && d->statisticsAvailable) min = const_cast(this)->statistics().minimum; else { int start, end; if (count == 0) { start = 0; end = rowCount(); } else if (count > 0) { start = 0; end = qMin(rowCount(), count); } else { start = qMax(rowCount() + count, 0); end = rowCount(); } return minimum(start, end); } return min; } /*! * \brief Column::minimum * Calculates the minimum value in the column between the \p startIndex and \p endIndex, endIndex is excluded. * If startIndex is greater than endIndex the indices are swapped * \p startIndex * \p endIndex */ double Column::minimum(int startIndex, int endIndex) const { double min = INFINITY; if (rowCount() == 0) return min; if (startIndex > endIndex && startIndex >= 0 && endIndex >= 0) std::swap(startIndex, endIndex); startIndex = qMax(startIndex, 0); endIndex = qMax(endIndex, 0); startIndex = qMin(startIndex, rowCount() - 1); endIndex = qMin(endIndex, rowCount() - 1); int foundIndex = 0; ColumnMode mode = columnMode(); Properties property = properties(); if (property == Properties::No) { // skipping values is only in Properties::No needed, because // when there are invalid values the property must be Properties::No switch (mode) { case Numeric: { auto* vec = static_cast*>(data()); for (int row = startIndex; row < endIndex; ++row) { if (!isValid(row) || isMasked(row)) continue; const double val = vec->at(row); if (std::isnan(val)) continue; if (val < min) min = val; } break; } case Integer: { auto* vec = static_cast*>(data()); for (int row = startIndex; row < endIndex; ++row) { if (!isValid(row) || isMasked(row)) continue; const int val = vec->at(row); if (val < min) min = val; } break; } case BigInt: { auto* vec = static_cast*>(data()); for (int row = startIndex; row < endIndex; ++row) { if (!isValid(row) || isMasked(row)) continue; const qint64 val = vec->at(row); if (val < min) min = val; } break; } case Text: break; case DateTime: { auto* vec = static_cast*>(data()); for (int row = startIndex; row < endIndex; ++row) { if (!isValid(row) || isMasked(row)) continue; const qint64 val = vec->at(row).toMSecsSinceEpoch(); if (val < min) min = val; } break; } case Day: case Month: default: break; } return min; } // use the properties knowledge to determine maximum faster if (property == Properties::Constant || property == Properties::MonotonicIncreasing) foundIndex = startIndex; else if (property == Properties::MonotonicDecreasing) foundIndex = endIndex; switch (mode) { case Numeric: case Integer: case BigInt: return valueAt(foundIndex); case DateTime: case Month: case Day: return dateTimeAt(foundIndex).toMSecsSinceEpoch(); case Text: default: break; } return min; } /*! * calculates the maximal value in the column. * for \c count = 0, the maximum of all elements is returned. * for \c count > 0, the maximum of the first \p count elements is returned. * for \c count < 0, the maximum of the last \p count elements is returned. */ double Column::maximum(int count) const { double max = -INFINITY; if (count == 0 && d->statisticsAvailable) max = const_cast(this)->statistics().maximum; else { int start, end; if (count == 0) { start = 0; end = rowCount(); } else if (count > 0) { start = 0; end = qMin(rowCount(), count); } else { start = qMax(rowCount() + count, 0); end = rowCount(); } return maximum(start, end); } return max; } /*! * \brief Column::maximum * Calculates the maximum value in the column between the \p startIndex and \p endIndex. * If startIndex is greater than endIndex the indices are swapped * \p startIndex * \p endIndex */ double Column::maximum(int startIndex, int endIndex) const { double max = -INFINITY; if (rowCount() == 0) return max; if (startIndex > endIndex && startIndex >= 0 && endIndex >= 0) std::swap(startIndex, endIndex); startIndex = qMax(startIndex, 0); endIndex = qMax(endIndex, 0); startIndex = qMin(startIndex, rowCount() - 1); endIndex = qMin(endIndex, rowCount() - 1); int foundIndex = 0; ColumnMode mode = columnMode(); Properties property = properties(); if (property == Properties::No) { switch (mode) { case Numeric: { auto* vec = static_cast*>(data()); for (int row = startIndex; row < endIndex; ++row) { if (!isValid(row) || isMasked(row)) continue; const double val = vec->at(row); if (std::isnan(val)) continue; if (val > max) max = val; } break; } case Integer: { auto* vec = static_cast*>(data()); for (int row = startIndex; row < endIndex; ++row) { if (!isValid(row) || isMasked(row)) continue; const int val = vec->at(row); if (val > max) max = val; } break; } case BigInt: { auto* vec = static_cast*>(data()); for (int row = startIndex; row < endIndex; ++row) { if (!isValid(row) || isMasked(row)) continue; const qint64 val = vec->at(row); if (val > max) max = val; } break; } case Text: break; case DateTime: { auto* vec = static_cast*>(data()); for (int row = startIndex; row < endIndex; ++row) { if (!isValid(row) || isMasked(row)) continue; const qint64 val = vec->at(row).toMSecsSinceEpoch(); if (val > max) max = val; } break; } case Day: case Month: default: break; } return max; } // use the properties knowledge to determine maximum faster if (property == Properties::Constant || property == Properties::MonotonicDecreasing) foundIndex = startIndex; else if (property == Properties::MonotonicIncreasing) foundIndex = endIndex; switch (mode) { case Numeric: case Integer: case BigInt: return valueAt(foundIndex); case DateTime: case Month: case Day: return dateTimeAt(foundIndex).toMSecsSinceEpoch(); case Text: default: break; } return max; } /*! * calculates log2(x)+1 for an integer value. * Used in y(double x) to calculate the maximum steps * source: https://stackoverflow.com/questions/11376288/fast-computing-of-log2-for-64-bit-integers * source: https://graphics.stanford.edu/~seander/bithacks.html#IntegerLogLookup * @param value * @return returns calculated value */ // TODO: testing if it is faster than calculating log2. int Column::calculateMaxSteps (unsigned int value) { const std::array LogTable256 = { -1,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3, 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7 }; unsigned int r; // r will be lg(v) unsigned int t, tt; // temporaries if ((tt = value >> 16)) r = (t = tt >> 8) ? 24 + LogTable256[t] : 16 + LogTable256[tt]; else r = (t = value >> 8) ? 8 + LogTable256[t] : LogTable256[value]; return r+1; } /*! * Find index which corresponds to a @p x . In a vector of values * When monotonic increasing or decreasing a different algorithm will be used, which needs less steps (mean) (log_2(rowCount)) to find the value. * @param x * @return -1 if index not found, otherwise the index */ int Column::indexForValue(double x, QVector& column, Properties properties) { int rowCount = column.count(); if (rowCount == 0) return -1; double prevValue = 0; //qint64 prevValueDateTime = 0; if (properties == AbstractColumn::Properties::MonotonicIncreasing || properties == AbstractColumn::Properties::MonotonicDecreasing) { // bisects the index every time, so it is possible to find the value in log_2(rowCount) steps bool increase = true; if(properties == AbstractColumn::Properties::MonotonicDecreasing) increase = false; int lowerIndex = 0; int higherIndex = rowCount-1; unsigned int maxSteps = calculateMaxSteps(static_cast(rowCount))+1; for (unsigned int i = 0; i < maxSteps; i++) { // so no log_2(rowCount) needed int index = lowerIndex + round(static_cast(higherIndex - lowerIndex)/2); double value = column[index]; if (higherIndex - lowerIndex < 2) { if (qAbs(column[lowerIndex] - x) < qAbs(column[higherIndex] - x)) index = lowerIndex; else index = higherIndex; return index; } if (value > x && increase) higherIndex = index; else if (value >= x && !increase) lowerIndex = index; else if (value <= x && increase) lowerIndex = index; else if (value < x && !increase) higherIndex = index; } } else if (properties == AbstractColumn::Properties::Constant) { return 0; } else { // AbstractColumn::Properties::No // naiv way int index = 0; prevValue = column[0]; for (int row = 0; row < rowCount; row++) { double value = column[row]; if (qAbs(value - x) <= qAbs(prevValue - x)) { // "<=" prevents also that row - 1 become < 0 prevValue = value; index = row; } } return index; } return -1; } /*! * Find index which corresponds to a @p x . In a vector of values * When monotonic increasing or decreasing a different algorithm will be used, which needs less steps (mean) (log_2(rowCount)) to find the value. * @param x * @return -1 if index not found, otherwise the index */ int Column::indexForValue(const double x, const QVector& points, Properties properties) { int rowCount = points.count(); if (rowCount == 0) return -1; double prevValue = 0; //qint64 prevValueDateTime = 0; if (properties == AbstractColumn::Properties::MonotonicIncreasing || properties == AbstractColumn::Properties::MonotonicDecreasing) { // bisects the index every time, so it is possible to find the value in log_2(rowCount) steps bool increase = true; if(properties == AbstractColumn::Properties::MonotonicDecreasing) increase = false; int lowerIndex = 0; int higherIndex = rowCount - 1; unsigned int maxSteps = calculateMaxSteps(static_cast(rowCount))+1; for (unsigned int i = 0; i < maxSteps; i++) { // so no log_2(rowCount) needed int index = lowerIndex + round(static_cast(higherIndex - lowerIndex)/2); double value = points[index].x(); if (higherIndex - lowerIndex < 2) { if (qAbs(points[lowerIndex].x() - x) < qAbs(points[higherIndex].x() - x)) index = lowerIndex; else index = higherIndex; return index; } if (value > x && increase) higherIndex = index; else if (value >= x && !increase) lowerIndex = index; else if (value <= x && increase) lowerIndex = index; else if (value < x && !increase) higherIndex = index; } } else if (properties == AbstractColumn::Properties::Constant) { return 0; } else { // AbstractColumn::Properties::No // naiv way prevValue = points[0].x(); int index = 0; for (int row = 0; row < rowCount; row++) { double value = points[row].x(); if (qAbs(value - x) <= qAbs(prevValue - x)) { // "<=" prevents also that row - 1 become < 0 prevValue = value; index = row; } } return index; } return -1; } /*! * Find index which corresponds to a @p x . In a vector of values * When monotonic increasing or decreasing a different algorithm will be used, which needs less steps (mean) (log_2(rowCount)) to find the value. * @param x * @return -1 if index not found, otherwise the index */ int Column::indexForValue(double x, QVector& lines, Properties properties) { int rowCount = lines.count(); if (rowCount == 0) return -1; // use only p1 to find index double prevValue = 0; //qint64 prevValueDateTime = 0; if (properties == AbstractColumn::Properties::MonotonicIncreasing || properties == AbstractColumn::Properties::MonotonicDecreasing) { // bisects the index every time, so it is possible to find the value in log_2(rowCount) steps bool increase = true; if(properties == AbstractColumn::Properties::MonotonicDecreasing) increase = false; int lowerIndex = 0; int higherIndex = rowCount-1; unsigned int maxSteps = calculateMaxSteps(static_cast(rowCount))+1; for (unsigned int i = 0; i < maxSteps; i++) { // so no log_2(rowCount) needed int index = lowerIndex + round(static_cast(higherIndex - lowerIndex)/2); double value = lines[index].p1().x(); if (higherIndex - lowerIndex < 2) { if (qAbs(lines[lowerIndex].p1().x() - x) < qAbs(lines[higherIndex].p1().x() - x)) index = lowerIndex; else index = higherIndex; return index; } if (value > x && increase) higherIndex = index; else if (value >= x && !increase) lowerIndex = index; else if (value <= x && increase) lowerIndex = index; else if (value < x && !increase) higherIndex = index; } } else if (properties == AbstractColumn::Properties::Constant) { return 0; } else { // AbstractColumn::Properties::No // naiv way int index = 0; prevValue = lines[0].p1().x(); for (int row = 0; row < rowCount; row++) { double value = lines[row].p1().x(); if (qAbs(value - x) <= qAbs(prevValue - x)) { // "<=" prevents also that row - 1 become < 0 prevValue = value; index = row; } } return index; } return -1; } int Column::indexForValue(double x) const { double prevValue = 0; qint64 prevValueDateTime = 0; AbstractColumn::ColumnMode mode = columnMode(); int property = properties(); if (property == AbstractColumn::Properties::MonotonicIncreasing || property == AbstractColumn::Properties::MonotonicDecreasing) { // bisects the index every time, so it is possible to find the value in log_2(rowCount) steps bool increase = (property != AbstractColumn::Properties::MonotonicDecreasing); int lowerIndex = 0; int higherIndex = rowCount() - 1; unsigned int maxSteps = calculateMaxSteps(static_cast(rowCount()))+1; if ((mode == AbstractColumn::ColumnMode::Numeric || mode == AbstractColumn::ColumnMode::Integer || mode == AbstractColumn::ColumnMode::BigInt)) { for (unsigned int i = 0; i < maxSteps; i++) { // so no log_2(rowCount) needed int index = lowerIndex + round(static_cast(higherIndex - lowerIndex)/2); double value = valueAt(index); if (higherIndex - lowerIndex < 2) { if (qAbs(valueAt(lowerIndex) - x) < qAbs(valueAt(higherIndex) - x)) index = lowerIndex; else index = higherIndex; return index; } if (value > x && increase) higherIndex = index; else if (value >= x && !increase) lowerIndex = index; else if (value <= x && increase) lowerIndex = index; else if (value < x && !increase) higherIndex = index; } } else if ((mode == AbstractColumn::ColumnMode::DateTime || mode == AbstractColumn::ColumnMode::Month || mode == AbstractColumn::ColumnMode::Day)) { qint64 xInt64 = static_cast(x); for (unsigned int i = 0; i < maxSteps; i++) { // so no log_2(rowCount) needed int index = lowerIndex + round(static_cast(higherIndex - lowerIndex)/2); qint64 value = dateTimeAt(index).toMSecsSinceEpoch(); if (higherIndex - lowerIndex < 2) { if (abs(dateTimeAt(lowerIndex).toMSecsSinceEpoch() - xInt64) < abs(dateTimeAt(higherIndex).toMSecsSinceEpoch() - xInt64)) index = lowerIndex; else index = higherIndex; return index; } if (value > xInt64 && increase) higherIndex = index; else if (value >= xInt64 && !increase) lowerIndex = index; else if (value <= xInt64 && increase) lowerIndex = index; else if (value < xInt64 && !increase) higherIndex = index; } } } else if (property == AbstractColumn::Properties::Constant) { if (rowCount() > 0) return 0; else return -1; } else { // naiv way int index = 0; if ((mode == AbstractColumn::ColumnMode::Numeric || mode == AbstractColumn::ColumnMode::Integer || mode == AbstractColumn::ColumnMode::BigInt)) { for (int row = 0; row < rowCount(); row++) { if (!isValid(row) || isMasked(row)) continue; if (row == 0) prevValue = valueAt(row); double value = valueAt(row); if (abs(value - x) <= abs(prevValue - x)) { // <= prevents also that row - 1 become < 0 if (row < rowCount() - 1) { prevValue = value; index = row; } } } return index; } else if ((mode == AbstractColumn::ColumnMode::DateTime || mode == AbstractColumn::ColumnMode::Month || mode == AbstractColumn::ColumnMode::Day)) { qint64 xInt64 = static_cast(x); for (int row = 0; row < rowCount(); row++) { if (!isValid(row) || isMasked(row)) continue; if (row == 0) prevValueDateTime = dateTimeAt(row).toMSecsSinceEpoch(); qint64 value = dateTimeAt(row).toMSecsSinceEpoch(); if (abs(value - xInt64) <= abs(prevValueDateTime - xInt64)) { // "<=" prevents also that row - 1 become < 0 prevValueDateTime = value; index = row; } } return index; } } return -1; } /*! * Finds the minimal and maximal index which are between v1 and v2 * \brief Column::indicesForX * \param v1 * \param v2 * \param start * \param end * \return */ bool Column::indicesMinMax(double v1, double v2, int& start, int& end) const { start = -1; end = -1; if (rowCount() == 0) return false; // Assumption: v1 is always the smaller value if (v1 > v2) qSwap(v1, v2); Properties property = properties(); if (property == Properties::MonotonicIncreasing || property == Properties::MonotonicDecreasing) { start = indexForValue(v1); end = indexForValue(v2); switch (columnMode()) { case Integer: case BigInt: case Numeric: { if (start > 0 && valueAt(start - 1) <= v2 && valueAt(start - 1) >= v1) start--; if (end < rowCount() - 1 && valueAt(end + 1) <= v2 && valueAt(end + 1) >= v1) end++; break; } case DateTime: case Month: case Day: { qint64 v1int64 = v1; qint64 v2int64 = v2; qint64 value; if (start > 0) { value = dateTimeAt(start -1).toMSecsSinceEpoch(); if (value <= v2int64 && value >= v1int64) start--; } if (end > rowCount() - 1) { value = dateTimeAt(end + 1).toMSecsSinceEpoch(); if (value <= v2int64 && value >= v1int64) end++; } break; } case Text: return false; } return true; } else if (property == Properties::Constant) { start = 0; end = rowCount() - 1; return true; } // property == Properties::No switch (columnMode()) { case Integer: case BigInt: case Numeric: { double value; for (int i = 0; i < rowCount(); i++) { if (!isValid(i) || isMasked(i)) continue; value = valueAt(i); if (value <= v2 && value >= v1) { end = i; if (start < 0) start = i; } } break; } case DateTime: case Month: case Day: { qint64 value; qint64 v2int64 = v2; qint64 v1int64 = v2; for (int i = 0; i < rowCount(); i++) { if (!isValid(i) || isMasked(i)) continue; value = dateTimeAt(i).toMSecsSinceEpoch(); if (value <= v2int64 && value >= v1int64) { end = i; if (start < 0) start = i; } } break; } case Text: return false; } return true; } diff --git a/src/backend/datapicker/Datapicker.cpp b/src/backend/datapicker/Datapicker.cpp index f25cf627f..31d8aae79 100644 --- a/src/backend/datapicker/Datapicker.cpp +++ b/src/backend/datapicker/Datapicker.cpp @@ -1,390 +1,390 @@ /*************************************************************************** File : Datapicker.cpp Project : LabPlot Description : Datapicker -------------------------------------------------------------------- Copyright : (C) 2015 by Ankit Wagadre (wagadre.ankit@gmail.com) Copyright : (C) 2015-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 "Datapicker.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/datapicker/DatapickerImage.h" #include "backend/lib/XmlStreamReader.h" #include "commonfrontend/datapicker/DatapickerView.h" #include "backend/datapicker/DatapickerCurve.h" #include "backend/datapicker/Transform.h" #include "backend/datapicker/DatapickerPoint.h" #include #include "QIcon" #include /** * \class Datapicker * \brief Top-level container for DatapickerCurve and DatapickerImage. * \ingroup backend */ Datapicker::Datapicker(const QString& name, const bool loading) : AbstractPart(name, AspectType::Datapicker), m_transform(new Transform()) { connect(this, &Datapicker::aspectAdded, this, &Datapicker::handleAspectAdded); connect(this, &Datapicker::aspectAboutToBeRemoved, this, &Datapicker::handleAspectAboutToBeRemoved); if (!loading) init(); } Datapicker::~Datapicker() { delete m_transform; } void Datapicker::init() { m_image = new DatapickerImage(i18n("Plot")); m_image->setHidden(true); setUndoAware(false); addChild(m_image); setUndoAware(true); connect(m_image, &DatapickerImage::statusInfo, this, &Datapicker::statusInfo); } /*! Returns an icon to be used in the project explorer. */ QIcon Datapicker::icon() const { return QIcon::fromTheme("color-picker-black"); } /*! * Returns a new context menu. The caller takes ownership of the menu. */ QMenu* Datapicker::createContextMenu() { QMenu* menu = AbstractPart::createContextMenu(); Q_ASSERT(menu); m_image->createContextMenu(menu); return menu; } QWidget* Datapicker::view() const { if (!m_partView) { m_view = new DatapickerView(const_cast(this)); m_partView = m_view; } return m_partView; } bool Datapicker::exportView() const { Spreadsheet* s = currentSpreadsheet(); bool ret; if (s) ret = s->exportView(); else ret = m_image->exportView(); return ret; } bool Datapicker::printView() { Spreadsheet* s = currentSpreadsheet(); bool ret; if (s) ret = s->printView(); else ret = m_image->printView(); return ret; } bool Datapicker::printPreview() const { Spreadsheet* s = currentSpreadsheet(); bool ret; if (s) ret = s->printPreview(); else ret = m_image->printPreview(); return ret; } DatapickerCurve* Datapicker::activeCurve() { return m_activeCurve; } Spreadsheet* Datapicker::currentSpreadsheet() const { if (!m_view) return nullptr; const int index = m_view->currentIndex(); if (index > 0) { auto* curve = child(index-1); return curve->child(0); } return nullptr; } DatapickerImage* Datapicker::image() const { return m_image; } /*! this slot is called when a datapicker child is selected in the project explorer. emits \c datapickerItemSelected() to forward this event to the \c DatapickerView in order to select the corresponding tab. */ void Datapicker::childSelected(const AbstractAspect* aspect) { m_activeCurve = dynamic_cast(const_cast(aspect)); int index = -1; if (m_activeCurve) { //if one of the curves is currently selected, select the image with the plot (the very first child) index = 0; emit statusInfo(i18n("%1, active curve \"%2\"", this->name(), m_activeCurve->name())); emit requestUpdateActions(); } else { const auto* curve = aspect->ancestor(); index = indexOfChild(curve); ++index; //+1 because of the hidden plot image being the first child and shown in the first tab in the view } emit datapickerItemSelected(index); } /*! this slot is called when a worksheet element is deselected in the project explorer. */ void Datapicker::childDeselected(const AbstractAspect* aspect) { Q_UNUSED(aspect); } /*! * Emits the signal to select or to deselect the datapicker item (spreadsheet or image) with the index \c index * in the project explorer, if \c selected=true or \c selected=false, respectively. * The signal is handled in \c AspectTreeModel and forwarded to the tree view in \c ProjectExplorer. * This function is called in \c DatapickerView when the current tab was changed */ void Datapicker::setChildSelectedInView(int index, bool selected) { //select/deselect the datapicker itself if the first tab "representing" the plot image and the curves was selected in the view if (index == 0) { if (selected) emit childAspectSelectedInView(this); else { emit childAspectDeselectedInView(this); //deselect also all curves (they don't have any tab index in the view) that were potentially selected before for (const auto* curve : children()) emit childAspectDeselectedInView(curve); } return; } --index; //-1 because of the first tab in the view being reserved for the plot image and curves //select/deselect the data spreadhseets - auto spreadsheets = children(AbstractAspect::Recursive); + auto spreadsheets = children(ChildIndexFlag::Recursive); const AbstractAspect* aspect = spreadsheets.at(index); if (selected) { emit childAspectSelectedInView(aspect); //deselect the datapicker in the project explorer, if a child (spreadsheet or image) was selected. //prevents unwanted multiple selection with datapicker if it was selected before. emit childAspectDeselectedInView(this); } else { emit childAspectDeselectedInView(aspect); //deselect also all children that were potentially selected before (columns of a spreadsheet) for (const auto* child : aspect->children()) emit childAspectDeselectedInView(child); } } /*! Selects or deselects the datapicker or its current active curve in the project explorer. This function is called in \c DatapickerImageView. */ void Datapicker::setSelectedInView(const bool b) { if (b) emit childAspectSelectedInView(this); else emit childAspectDeselectedInView(this); } void Datapicker::addNewPoint(QPointF pos, AbstractAspect* parentAspect) { - auto points = parentAspect->children(AbstractAspect::IncludeHidden); + auto points = parentAspect->children(ChildIndexFlag::IncludeHidden); auto* newPoint = new DatapickerPoint(i18n("Point %1", points.count() + 1)); newPoint->setPosition(pos); newPoint->setHidden(true); beginMacro(i18n("%1: add %2", parentAspect->name(), newPoint->name())); parentAspect->addChild(newPoint); newPoint->retransform(); auto* datapickerCurve = static_cast(parentAspect); if (m_image == parentAspect) { DatapickerImage::ReferencePoints axisPoints = m_image->axisPoints(); axisPoints.scenePos[points.count()].setX(pos.x()); axisPoints.scenePos[points.count()].setY(pos.y()); m_image->setAxisPoints(axisPoints); } else if (datapickerCurve) { newPoint->initErrorBar(datapickerCurve->curveErrorTypes()); datapickerCurve->updatePoint(newPoint); } endMacro(); emit requestUpdateActions(); } QVector3D Datapicker::mapSceneToLogical(QPointF point) const { return m_transform->mapSceneToLogical(point, m_image->axisPoints()); } QVector3D Datapicker::mapSceneLengthToLogical(QPointF point) const { return m_transform->mapSceneLengthToLogical(point, m_image->axisPoints()); } void Datapicker::handleAspectAboutToBeRemoved(const AbstractAspect* aspect) { const auto* curve = qobject_cast(aspect); if (curve) { //clear scene - auto points = curve->children(IncludeHidden); + auto points = curve->children(ChildIndexFlag::IncludeHidden); for (auto* point : points) handleChildAspectAboutToBeRemoved(point); if (curve == m_activeCurve) { m_activeCurve = nullptr; emit statusInfo(QString()); } } else handleChildAspectAboutToBeRemoved(aspect); emit requestUpdateActions(); } void Datapicker::handleAspectAdded(const AbstractAspect* aspect) { const auto* addedPoint = qobject_cast(aspect); const auto* curve = qobject_cast(aspect); if (addedPoint) handleChildAspectAdded(addedPoint); else if (curve) { connect(m_image, &DatapickerImage::axisPointsChanged, curve, &DatapickerCurve::updatePoints); - auto points = curve->children(IncludeHidden); + auto points = curve->children(ChildIndexFlag::IncludeHidden); for (auto* point : points) handleChildAspectAdded(point); } else return; qreal zVal = 0; - auto points = m_image->children(IncludeHidden); + auto points = m_image->children(ChildIndexFlag::IncludeHidden); for (auto* point : points) point->graphicsItem()->setZValue(zVal++); for (const auto* curve : children()) { - for (auto* point : curve->children(IncludeHidden)) + for (auto* point : curve->children(ChildIndexFlag::IncludeHidden)) point->graphicsItem()->setZValue(zVal++); } emit requestUpdateActions(); } void Datapicker::handleChildAspectAboutToBeRemoved(const AbstractAspect* aspect) { const auto* removedPoint = qobject_cast(aspect); if (removedPoint) { QGraphicsItem* item = removedPoint->graphicsItem(); Q_ASSERT(item != nullptr); Q_ASSERT(m_image != nullptr); m_image->scene()->removeItem(item); } } void Datapicker::handleChildAspectAdded(const AbstractAspect* aspect) { const auto* addedPoint = qobject_cast(aspect); if (addedPoint) { QGraphicsItem* item = addedPoint->graphicsItem(); Q_ASSERT(item != nullptr); Q_ASSERT(m_image != nullptr); m_image->scene()->addItem(item); } } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## //! Save as XML void Datapicker::save(QXmlStreamWriter* writer) const { writer->writeStartElement( "datapicker" ); writeBasicAttributes(writer); writeCommentElement(writer); //serialize all children - for (auto* child : children(IncludeHidden)) + for (auto* child : children(ChildIndexFlag::IncludeHidden)) child->save(writer); writer->writeEndElement(); // close "datapicker" section } //! Load from XML bool Datapicker::load(XmlStreamReader* reader, bool preview) { if (!readBasicAttributes(reader)) return false; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "datapicker") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (reader->name() == "datapickerImage") { DatapickerImage* plot = new DatapickerImage(i18n("Plot"), true); if (!plot->load(reader, preview)) { delete plot; return false; } else { plot->setHidden(true); addChild(plot); m_image = plot; } } else if (reader->name() == "datapickerCurve") { DatapickerCurve* curve = new DatapickerCurve(QString()); if (!curve->load(reader, preview)) { delete curve; return false; } else addChild(curve); } else { // unknown element reader->raiseWarning(i18n("unknown datapicker element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } - for (auto* aspect : children(IncludeHidden)) { - for (auto* point : aspect->children(IncludeHidden)) + for (auto* aspect : children(ChildIndexFlag::IncludeHidden)) { + for (auto* point : aspect->children(ChildIndexFlag::IncludeHidden)) handleAspectAdded(point); } return true; } diff --git a/src/backend/datapicker/DatapickerCurve.cpp b/src/backend/datapicker/DatapickerCurve.cpp index aeb632c45..29ea74b68 100644 --- a/src/backend/datapicker/DatapickerCurve.cpp +++ b/src/backend/datapicker/DatapickerCurve.cpp @@ -1,568 +1,568 @@ /*************************************************************************** File : DatapickerCurve.cpp Project : LabPlot Description : container for Curve-Point and Datasheet/Spreadsheet of datapicker -------------------------------------------------------------------- Copyright : (C) 2015 by Ankit Wagadre (wagadre.ankit@gmail.com) Copyright : (C) 2015-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 "DatapickerCurve.h" #include "backend/datapicker/DatapickerCurvePrivate.h" #include "backend/datapicker/Datapicker.h" #include "backend/datapicker/DatapickerPoint.h" #include "backend/lib/commandtemplates.h" #include "backend/lib/XmlStreamReader.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/worksheet/Worksheet.h" #include #include #include #include #include /** * \class DatapickerCurve * \brief Top-level container for Curve-Point and Datasheet/Spreadsheet of datapicker. * \ingroup backend */ DatapickerCurve::DatapickerCurve(const QString &name) : AbstractAspect(name, AspectType::DatapickerCurve), d_ptr(new DatapickerCurvePrivate(this)) { init(); } DatapickerCurve::DatapickerCurve(const QString &name, DatapickerCurvePrivate *dd) : AbstractAspect(name, AspectType::DatapickerCurve), d_ptr(dd) { init(); } DatapickerCurve::~DatapickerCurve() { delete d_ptr; } void DatapickerCurve::init() { Q_D(DatapickerCurve); KConfig config; KConfigGroup group; group = config.group("DatapickerCurve"); d->curveErrorTypes.x = (ErrorType) group.readEntry("CurveErrorType_X", (int) NoError); d->curveErrorTypes.y = (ErrorType) group.readEntry("CurveErrorType_X", (int) NoError); // point properties d->pointStyle = (Symbol::Style)group.readEntry("PointStyle", (int)Symbol::Cross); d->pointSize = group.readEntry("Size", Worksheet::convertToSceneUnits(7, Worksheet::Point)); d->pointRotationAngle = group.readEntry("Rotation", 0.0); d->pointOpacity = group.readEntry("Opacity", 1.0); d->pointBrush.setStyle( (Qt::BrushStyle)group.readEntry("FillingStyle", (int)Qt::NoBrush) ); d->pointBrush.setColor( group.readEntry("FillingColor", QColor(Qt::black)) ); d->pointPen.setStyle( (Qt::PenStyle)group.readEntry("BorderStyle", (int)Qt::SolidLine) ); d->pointPen.setColor( group.readEntry("BorderColor", QColor(Qt::red)) ); d->pointPen.setWidthF( group.readEntry("BorderWidth", Worksheet::convertToSceneUnits(1, Worksheet::Point)) ); d->pointErrorBarSize = group.readEntry("ErrorBarSize", Worksheet::convertToSceneUnits(8, Worksheet::Point)); d->pointErrorBarBrush.setStyle( (Qt::BrushStyle)group.readEntry("ErrorBarFillingStyle", (int)Qt::NoBrush) ); d->pointErrorBarBrush.setColor( group.readEntry("ErrorBarFillingColor", QColor(Qt::black)) ); d->pointErrorBarPen.setStyle( (Qt::PenStyle)group.readEntry("ErrorBarBorderStyle", (int)Qt::SolidLine) ); d->pointErrorBarPen.setColor( group.readEntry("ErrorBarBorderColor", QColor(Qt::black)) ); d->pointErrorBarPen.setWidthF( group.readEntry("ErrorBarBorderWidth", Worksheet::convertToSceneUnits(1, Worksheet::Point)) ); d->pointVisibility = group.readEntry("PointVisibility", true); } /*! Returns an icon to be used in the project explorer. */ QIcon DatapickerCurve::icon() const { return QIcon::fromTheme("labplot-xy-curve"); } Column* DatapickerCurve::appendColumn(const QString& name) { Column* col = new Column(i18n("Column"), AbstractColumn::Numeric); col->insertRows(0, m_datasheet->rowCount()); col->setName(name); m_datasheet->addChild(col); return col; } //############################################################################## //########################## getter methods ################################## //############################################################################## BASIC_SHARED_D_READER_IMPL(DatapickerCurve, DatapickerCurve::Errors, curveErrorTypes, curveErrorTypes) BASIC_SHARED_D_READER_IMPL(DatapickerCurve, Symbol::Style, pointStyle, pointStyle) BASIC_SHARED_D_READER_IMPL(DatapickerCurve, qreal, pointOpacity, pointOpacity) BASIC_SHARED_D_READER_IMPL(DatapickerCurve, qreal, pointRotationAngle, pointRotationAngle) BASIC_SHARED_D_READER_IMPL(DatapickerCurve, qreal, pointSize, pointSize) CLASS_SHARED_D_READER_IMPL(DatapickerCurve, QBrush, pointBrush, pointBrush) CLASS_SHARED_D_READER_IMPL(DatapickerCurve, QPen, pointPen, pointPen) BASIC_SHARED_D_READER_IMPL(DatapickerCurve, qreal, pointErrorBarSize, pointErrorBarSize) CLASS_SHARED_D_READER_IMPL(DatapickerCurve, QBrush, pointErrorBarBrush, pointErrorBarBrush) CLASS_SHARED_D_READER_IMPL(DatapickerCurve, QPen, pointErrorBarPen, pointErrorBarPen) BASIC_SHARED_D_READER_IMPL(DatapickerCurve, bool, pointVisibility, pointVisibility) BASIC_SHARED_D_READER_IMPL(DatapickerCurve, AbstractColumn*, posXColumn, posXColumn) QString& DatapickerCurve::posXColumnPath() const { return d_ptr->posXColumnPath; } BASIC_SHARED_D_READER_IMPL(DatapickerCurve, AbstractColumn*, posYColumn, posYColumn) QString& DatapickerCurve::posYColumnPath() const { return d_ptr->posYColumnPath; } BASIC_SHARED_D_READER_IMPL(DatapickerCurve, AbstractColumn*, posZColumn, posZColumn) QString& DatapickerCurve::posZColumnPath() const { return d_ptr->posZColumnPath; } BASIC_SHARED_D_READER_IMPL(DatapickerCurve, AbstractColumn*, plusDeltaXColumn, plusDeltaXColumn) QString& DatapickerCurve::plusDeltaXColumnPath() const { return d_ptr->plusDeltaXColumnPath; } BASIC_SHARED_D_READER_IMPL(DatapickerCurve, AbstractColumn*, minusDeltaXColumn, minusDeltaXColumn) QString& DatapickerCurve::minusDeltaXColumnPath() const { return d_ptr->minusDeltaXColumnPath; } BASIC_SHARED_D_READER_IMPL(DatapickerCurve, AbstractColumn*, plusDeltaYColumn, plusDeltaYColumn) QString& DatapickerCurve::plusDeltaYColumnPath() const { return d_ptr->plusDeltaYColumnPath; } BASIC_SHARED_D_READER_IMPL(DatapickerCurve, AbstractColumn*, minusDeltaYColumn, minusDeltaYColumn) QString& DatapickerCurve::minusDeltaYColumnPath() const { return d_ptr->minusDeltaYColumnPath; } //############################################################################## //######################### setter methods ################################### //############################################################################## void DatapickerCurve::addDatasheet(DatapickerImage::GraphType type) { Q_D(DatapickerCurve); m_datasheet = new Spreadsheet(i18n("Data")); addChild(m_datasheet); QString xLabel('x'); QString yLabel('y'); if (type == DatapickerImage::PolarInDegree) { xLabel = QLatin1String("r"); yLabel = QLatin1String("y(deg)"); } else if (type == DatapickerImage::PolarInRadians) { xLabel = QLatin1String("r"); yLabel = QLatin1String("y(rad)"); } else if (type == DatapickerImage::LogarithmicX) { xLabel = QLatin1String("log(x)"); yLabel = QLatin1String("y"); } else if (type == DatapickerImage::LogarithmicY) { xLabel = QLatin1String("x"); yLabel = QLatin1String("log(y)"); } if (type == DatapickerImage::Ternary) d->posZColumn = appendColumn(i18n("c")); d->posXColumn = m_datasheet->column(0); d->posXColumn->setName(xLabel); d->posYColumn = m_datasheet->column(1); d->posYColumn->setName(yLabel); } STD_SETTER_CMD_IMPL_S(DatapickerCurve, SetCurveErrorTypes, DatapickerCurve::Errors, curveErrorTypes) void DatapickerCurve::setCurveErrorTypes(const DatapickerCurve::Errors errors) { Q_D(DatapickerCurve); if (d->curveErrorTypes.x != errors.x || d->curveErrorTypes.y != errors.y) { beginMacro(i18n("%1: set xy-error type", name())); exec(new DatapickerCurveSetCurveErrorTypesCmd(d, errors, ki18n("%1: set xy-error type"))); if ( errors.x != NoError && !d->plusDeltaXColumn ) setPlusDeltaXColumn(appendColumn(QLatin1String("+delta_x"))); else if ( d->plusDeltaXColumn && errors.x == NoError ) { d->plusDeltaXColumn->remove(); d->plusDeltaXColumn = nullptr; } if ( errors.x == AsymmetricError && !d->minusDeltaXColumn ) setMinusDeltaXColumn(appendColumn(QLatin1String("-delta_x"))); else if ( d->minusDeltaXColumn && errors.x != AsymmetricError ) { d->minusDeltaXColumn->remove(); d->minusDeltaXColumn = nullptr; } if ( errors.y != NoError && !d->plusDeltaYColumn ) setPlusDeltaYColumn(appendColumn(QLatin1String("+delta_y"))); else if ( d->plusDeltaYColumn && errors.y == NoError ) { d->plusDeltaYColumn->remove(); d->plusDeltaYColumn = nullptr; } if ( errors.y == AsymmetricError && !d->minusDeltaYColumn ) setMinusDeltaYColumn(appendColumn(QLatin1String("-delta_y"))); else if ( d->minusDeltaYColumn && errors.y != AsymmetricError ) { d->minusDeltaYColumn->remove(); d->minusDeltaYColumn = nullptr; } endMacro(); } } STD_SETTER_CMD_IMPL_S(DatapickerCurve, SetPosXColumn, AbstractColumn*, posXColumn) void DatapickerCurve::setPosXColumn(AbstractColumn* column) { Q_D(DatapickerCurve); if (d->posXColumn != column) exec(new DatapickerCurveSetPosXColumnCmd(d, column, ki18n("%1: set position X column"))); } STD_SETTER_CMD_IMPL_S(DatapickerCurve, SetPosYColumn, AbstractColumn*, posYColumn) void DatapickerCurve::setPosYColumn(AbstractColumn* column) { Q_D(DatapickerCurve); if (d->posYColumn != column) exec(new DatapickerCurveSetPosYColumnCmd(d, column, ki18n("%1: set position Y column"))); } STD_SETTER_CMD_IMPL_S(DatapickerCurve, SetPosZColumn, AbstractColumn*, posZColumn) void DatapickerCurve::setPosZColumn(AbstractColumn* column) { Q_D(DatapickerCurve); if (d->posZColumn != column) exec(new DatapickerCurveSetPosZColumnCmd(d, column, ki18n("%1: set position Z column"))); } STD_SETTER_CMD_IMPL_S(DatapickerCurve, SetPlusDeltaXColumn, AbstractColumn*, plusDeltaXColumn) void DatapickerCurve::setPlusDeltaXColumn(AbstractColumn* column) { Q_D(DatapickerCurve); if (d->plusDeltaXColumn != column) exec(new DatapickerCurveSetPlusDeltaXColumnCmd(d, column, ki18n("%1: set +delta_X column"))); } STD_SETTER_CMD_IMPL_S(DatapickerCurve, SetMinusDeltaXColumn, AbstractColumn*, minusDeltaXColumn) void DatapickerCurve::setMinusDeltaXColumn(AbstractColumn* column) { Q_D(DatapickerCurve); if (d->minusDeltaXColumn != column) exec(new DatapickerCurveSetMinusDeltaXColumnCmd(d, column, ki18n("%1: set -delta_X column"))); } STD_SETTER_CMD_IMPL_S(DatapickerCurve, SetPlusDeltaYColumn, AbstractColumn*, plusDeltaYColumn) void DatapickerCurve::setPlusDeltaYColumn(AbstractColumn* column) { Q_D(DatapickerCurve); if (d->plusDeltaYColumn != column) exec(new DatapickerCurveSetPlusDeltaYColumnCmd(d, column, ki18n("%1: set +delta_Y column"))); } STD_SETTER_CMD_IMPL_S(DatapickerCurve, SetMinusDeltaYColumn, AbstractColumn*, minusDeltaYColumn) void DatapickerCurve::setMinusDeltaYColumn(AbstractColumn* column) { Q_D(DatapickerCurve); if (d->minusDeltaYColumn != column) exec(new DatapickerCurveSetMinusDeltaYColumnCmd(d, column, ki18n("%1: set -delta_Y column"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointStyle, Symbol::Style, pointStyle, retransform) void DatapickerCurve::setPointStyle(Symbol::Style newStyle) { Q_D(DatapickerCurve); if (newStyle != d->pointStyle) exec(new DatapickerCurveSetPointStyleCmd(d, newStyle, ki18n("%1: set point's style"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointSize, qreal, pointSize, retransform) void DatapickerCurve::setPointSize(qreal value) { Q_D(DatapickerCurve); if (!qFuzzyCompare(1 + value, 1 + d->pointSize)) exec(new DatapickerCurveSetPointSizeCmd(d, value, ki18n("%1: set point's size"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointRotationAngle, qreal, pointRotationAngle, retransform) void DatapickerCurve::setPointRotationAngle(qreal angle) { Q_D(DatapickerCurve); if (!qFuzzyCompare(1 + angle, 1 + d->pointRotationAngle)) exec(new DatapickerCurveSetPointRotationAngleCmd(d, angle, ki18n("%1: rotate point"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointBrush, QBrush, pointBrush, retransform) void DatapickerCurve::setPointBrush(const QBrush& newBrush) { Q_D(DatapickerCurve); if (newBrush != d->pointBrush) exec(new DatapickerCurveSetPointBrushCmd(d, newBrush, ki18n("%1: set point's filling"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointPen, QPen, pointPen, retransform) void DatapickerCurve::setPointPen(const QPen &newPen) { Q_D(DatapickerCurve); if (newPen != d->pointPen) exec(new DatapickerCurveSetPointPenCmd(d, newPen, ki18n("%1: set outline style"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointOpacity, qreal, pointOpacity, retransform) void DatapickerCurve::setPointOpacity(qreal newOpacity) { Q_D(DatapickerCurve); if (newOpacity != d->pointOpacity) exec(new DatapickerCurveSetPointOpacityCmd(d, newOpacity, ki18n("%1: set point's opacity"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointErrorBarSize, qreal, pointErrorBarSize, retransform) void DatapickerCurve::setPointErrorBarSize(qreal size) { Q_D(DatapickerCurve); if (size != d->pointErrorBarSize) exec(new DatapickerCurveSetPointErrorBarSizeCmd(d, size, ki18n("%1: set error bar size"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointErrorBarBrush, QBrush, pointErrorBarBrush, retransform) void DatapickerCurve::setPointErrorBarBrush(const QBrush &brush) { Q_D(DatapickerCurve); if (brush != d->pointErrorBarBrush) exec(new DatapickerCurveSetPointErrorBarBrushCmd(d, brush, ki18n("%1: set error bar filling"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointErrorBarPen, QPen, pointErrorBarPen, retransform) void DatapickerCurve::setPointErrorBarPen(const QPen &pen) { Q_D(DatapickerCurve); if (pen != d->pointErrorBarPen) exec(new DatapickerCurveSetPointErrorBarPenCmd(d, pen, ki18n("%1: set error bar outline style"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerCurve, SetPointVisibility, bool, pointVisibility, retransform) void DatapickerCurve::setPointVisibility(bool on) { Q_D(DatapickerCurve); if (on != d->pointVisibility) exec(new DatapickerCurveSetPointVisibilityCmd(d, on, on ? ki18n("%1: set visible") : ki18n("%1: set invisible"))); } void DatapickerCurve::setPrinting(bool on) { - for (auto* point : children(IncludeHidden)) + for (auto* point : children(AbstractAspect::ChildIndexFlag::IncludeHidden)) point->setPrinting(on); } /*! Selects or deselects the Datapicker/Curve in the project explorer. This function is called in \c DatapickerImageView. */ void DatapickerCurve::setSelectedInView(bool b) { if (b) emit childAspectSelectedInView(this); else emit childAspectDeselectedInView(this); } //############################################################################## //###### SLOTs for changes triggered via QActions in the context menu ######## //############################################################################## void DatapickerCurve::updatePoints() { - for (auto* point : children(IncludeHidden)) + for (auto* point : children(ChildIndexFlag::IncludeHidden)) updatePoint(point); } /*! Update datasheet for corresponding curve-point, it is called every time whenever there is any change in position of curve-point or its error-bar so keep it undo unaware no need to create extra entry in undo stack */ void DatapickerCurve::updatePoint(const DatapickerPoint* point) { Q_D(DatapickerCurve); //TODO: this check shouldn't be required. //redesign the retransform()-call in load() to avoid it. if (!parentAspect()) return; auto* datapicker = static_cast(parentAspect()); - int row = indexOfChild(point, AbstractAspect::IncludeHidden); + int row = indexOfChild(point, ChildIndexFlag::IncludeHidden); QVector3D data = datapicker->mapSceneToLogical(point->position()); if (d->posXColumn) d->posXColumn->setValueAt(row, data.x()); if (d->posYColumn) d->posYColumn->setValueAt(row, data.y()); if (d->posZColumn) d->posZColumn->setValueAt(row, data.y()); if (d->plusDeltaXColumn) { data = datapicker->mapSceneLengthToLogical(QPointF(point->plusDeltaXPos().x(), 0)); d->plusDeltaXColumn->setValueAt(row, qAbs(data.x())); } if (d->minusDeltaXColumn) { data = datapicker->mapSceneLengthToLogical(QPointF(point->minusDeltaXPos().x(), 0)); d->minusDeltaXColumn->setValueAt(row, qAbs(data.x())); } if (d->plusDeltaYColumn) { data = datapicker->mapSceneLengthToLogical(QPointF(0, point->plusDeltaYPos().y())); d->plusDeltaYColumn->setValueAt(row, qAbs(data.y())); } if (d->minusDeltaYColumn) { data = datapicker->mapSceneLengthToLogical(QPointF(0, point->minusDeltaYPos().y())); d->minusDeltaYColumn->setValueAt(row, qAbs(data.y())); } } //############################################################################## //####################### Private implementation ############################### //############################################################################## DatapickerCurvePrivate::DatapickerCurvePrivate(DatapickerCurve *curve) : q(curve) { } QString DatapickerCurvePrivate::name() const { return q->name(); } void DatapickerCurvePrivate::retransform() { - auto points = q->children(AbstractAspect::IncludeHidden); + auto points = q->children(AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* point : points) point->retransform(); } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## //! Save as XML void DatapickerCurve::save(QXmlStreamWriter* writer) const { Q_D(const DatapickerCurve); writer->writeStartElement("datapickerCurve"); writeBasicAttributes(writer); writeCommentElement(writer); //general writer->writeStartElement("general"); WRITE_COLUMN(d->posXColumn, posXColumn); WRITE_COLUMN(d->posYColumn, posYColumn); WRITE_COLUMN(d->posZColumn, posZColumn); WRITE_COLUMN(d->plusDeltaXColumn, plusDeltaXColumn); WRITE_COLUMN(d->minusDeltaXColumn, minusDeltaXColumn); WRITE_COLUMN(d->plusDeltaYColumn, plusDeltaYColumn); WRITE_COLUMN(d->minusDeltaYColumn, minusDeltaYColumn); writer->writeAttribute( "curveErrorType_X", QString::number(d->curveErrorTypes.x) ); writer->writeAttribute( "curveErrorType_Y", QString::number(d->curveErrorTypes.y) ); writer->writeEndElement(); //symbol properties writer->writeStartElement("symbolProperties"); writer->writeAttribute( "pointRotationAngle", QString::number(d->pointRotationAngle) ); writer->writeAttribute( "pointOpacity", QString::number(d->pointOpacity) ); writer->writeAttribute( "pointSize", QString::number(d->pointSize) ); writer->writeAttribute( "pointStyle", QString::number(d->pointStyle) ); writer->writeAttribute( "pointVisibility", QString::number(d->pointVisibility) ); WRITE_QBRUSH(d->pointBrush); WRITE_QPEN(d->pointPen); writer->writeEndElement(); //error bar properties writer->writeStartElement("errorBarProperties"); writer->writeAttribute( "pointErrorBarSize", QString::number(d->pointErrorBarSize) ); WRITE_QBRUSH(d->pointErrorBarBrush); WRITE_QPEN(d->pointErrorBarPen); writer->writeEndElement(); //serialize all children - for (auto* child : children(IncludeHidden)) + for (auto* child : children(ChildIndexFlag::IncludeHidden)) child->save(writer); writer->writeEndElement(); // close section } //! Load from XML bool DatapickerCurve::load(XmlStreamReader* reader, bool preview) { Q_D(DatapickerCurve); 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() == "datapickerCurve") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (!preview && reader->name() == "general") { attribs = reader->attributes(); READ_INT_VALUE("curveErrorType_X", curveErrorTypes.x, ErrorType); READ_INT_VALUE("curveErrorType_Y", curveErrorTypes.y, ErrorType); READ_COLUMN(posXColumn); READ_COLUMN(posYColumn); READ_COLUMN(posZColumn); READ_COLUMN(plusDeltaXColumn); READ_COLUMN(minusDeltaXColumn); READ_COLUMN(plusDeltaYColumn); READ_COLUMN(minusDeltaYColumn); } else if (!preview && reader->name() == "symbolProperties") { attribs = reader->attributes(); READ_DOUBLE_VALUE("pointRotationAngle", pointRotationAngle); READ_DOUBLE_VALUE("pointOpacity", pointOpacity); READ_DOUBLE_VALUE("pointSize", pointSize); READ_INT_VALUE("pointStyle", pointStyle, Symbol::Style); READ_INT_VALUE("pointVisibility", pointVisibility, bool); READ_QBRUSH(d->pointBrush); READ_QPEN(d->pointPen); } else if (!preview && reader->name() == "errorBarProperties") { attribs = reader->attributes(); READ_DOUBLE_VALUE("pointErrorBarSize", pointErrorBarSize); READ_QBRUSH(d->pointErrorBarBrush); READ_QPEN(d->pointErrorBarPen); } else if (reader->name() == "datapickerPoint") { DatapickerPoint* curvePoint = new DatapickerPoint(QString()); curvePoint->setHidden(true); if (!curvePoint->load(reader, preview)) { delete curvePoint; return false; } else { addChild(curvePoint); curvePoint->initErrorBar(curveErrorTypes()); } } else if (reader->name() == "spreadsheet") { Spreadsheet* datasheet = new Spreadsheet("spreadsheet", true); if (!datasheet->load(reader, preview)) { delete datasheet; return false; } else { addChild(datasheet); m_datasheet = datasheet; } } else { // unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } d->retransform(); return true; } diff --git a/src/backend/datapicker/DatapickerImage.cpp b/src/backend/datapicker/DatapickerImage.cpp index af91e71c1..edcb04b96 100644 --- a/src/backend/datapicker/DatapickerImage.cpp +++ b/src/backend/datapicker/DatapickerImage.cpp @@ -1,685 +1,685 @@ /*************************************************************************** File : DatapickerImage.cpp Project : LabPlot Description : Worksheet for Datapicker -------------------------------------------------------------------- Copyright : (C) 2015 by Ankit Wagadre (wagadre.ankit@gmail.com) Copyright : (C) 2015-2019 by 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 "DatapickerImage.h" #include "DatapickerImagePrivate.h" #include "backend/datapicker/ImageEditor.h" #include "backend/lib/commandtemplates.h" #include "backend/lib/XmlStreamReader.h" #include "backend/datapicker/DatapickerPoint.h" #include "backend/datapicker/Segments.h" #include "backend/worksheet/Worksheet.h" #include "commonfrontend/datapicker/DatapickerImageView.h" #include "kdefrontend/worksheet/ExportWorksheetDialog.h" #include "backend/lib/trace.h" #include #include #include #include #include #include #include #include #include /** * \class DatapickerImage * \brief container to open image/plot. * * Top-level container for DatapickerPoint. * * * \ingroup datapicker */ DatapickerImage::DatapickerImage(const QString& name, bool loading) : AbstractPart(name, AspectType::DatapickerImage), foregroundBins( new int[ImageEditor::colorAttributeMax(Foreground) + 1]), hueBins( new int[ImageEditor::colorAttributeMax(Hue) + 1]), saturationBins( new int[ImageEditor::colorAttributeMax(Saturation) + 1]), valueBins( new int[ImageEditor::colorAttributeMax(Value) + 1]), intensityBins( new int[ImageEditor::colorAttributeMax(Intensity) + 1]), d(new DatapickerImagePrivate(this)), m_segments(new Segments(this)) { if (!loading) init(); } DatapickerImage::~DatapickerImage() { delete [] hueBins; delete [] saturationBins; delete [] valueBins; delete [] intensityBins; delete [] foregroundBins; delete m_segments; delete d; } void DatapickerImage::init() { KConfig config; KConfigGroup group = config.group( "DatapickerImage" ); //general properties d->fileName = group.readEntry("FileName", QString()); d->rotationAngle = group.readEntry("RotationAngle", 0.0); d->minSegmentLength = group.readEntry("MinSegmentLength", 30); d->pointSeparation = group.readEntry("PointSeparation", 30); d->axisPoints.type = (DatapickerImage::GraphType) group.readEntry("GraphType", (int) DatapickerImage::Cartesian); d->axisPoints.ternaryScale = group.readEntry("TernaryScale", 1); //edit image settings d->plotImageType = DatapickerImage::OriginalImage; d->settings.foregroundThresholdHigh = group.readEntry("ForegroundThresholdHigh", 90); d->settings.foregroundThresholdLow = group.readEntry("ForegroundThresholdLow", 30); d->settings.hueThresholdHigh = group.readEntry("HueThresholdHigh", 360); d->settings.hueThresholdLow = group.readEntry("HueThresholdLow", 0); d->settings.intensityThresholdHigh = group.readEntry("IntensityThresholdHigh", 100); d->settings.intensityThresholdLow = group.readEntry("IntensityThresholdLow", 20); d->settings.saturationThresholdHigh = group.readEntry("SaturationThresholdHigh", 100); d->settings.saturationThresholdLow = group.readEntry("SaturationThresholdLow", 30); d->settings.valueThresholdHigh = group.readEntry("ValueThresholdHigh", 90); d->settings.valueThresholdLow = group.readEntry("ValueThresholdLow", 30); // reference point symbol properties d->pointStyle = (Symbol::Style)group.readEntry("PointStyle", (int)Symbol::Cross); d->pointSize = group.readEntry("Size", Worksheet::convertToSceneUnits(7, Worksheet::Point)); d->pointRotationAngle = group.readEntry("Rotation", 0.0); d->pointOpacity = group.readEntry("Opacity", 1.0); d->pointBrush.setStyle( (Qt::BrushStyle)group.readEntry("FillingStyle", (int)Qt::NoBrush) ); d->pointBrush.setColor( group.readEntry("FillingColor", QColor(Qt::black)) ); d->pointPen.setStyle( (Qt::PenStyle)group.readEntry("BorderStyle", (int)Qt::SolidLine) ); d->pointPen.setColor( group.readEntry("BorderColor", QColor(Qt::red)) ); d->pointPen.setWidthF( group.readEntry("BorderWidth", Worksheet::convertToSceneUnits(1, Worksheet::Point)) ); d->pointVisibility = group.readEntry("PointVisibility", true); } /*! Returns an icon to be used in the project explorer. */ QIcon DatapickerImage::icon() const { return QIcon::fromTheme("image-x-generic"); } /*! Return a new context menu */ QMenu* DatapickerImage::createContextMenu() { QMenu* menu = new QMenu(nullptr); emit requestProjectContextMenu(menu); return menu; } void DatapickerImage::createContextMenu(QMenu* menu) { emit requestProjectContextMenu(menu); } //! Construct a primary view on me. /** * This method may be called multiple times during the life time of an Aspect, or it might not get * called at all. Aspects must not depend on the existence of a view for their operation. */ QWidget* DatapickerImage::view() const { if (!m_partView) { m_view = new DatapickerImageView(const_cast(this)); m_partView = m_view; connect(m_view, &DatapickerImageView::statusInfo, this, &DatapickerImage::statusInfo); } return m_partView; } bool DatapickerImage::exportView() const { auto* dlg = new ExportWorksheetDialog(m_view); dlg->setFileName(name()); bool ret; if ( (ret = (dlg->exec() == QDialog::Accepted)) ) { const QString path = dlg->path(); const WorksheetView::ExportFormat format = dlg->exportFormat(); const int resolution = dlg->exportResolution(); WAIT_CURSOR; m_view->exportToFile(path, format, resolution); RESET_CURSOR; } delete dlg; return ret; } bool DatapickerImage::printView() { QPrinter printer; auto* dlg = new QPrintDialog(&printer, m_view); bool ret; dlg->setWindowTitle(i18nc("@title:window", "Print Datapicker Image")); if ( (ret = (dlg->exec() == QDialog::Accepted)) ) m_view->print(&printer); delete dlg; return ret; } bool DatapickerImage::printPreview() const { QPrintPreviewDialog* dlg = new QPrintPreviewDialog(m_view); connect(dlg, &QPrintPreviewDialog::paintRequested, m_view, &DatapickerImageView::print); return dlg->exec(); } /*! Selects or deselects the Datapicker/DatapickerImage in the project explorer. This function is called in \c DatapickerImageView. The DatapickerImage gets deselected if there are selected items in the view, and selected if there are no selected items in the view. */ void DatapickerImage::setSelectedInView(const bool b) { if (b) emit childAspectSelectedInView(this); else emit childAspectDeselectedInView(this); } void DatapickerImage::setSegmentsHoverEvent(const bool on) { m_segments->setAcceptHoverEvents(on); } QGraphicsScene* DatapickerImage::scene() const { return d->m_scene; } QRectF DatapickerImage::pageRect() const { return d->m_scene->sceneRect(); } void DatapickerImage::setPlotImageType(const DatapickerImage::PlotImageType type) { d->plotImageType = type; if (d->plotImageType == DatapickerImage::ProcessedImage) d->discretize(); emit requestUpdate(); } DatapickerImage::PlotImageType DatapickerImage::plotImageType() { return d->plotImageType; } /* =============================== getter methods for background options ================================= */ CLASS_D_READER_IMPL(DatapickerImage, QString, fileName, fileName) CLASS_D_READER_IMPL(DatapickerImage, DatapickerImage::ReferencePoints, axisPoints, axisPoints) CLASS_D_READER_IMPL(DatapickerImage, DatapickerImage::EditorSettings, settings, settings) BASIC_D_READER_IMPL(DatapickerImage, float, rotationAngle, rotationAngle) BASIC_D_READER_IMPL(DatapickerImage, DatapickerImage::PointsType, plotPointsType, plotPointsType) BASIC_D_READER_IMPL(DatapickerImage, int, pointSeparation, pointSeparation) BASIC_D_READER_IMPL(DatapickerImage, int, minSegmentLength, minSegmentLength) BASIC_D_READER_IMPL(DatapickerImage, Symbol::Style, pointStyle, pointStyle) BASIC_D_READER_IMPL(DatapickerImage, qreal, pointOpacity, pointOpacity) BASIC_D_READER_IMPL(DatapickerImage, qreal, pointRotationAngle, pointRotationAngle) BASIC_D_READER_IMPL(DatapickerImage, qreal, pointSize, pointSize) CLASS_D_READER_IMPL(DatapickerImage, QBrush, pointBrush, pointBrush) CLASS_D_READER_IMPL(DatapickerImage, QPen, pointPen, pointPen) BASIC_D_READER_IMPL(DatapickerImage, bool, pointVisibility, pointVisibility) /* ============================ setter methods and undo commands for background options ================= */ STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetFileName, QString, fileName, updateFileName) void DatapickerImage::setFileName(const QString& fileName) { if (fileName!= d->fileName) { beginMacro(i18n("%1: upload new image", name())); exec(new DatapickerImageSetFileNameCmd(d, fileName, ki18n("%1: upload image"))); endMacro(); } } STD_SETTER_CMD_IMPL_S(DatapickerImage, SetRotationAngle, float, rotationAngle) void DatapickerImage::setRotationAngle(float angle) { if (angle != d->rotationAngle) exec(new DatapickerImageSetRotationAngleCmd(d, angle, ki18n("%1: set rotation angle"))); } STD_SETTER_CMD_IMPL_S(DatapickerImage, SetAxisPoints, DatapickerImage::ReferencePoints, axisPoints) void DatapickerImage::setAxisPoints(const DatapickerImage::ReferencePoints& points) { if (memcmp(&points, &d->axisPoints, sizeof(points)) != 0) exec(new DatapickerImageSetAxisPointsCmd(d, points, ki18n("%1: set Axis points"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetSettings, DatapickerImage::EditorSettings, settings, discretize) void DatapickerImage::setSettings(const DatapickerImage::EditorSettings& editorSettings) { if (memcmp(&editorSettings, &d->settings, sizeof(editorSettings)) != 0) exec(new DatapickerImageSetSettingsCmd(d, editorSettings, ki18n("%1: set editor settings"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetMinSegmentLength, int, minSegmentLength, makeSegments) void DatapickerImage::setminSegmentLength(const int value) { if (d->minSegmentLength != value) exec(new DatapickerImageSetMinSegmentLengthCmd(d, value, ki18n("%1: set minimum segment length"))); ; } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetPointStyle, Symbol::Style, pointStyle, retransform) void DatapickerImage::setPointStyle(Symbol::Style newStyle) { if (newStyle != d->pointStyle) exec(new DatapickerImageSetPointStyleCmd(d, newStyle, ki18n("%1: set point's style"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetPointSize, qreal, pointSize, retransform) void DatapickerImage::setPointSize(qreal value) { if (!qFuzzyCompare(1 + value, 1 + d->pointSize)) exec(new DatapickerImageSetPointSizeCmd(d, value, ki18n("%1: set point's size"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetPointRotationAngle, qreal, pointRotationAngle, retransform) void DatapickerImage::setPointRotationAngle(qreal angle) { if (!qFuzzyCompare(1 + angle, 1 + d->pointRotationAngle)) exec(new DatapickerImageSetPointRotationAngleCmd(d, angle, ki18n("%1: rotate point"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetPointBrush, QBrush, pointBrush, retransform) void DatapickerImage::setPointBrush(const QBrush& newBrush) { if (newBrush != d->pointBrush) exec(new DatapickerImageSetPointBrushCmd(d, newBrush, ki18n("%1: set point's filling"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetPointPen, QPen, pointPen, retransform) void DatapickerImage::setPointPen(const QPen &newPen) { if (newPen != d->pointPen) exec(new DatapickerImageSetPointPenCmd(d, newPen, ki18n("%1: set outline style"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetPointOpacity, qreal, pointOpacity, retransform) void DatapickerImage::setPointOpacity(qreal newOpacity) { if (newOpacity != d->pointOpacity) exec(new DatapickerImageSetPointOpacityCmd(d, newOpacity, ki18n("%1: set point's opacity"))); } STD_SETTER_CMD_IMPL_F_S(DatapickerImage, SetPointVisibility, bool, pointVisibility, retransform) void DatapickerImage::setPointVisibility(const bool on) { if (on != d->pointVisibility) exec(new DatapickerImageSetPointVisibilityCmd(d, on, on ? ki18n("%1: set visible") : ki18n("%1: set invisible"))); } void DatapickerImage::setPrinting(bool on) const { - auto points = parentAspect()->children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + auto points = parentAspect()->children(ChildIndexFlag::Recursive | ChildIndexFlag::IncludeHidden); for (auto* point : points) point->setPrinting(on); } void DatapickerImage::setPlotPointsType(const PointsType pointsType) { if (d->plotPointsType == pointsType) return; d->plotPointsType = pointsType; if (pointsType == DatapickerImage::AxisPoints) { //clear image - auto points = children(AbstractAspect::IncludeHidden); + auto points = children(ChildIndexFlag::IncludeHidden); if (!points.isEmpty()) { beginMacro(i18n("%1: remove all axis points", name())); for (auto* point : points) point->remove(); endMacro(); } m_segments->setSegmentsVisible(false); } else if (pointsType == DatapickerImage::CurvePoints) m_segments->setSegmentsVisible(false); else if (pointsType == DatapickerImage::SegmentPoints) { d->makeSegments(); m_segments->setSegmentsVisible(true); } } void DatapickerImage::setPointSeparation(const int value) { d->pointSeparation = value; } //############################################################################## //###################### Private implementation ############################### //############################################################################## DatapickerImagePrivate::DatapickerImagePrivate(DatapickerImage *owner) : q(owner), pageRect(0, 0, 1000, 1000), m_scene(new QGraphicsScene(pageRect)) { } QString DatapickerImagePrivate::name() const { return q->name(); } void DatapickerImagePrivate::retransform() { - auto points = q->children(AbstractAspect::IncludeHidden); + auto points = q->children(AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* point : points) point->retransform(); } bool DatapickerImagePrivate::uploadImage(const QString& address) { bool rc = q->originalPlotImage.load(address); if (rc) { //convert the image to 32bit-format if this is not the case yet QImage::Format format = q->originalPlotImage.format(); if (format != QImage::Format_RGB32 && format != QImage::Format_ARGB32 && format != QImage::Format_ARGB32_Premultiplied) q->originalPlotImage = q->originalPlotImage.convertToFormat(QImage::Format_RGB32); q->processedPlotImage = q->originalPlotImage; q->background = ImageEditor::findBackgroundColor(&q->originalPlotImage); //upload Histogram ImageEditor::uploadHistogram(q->intensityBins, &q->originalPlotImage, q->background, DatapickerImage::Intensity); ImageEditor::uploadHistogram(q->foregroundBins, &q->originalPlotImage, q->background, DatapickerImage::Foreground); ImageEditor::uploadHistogram(q->hueBins, &q->originalPlotImage, q->background, DatapickerImage::Hue); ImageEditor::uploadHistogram(q->saturationBins, &q->originalPlotImage, q->background, DatapickerImage::Saturation); ImageEditor::uploadHistogram(q->valueBins, &q->originalPlotImage, q->background, DatapickerImage::Value); discretize(); //resize the screen double w = Worksheet::convertToSceneUnits(q->originalPlotImage.width(), Worksheet::Inch)/QApplication::desktop()->physicalDpiX(); double h = Worksheet::convertToSceneUnits(q->originalPlotImage.height(), Worksheet::Inch)/QApplication::desktop()->physicalDpiX(); m_scene->setSceneRect(0, 0, w, h); q->isLoaded = true; } return rc; } void DatapickerImagePrivate::discretize() { PERFTRACE("DatapickerImagePrivate::discretize()"); if (plotImageType != DatapickerImage::ProcessedImage) return; ImageEditor::discretize(&q->processedPlotImage, &q->originalPlotImage, settings, q->background); if (plotPointsType != DatapickerImage::SegmentPoints) emit q->requestUpdate(); else makeSegments(); } void DatapickerImagePrivate::makeSegments() { if (plotPointsType != DatapickerImage::SegmentPoints) return; PERFTRACE("DatapickerImagePrivate::makeSegments()"); q->m_segments->makeSegments(q->processedPlotImage); q->m_segments->setSegmentsVisible(true); emit q->requestUpdate(); } DatapickerImagePrivate::~DatapickerImagePrivate() { delete m_scene; } void DatapickerImagePrivate::updateFileName() { WAIT_CURSOR; q->isLoaded = false; const QString& address = fileName.trimmed(); if (!address.isEmpty()) { if (uploadImage(address)) fileName = address; } else { //hide segments if they are visible q->m_segments->setSegmentsVisible(false); } - auto points = q->parentAspect()->children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + auto points = q->parentAspect()->children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); if (!points.isEmpty()) { for (auto* point : points) point->remove(); } emit q->requestUpdate(); emit q->requestUpdateActions(); RESET_CURSOR; } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## //! Save as XML void DatapickerImage::save(QXmlStreamWriter* writer) const { writer->writeStartElement( "datapickerImage" ); writeBasicAttributes(writer); //general properties writer->writeStartElement( "general" ); writer->writeAttribute( "fileName", d->fileName ); writer->writeAttribute( "plotPointsType", QString::number(d->plotPointsType) ); writer->writeEndElement(); writer->writeStartElement( "axisPoint" ); writer->writeAttribute( "graphType", QString::number(d->axisPoints.type) ); writer->writeAttribute( "ternaryScale", QString::number(d->axisPoints.ternaryScale) ); writer->writeAttribute( "axisPointLogicalX1", QString::number(d->axisPoints.logicalPos[0].x()) ); writer->writeAttribute( "axisPointLogicalY1", QString::number(d->axisPoints.logicalPos[0].y()) ); writer->writeAttribute( "axisPointLogicalX2", QString::number(d->axisPoints.logicalPos[1].x()) ); writer->writeAttribute( "axisPointLogicalY2", QString::number(d->axisPoints.logicalPos[1].y()) ); writer->writeAttribute( "axisPointLogicalX3", QString::number(d->axisPoints.logicalPos[2].x()) ); writer->writeAttribute( "axisPointLogicalY3", QString::number(d->axisPoints.logicalPos[2].y()) ); writer->writeAttribute( "axisPointLogicalZ1", QString::number(d->axisPoints.logicalPos[0].z()) ); writer->writeAttribute( "axisPointLogicalZ2", QString::number(d->axisPoints.logicalPos[1].z()) ); writer->writeAttribute( "axisPointLogicalZ3", QString::number(d->axisPoints.logicalPos[2].z()) ); writer->writeAttribute( "axisPointSceneX1", QString::number(d->axisPoints.scenePos[0].x()) ); writer->writeAttribute( "axisPointSceneY1", QString::number(d->axisPoints.scenePos[0].y()) ); writer->writeAttribute( "axisPointSceneX2", QString::number(d->axisPoints.scenePos[1].x()) ); writer->writeAttribute( "axisPointSceneY2", QString::number(d->axisPoints.scenePos[1].y()) ); writer->writeAttribute( "axisPointSceneX3", QString::number(d->axisPoints.scenePos[2].x()) ); writer->writeAttribute( "axisPointSceneY3", QString::number(d->axisPoints.scenePos[2].y()) ); writer->writeEndElement(); //editor and segment settings writer->writeStartElement( "editorSettings" ); writer->writeAttribute( "plotImageType", QString::number(d->plotImageType) ); writer->writeAttribute( "rotationAngle", QString::number(d->rotationAngle) ); writer->writeAttribute( "minSegmentLength", QString::number(d->minSegmentLength) ); writer->writeAttribute( "pointSeparation", QString::number(d->pointSeparation) ); writer->writeAttribute( "foregroundThresholdHigh", QString::number(d->settings.foregroundThresholdHigh) ); writer->writeAttribute( "foregroundThresholdLow", QString::number(d->settings.foregroundThresholdLow) ); writer->writeAttribute( "hueThresholdHigh", QString::number(d->settings.hueThresholdHigh) ); writer->writeAttribute( "hueThresholdLow", QString::number(d->settings.hueThresholdLow) ); writer->writeAttribute( "intensityThresholdHigh", QString::number(d->settings.intensityThresholdHigh) ); writer->writeAttribute( "intensityThresholdLow", QString::number(d->settings.intensityThresholdLow) ); writer->writeAttribute( "saturationThresholdHigh", QString::number(d->settings.saturationThresholdHigh) ); writer->writeAttribute( "saturationThresholdLow", QString::number(d->settings.saturationThresholdLow) ); writer->writeAttribute( "valueThresholdHigh", QString::number(d->settings.valueThresholdHigh) ); writer->writeAttribute( "valueThresholdLow", QString::number(d->settings.valueThresholdLow) ); writer->writeEndElement(); //symbol properties writer->writeStartElement( "symbolProperties" ); writer->writeAttribute( "pointRotationAngle", QString::number(d->pointRotationAngle) ); writer->writeAttribute( "pointOpacity", QString::number(d->pointOpacity) ); writer->writeAttribute( "pointSize", QString::number(d->pointSize) ); writer->writeAttribute( "pointStyle", QString::number(d->pointStyle) ); writer->writeAttribute( "pointVisibility", QString::number(d->pointVisibility) ); WRITE_QBRUSH(d->pointBrush); WRITE_QPEN(d->pointPen); writer->writeEndElement(); //serialize all children - for (auto* child : children(IncludeHidden)) + for (auto* child : children(ChildIndexFlag::IncludeHidden)) child->save(writer); writer->writeEndElement(); } //! Load from XML bool DatapickerImage::load(XmlStreamReader* reader, bool preview) { 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() == "datapickerImage") break; if (!reader->isStartElement()) continue; if (!preview && reader->name() == "general") { attribs = reader->attributes(); str = attribs.value("fileName").toString(); d->fileName = str; READ_INT_VALUE("plotPointsType", plotPointsType, DatapickerImage::PointsType); } else if (!preview && reader->name() == "axisPoint") { attribs = reader->attributes(); READ_INT_VALUE("graphType", axisPoints.type, DatapickerImage::GraphType); READ_INT_VALUE("ternaryScale", axisPoints.ternaryScale, int); str = attribs.value("axisPointLogicalX1").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalX1").toString()); else d->axisPoints.logicalPos[0].setX(str.toDouble()); str = attribs.value("axisPointLogicalY1").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalY1").toString()); else d->axisPoints.logicalPos[0].setY(str.toDouble()); str = attribs.value("axisPointLogicalZ1").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalZ1").toString()); else d->axisPoints.logicalPos[0].setZ(str.toDouble()); str = attribs.value("axisPointLogicalX2").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalX2").toString()); else d->axisPoints.logicalPos[1].setX(str.toDouble()); str = attribs.value("axisPointLogicalY2").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalY2").toString()); else d->axisPoints.logicalPos[1].setY(str.toDouble()); str = attribs.value("axisPointLogicalZ2").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalZ2").toString()); else d->axisPoints.logicalPos[1].setZ(str.toDouble()); str = attribs.value("axisPointLogicalX3").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalX3").toString()); else d->axisPoints.logicalPos[2].setX(str.toDouble()); str = attribs.value("axisPointLogicalY3").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalY3").toString()); else d->axisPoints.logicalPos[2].setY(str.toDouble()); str = attribs.value("axisPointLogicalZ3").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointLogicalZ3").toString()); else d->axisPoints.logicalPos[2].setZ(str.toDouble()); str = attribs.value("axisPointSceneX1").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointSceneX1").toString()); else d->axisPoints.scenePos[0].setX(str.toDouble()); str = attribs.value("axisPointSceneY1").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointSceneY1").toString()); else d->axisPoints.scenePos[0].setY(str.toDouble()); str = attribs.value("axisPointSceneX2").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointSceneX2").toString()); else d->axisPoints.scenePos[1].setX(str.toDouble()); str = attribs.value("axisPointSceneY2").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointSceneY2").toString()); else d->axisPoints.scenePos[1].setY(str.toDouble()); str = attribs.value("axisPointSceneX3").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointSceneX3").toString()); else d->axisPoints.scenePos[2].setX(str.toDouble()); str = attribs.value("axisPointSceneY3").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("axisPointSceneY3").toString()); else d->axisPoints.scenePos[2].setY(str.toDouble()); } else if (!preview && reader->name() == "editorSettings") { attribs = reader->attributes(); READ_INT_VALUE("plotImageType", plotImageType, DatapickerImage::PlotImageType); READ_DOUBLE_VALUE("rotationAngle", rotationAngle); READ_INT_VALUE("minSegmentLength", minSegmentLength, int); READ_INT_VALUE("pointSeparation", pointSeparation, int); READ_INT_VALUE("foregroundThresholdHigh", settings.foregroundThresholdHigh, int); READ_INT_VALUE("foregroundThresholdLow", settings.foregroundThresholdLow, int); READ_INT_VALUE("hueThresholdHigh", settings.hueThresholdHigh, int); READ_INT_VALUE("hueThresholdLow", settings.hueThresholdLow, int); READ_INT_VALUE("intensityThresholdHigh", settings.intensityThresholdHigh, int); READ_INT_VALUE("intensityThresholdLow", settings.intensityThresholdLow, int); READ_INT_VALUE("saturationThresholdHigh", settings.saturationThresholdHigh, int); READ_INT_VALUE("saturationThresholdLow", settings.saturationThresholdLow, int); READ_INT_VALUE("valueThresholdHigh", settings.valueThresholdHigh, int); READ_INT_VALUE("valueThresholdLow", settings.valueThresholdLow, int); } else if (!preview && reader->name() == "symbolProperties") { attribs = reader->attributes(); READ_DOUBLE_VALUE("pointRotationAngle", pointRotationAngle); READ_DOUBLE_VALUE("pointOpacity", pointOpacity); READ_DOUBLE_VALUE("pointSize", pointSize); READ_INT_VALUE("pointStyle", pointStyle, Symbol::Style); READ_INT_VALUE("pointVisibility", pointVisibility, bool); READ_QBRUSH(d->pointBrush); READ_QPEN(d->pointPen); } else if (reader->name() == "datapickerPoint") { DatapickerPoint* datapickerPoint = new DatapickerPoint(QString()); datapickerPoint->setHidden(true); if (!datapickerPoint->load(reader, preview)) { delete datapickerPoint; return false; } else addChild(datapickerPoint); } else { // unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } d->uploadImage(d->fileName); d->retransform(); return true; } diff --git a/src/backend/datapicker/Segment.cpp b/src/backend/datapicker/Segment.cpp index 9f0d8b3e8..c268ecc69 100644 --- a/src/backend/datapicker/Segment.cpp +++ b/src/backend/datapicker/Segment.cpp @@ -1,199 +1,199 @@ /*************************************************************************** File : Segment.cpp Project : LabPlot Description : Graphics-item for curve of Datapicker -------------------------------------------------------------------- Copyright : (C) 2015 by Ankit Wagadre (wagadre.ankit@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 "Segment.h" #include "SegmentPrivate.h" #include "backend/datapicker/DatapickerImage.h" #include "backend/datapicker/DatapickerPoint.h" #include "backend/worksheet/Worksheet.h" #include "backend/datapicker/Datapicker.h" #include #include #include #include /** * \class Segment * \brief graphics-item class for curve-segment */ Segment::Segment(DatapickerImage* image) : m_image(image), d_ptr(new SegmentPrivate(this)) { m_image->scene()->addItem(this->graphicsItem()); } QGraphicsItem* Segment::graphicsItem() const { return d_ptr; } void Segment::setParentGraphicsItem(QGraphicsItem* item) { Q_D(Segment); d->setParentItem(item); } void Segment::retransform() { Q_D(Segment); d->retransform(); } bool Segment::isVisible() const { Q_D(const Segment); return d->isVisible(); } void Segment::setVisible(bool on) { Q_D(Segment); d->setVisible(on); } //############################################################################## //####################### Private implementation ############################### //############################################################################## SegmentPrivate::SegmentPrivate(Segment *owner) : scaleFactor(Worksheet::convertToSceneUnits(1, Worksheet::Inch)/QApplication::desktop()->physicalDpiX()), q(owner) { setFlag(QGraphicsItem::ItemIsSelectable); setFlag(QGraphicsItem::ItemSendsGeometryChanges); setAcceptHoverEvents(true); setVisible(false); pen = QPen(Qt::green, 3, Qt::SolidLine); } /*! calculates the position and the bounding box of the item. Called on geometry or properties changes. */ void SegmentPrivate::retransform() { QMatrix matrix; matrix.scale(scaleFactor, scaleFactor); for (auto* line : q->path) { const QLine& scaledLine = matrix.map(*line); linePath.moveTo(scaledLine.p1()); linePath.lineTo(scaledLine.p2()); } recalcShapeAndBoundingRect(); } /*! Returns the outer bounds of the item as a rectangle. */ QRectF SegmentPrivate::boundingRect() const { return boundingRectangle; } /*! Returns the shape of this item as a QPainterPath in local coordinates. */ QPainterPath SegmentPrivate::shape() const { return itemShape; } /*! recalculates the outer bounds and the shape of the item. */ void SegmentPrivate::recalcShapeAndBoundingRect() { prepareGeometryChange(); boundingRectangle = linePath.boundingRect(); itemShape = QPainterPath(); itemShape.addRect(boundingRectangle); } void SegmentPrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { Q_UNUSED(option) Q_UNUSED(widget) painter->setPen(pen); painter->drawPath(linePath); if (m_hovered && !isSelected()) { painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), 2, Qt::SolidLine)); painter->drawPath(linePath); } // if (isSelected()) { // painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), 2, Qt::SolidLine)); // painter->setOpacity(selectedOpacity); // painter->drawPath(itemShape); // } } void SegmentPrivate::hoverEnterEvent(QGraphicsSceneHoverEvent*) { if (!isSelected()) { m_hovered = true; update(); } } void SegmentPrivate::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { if (m_hovered) { m_hovered = false; update(); } } QVariant SegmentPrivate::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemSelectedChange && value == true) { auto* datapicker = static_cast(q->m_image->parentAspect()); if (datapicker->activeCurve()) { int count = 0; QList posList; posList.clear(); for (QLine* line : q->path) { const int l = (line->y1() > line->y2())?line->y2():line->y1(); const int h = (line->y1() > line->y2())?line->y1():line->y2(); for (int i = l; i <= h; ++i) { if (count%q->m_image->pointSeparation() == 0) { bool positionUsed = false; - const auto points = datapicker->activeCurve()->children(AbstractAspect::IncludeHidden); + const auto points = datapicker->activeCurve()->children(AbstractAspect::ChildIndexFlag::IncludeHidden); for (const auto* point : points) { if (point->position() == QPoint(line->x1(), i)*scaleFactor) positionUsed = true; } if (!positionUsed) posList<x1(), i)*scaleFactor; } count++; } } if (!posList.isEmpty()) { datapicker->activeCurve()->beginMacro(i18n("%1: draw points over segment", datapicker->activeCurve()->name())); for (const QPointF& pos : posList) datapicker->addNewPoint(pos, datapicker->activeCurve()); datapicker->activeCurve()->endMacro(); } } //no need to keep segment selected return false; } return QGraphicsItem::itemChange(change, value); } diff --git a/src/backend/datasources/LiveDataSource.cpp b/src/backend/datasources/LiveDataSource.cpp index a05e8adea..247db15da 100644 --- a/src/backend/datasources/LiveDataSource.cpp +++ b/src/backend/datasources/LiveDataSource.cpp @@ -1,943 +1,943 @@ /*************************************************************************** File : LiveDataSource.cpp Project : LabPlot Description : Represents live data source -------------------------------------------------------------------- Copyright : (C) 2009-2019 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2017 Fabian Kristof (fkristofszabolcs@gmail.com) Copyright : (C) 2018 Stefan Gerlach (stefan.gerlach@uni.kn) ***************************************************************************/ /*************************************************************************** * * * 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/LiveDataSource.h" #include "backend/datasources/filters/AsciiFilter.h" #include "backend/datasources/filters/FITSFilter.h" #include "backend/datasources/filters/BinaryFilter.h" #include "backend/datasources/filters/ROOTFilter.h" #include "backend/core/Project.h" #include "commonfrontend/spreadsheet/SpreadsheetView.h" #include "kdefrontend/spreadsheet/PlotDataDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*! \class LiveDataSource \brief Represents data stored in a file. Reading and writing is done with the help of appropriate I/O-filters. \ingroup datasources */ LiveDataSource::LiveDataSource(const QString& name, bool loading) : Spreadsheet(name, loading, AspectType::LiveDataSource), m_updateTimer(new QTimer(this)), m_watchTimer(new QTimer(this)) { initActions(); connect(m_updateTimer, &QTimer::timeout, this, &LiveDataSource::read); connect(m_watchTimer, &QTimer::timeout, this, &LiveDataSource::readOnUpdate); } LiveDataSource::~LiveDataSource() { //stop reading before deleting the objects pauseReading(); delete m_filter; delete m_fileSystemWatcher; delete m_localSocket; delete m_tcpSocket; delete m_serialPort; } void LiveDataSource::initActions() { m_plotDataAction = new QAction(QIcon::fromTheme("office-chart-line"), i18n("Plot data"), this); connect(m_plotDataAction, &QAction::triggered, this, &LiveDataSource::plotData); m_watchTimer->setSingleShot(true); m_watchTimer->setInterval(100); } QWidget* LiveDataSource::view() const { if (!m_partView) { m_view = new SpreadsheetView(const_cast(this), true); m_partView = m_view; } return m_partView; } /*! * \brief Returns a list with the names of the available ports */ QStringList LiveDataSource::availablePorts() { QStringList ports; // qDebug() << "available ports count:" << QSerialPortInfo::availablePorts().size(); for (const QSerialPortInfo& sp : QSerialPortInfo::availablePorts()) { ports.append(sp.portName()); DEBUG(" port " << STDSTRING(sp.portName()) << ": " << STDSTRING(sp.systemLocation()) << STDSTRING(sp.description()) << ' ' << STDSTRING(sp.manufacturer()) << ' ' << STDSTRING(sp.serialNumber())); } // For Testing: // ports.append("/dev/pts/26"); return ports; } /*! * \brief Returns a list with the supported baud rates */ QStringList LiveDataSource::supportedBaudRates() { QStringList baudRates; for (const auto& baud : QSerialPortInfo::standardBaudRates()) baudRates.append(QString::number(baud)); return baudRates; } /*! * \brief Updates this data source at this moment */ void LiveDataSource::updateNow() { DEBUG("LiveDataSource::updateNow() update interval = " << m_updateInterval); if (m_updateType == TimeInterval) m_updateTimer->stop(); else m_pending = false; read(); //restart the timer after update if (m_updateType == TimeInterval && !m_paused) m_updateTimer->start(m_updateInterval); } /*! * \brief Continue reading from the live data source after it was paused */ void LiveDataSource::continueReading() { m_paused = false; if (m_pending) { m_pending = false; updateNow(); } } /*! * \brief Pause the reading of the live data source */ void LiveDataSource::pauseReading() { m_paused = true; if (m_updateType == TimeInterval) { m_pending = true; m_updateTimer->stop(); } } void LiveDataSource::setFileName(const QString& name) { m_fileName = name; } QString LiveDataSource::fileName() const { return m_fileName; } /*! * \brief Sets the local socket's server name to name * \param name */ void LiveDataSource::setLocalSocketName(const QString& name) { m_localSocketName = name; } QString LiveDataSource::localSocketName() const { return m_localSocketName; } void LiveDataSource::setFileType(AbstractFileFilter::FileType type) { m_fileType = type; } AbstractFileFilter::FileType LiveDataSource::fileType() const { return m_fileType; } void LiveDataSource::setFilter(AbstractFileFilter* f) { delete m_filter; m_filter = f; } AbstractFileFilter* LiveDataSource::filter() const { return m_filter; } /*! * \brief Sets the serial port's baud rate * \param baudrate */ void LiveDataSource::setBaudRate(int baudrate) { m_baudRate = baudrate; } int LiveDataSource::baudRate() const { return m_baudRate; } /*! * \brief Sets the source's update interval to \c interval * \param interval */ void LiveDataSource::setUpdateInterval(int interval) { m_updateInterval = interval; if (!m_paused) m_updateTimer->start(m_updateInterval); } int LiveDataSource::updateInterval() const { return m_updateInterval; } /*! * \brief Sets how many values we should keep when keepLastValues is true * \param keepnvalues */ void LiveDataSource::setKeepNValues(int keepnvalues) { m_keepNValues = keepnvalues; } int LiveDataSource::keepNValues() const { return m_keepNValues; } /*! * \brief Sets the network socket's port to port * \param port */ void LiveDataSource::setPort(quint16 port) { m_port = port; } void LiveDataSource::setBytesRead(qint64 bytes) { m_bytesRead = bytes; } int LiveDataSource::bytesRead() const { return m_bytesRead; } int LiveDataSource::port() const { return m_port; } /*! * \brief Sets the serial port's name to name * \param name */ void LiveDataSource::setSerialPort(const QString& name) { m_serialPortName = name; } QString LiveDataSource::serialPortName() const { return m_serialPortName; } bool LiveDataSource::isPaused() const { return m_paused; } /*! * \brief Sets the sample size to size * \param size */ void LiveDataSource::setSampleSize(int size) { m_sampleSize = size; } int LiveDataSource::sampleSize() const { return m_sampleSize; } /*! * \brief Sets the source's type to sourcetype * \param sourcetype */ void LiveDataSource::setSourceType(SourceType sourcetype) { m_sourceType = sourcetype; } LiveDataSource::SourceType LiveDataSource::sourceType() const { return m_sourceType; } /*! * \brief Sets the source's reading type to readingType * \param readingType */ void LiveDataSource::setReadingType(ReadingType readingType) { m_readingType = readingType; } LiveDataSource::ReadingType LiveDataSource::readingType() const { return m_readingType; } /*! * \brief Sets the source's update type to updatetype and handles this change * \param updatetype */ void LiveDataSource::setUpdateType(UpdateType updatetype) { switch (updatetype) { case NewData: { m_updateTimer->stop(); if (!m_fileSystemWatcher) m_fileSystemWatcher = new QFileSystemWatcher(this); m_fileSystemWatcher->addPath(m_fileName); QFileInfo file(m_fileName); // If the watched file currently does not exist (because it is recreated for instance), watch its containing // directory instead. Once the file exists again, switch to watching the file in readOnUpdate(). // Reading will only start 100ms after the last update, to prevent continuous re-reading while the file is updated. // If the watched file intentionally is updated more often than that, the user should switch to periodic reading. if (m_fileSystemWatcher->files().contains(m_fileName)) m_fileSystemWatcher->removePath(file.absolutePath()); else m_fileSystemWatcher->addPath(file.absolutePath()); connect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, [&](){ m_watchTimer->start(); }); connect(m_fileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, [&](){ m_watchTimer->start(); }); break; } case TimeInterval: delete m_fileSystemWatcher; m_fileSystemWatcher = nullptr; break; } m_updateType = updatetype; } LiveDataSource::UpdateType LiveDataSource::updateType() const { return m_updateType; } /*! * \brief Sets the network socket's host * \param host */ void LiveDataSource::setHost(const QString& host) { m_host = host.simplified(); } QString LiveDataSource::host() const { return m_host; } /*! sets whether only a link to the file is saved in the project file (\c b=true) or the whole content of the file (\c b=false). */ void LiveDataSource::setFileLinked(bool b) { m_fileLinked = b; } /*! returns \c true if only a link to the file is saved in the project file. \c false otherwise. */ bool LiveDataSource::isFileLinked() const { return m_fileLinked; } void LiveDataSource::setUseRelativePath(bool b) { m_relativePath = b; } bool LiveDataSource::useRelativePath() const { return m_relativePath; } QIcon LiveDataSource::icon() const { QIcon icon; switch (m_fileType) { case AbstractFileFilter::Ascii: icon = QIcon::fromTheme("text-plain"); break; case AbstractFileFilter::Binary: icon = QIcon::fromTheme("application-octet-stream"); break; case AbstractFileFilter::Image: icon = QIcon::fromTheme("image-x-generic"); break; // TODO: missing icons case AbstractFileFilter::HDF5: case AbstractFileFilter::NETCDF: break; case AbstractFileFilter::FITS: icon = QIcon::fromTheme("kstars_fitsviewer"); break; case AbstractFileFilter::JSON: icon = QIcon::fromTheme("application-json"); break; case AbstractFileFilter::ROOT: case AbstractFileFilter::NgspiceRawAscii: case AbstractFileFilter::NgspiceRawBinary: break; } return icon; } QMenu* LiveDataSource::createContextMenu() { QMenu* menu = AbstractPart::createContextMenu(); QAction* firstAction = nullptr; // if we're populating the context menu for the project explorer, then //there're already actions available there. Skip the first title-action //and insert the action at the beginning of the menu. if (menu->actions().size() > 1) firstAction = menu->actions().at(1); menu->insertAction(firstAction, m_plotDataAction); menu->insertSeparator(firstAction); return menu; } //############################################################################## //################################# SLOTS #################################### //############################################################################## /* * Called when the watch timer times out, i.e. when modifying the file or directory * presumably has finished. Also see LiveDataSource::setUpdateType(). */ void LiveDataSource::readOnUpdate() { if (!m_fileSystemWatcher->files().contains(m_fileName)) { m_fileSystemWatcher->addPath(m_fileName); QFileInfo file(m_fileName); if (m_fileSystemWatcher->files().contains(m_fileName)) m_fileSystemWatcher->removePath(file.absolutePath()); else { m_fileSystemWatcher->addPath(file.absolutePath()); return; } } if (m_paused) // flag file for reading, once the user decides to continue reading m_pending = true; else read(); } /* * called periodically or on new data changes (file changed, new data in the socket, etc.) */ void LiveDataSource::read() { DEBUG("\nLiveDataSource::read()"); if (!m_filter) return; if (m_reading) return; m_reading = true; //initialize the device (file, socket, serial port) when calling this function for the first time if (!m_prepared) { DEBUG(" Preparing device: update type = " << ENUM_TO_STRING(LiveDataSource, UpdateType, m_updateType)); switch (m_sourceType) { case FileOrPipe: delete m_device; m_device = new QFile(m_fileName); break; case NetworkTcpSocket: m_tcpSocket = new QTcpSocket(this); m_device = m_tcpSocket; m_tcpSocket->connectToHost(m_host, m_port, QIODevice::ReadOnly); connect(m_tcpSocket, &QTcpSocket::readyRead, this, &LiveDataSource::readyRead); connect(m_tcpSocket, static_cast(&QTcpSocket::error), this, &LiveDataSource::tcpSocketError); break; case NetworkUdpSocket: m_udpSocket = new QUdpSocket(this); m_device = m_udpSocket; m_udpSocket->bind(QHostAddress(m_host), m_port); m_udpSocket->connectToHost(m_host, 0, QUdpSocket::ReadOnly); // only connect to readyRead when update is on new data if (m_updateType == NewData) connect(m_udpSocket, &QUdpSocket::readyRead, this, &LiveDataSource::readyRead); connect(m_udpSocket, static_cast(&QUdpSocket::error), this, &LiveDataSource::tcpSocketError); break; case LocalSocket: m_localSocket = new QLocalSocket(this); m_device = m_localSocket; m_localSocket->connectToServer(m_localSocketName, QLocalSocket::ReadOnly); connect(m_localSocket, &QLocalSocket::readyRead, this, &LiveDataSource::readyRead); connect(m_localSocket, static_cast(&QLocalSocket::error), this, &LiveDataSource::localSocketError); break; case SerialPort: m_serialPort = new QSerialPort(this); m_device = m_serialPort; DEBUG(" Serial: " << STDSTRING(m_serialPortName) << ", " << m_baudRate); m_serialPort->setBaudRate(m_baudRate); m_serialPort->setPortName(m_serialPortName); m_serialPort->open(QIODevice::ReadOnly); // only connect to readyRead when update is on new data if (m_updateType == NewData) connect(m_serialPort, &QSerialPort::readyRead, this, &LiveDataSource::readyRead); connect(m_serialPort, static_cast(&QSerialPort::error), this, &LiveDataSource::serialPortError); break; case MQTT: break; } m_prepared = true; } qint64 bytes = 0; switch (m_sourceType) { case FileOrPipe: DEBUG("Reading FileOrPipe. type = " << ENUM_TO_STRING(AbstractFileFilter, FileType, m_fileType)); switch (m_fileType) { case AbstractFileFilter::Ascii: if (m_readingType == LiveDataSource::ReadingType::WholeFile) { static_cast(m_filter)->readFromLiveDevice(*m_device, this, 0); } else { bytes = static_cast(m_filter)->readFromLiveDevice(*m_device, this, m_bytesRead); m_bytesRead += bytes; DEBUG("Read " << bytes << " bytes, in total: " << m_bytesRead); } break; case AbstractFileFilter::Binary: //TODO: not implemented yet // bytes = qSharedPointerCast(m_filter)->readFromLiveDevice(*m_file, this, m_bytesRead); // m_bytesRead += bytes; case AbstractFileFilter::ROOT: case AbstractFileFilter::NgspiceRawAscii: case AbstractFileFilter::NgspiceRawBinary: //only re-reading of the whole file is supported m_filter->readDataFromFile(m_fileName, this); break; //TODO: other types not implemented yet case AbstractFileFilter::Image: case AbstractFileFilter::HDF5: case AbstractFileFilter::NETCDF: case AbstractFileFilter::FITS: case AbstractFileFilter::JSON: break; } break; case NetworkTcpSocket: DEBUG("reading from TCP socket. state before abort = " << m_tcpSocket->state()); m_tcpSocket->abort(); m_tcpSocket->connectToHost(m_host, m_port, QIODevice::ReadOnly); DEBUG("reading from TCP socket. state after reconnect = " << m_tcpSocket->state()); break; case NetworkUdpSocket: DEBUG(" Reading from UDP socket. state = " << m_udpSocket->state()); // reading data here if (m_fileType == AbstractFileFilter::Ascii) static_cast(m_filter)->readFromLiveDeviceNotFile(*m_device, this); break; case LocalSocket: DEBUG(" Reading from local socket. state before abort = " << m_localSocket->state()); if (m_localSocket->state() == QLocalSocket::ConnectingState) m_localSocket->abort(); m_localSocket->connectToServer(m_localSocketName, QLocalSocket::ReadOnly); if (m_localSocket->waitForConnected()) m_localSocket->waitForReadyRead(); DEBUG(" Reading from local socket. state after reconnect = " << m_localSocket->state()); break; case SerialPort: DEBUG(" Reading from serial port"); // reading data here if (m_fileType == AbstractFileFilter::Ascii) static_cast(m_filter)->readFromLiveDeviceNotFile(*m_device, this); break; case MQTT: break; } m_reading = false; } /*! * Slot for the signal that is emitted once every time new data is available for reading from the device (not UDP or Serial). * It will only be emitted again once new data is available, such as when a new payload of network data has arrived on the network socket, * or when a new block of data has been appended to your device. */ void LiveDataSource::readyRead() { DEBUG("LiveDataSource::readyRead() update type = " << ENUM_TO_STRING(LiveDataSource,UpdateType,m_updateType)); DEBUG(" REMAINING TIME = " << m_updateTimer->remainingTime()); if (m_fileType == AbstractFileFilter::Ascii) static_cast(m_filter)->readFromLiveDeviceNotFile(*m_device, this); //TODO: not implemented yet // else if (m_fileType == AbstractFileFilter::Binary) // dynamic_cast(m_filter)->readFromLiveDeviceNotFile(*m_device, this); //since we won't have the timer to call read() where we create new connections //for sequential devices in read() we just request data/connect to servers if (m_updateType == NewData) read(); } void LiveDataSource::localSocketError(QLocalSocket::LocalSocketError socketError) { Q_UNUSED(socketError); /*disconnect(m_localSocket, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(localSocketError(QLocalSocket::LocalSocketError))); disconnect(m_localSocket, SIGNAL(readyRead()), this, SLOT(readyRead()));*/ /*switch (socketError) { case QLocalSocket::ServerNotFoundError: QMessageBox::critical(0, i18n("Local Socket Error"), i18n("The socket was not found. Please check the socket name.")); break; case QLocalSocket::ConnectionRefusedError: QMessageBox::critical(0, i18n("Local Socket Error"), i18n("The connection was refused by the peer")); break; case QLocalSocket::PeerClosedError: QMessageBox::critical(0, i18n("Local Socket Error"), i18n("The socket has closed the connection.")); break; default: QMessageBox::critical(0, i18n("Local Socket Error"), i18n("The following error occurred: %1.", m_localSocket->errorString())); }*/ } void LiveDataSource::tcpSocketError(QAbstractSocket::SocketError socketError) { Q_UNUSED(socketError); /*switch (socketError) { case QAbstractSocket::ConnectionRefusedError: QMessageBox::critical(0, i18n("TCP Socket Error"), i18n("The connection was refused by the peer. Make sure the server is running and check the host name and port settings.")); break; case QAbstractSocket::RemoteHostClosedError: QMessageBox::critical(0, i18n("TCP Socket Error"), i18n("The remote host closed the connection.")); break; case QAbstractSocket::HostNotFoundError: QMessageBox::critical(0, i18n("TCP Socket Error"), i18n("The host was not found. Please check the host name and port settings.")); break; default: QMessageBox::critical(0, i18n("TCP Socket Error"), i18n("The following error occurred: %1.", m_tcpSocket->errorString())); }*/ } void LiveDataSource::serialPortError(QSerialPort::SerialPortError serialPortError) { switch (serialPortError) { case QSerialPort::DeviceNotFoundError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Failed to open the device.")); break; case QSerialPort::PermissionError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Failed to open the device. Please check your permissions on this device.")); break; case QSerialPort::OpenError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Device already opened.")); break; case QSerialPort::NotOpenError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("The device is not opened.")); break; case QSerialPort::ReadError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Failed to read data.")); break; case QSerialPort::ResourceError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("Failed to read data. The device is removed.")); break; case QSerialPort::TimeoutError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("The device timed out.")); break; #ifndef _MSC_VER //MSVC complains about the usage of deprecated enums, g++ and clang complain about missing enums case QSerialPort::ParityError: case QSerialPort::FramingError: case QSerialPort::BreakConditionError: #endif case QSerialPort::WriteError: case QSerialPort::UnsupportedOperationError: case QSerialPort::UnknownError: QMessageBox::critical(nullptr, i18n("Serial Port Error"), i18n("The following error occurred: %1.", m_serialPort->errorString())); break; case QSerialPort::NoError: break; } } void LiveDataSource::plotData() { auto* dlg = new PlotDataDialog(this); dlg->exec(); } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## /*! Saves as XML. */ void LiveDataSource::save(QXmlStreamWriter* writer) const { writer->writeStartElement("liveDataSource"); writeBasicAttributes(writer); writeCommentElement(writer); //general writer->writeStartElement("general"); switch (m_sourceType) { case FileOrPipe: writer->writeAttribute("fileType", QString::number(m_fileType)); writer->writeAttribute("fileLinked", QString::number(m_fileLinked)); writer->writeAttribute("relativePath", QString::number(m_relativePath)); if (m_relativePath) { //convert from the absolute to the relative path and save it const Project* p = const_cast(this)->project(); QFileInfo fi(p->fileName()); writer->writeAttribute("fileName", fi.dir().relativeFilePath(m_fileName)); }else writer->writeAttribute("fileName", m_fileName); break; case SerialPort: writer->writeAttribute("baudRate", QString::number(m_baudRate)); writer->writeAttribute("serialPortName", m_serialPortName); break; case NetworkTcpSocket: case NetworkUdpSocket: writer->writeAttribute("host", m_host); writer->writeAttribute("port", QString::number(m_port)); break; case LocalSocket: break; case MQTT: break; default: break; } writer->writeAttribute("updateType", QString::number(m_updateType)); writer->writeAttribute("readingType", QString::number(m_readingType)); writer->writeAttribute("sourceType", QString::number(m_sourceType)); writer->writeAttribute("keepNValues", QString::number(m_keepNValues)); if (m_updateType == TimeInterval) writer->writeAttribute("updateInterval", QString::number(m_updateInterval)); if (m_readingType != TillEnd) writer->writeAttribute("sampleSize", QString::number(m_sampleSize)); writer->writeEndElement(); //general //filter m_filter->save(writer); //columns if (!m_fileLinked) { - for (auto* col : children(IncludeHidden)) + for (auto* col : children(ChildIndexFlag::IncludeHidden)) col->save(writer); } writer->writeEndElement(); // "liveDataSource" } /*! Loads from XML. */ bool LiveDataSource::load(XmlStreamReader* reader, bool preview) { 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() == "liveDataSource" || reader->name() == "LiveDataSource")) //TODO: remove "LiveDataSources" in couple of releases 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("fileName").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("fileName").toString()); else m_fileName = str; str = attribs.value("fileType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("fileType").toString()); else m_fileType = (AbstractFileFilter::FileType)str.toInt(); str = attribs.value("fileLinked").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("fileLinked").toString()); else m_fileLinked = str.toInt(); str = attribs.value("relativePath").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("relativePath").toString()); else m_relativePath = str.toInt(); str = attribs.value("updateType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("updateType").toString()); else m_updateType = static_cast(str.toInt()); str = attribs.value("sourceType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("sourceType").toString()); else m_sourceType = static_cast(str.toInt()); str = attribs.value("readingType").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("readingType").toString()); else m_readingType = static_cast(str.toInt()); if (m_updateType == TimeInterval) { str = attribs.value("updateInterval").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("updateInterval").toString()); else m_updateInterval = str.toInt(); } if (m_readingType != TillEnd) { str = attribs.value("sampleSize").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("sampleSize").toString()); else m_sampleSize = str.toInt(); } switch (m_sourceType) { case SerialPort: str = attribs.value("baudRate").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("baudRate").toString()); else m_baudRate = str.toInt(); str = attribs.value("serialPortName").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("serialPortName").toString()); else m_serialPortName = str; break; case NetworkTcpSocket: case NetworkUdpSocket: str = attribs.value("host").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("host").toString()); else m_host = str; str = attribs.value("port").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("port").toString()); else m_host = str; break; case MQTT: break; case FileOrPipe: break; case LocalSocket: break; default: break; } } else if (reader->name() == "asciiFilter") { setFilter(new AsciiFilter); if (!m_filter->load(reader)) return false; } else if (reader->name() == "rootFilter") { setFilter(new ROOTFilter); if (!m_filter->load(reader)) return false; } else if (reader->name() == "column") { Column* column = new Column(QString(), AbstractColumn::Text); if (!column->load(reader, preview)) { delete column; setColumnCount(0); return false; } addChild(column); } else {// unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } return !reader->hasError(); } void LiveDataSource::finalizeLoad() { //convert from the relative path saved in the project file to the absolute file to work with if (m_relativePath) { QFileInfo fi(project()->fileName()); m_fileName = fi.dir().absoluteFilePath(m_fileName); } //read the content of the file if it was only linked if (m_fileLinked && QFile::exists(m_fileName)) this->read(); //call setUpdateType() to start watching the file for changes, is required setUpdateType(m_updateType); } diff --git a/src/backend/datasources/projects/OriginProjectParser.cpp b/src/backend/datasources/projects/OriginProjectParser.cpp index cd8130762..aedd95051 100644 --- a/src/backend/datasources/projects/OriginProjectParser.cpp +++ b/src/backend/datasources/projects/OriginProjectParser.cpp @@ -1,2143 +1,2143 @@ /*************************************************************************** File : OriginProjectParser.h Project : LabPlot Description : parser for Origin projects -------------------------------------------------------------------- Copyright : (C) 2017-2018 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2017-2019 Stefan Gerlach (stefan.gerlach@uni.kn) ***************************************************************************/ /*************************************************************************** * * * 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/projects/OriginProjectParser.h" #include "backend/core/column/Column.h" #include "backend/core/datatypes/Double2StringFilter.h" #include "backend/core/datatypes/DateTime2StringFilter.h" #include "backend/core/Project.h" #include "backend/core/Workbook.h" #include "backend/matrix/Matrix.h" #include "backend/note/Note.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/worksheet/Worksheet.h" #include "backend/worksheet/plots/cartesian/Axis.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/worksheet/plots/cartesian/CartesianPlotLegend.h" #include "backend/worksheet/plots/cartesian/XYCurve.h" #include "backend/worksheet/plots/cartesian/XYEquationCurve.h" #include "backend/worksheet/TextLabel.h" #include #include #include #include #include #include /*! \class OriginProjectParser \brief parser for Origin projects. \ingroup datasources */ OriginProjectParser::OriginProjectParser() : ProjectParser() { m_topLevelClasses = {AspectType::Folder, AspectType::Workbook, AspectType::Spreadsheet, AspectType::Matrix, AspectType::Worksheet, AspectType::Note}; } bool OriginProjectParser::isOriginProject(const QString& fileName) { //TODO add opju later when liborigin supports it return fileName.endsWith(QLatin1String(".opj"), Qt::CaseInsensitive); } void OriginProjectParser::setImportUnusedObjects(bool importUnusedObjects) { m_importUnusedObjects = importUnusedObjects; } bool OriginProjectParser::hasUnusedObjects() { m_originFile = new OriginFile((const char*)m_projectFileName.toLocal8Bit()); if (!m_originFile->parse()) { delete m_originFile; m_originFile = nullptr; return false; } for (unsigned int i = 0; i < m_originFile->spreadCount(); i++) { const Origin::SpreadSheet& spread = m_originFile->spread(i); if (spread.objectID < 0) return true; } for (unsigned int i = 0; i < m_originFile->excelCount(); i++) { const Origin::Excel& excel = m_originFile->excel(i); if (excel.objectID < 0) return true; } for (unsigned int i = 0; i < m_originFile->matrixCount(); i++) { const Origin::Matrix& originMatrix = m_originFile->matrix(i); if (originMatrix.objectID < 0) return true; } delete m_originFile; m_originFile = nullptr; return false; } QString OriginProjectParser::supportedExtensions() { //TODO add opju later when liborigin supports it static const QString extensions = "*.opj *.OPJ"; return extensions; } unsigned int OriginProjectParser::findSpreadByName(const QString& name) { for (unsigned int i = 0; i < m_originFile->spreadCount(); i++) { const Origin::SpreadSheet& spread = m_originFile->spread(i); if (spread.name == name.toStdString()) { m_spreadNameList << name; return i; } } return 0; } unsigned int OriginProjectParser::findMatrixByName(const QString& name) { for (unsigned int i = 0; i < m_originFile->matrixCount(); i++) { const Origin::Matrix& originMatrix = m_originFile->matrix(i); if (originMatrix.name == name.toStdString()) { m_matrixNameList << name; return i; } } return 0; } unsigned int OriginProjectParser::findExcelByName(const QString& name) { for (unsigned int i = 0; i < m_originFile->excelCount(); i++) { const Origin::Excel& excel = m_originFile->excel(i); if (excel.name == name.toStdString()) { m_excelNameList << name; return i; } } return 0; } unsigned int OriginProjectParser::findGraphByName(const QString& name) { for (unsigned int i = 0; i < m_originFile->graphCount(); i++) { const Origin::Graph& graph = m_originFile->graph(i); if (graph.name == name.toStdString()) { m_graphNameList << name; return i; } } return 0; } unsigned int OriginProjectParser::findNoteByName(const QString& name) { for (unsigned int i = 0; i < m_originFile->noteCount(); i++) { const Origin::Note& originNote = m_originFile->note(i); if (originNote.name == name.toStdString()) { m_noteNameList << name; return i; } } return 0; } //############################################################################## //############## Deserialization from Origin's project tree #################### //############################################################################## bool OriginProjectParser::load(Project* project, bool preview) { DEBUG("OriginProjectParser::load()"); //read and parse the m_originFile-file m_originFile = new OriginFile((const char*)m_projectFileName.toLocal8Bit()); if (!m_originFile->parse()) { delete m_originFile; m_originFile = nullptr; return false; } //Origin project tree and the iterator pointing to the root node const tree* projectTree = m_originFile->project(); tree::iterator projectIt = projectTree->begin(projectTree->begin()); m_spreadNameList.clear(); m_excelNameList.clear(); m_matrixNameList.clear(); m_graphNameList.clear(); m_noteNameList.clear(); //convert the project tree from liborigin's representation to LabPlot's project object project->setIsLoading(true); if (projectIt.node) { // only opj files from version >= 6.0 do have project tree DEBUG(" have a project tree"); QString name(QString::fromLatin1(projectIt->name.c_str())); project->setName(name); project->setCreationTime(creationTime(projectIt)); loadFolder(project, projectIt, preview); } else { // for lower versions put all windows on rootfolder DEBUG(" have no project tree"); int pos = m_projectFileName.lastIndexOf(QDir::separator()) + 1; project->setName((const char*)m_projectFileName.mid(pos).toLocal8Bit()); } // imports all loose windows (like prior version 6 which has no project tree) handleLooseWindows(project, preview); //restore column pointers: //1. extend the pathes to contain the parent structures first //2. restore the pointers from the pathes - const QVector columns = project->children(AbstractAspect::Recursive); - const QVector spreadsheets = project->children(AbstractAspect::Recursive); - for (auto* curve : project->children(AbstractAspect::Recursive)) { + const QVector columns = project->children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector spreadsheets = project->children(AbstractAspect::ChildIndexFlag::Recursive); + for (auto* curve : project->children(AbstractAspect::ChildIndexFlag::Recursive)) { curve->suppressRetransform(true); //x-column QString spreadsheetName = curve->xColumnPath().left(curve->xColumnPath().indexOf(QLatin1Char('/'))); for (const auto* spreadsheet : spreadsheets) { if (spreadsheet->name() == spreadsheetName) { const QString& newPath = spreadsheet->parentAspect()->path() + '/' + curve->xColumnPath(); curve->setXColumnPath(newPath); for (auto* column : columns) { if (!column) continue; if (column->path() == newPath) { curve->setXColumn(column); break; } } break; } } //x-column spreadsheetName = curve->yColumnPath().left(curve->yColumnPath().indexOf(QLatin1Char('/'))); for (const auto* spreadsheet : spreadsheets) { if (spreadsheet->name() == spreadsheetName) { const QString& newPath = spreadsheet->parentAspect()->path() + '/' + curve->yColumnPath(); curve->setYColumnPath(newPath); for (auto* column : columns) { if (!column) continue; if (column->path() == newPath) { curve->setYColumn(column); break; } } break; } } //TODO: error columns curve->suppressRetransform(false); } if (!preview) { - for (auto* plot : project->children(AbstractAspect::Recursive)) { + for (auto* plot : project->children(AbstractAspect::ChildIndexFlag::Recursive)) { plot->setIsLoading(false); plot->retransform(); } } emit project->loaded(); project->setIsLoading(false); delete m_originFile; m_originFile = nullptr; return true; } bool OriginProjectParser::loadFolder(Folder* folder, tree::iterator baseIt, bool preview) { DEBUG("OriginProjectParser::loadFolder()") const tree* projectTree = m_originFile->project(); // do not skip anything if pathesToLoad() contains only root folder bool containsRootFolder = (folder->pathesToLoad().size() == 1 && folder->pathesToLoad().contains(folder->path())); if (containsRootFolder) { DEBUG(" pathesToLoad contains only folder path \"" << STDSTRING(folder->path()) << "\". Clearing pathes to load.") folder->setPathesToLoad(QStringList()); } //load folder's children: logic for reading the selected objects only is similar to Folder::readChildAspectElement for (tree::sibling_iterator it = projectTree->begin(baseIt); it != projectTree->end(baseIt); ++it) { QString name(QString::fromLatin1(it->name.c_str())); //name of the current child DEBUG(" * folder item name = " << STDSTRING(name)) //check whether we need to skip the loading of the current child if (!folder->pathesToLoad().isEmpty()) { //child's path is not available yet (child not added yet) -> construct the path manually const QString childPath = folder->path() + '/' + name; DEBUG(" path = " << STDSTRING(childPath)) //skip the current child aspect it is not in the list of aspects to be loaded if (folder->pathesToLoad().indexOf(childPath) == -1) { DEBUG(" skip it!") continue; } } //load top-level children AbstractAspect* aspect = nullptr; switch (it->type) { case Origin::ProjectNode::Folder: { DEBUG(" top level folder"); Folder* f = new Folder(name); if (!folder->pathesToLoad().isEmpty()) { //a child folder to be read -> provide the list of aspects to be loaded to the child folder, too. //since the child folder and all its children are not added yet (path() returns empty string), //we need to remove the path of the current child folder from the full pathes provided in pathesToLoad. //E.g. we want to import the path "Project/Folder/Spreadsheet" in the following project // Project // \Spreadsheet // \Folder // \Spreadsheet // //Here, we remove the part "Project/Folder/" and proceed for this child folder with "Spreadsheet" only. //With this the logic above where it is determined whether to import the child aspect or not works out. //manually construct the path of the child folder to be read const QString& curFolderPath = folder->path() + '/' + name; //remove the path of the current child folder QStringList pathesToLoadNew; for (const auto& path : folder->pathesToLoad()) { if (path.startsWith(curFolderPath)) pathesToLoadNew << path.right(path.length() - curFolderPath.length()); } f->setPathesToLoad(pathesToLoadNew); } loadFolder(f, it, preview); aspect = f; break; } case Origin::ProjectNode::SpreadSheet: { DEBUG(" top level spreadsheet"); Spreadsheet* spreadsheet = new Spreadsheet(name); loadSpreadsheet(spreadsheet, preview, name); aspect = spreadsheet; break; } case Origin::ProjectNode::Graph: { DEBUG(" top level graph"); Worksheet* worksheet = new Worksheet(name); worksheet->setIsLoading(true); worksheet->setTheme(QString()); loadWorksheet(worksheet, preview); aspect = worksheet; break; } case Origin::ProjectNode::Matrix: { DEBUG(" top level matrix"); const Origin::Matrix& originMatrix = m_originFile->matrix(findMatrixByName(name)); DEBUG(" matrix name = " << originMatrix.name); DEBUG(" number of sheets = " << originMatrix.sheets.size()); if (originMatrix.sheets.size() == 1) { // single sheet -> load into a matrix Matrix* matrix = new Matrix(name); loadMatrix(matrix, preview); aspect = matrix; } else { // multiple sheets -> load into a workbook Workbook* workbook = new Workbook(name); loadMatrixWorkbook(workbook, preview); aspect = workbook; } break; } case Origin::ProjectNode::Excel: { DEBUG(" top level excel"); Workbook* workbook = new Workbook(name); loadWorkbook(workbook, preview); aspect = workbook; break; } case Origin::ProjectNode::Note: { DEBUG("top level note"); Note* note = new Note(name); loadNote(note, preview); aspect = note; break; } case Origin::ProjectNode::Graph3D: default: //TODO: add UnsupportedAspect break; } if (aspect) { folder->addChildFast(aspect); aspect->setCreationTime(creationTime(it)); aspect->setIsLoading(false); } } // ResultsLog QString resultsLog = QString::fromLatin1(m_originFile->resultsLogString().c_str()); if (resultsLog.length() > 0) { DEBUG("Results log:\t\tyes"); Note* note = new Note("ResultsLog"); if (preview) folder->addChildFast(note); else { //only import the log if it is in the list of aspects to be loaded const QString childPath = folder->path() + '/' + note->name(); if (folder->pathesToLoad().indexOf(childPath) != -1) { note->setText(resultsLog); folder->addChildFast(note); } } } else DEBUG("Results log:\t\tno"); return folder; } void OriginProjectParser::handleLooseWindows(Folder* folder, bool preview) { DEBUG("OriginProjectParser::handleLooseWindows()"); QDEBUG("pathes to load:" << folder->pathesToLoad()); m_spreadNameList.removeDuplicates(); m_excelNameList.removeDuplicates(); m_matrixNameList.removeDuplicates(); m_graphNameList.removeDuplicates(); m_noteNameList.removeDuplicates(); QDEBUG(" spreads =" << m_spreadNameList); QDEBUG(" excels =" << m_excelNameList); QDEBUG(" matrices =" << m_matrixNameList); QDEBUG(" graphs =" << m_graphNameList); QDEBUG(" notes =" << m_noteNameList); DEBUG("Number of spreads loaded:\t" << m_spreadNameList.size() << ", in file: " << m_originFile->spreadCount()); DEBUG("Number of excels loaded:\t" << m_excelNameList.size() << ", in file: " << m_originFile->excelCount()); DEBUG("Number of matrices loaded:\t" << m_matrixNameList.size() << ", in file: " << m_originFile->matrixCount()); DEBUG("Number of graphs loaded:\t" << m_graphNameList.size() << ", in file: " << m_originFile->graphCount()); DEBUG("Number of notes loaded:\t\t" << m_noteNameList.size() << ", in file: " << m_originFile->noteCount()); // loop over all spreads to find loose ones for (unsigned int i = 0; i < m_originFile->spreadCount(); i++) { AbstractAspect* aspect = nullptr; const Origin::SpreadSheet& spread = m_originFile->spread(i); QString name = QString::fromStdString(spread.name); DEBUG(" spread.objectId = " << spread.objectID); // skip unused spreads if selected if (spread.objectID < 0 && !m_importUnusedObjects) { DEBUG(" Dropping unused loose spread: " << STDSTRING(name)); continue; } const QString childPath = folder->path() + '/' + name; // we could also use spread.loose if (!m_spreadNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) { DEBUG(" Adding loose spread: " << STDSTRING(name)); Spreadsheet* spreadsheet = new Spreadsheet(name); loadSpreadsheet(spreadsheet, preview, name); aspect = spreadsheet; } if (aspect) { folder->addChildFast(aspect); DEBUG(" creation time as reported by liborigin: " << spread.creationDate); aspect->setCreationTime(QDateTime::fromTime_t(spread.creationDate)); } } // loop over all excels to find loose ones for (unsigned int i = 0; i < m_originFile->excelCount(); i++) { AbstractAspect* aspect = nullptr; const Origin::Excel& excel = m_originFile->excel(i); QString name = QString::fromStdString(excel.name); DEBUG(" excel.objectId = " << excel.objectID); // skip unused data sets if selected if (excel.objectID < 0 && !m_importUnusedObjects) { DEBUG(" Dropping unused loose excel: " << STDSTRING(name)); continue; } const QString childPath = folder->path() + '/' + name; // we could also use excel.loose if (!m_excelNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) { DEBUG(" Adding loose excel: " << STDSTRING(name)); DEBUG(" containing number of sheets = " << excel.sheets.size()); Workbook* workbook = new Workbook(name); loadWorkbook(workbook, preview); aspect = workbook; } if (aspect) { folder->addChildFast(aspect); DEBUG(" creation time as reported by liborigin: " << excel.creationDate); aspect->setCreationTime(QDateTime::fromTime_t(excel.creationDate)); } } // loop over all matrices to find loose ones for (unsigned int i = 0; i < m_originFile->matrixCount(); i++) { AbstractAspect* aspect = nullptr; const Origin::Matrix& originMatrix = m_originFile->matrix(i); QString name = QString::fromStdString(originMatrix.name); DEBUG(" originMatrix.objectId = " << originMatrix.objectID); // skip unused data sets if selected if (originMatrix.objectID < 0 && !m_importUnusedObjects) { DEBUG(" Dropping unused loose matrix: " << STDSTRING(name)); continue; } const QString childPath = folder->path() + '/' + name; if (!m_matrixNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) { DEBUG(" Adding loose matrix: " << STDSTRING(name)); DEBUG(" containing number of sheets = " << originMatrix.sheets.size()); if (originMatrix.sheets.size() == 1) { // single sheet -> load into a matrix Matrix* matrix = new Matrix(name); loadMatrix(matrix, preview); aspect = matrix; } else { // multiple sheets -> load into a workbook Workbook* workbook = new Workbook(name); loadMatrixWorkbook(workbook, preview); aspect = workbook; } } if (aspect) { folder->addChildFast(aspect); aspect->setCreationTime(QDateTime::fromTime_t(originMatrix.creationDate)); } } // handle loose graphs (is this even possible?) for (unsigned int i = 0; i < m_originFile->graphCount(); i++) { AbstractAspect* aspect = nullptr; const Origin::Graph& graph = m_originFile->graph(i); QString name = QString::fromStdString(graph.name); DEBUG(" graph.objectId = " << graph.objectID); // skip unused graph if selected if (graph.objectID < 0 && !m_importUnusedObjects) { DEBUG(" Dropping unused loose graph: " << STDSTRING(name)); continue; } const QString childPath = folder->path() + '/' + name; if (!m_graphNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) { DEBUG(" Adding loose graph: " << STDSTRING(name)); Worksheet* worksheet = new Worksheet(name); loadWorksheet(worksheet, preview); aspect = worksheet; } if (aspect) { folder->addChildFast(aspect); aspect->setCreationTime(QDateTime::fromTime_t(graph.creationDate)); } } // handle loose notes (is this even possible?) for (unsigned int i = 0; i < m_originFile->noteCount(); i++) { AbstractAspect* aspect = nullptr; const Origin::Note& originNote = m_originFile->note(i); QString name = QString::fromStdString(originNote.name); DEBUG(" originNote.objectId = " << originNote.objectID); // skip unused notes if selected if (originNote.objectID < 0 && !m_importUnusedObjects) { DEBUG(" Dropping unused loose note: " << STDSTRING(name)); continue; } const QString childPath = folder->path() + '/' + name; if (!m_noteNameList.contains(name) && (preview || folder->pathesToLoad().indexOf(childPath) != -1)) { DEBUG(" Adding loose note: " << STDSTRING(name)); Note* note = new Note(name); loadNote(note, preview); aspect = note; } if (aspect) { folder->addChildFast(aspect); aspect->setCreationTime(QDateTime::fromTime_t(originNote.creationDate)); } } } bool OriginProjectParser::loadWorkbook(Workbook* workbook, bool preview) { DEBUG("loadWorkbook()"); //load workbook sheets const Origin::Excel& excel = m_originFile->excel(findExcelByName(workbook->name())); DEBUG(" excel name = " << excel.name); DEBUG(" number of sheets = " << excel.sheets.size()); for (unsigned int s = 0; s < excel.sheets.size(); ++s) { Spreadsheet* spreadsheet = new Spreadsheet(QString::fromLatin1(excel.sheets[s].name.c_str())); loadSpreadsheet(spreadsheet, preview, workbook->name(), s); workbook->addChildFast(spreadsheet); } return true; } // load spreadsheet from spread (sheetIndex == -1) or from excel (only sheet sheetIndex) bool OriginProjectParser::loadSpreadsheet(Spreadsheet* spreadsheet, bool preview, const QString& name, int sheetIndex) { DEBUG("loadSpreadsheet() sheetIndex = " << sheetIndex); //load spreadsheet data Origin::SpreadSheet spread; Origin::Excel excel; if (sheetIndex == -1) // spread spread = m_originFile->spread(findSpreadByName(name)); else { excel = m_originFile->excel(findExcelByName(name)); spread = excel.sheets[sheetIndex]; } const size_t cols = spread.columns.size(); int rows = 0; for (size_t j = 0; j < cols; ++j) rows = std::max((int)spread.columns[j].data.size(), rows); // alternative: int rows = excel.maxRows; DEBUG("loadSpreadsheet() cols/maxRows = " << cols << "/" << rows); //TODO QLocale locale = mw->locale(); spreadsheet->setRowCount(rows); spreadsheet->setColumnCount((int)cols); if (sheetIndex == -1) spreadsheet->setComment(QString::fromLatin1(spread.label.c_str())); else spreadsheet->setComment(QString::fromLatin1(excel.label.c_str())); //in Origin column width is measured in characters, we need to convert to pixels //TODO: determine the font used in Origin in order to get the same column width as in Origin QFont font; QFontMetrics fm(font); const int scaling_factor = fm.maxWidth(); for (size_t j = 0; j < cols; ++j) { Origin::SpreadColumn column = spread.columns[j]; Column* col = spreadsheet->column((int)j); QString name(column.name.c_str()); col->setName(name.replace(QRegExp(".*_"), QString())); if (preview) continue; //TODO: we don't support any formulas for cells yet. // if (column.command.size() > 0) // col->setFormula(Interval(0, rows), QString(column.command.c_str())); col->setComment(QString::fromLatin1(column.comment.c_str())); col->setWidth((int)column.width * scaling_factor); //plot designation switch (column.type) { case Origin::SpreadColumn::X: col->setPlotDesignation(AbstractColumn::X); break; case Origin::SpreadColumn::Y: col->setPlotDesignation(AbstractColumn::Y); break; case Origin::SpreadColumn::Z: col->setPlotDesignation(AbstractColumn::Z); break; case Origin::SpreadColumn::XErr: col->setPlotDesignation(AbstractColumn::XError); break; case Origin::SpreadColumn::YErr: col->setPlotDesignation(AbstractColumn::YError); break; case Origin::SpreadColumn::Label: case Origin::SpreadColumn::NONE: default: col->setPlotDesignation(AbstractColumn::NoDesignation); } QString format; switch (column.valueType) { case Origin::Numeric: { for (unsigned int i = column.beginRow; i < column.endRow; ++i) { const double value = column.data.at(i).as_double(); if (value != _ONAN) col->setValueAt(i, value); } loadColumnNumericFormat(column, col); break; } case Origin::TextNumeric: { //A TextNumeric column can contain numeric and string values, there is no equivalent column mode in LabPlot. // -> Set the column mode as 'Numeric' or 'Text' depending on the type of first non-empty element in column. for (unsigned int i = column.beginRow; i < column.endRow; ++i) { const Origin::variant value(column.data.at(i)); if (value.type() == Origin::Variant::V_DOUBLE) { if (value.as_double() != _ONAN) break; } else { if (value.as_string() != nullptr) { col->setColumnMode(AbstractColumn::Text); break; } } } if (col->columnMode() == AbstractColumn::Numeric) { for (unsigned int i = column.beginRow; i < column.endRow; ++i) { const double value = column.data.at(i).as_double(); if (column.data.at(i).type() == Origin::Variant::V_DOUBLE && value != _ONAN) col->setValueAt(i, value); } loadColumnNumericFormat(column, col); } else { for (unsigned int i = column.beginRow; i < column.endRow; ++i) { const Origin::variant value(column.data.at(i)); if (value.type() == Origin::Variant::V_STRING) { if (value.as_string() != nullptr) col->setTextAt(i, value.as_string()); } else { if (value.as_double() != _ONAN) col->setTextAt(i, QString::number(value.as_double())); } } } break; } case Origin::Text: col->setColumnMode(AbstractColumn::Text); for (int i = 0; i < min((int)column.data.size(), rows); ++i) col->setTextAt(i, column.data[i].as_string()); break; case Origin::Time: { switch (column.valueTypeSpecification + 128) { case Origin::TIME_HH_MM: format = "hh:mm"; break; case Origin::TIME_HH: format = "hh"; break; case Origin::TIME_HH_MM_SS: format = "hh:mm:ss"; break; case Origin::TIME_HH_MM_SS_ZZ: format = "hh:mm:ss.zzz"; break; case Origin::TIME_HH_AP: format = "hh ap"; break; case Origin::TIME_HH_MM_AP: format = "hh:mm ap"; break; case Origin::TIME_MM_SS: format = "mm:ss"; break; case Origin::TIME_MM_SS_ZZ: format = "mm:ss.zzz"; break; case Origin::TIME_HHMM: format = "hhmm"; break; case Origin::TIME_HHMMSS: format = "hhmmss"; break; case Origin::TIME_HH_MM_SS_ZZZ: format = "hh:mm:ss.zzz"; break; } for (int i = 0; i < min((int)column.data.size(), rows); ++i) col->setValueAt(i, column.data[i].as_double()); col->setColumnMode(AbstractColumn::DateTime); DateTime2StringFilter *filter = static_cast(col->outputFilter()); filter->setFormat(format); break; } case Origin::Date: { switch (column.valueTypeSpecification) { case Origin::DATE_DD_MM_YYYY: format = "dd/MM/yyyy"; break; case Origin::DATE_DD_MM_YYYY_HH_MM: format = "dd/MM/yyyy HH:mm"; break; case Origin::DATE_DD_MM_YYYY_HH_MM_SS: format = "dd/MM/yyyy HH:mm:ss"; break; case Origin::DATE_DDMMYYYY: case Origin::DATE_DDMMYYYY_HH_MM: case Origin::DATE_DDMMYYYY_HH_MM_SS: format = "dd.MM.yyyy"; break; case Origin::DATE_MMM_D: format = "MMM d"; break; case Origin::DATE_M_D: format = "M/d"; break; case Origin::DATE_D: format = 'd'; break; case Origin::DATE_DDD: case Origin::DATE_DAY_LETTER: format = "ddd"; break; case Origin::DATE_YYYY: format = "yyyy"; break; case Origin::DATE_YY: format = "yy"; break; case Origin::DATE_YYMMDD: case Origin::DATE_YYMMDD_HH_MM: case Origin::DATE_YYMMDD_HH_MM_SS: case Origin::DATE_YYMMDD_HHMM: case Origin::DATE_YYMMDD_HHMMSS: format = "yyMMdd"; break; case Origin::DATE_MMM: case Origin::DATE_MONTH_LETTER: format = "MMM"; break; case Origin::DATE_M_D_YYYY: format = "M-d-yyyy"; break; default: format = "dd.MM.yyyy"; } for (int i = 0; i < min((int)column.data.size(), rows); ++i) col->setValueAt(i, column.data[i].as_double()); col->setColumnMode(AbstractColumn::DateTime); DateTime2StringFilter *filter = static_cast(col->outputFilter()); filter->setFormat(format); break; } case Origin::Month: { switch (column.valueTypeSpecification) { case Origin::MONTH_MMM: format = "MMM"; break; case Origin::MONTH_MMMM: format = "MMMM"; break; case Origin::MONTH_LETTER: format = 'M'; break; } for (int i = 0; i < min((int)column.data.size(), rows); ++i) col->setValueAt(i, column.data[i].as_double()); col->setColumnMode(AbstractColumn::Month); DateTime2StringFilter *filter = static_cast(col->outputFilter()); filter->setFormat(format); break; } case Origin::Day: { switch (column.valueTypeSpecification) { case Origin::DAY_DDD: format = "ddd"; break; case Origin::DAY_DDDD: format = "dddd"; break; case Origin::DAY_LETTER: format = 'd'; break; } for (int i = 0; i < min((int)column.data.size(), rows); ++i) col->setValueAt(i, column.data[i].as_double()); col->setColumnMode(AbstractColumn::Day); DateTime2StringFilter *filter = static_cast(col->outputFilter()); filter->setFormat(format); break; } case Origin::ColumnHeading: case Origin::TickIndexedDataset: case Origin::Categorical: break; } } //TODO: "hidden" not supported yet // if (spread.hidden || spread.loose) // mw->hideWindow(spreadsheet); return true; } void OriginProjectParser::loadColumnNumericFormat(const Origin::SpreadColumn& originColumn, Column* column) const { if (originColumn.numericDisplayType != 0) { int fi = 0; switch (originColumn.valueTypeSpecification) { case Origin::Decimal: fi = 1; break; case Origin::Scientific: fi = 2; break; case Origin::Engineering: case Origin::DecimalWithMarks: break; } Double2StringFilter* filter = static_cast(column->outputFilter()); filter->setNumericFormat(fi); filter->setNumDigits(originColumn.decimalPlaces); } } bool OriginProjectParser::loadMatrixWorkbook(Workbook* workbook, bool preview) { DEBUG("loadMatrixWorkbook()"); //load matrix workbook sheets const Origin::Matrix& originMatrix = m_originFile->matrix(findMatrixByName(workbook->name())); for (size_t s = 0; s < originMatrix.sheets.size(); ++s) { Matrix* matrix = new Matrix(QString::fromLatin1(originMatrix.sheets[s].name.c_str())); loadMatrix(matrix, preview, s, workbook->name()); workbook->addChildFast(matrix); } return true; } bool OriginProjectParser::loadMatrix(Matrix* matrix, bool preview, size_t sheetIndex, const QString& mwbName) { DEBUG("loadMatrix()"); //import matrix data const Origin::Matrix& originMatrix = m_originFile->matrix(findMatrixByName(mwbName)); if (preview) return true; //in Origin column width is measured in characters, we need to convert to pixels //TODO: determine the font used in Origin in order to get the same column width as in Origin QFont font; QFontMetrics fm(font); const int scaling_factor = fm.maxWidth(); const Origin::MatrixSheet& layer = originMatrix.sheets[sheetIndex]; const int colCount = layer.columnCount; const int rowCount = layer.rowCount; matrix->setRowCount(rowCount); matrix->setColumnCount(colCount); matrix->setFormula(layer.command.c_str()); //TODO: how to handle different widths for different columns? for (int j = 0; j < colCount; j++) matrix->setColumnWidth(j, layer.width * scaling_factor); //TODO: check column major vs. row major to improve the performance here for (int i = 0; i < rowCount; i++) { for (int j = 0; j < colCount; j++) matrix->setCell(i, j, layer.data[j + i*colCount]); } char format = 'g'; //TODO: prec not support by Matrix //int prec = 6; switch (layer.valueTypeSpecification) { case 0: //Decimal 1000 format = 'f'; // prec = layer.decimalPlaces; break; case 1: //Scientific format = 'e'; // prec = layer.decimalPlaces; break; case 2: //Engineering case 3: //Decimal 1,000 format = 'g'; // prec = layer.significantDigits; break; } matrix->setNumericFormat(format); return true; } bool OriginProjectParser::loadWorksheet(Worksheet* worksheet, bool preview) { DEBUG("OriginProjectParser::loadWorksheet()"); //load worksheet data const Origin::Graph& graph = m_originFile->graph(findGraphByName(worksheet->name())); DEBUG(" graph name = " << graph.name); worksheet->setComment(graph.label.c_str()); //TODO: width, height, view mode (print view, page view, window view, draft view) //Origin allows to freely resize the window and ajusts the size of the plot (layer) automatically //by keeping a certain width-to-height ratio. It's not clear what the actual size of the plot/layer is and how to handle this. //For now we simply create a new wokrsheet here with it's default size and make it using the whole view size. //Later we can decide to use one of the following properties: // 1) Window.frameRect gives Rect-corner coordinates (in pixels) of the Window object // 2) GraphLayer.clientRect gives Rect-corner coordinates (pixels) of the Layer inside the (printer?) page. // 3) Graph.width, Graph.height give the (printer?) page size in pixels. // const QRectF size(0, 0, // Worksheet::convertToSceneUnits(graph.width/600., Worksheet::Inch), // Worksheet::convertToSceneUnits(graph.height/600., Worksheet::Inch)); // worksheet->setPageRect(size); worksheet->setUseViewSize(true); QHash textLabelPositions; // worksheet background color const Origin::ColorGradientDirection bckgColorGradient = graph.windowBackgroundColorGradient; const Origin::Color bckgBaseColor = graph.windowBackgroundColorBase; const Origin::Color bckgEndColor = graph.windowBackgroundColorEnd; worksheet->setBackgroundColorStyle(backgroundColorStyle(bckgColorGradient)); switch (bckgColorGradient) { case Origin::ColorGradientDirection::NoGradient: case Origin::ColorGradientDirection::TopLeft: case Origin::ColorGradientDirection::Left: case Origin::ColorGradientDirection::BottomLeft: case Origin::ColorGradientDirection::Top: worksheet->setBackgroundFirstColor(color(bckgEndColor)); worksheet->setBackgroundSecondColor(color(bckgBaseColor)); break; case Origin::ColorGradientDirection::Center: break; case Origin::ColorGradientDirection::Bottom: case Origin::ColorGradientDirection::TopRight: case Origin::ColorGradientDirection::Right: case Origin::ColorGradientDirection::BottomRight: worksheet->setBackgroundFirstColor(color(bckgBaseColor)); worksheet->setBackgroundSecondColor(color(bckgEndColor)); } //TODO: do we need changes on the worksheet layout? //add plots int index = 1; for (const auto& layer : graph.layers) { if (!layer.is3D()) { CartesianPlot* plot = new CartesianPlot(i18n("Plot%1", QString::number(index))); worksheet->addChildFast(plot); plot->setIsLoading(true); //TODO: width, height //background color const Origin::Color& regColor = layer.backgroundColor; if (regColor.type == Origin::Color::None) plot->plotArea()->setBackgroundOpacity(0); else plot->plotArea()->setBackgroundFirstColor(color(regColor)); //border if (layer.borderType == Origin::BorderType::None) plot->plotArea()->setBorderPen(QPen(Qt::NoPen)); else plot->plotArea()->setBorderPen(QPen(Qt::SolidLine)); //ranges plot->setAutoScaleX(false); plot->setAutoScaleY(false); const Origin::GraphAxis& originXAxis = layer.xAxis; const Origin::GraphAxis& originYAxis = layer.yAxis; plot->setXMin(originXAxis.min); plot->setXMax(originXAxis.max); plot->setYMin(originYAxis.min); plot->setYMax(originYAxis.max); //scales switch (originXAxis.scale) { case Origin::GraphAxis::Linear: plot->setXScale(CartesianPlot::ScaleLinear); break; case Origin::GraphAxis::Log10: plot->setXScale(CartesianPlot::ScaleLog10); break; case Origin::GraphAxis::Ln: plot->setXScale(CartesianPlot::ScaleLn); break; case Origin::GraphAxis::Log2: plot->setXScale(CartesianPlot::ScaleLog2); break; case Origin::GraphAxis::Probability: case Origin::GraphAxis::Probit: case Origin::GraphAxis::Reciprocal: case Origin::GraphAxis::OffsetReciprocal: case Origin::GraphAxis::Logit: //TODO: plot->setXScale(CartesianPlot::ScaleLinear); break; } switch (originYAxis.scale) { case Origin::GraphAxis::Linear: plot->setYScale(CartesianPlot::ScaleLinear); break; case Origin::GraphAxis::Log10: plot->setYScale(CartesianPlot::ScaleLog10); break; case Origin::GraphAxis::Ln: plot->setYScale(CartesianPlot::ScaleLn); break; case Origin::GraphAxis::Log2: plot->setYScale(CartesianPlot::ScaleLog2); break; case Origin::GraphAxis::Probability: case Origin::GraphAxis::Probit: case Origin::GraphAxis::Reciprocal: case Origin::GraphAxis::OffsetReciprocal: case Origin::GraphAxis::Logit: //TODO: plot->setYScale(CartesianPlot::ScaleLinear); break; } //axes //x bottom if (layer.curves.size()) { Origin::GraphCurve originCurve = layer.curves[0]; QString xColumnName = QString::fromLatin1(originCurve.xColumnName.c_str()); //TODO: "Partikelgrö" DEBUG(" xColumnName = " << STDSTRING(xColumnName)); QDEBUG(" UTF8 xColumnName = " << xColumnName.toUtf8()); QString yColumnName = QString::fromLatin1(originCurve.yColumnName.c_str()); if (!originXAxis.formatAxis[0].hidden) { Axis* axis = new Axis("x", Axis::AxisHorizontal); axis->setSuppressRetransform(true); axis->setPosition(Axis::AxisBottom); plot->addChildFast(axis); loadAxis(originXAxis, axis, 0, xColumnName); axis->setSuppressRetransform(false); } //x top if (!originXAxis.formatAxis[1].hidden) { Axis* axis = new Axis("x top", Axis::AxisHorizontal); axis->setPosition(Axis::AxisTop); axis->setSuppressRetransform(true); plot->addChildFast(axis); loadAxis(originXAxis, axis, 1, xColumnName); axis->setSuppressRetransform(false); } //y left if (!originYAxis.formatAxis[0].hidden) { Axis* axis = new Axis("y", Axis::AxisVertical); axis->setSuppressRetransform(true); axis->setPosition(Axis::AxisLeft); plot->addChildFast(axis); loadAxis(originYAxis, axis, 0, yColumnName); axis->setSuppressRetransform(false); } //y right if (!originYAxis.formatAxis[1].hidden) { Axis* axis = new Axis("y right", Axis::AxisVertical); axis->setSuppressRetransform(true); axis->setPosition(Axis::AxisRight); plot->addChildFast(axis); loadAxis(originYAxis, axis, 1, yColumnName); axis->setSuppressRetransform(false); } } else { //TODO: ? } //range breaks //TODO //add legend if available const Origin::TextBox& originLegend = layer.legend; const QString& legendText = QString::fromLatin1(originLegend.text.c_str()); DEBUG(" legend text = " << STDSTRING(legendText)); if (!originLegend.text.empty()) { CartesianPlotLegend* legend = new CartesianPlotLegend(plot, i18n("legend")); //Origin's legend uses "\l(...)" or "\L(...)" string to format the legend symbol // and "%(...) to format the legend text for each curve //s. a. https://www.originlab.com/doc/Origin-Help/Legend-ManualControl //the text before these formatting tags, if available, is interpreted as the legend title QString legendTitle; //search for the first occurrence of the legend symbol substring int index = legendText.indexOf(QLatin1String("\\l("), 0, Qt::CaseInsensitive); if (index != -1) legendTitle = legendText.left(index); else { //check legend text index = legendText.indexOf(QLatin1String("%(")); if (index != -1) legendTitle = legendText.left(index); } legendTitle = legendTitle.trimmed(); if (!legendTitle.isEmpty()) legendTitle = parseOriginText(legendTitle); DEBUG(" legend title = " << STDSTRING(legendTitle)); legend->title()->setText(legendTitle); //TODO: text color //const Origin::Color& originColor = originLegend.color; //position //TODO: for the first release with OPJ support we put the legend to the bottom left corner, //in the next release we'll evaluate originLegend.clientRect giving the position inside of the whole page in Origin. //In Origin the legend can be placed outside of the plot which is not possible in LabPlot. //To achieve this we'll need to increase padding area in the plot and to place the legend outside of the plot area. CartesianPlotLegend::PositionWrapper position; position.horizontalPosition = CartesianPlotLegend::hPositionRight; position.verticalPosition = CartesianPlotLegend::vPositionBottom; legend->setPosition(position); //rotation legend->setRotationAngle(originLegend.rotation); //border line if (originLegend.borderType == Origin::BorderType::None) legend->setBorderPen(QPen(Qt::NoPen)); else legend->setBorderPen(QPen(Qt::SolidLine)); //background color, determine it with the help of the border type if (originLegend.borderType == Origin::BorderType::DarkMarble) legend->setBackgroundFirstColor(Qt::darkGray); else if (originLegend.borderType == Origin::BorderType::BlackOut) legend->setBackgroundFirstColor(Qt::black); else legend->setBackgroundFirstColor(Qt::white); plot->addLegend(legend); } //texts for (const auto& s : layer.texts) { DEBUG("EXTRA TEXT = " << s.text.c_str()); TextLabel* label = new TextLabel("text label"); label->setText(parseOriginText(QString::fromLatin1(s.text.c_str()))); plot->addChild(label); label->setParentGraphicsItem(plot->graphicsItem()); //position //determine the relative position inside of the layer rect const float horRatio = (float)(s.clientRect.left-layer.clientRect.left)/(layer.clientRect.right-layer.clientRect.left); const float vertRatio = (float)(s.clientRect.top-layer.clientRect.top)/(layer.clientRect.bottom-layer.clientRect.top); textLabelPositions[label] = QSizeF(horRatio, vertRatio); DEBUG("horizontal/vertical ratio = " << horRatio << ", " << vertRatio); //rotation label->setRotationAngle(s.rotation); //TODO: // Color color; // unsigned short fontSize; // int tab; // BorderType borderType; // Attach attach; } //curves int curveIndex = 1; for (const auto& originCurve : layer.curves) { QString data(originCurve.dataName.c_str()); switch (data[0].toLatin1()) { case 'T': case 'E': { if (originCurve.type == Origin::GraphCurve::Line || originCurve.type == Origin::GraphCurve::Scatter || originCurve.type == Origin::GraphCurve::LineSymbol || originCurve.type == Origin::GraphCurve::ErrorBar || originCurve.type == Origin::GraphCurve::XErrorBar) { // parse and use legend text // find substring between %c{curveIndex} and %c{curveIndex+1} int pos1 = legendText.indexOf(QString("\\c{%1}").arg(curveIndex)) + 5; int pos2 = legendText.indexOf(QString("\\c{%1}").arg(curveIndex+1)); QString curveText = legendText.mid(pos1, pos2 - pos1); // replace %(1), %(2), etc. with curve name curveText.replace(QString("%(%1)").arg(curveIndex), QString::fromLatin1(originCurve.yColumnName.c_str())); curveText = curveText.trimmed(); DEBUG(" curve " << curveIndex << " text = " << STDSTRING(curveText)); //XYCurve* xyCurve = new XYCurve(i18n("Curve%1", QString::number(curveIndex))); //TODO: curve (legend) does not support HTML text yet. //XYCurve* xyCurve = new XYCurve(curveText); XYCurve* curve = new XYCurve(QString::fromLatin1(originCurve.yColumnName.c_str())); const QString& tableName = data.right(data.length() - 2); curve->setXColumnPath(tableName + '/' + originCurve.xColumnName.c_str()); curve->setYColumnPath(tableName + '/' + originCurve.yColumnName.c_str()); curve->suppressRetransform(true); if (!preview) loadCurve(originCurve, curve); plot->addChildFast(curve); curve->suppressRetransform(false); } else if (originCurve.type == Origin::GraphCurve::Column) { //vertical bars } else if (originCurve.type == Origin::GraphCurve::Bar) { //horizontal bars } else if (originCurve.type == Origin::GraphCurve::Histogram) { } } break; case 'F': { Origin::Function function; const vector::difference_type funcIndex = m_originFile->functionIndex(data.right(data.length()-2).toStdString().c_str()); if (funcIndex < 0) { ++curveIndex; continue; } function = m_originFile->function(funcIndex); XYEquationCurve* xyEqCurve = new XYEquationCurve(function.name.c_str()); XYEquationCurve::EquationData eqData; eqData.count = function.totalPoints; eqData.expression1 = QString(function.formula.c_str()); if (function.type == Origin::Function::Polar) { eqData.type = XYEquationCurve::Polar; //replace 'x' by 'phi' eqData.expression1 = eqData.expression1.replace('x', "phi"); //convert from degrees to radians eqData.min = QString::number(function.begin/180) + QLatin1String("*pi"); eqData.max = QString::number(function.end/180) + QLatin1String("*pi"); } else { eqData.expression1 = QString(function.formula.c_str()); eqData.min = QString::number(function.begin); eqData.max = QString::number(function.end); } xyEqCurve->suppressRetransform(true); xyEqCurve->setEquationData(eqData); if (!preview) loadCurve(originCurve, xyEqCurve); plot->addChildFast(xyEqCurve); xyEqCurve->suppressRetransform(false); } } ++curveIndex; } } else { //no support for 3D plots yet //TODO: add an "UnsupportedAspect" here } ++index; } if (!preview) { worksheet->updateLayout(); //worksheet and plots got their sizes, //-> position all text labels inside the plots correctly by converting //the relative positions determined above to the absolute values QHash::const_iterator it = textLabelPositions.constBegin(); while (it != textLabelPositions.constEnd()) { TextLabel* label = it.key(); const QSizeF& ratios = it.value(); const CartesianPlot* plot = static_cast(label->parentAspect()); TextLabel::PositionWrapper position = label->position(); position.point.setX(plot->dataRect().width()*(ratios.width()-0.5)); position.point.setY(plot->dataRect().height()*(ratios.height()-0.5)); label->setPosition(position); ++it; } } return true; } /* * sets the axis properties (format and ticks) as defined in \c originAxis in \c axis, * \c index being 0 or 1 for "top" and "bottom" or "left" and "right" for horizontal or vertical axes, respectively. */ void OriginProjectParser::loadAxis(const Origin::GraphAxis& originAxis, Axis* axis, int index, const QString& axisTitle) const { // int axisPosition; // possible values: // 0: Axis is at default position // 1: Axis is at (axisPositionValue)% from standard position // 2: Axis is at (axisPositionValue) position of orthogonal axis // double axisPositionValue; // bool zeroLine; // bool oppositeLine; //ranges axis->setStart(originAxis.min); axis->setEnd(originAxis.max); //ticks axis->setMajorTicksType(Axis::TicksSpacing); axis->setMajorTicksSpacing(originAxis.step); axis->setMinorTicksType(Axis::TicksTotalNumber); axis->setMinorTicksNumber(originAxis.minorTicks); //scale switch (originAxis.scale) { case Origin::GraphAxis::Linear: axis->setScale(Axis::ScaleLinear); break; case Origin::GraphAxis::Log10: axis->setScale(Axis::ScaleLog10); break; case Origin::GraphAxis::Ln: axis->setScale(Axis::ScaleLn); break; case Origin::GraphAxis::Log2: axis->setScale(Axis::ScaleLog2); break; case Origin::GraphAxis::Probability: case Origin::GraphAxis::Probit: case Origin::GraphAxis::Reciprocal: case Origin::GraphAxis::OffsetReciprocal: case Origin::GraphAxis::Logit: //TODO: axis->setScale(Axis::ScaleLinear); break; } //major grid const Origin::GraphGrid& majorGrid = originAxis.majorGrid; QPen gridPen = axis->majorGridPen(); Qt::PenStyle penStyle(Qt::NoPen); if (!majorGrid.hidden) { switch (majorGrid.style) { case Origin::GraphCurve::Solid: penStyle = Qt::SolidLine; break; case Origin::GraphCurve::Dash: case Origin::GraphCurve::ShortDash: penStyle = Qt::DashLine; break; case Origin::GraphCurve::Dot: case Origin::GraphCurve::ShortDot: penStyle = Qt::DotLine; break; case Origin::GraphCurve::DashDot: case Origin::GraphCurve::ShortDashDot: penStyle = Qt::DashDotLine; break; case Origin::GraphCurve::DashDotDot: penStyle = Qt::DashDotDotLine; break; } } gridPen.setStyle(penStyle); Origin::Color gridColor; gridColor.type = Origin::Color::ColorType::Regular; gridColor.regular = majorGrid.color; gridPen.setColor(OriginProjectParser::color(gridColor)); gridPen.setWidthF(Worksheet::convertToSceneUnits(majorGrid.width, Worksheet::Point)); axis->setMajorGridPen(gridPen); //minor grid const Origin::GraphGrid& minorGrid = originAxis.minorGrid; gridPen = axis->minorGridPen(); penStyle = Qt::NoPen; if (!minorGrid.hidden) { switch (minorGrid.style) { case Origin::GraphCurve::Solid: penStyle = Qt::SolidLine; break; case Origin::GraphCurve::Dash: case Origin::GraphCurve::ShortDash: penStyle = Qt::DashLine; break; case Origin::GraphCurve::Dot: case Origin::GraphCurve::ShortDot: penStyle = Qt::DotLine; break; case Origin::GraphCurve::DashDot: case Origin::GraphCurve::ShortDashDot: penStyle = Qt::DashDotLine; break; case Origin::GraphCurve::DashDotDot: penStyle = Qt::DashDotDotLine; break; } } gridPen.setStyle(penStyle); gridColor.regular = minorGrid.color; gridPen.setColor(OriginProjectParser::color(gridColor)); gridPen.setWidthF(Worksheet::convertToSceneUnits(minorGrid.width, Worksheet::Point)); axis->setMinorGridPen(gridPen); //process Origin::GraphAxisFormat const Origin::GraphAxisFormat& axisFormat = originAxis.formatAxis[index]; QPen pen; Origin::Color color; color.type = Origin::Color::ColorType::Regular; color.regular = axisFormat.color; pen.setColor(OriginProjectParser::color(color)); pen.setWidthF(Worksheet::convertToSceneUnits(axisFormat.thickness, Worksheet::Point)); axis->setLinePen(pen); axis->setMajorTicksLength( Worksheet::convertToSceneUnits(axisFormat.majorTickLength, Worksheet::Point) ); axis->setMajorTicksDirection( (Axis::TicksFlags) axisFormat.majorTicksType); axis->setMajorTicksPen(pen); axis->setMinorTicksLength( axis->majorTicksLength()/2); // minorTicksLength is half of majorTicksLength axis->setMinorTicksDirection( (Axis::TicksFlags) axisFormat.minorTicksType); axis->setMinorTicksPen(pen); QString titleText = parseOriginText(QString::fromLatin1(axisFormat.label.text.c_str())); DEBUG(" axis title text = " << STDSTRING(titleText)); //TODO: parseOriginText() returns html formatted string. What is axisFormat.color used for? //TODO: use axisFormat.fontSize to override the global font size for the hmtl string? //TODO: convert special character here too DEBUG(" curve name = " << STDSTRING(axisTitle)); titleText.replace("%(?X)", axisTitle); titleText.replace("%(?Y)", axisTitle); DEBUG(" axis title = " << STDSTRING(titleText)); axis->title()->setText(titleText); axis->title()->setRotationAngle(axisFormat.label.rotation); axis->setLabelsPrefix(axisFormat.prefix.c_str()); axis->setLabelsSuffix(axisFormat.suffix.c_str()); //TODO: handle string factor member in GraphAxisFormat //process Origin::GraphAxisTick const Origin::GraphAxisTick& tickAxis = originAxis.tickAxis[index]; if (tickAxis.showMajorLabels) { color.type = Origin::Color::ColorType::Regular; color.regular = tickAxis.color; axis->setLabelsColor(OriginProjectParser::color(color)); //TODO: how to set labels position (top vs. bottom)? } else { axis->setLabelsPosition(Axis::LabelsPosition::NoLabels); } //TODO: handle ValueType valueType member in GraphAxisTick //TODO: handle int valueTypeSpecification in GraphAxisTick //precision if (tickAxis.decimalPlaces == -1) axis->setLabelsAutoPrecision(true); else { axis->setLabelsPrecision(tickAxis.decimalPlaces); axis->setLabelsAutoPrecision(false); } QFont font; //TODO: font family? font.setPixelSize( Worksheet::convertToSceneUnits(tickAxis.fontSize, Worksheet::Point) ); font.setBold(tickAxis.fontBold); axis->setLabelsFont(font); //TODO: handle string dataName member in GraphAxisTick //TODO: handle string columnName member in GraphAxisTick axis->setLabelsRotationAngle(tickAxis.rotation); } void OriginProjectParser::loadCurve(const Origin::GraphCurve& originCurve, XYCurve* curve) const { //line properties QPen pen = curve->linePen(); Qt::PenStyle penStyle(Qt::NoPen); if (originCurve.type == Origin::GraphCurve::Line || originCurve.type == Origin::GraphCurve::LineSymbol) { switch (originCurve.lineConnect) { case Origin::GraphCurve::NoLine: curve->setLineType(XYCurve::NoLine); break; case Origin::GraphCurve::Straight: curve->setLineType(XYCurve::Line); break; case Origin::GraphCurve::TwoPointSegment: curve->setLineType(XYCurve::Segments2); break; case Origin::GraphCurve::ThreePointSegment: curve->setLineType(XYCurve::Segments3); break; case Origin::GraphCurve::BSpline: case Origin::GraphCurve::Bezier: case Origin::GraphCurve::Spline: curve->setLineType(XYCurve::SplineCubicNatural); break; case Origin::GraphCurve::StepHorizontal: curve->setLineType(XYCurve::StartHorizontal); break; case Origin::GraphCurve::StepVertical: curve->setLineType(XYCurve::StartVertical); break; case Origin::GraphCurve::StepHCenter: curve->setLineType(XYCurve::MidpointHorizontal); break; case Origin::GraphCurve::StepVCenter: curve->setLineType(XYCurve::MidpointVertical); break; } switch (originCurve.lineStyle) { case Origin::GraphCurve::Solid: penStyle = Qt::SolidLine; break; case Origin::GraphCurve::Dash: case Origin::GraphCurve::ShortDash: penStyle = Qt::DashLine; break; case Origin::GraphCurve::Dot: case Origin::GraphCurve::ShortDot: penStyle = Qt::DotLine; break; case Origin::GraphCurve::DashDot: case Origin::GraphCurve::ShortDashDot: penStyle = Qt::DashDotLine; break; case Origin::GraphCurve::DashDotDot: penStyle = Qt::DashDotDotLine; break; } pen.setStyle(penStyle); pen.setWidthF( Worksheet::convertToSceneUnits(originCurve.lineWidth, Worksheet::Point) ); pen.setColor(color(originCurve.lineColor)); curve->setLineOpacity(1 - originCurve.lineTransparency/255); //TODO: handle unsigned char boxWidth of Origin::GraphCurve } pen.setStyle(penStyle); curve->setLinePen(pen); //symbol properties if (originCurve.type == Origin::GraphCurve::Scatter || originCurve.type == Origin::GraphCurve::LineSymbol) { //try to map the different symbols, mapping is not exact curve->setSymbolsRotationAngle(0); switch (originCurve.symbolShape) { case 0: //NoSymbol curve->setSymbolsStyle(Symbol::NoSymbols); break; case 1: //Rect curve->setSymbolsStyle(Symbol::Square); break; case 2: //Ellipse case 20://Sphere curve->setSymbolsStyle(Symbol::Circle); break; case 3: //UTriangle curve->setSymbolsStyle(Symbol::EquilateralTriangle); break; case 4: //DTriangle curve->setSymbolsStyle(Symbol::EquilateralTriangle); break; case 5: //Diamond curve->setSymbolsStyle(Symbol::Diamond); break; case 6: //Cross + curve->setSymbolsStyle(Symbol::Cross); break; case 7: //Cross x curve->setSymbolsStyle(Symbol::Cross); break; case 8: //Snow curve->setSymbolsStyle(Symbol::Star4); break; case 9: //Horizontal - curve->setSymbolsStyle(Symbol::Line); curve->setSymbolsRotationAngle(90); break; case 10: //Vertical | curve->setSymbolsStyle(Symbol::Line); break; case 15: //LTriangle curve->setSymbolsStyle(Symbol::EquilateralTriangle); break; case 16: //RTriangle curve->setSymbolsStyle(Symbol::EquilateralTriangle); break; case 17: //Hexagon case 19: //Pentagon curve->setSymbolsStyle(Symbol::Square); break; case 18: //Star curve->setSymbolsStyle(Symbol::Star5); break; default: curve->setSymbolsStyle(Symbol::NoSymbols); } //symbol size curve->setSymbolsSize(Worksheet::convertToSceneUnits(originCurve.symbolSize, Worksheet::Point)); //symbol fill color QBrush brush = curve->symbolsBrush(); if (originCurve.symbolFillColor.type == Origin::Color::ColorType::Automatic) { //"automatic" color -> the color of the line, if available, has to be used, black otherwise if (curve->lineType() != XYCurve::NoLine) brush.setColor(curve->linePen().color()); else brush.setColor(Qt::black); } else brush.setColor(color(originCurve.symbolFillColor)); curve->setSymbolsBrush(brush); //symbol border/edge color and width QPen pen = curve->symbolsPen(); if (originCurve.symbolColor.type == Origin::Color::ColorType::Automatic) { //"automatic" color -> the color of the line, if available, has to be used, black otherwise if (curve->lineType() != XYCurve::NoLine) pen.setColor(curve->linePen().color()); else pen.setColor(Qt::black); } else pen.setColor(color(originCurve.symbolColor)); //border width (edge thickness in Origin) is given by percentage of the symbol radius pen.setWidthF(originCurve.symbolThickness/100.*curve->symbolsSize()/2.); curve->setSymbolsPen(pen); //handle unsigned char pointOffset member //handle bool connectSymbols member } else { curve->setSymbolsStyle(Symbol::NoSymbols); } //filling properties if (originCurve.fillArea) { //TODO: handle unsigned char fillAreaType; //with 'fillAreaType'=0x10 the area between the curve and the x-axis is filled //with 'fillAreaType'=0x14 the area included inside the curve is filled. First and last curve points are joined by a line to close the otherwise open area. //with 'fillAreaType'=0x12 the area excluded outside the curve is filled. The inverse of fillAreaType=0x14 is filled. //At the moment we only support the first type, so set it to XYCurve::FillingBelow curve->setFillingPosition(XYCurve::FillingBelow); if (originCurve.fillAreaPattern == 0) { curve->setFillingType(PlotArea::Color); } else { curve->setFillingType(PlotArea::Pattern); //map different patterns in originCurve.fillAreaPattern (has the values of Origin::FillPattern) to Qt::BrushStyle; switch (originCurve.fillAreaPattern) { case 0: curve->setFillingBrushStyle(Qt::NoBrush); break; case 1: case 2: case 3: curve->setFillingBrushStyle(Qt::BDiagPattern); break; case 4: case 5: case 6: curve->setFillingBrushStyle(Qt::FDiagPattern); break; case 7: case 8: case 9: curve->setFillingBrushStyle(Qt::DiagCrossPattern); break; case 10: case 11: case 12: curve->setFillingBrushStyle(Qt::HorPattern); break; case 13: case 14: case 15: curve->setFillingBrushStyle(Qt::VerPattern); break; case 16: case 17: case 18: curve->setFillingBrushStyle(Qt::CrossPattern); break; } } curve->setFillingFirstColor(color(originCurve.fillAreaColor)); curve->setFillingOpacity(1 - originCurve.fillAreaTransparency/255); //Color fillAreaPatternColor - color for the pattern lines, not supported //double fillAreaPatternWidth - width of the pattern lines, not supported //bool fillAreaWithLineTransparency - transparency of the pattern lines independent of the area transparency, not supported //TODO: //unsigned char fillAreaPatternBorderStyle; //Color fillAreaPatternBorderColor; //double fillAreaPatternBorderWidth; //The Border properties are used only in "Column/Bar" (histogram) plots. Those properties are: //fillAreaPatternBorderStyle for the line style (use enum Origin::LineStyle here) //fillAreaPatternBorderColor for the line color //fillAreaPatternBorderWidth for the line width } else curve->setFillingPosition(XYCurve::NoFilling); } bool OriginProjectParser::loadNote(Note* note, bool preview) { DEBUG("OriginProjectParser::loadNote()"); //load note data const Origin::Note& originNote = m_originFile->note(findNoteByName(note->name())); if (preview) return true; note->setComment(originNote.label.c_str()); note->setNote(QString::fromLatin1(originNote.text.c_str())); return true; } //############################################################################## //########################### Helper functions ################################ //############################################################################## QDateTime OriginProjectParser::creationTime(tree::iterator it) const { //this logic seems to be correct only for the first node (project node). For other nodes the current time is returned. char time_str[21]; strftime(time_str, sizeof(time_str), "%F %T", gmtime(&(*it).creationDate)); return QDateTime::fromString(QString(time_str), Qt::ISODate); } QString OriginProjectParser::parseOriginText(const QString &str) const { DEBUG("parseOriginText()"); QStringList lines = str.split('\n'); QString text; for (int i = 0; i < lines.size(); ++i) { if (i > 0) text.append("
"); text.append(parseOriginTags(lines[i])); } DEBUG(" PARSED TEXT = " << STDSTRING(text)); return text; } QColor OriginProjectParser::color(Origin::Color color) const { switch (color.type) { case Origin::Color::ColorType::Regular: switch (color.regular) { case Origin::Color::Black: return QColor{Qt::black}; case Origin::Color::Red: return QColor{Qt::red}; case Origin::Color::Green: return QColor{Qt::green}; case Origin::Color::Blue: return QColor{Qt::blue}; case Origin::Color::Cyan: return QColor{Qt::cyan}; case Origin::Color::Magenta: return QColor{Qt::magenta}; case Origin::Color::Yellow: return QColor{Qt::yellow}; case Origin::Color::DarkYellow: return QColor{Qt::darkYellow}; case Origin::Color::Navy: return QColor{0, 0, 128}; case Origin::Color::Purple: return QColor{128, 0, 128}; case Origin::Color::Wine: return QColor{128, 0, 0}; case Origin::Color::Olive: return QColor{0, 128, 0}; case Origin::Color::DarkCyan: return QColor{Qt::darkCyan}; case Origin::Color::Royal: return QColor{0, 0, 160}; case Origin::Color::Orange: return QColor{255, 128, 0}; case Origin::Color::Violet: return QColor{128, 0, 255}; case Origin::Color::Pink: return QColor{255, 0, 128}; case Origin::Color::White: return QColor{Qt::white}; case Origin::Color::LightGray: return QColor{Qt::lightGray}; case Origin::Color::Gray: return QColor{Qt::gray}; case Origin::Color::LTYellow: return QColor{255, 0, 128}; case Origin::Color::LTCyan: return QColor{128, 255, 255}; case Origin::Color::LTMagenta: return QColor{255, 128, 255}; case Origin::Color::DarkGray: return QColor{Qt::darkGray}; case Origin::Color::SpecialV7Axis: return QColor{Qt::black}; } break; case Origin::Color::ColorType::Custom: return QColor{color.custom[0], color.custom[1], color.custom[2]}; case Origin::Color::ColorType::None: case Origin::Color::ColorType::Automatic: case Origin::Color::ColorType::Increment: case Origin::Color::ColorType::Indexing: case Origin::Color::ColorType::RGB: case Origin::Color::ColorType::Mapping: break; } return QColor(Qt::white); } PlotArea::BackgroundColorStyle OriginProjectParser::backgroundColorStyle(Origin::ColorGradientDirection colorGradient) const { switch (colorGradient) { case Origin::ColorGradientDirection::NoGradient: return PlotArea::BackgroundColorStyle::SingleColor; case Origin::ColorGradientDirection::TopLeft: return PlotArea::BackgroundColorStyle::TopLeftDiagonalLinearGradient; case Origin::ColorGradientDirection::Left: return PlotArea::BackgroundColorStyle::HorizontalLinearGradient; case Origin::ColorGradientDirection::BottomLeft: return PlotArea::BackgroundColorStyle::BottomLeftDiagonalLinearGradient; case Origin::ColorGradientDirection::Top: return PlotArea::BackgroundColorStyle::VerticalLinearGradient; case Origin::ColorGradientDirection::Center: return PlotArea::BackgroundColorStyle::RadialGradient; case Origin::ColorGradientDirection::Bottom: return PlotArea::BackgroundColorStyle::VerticalLinearGradient; case Origin::ColorGradientDirection::TopRight: return PlotArea::BackgroundColorStyle::BottomLeftDiagonalLinearGradient; case Origin::ColorGradientDirection::Right: return PlotArea::BackgroundColorStyle::HorizontalLinearGradient; case Origin::ColorGradientDirection::BottomRight: return PlotArea::BackgroundColorStyle::TopLeftDiagonalLinearGradient; } return PlotArea::BackgroundColorStyle::SingleColor; } QString strreverse(const QString &str) { //QString reversing QByteArray ba = str.toLocal8Bit(); std::reverse(ba.begin(), ba.end()); return QString(ba); } QList> OriginProjectParser::charReplacementList() const { QList> replacements; // TODO: probably missed some. Is there any generic method? replacements << qMakePair(QString("ä"), QString("ä")); replacements << qMakePair(QString("ö"), QString("ö")); replacements << qMakePair(QString("ü"), QString("ü")); replacements << qMakePair(QString("Ä"), QString("Ä")); replacements << qMakePair(QString("Ö"), QString("Ö")); replacements << qMakePair(QString("Ü"), QString("Ü")); replacements << qMakePair(QString("ß"), QString("ß")); replacements << qMakePair(QString("€"), QString("€")); replacements << qMakePair(QString("£"), QString("£")); replacements << qMakePair(QString("¥"), QString("¥")); replacements << qMakePair(QString("¤"), QString("¤")); // krazy:exclude=spelling replacements << qMakePair(QString("¦"), QString("¦")); replacements << qMakePair(QString("§"), QString("§")); replacements << qMakePair(QString("µ"), QString("µ")); replacements << qMakePair(QString("¹"), QString("¹")); replacements << qMakePair(QString("²"), QString("²")); replacements << qMakePair(QString("³"), QString("³")); replacements << qMakePair(QString("¶"), QString("¶")); replacements << qMakePair(QString("ø"), QString("ø")); replacements << qMakePair(QString("æ"), QString("æ")); replacements << qMakePair(QString("ð"), QString("ð")); replacements << qMakePair(QString("ħ"), QString("ℏ")); replacements << qMakePair(QString("ĸ"), QString("κ")); replacements << qMakePair(QString("¢"), QString("¢")); replacements << qMakePair(QString("¼"), QString("¼")); replacements << qMakePair(QString("½"), QString("½")); replacements << qMakePair(QString("¾"), QString("¾")); replacements << qMakePair(QString("¬"), QString("¬")); replacements << qMakePair(QString("©"), QString("©")); replacements << qMakePair(QString("®"), QString("®")); replacements << qMakePair(QString("ª"), QString("ª")); replacements << qMakePair(QString("º"), QString("º")); replacements << qMakePair(QString("±"), QString("±")); replacements << qMakePair(QString("¿"), QString("¿")); replacements << qMakePair(QString("×"), QString("×")); replacements << qMakePair(QString("°"), QString("°")); replacements << qMakePair(QString("«"), QString("«")); replacements << qMakePair(QString("»"), QString("»")); replacements << qMakePair(QString("¯"), QString("¯")); replacements << qMakePair(QString("¸"), QString("¸")); replacements << qMakePair(QString("À"), QString("À")); replacements << qMakePair(QString("Á"), QString("Á")); replacements << qMakePair(QString("Â"), QString("Â")); replacements << qMakePair(QString("Ã"), QString("Ã")); replacements << qMakePair(QString("Å"), QString("Å")); replacements << qMakePair(QString("Æ"), QString("Æ")); replacements << qMakePair(QString("Ç"), QString("Ç")); replacements << qMakePair(QString("È"), QString("È")); replacements << qMakePair(QString("É"), QString("É")); replacements << qMakePair(QString("Ê"), QString("Ê")); replacements << qMakePair(QString("Ë"), QString("Ë")); replacements << qMakePair(QString("Ì"), QString("Ì")); replacements << qMakePair(QString("Í"), QString("Í")); replacements << qMakePair(QString("Î"), QString("Î")); replacements << qMakePair(QString("Ï"), QString("Ï")); replacements << qMakePair(QString("Ð"), QString("Ð")); replacements << qMakePair(QString("Ñ"), QString("Ñ")); replacements << qMakePair(QString("Ò"), QString("Ò")); replacements << qMakePair(QString("Ó"), QString("Ó")); replacements << qMakePair(QString("Ô"), QString("Ô")); replacements << qMakePair(QString("Õ"), QString("Õ")); replacements << qMakePair(QString("Ù"), QString("Ù")); replacements << qMakePair(QString("Ú"), QString("Ú")); replacements << qMakePair(QString("Û"), QString("Û")); replacements << qMakePair(QString("Ý"), QString("Ý")); replacements << qMakePair(QString("Þ"), QString("Þ")); replacements << qMakePair(QString("à"), QString("à")); replacements << qMakePair(QString("á"), QString("á")); replacements << qMakePair(QString("â"), QString("â")); replacements << qMakePair(QString("ã"), QString("ã")); replacements << qMakePair(QString("å"), QString("å")); replacements << qMakePair(QString("ç"), QString("ç")); replacements << qMakePair(QString("è"), QString("è")); replacements << qMakePair(QString("é"), QString("é")); replacements << qMakePair(QString("ê"), QString("ê")); replacements << qMakePair(QString("ë"), QString("ë")); replacements << qMakePair(QString("ì"), QString("ì")); replacements << qMakePair(QString("í"), QString("í")); replacements << qMakePair(QString("î"), QString("î")); replacements << qMakePair(QString("ï"), QString("ï")); replacements << qMakePair(QString("ñ"), QString("ñ")); replacements << qMakePair(QString("ò"), QString("ò")); replacements << qMakePair(QString("ó"), QString("ó")); replacements << qMakePair(QString("ô"), QString("ô")); replacements << qMakePair(QString("õ"), QString("õ")); replacements << qMakePair(QString("÷"), QString("÷")); replacements << qMakePair(QString("ù"), QString("ù")); replacements << qMakePair(QString("ú"), QString("ú")); replacements << qMakePair(QString("û"), QString("û")); replacements << qMakePair(QString("ý"), QString("ý")); replacements << qMakePair(QString("þ"), QString("þ")); replacements << qMakePair(QString("ÿ"), QString("ÿ")); replacements << qMakePair(QString("Œ"), QString("Œ")); replacements << qMakePair(QString("œ"), QString("œ")); replacements << qMakePair(QString("Š"), QString("Š")); replacements << qMakePair(QString("š"), QString("š")); replacements << qMakePair(QString("Ÿ"), QString("Ÿ")); replacements << qMakePair(QString("†"), QString("†")); replacements << qMakePair(QString("‡"), QString("‡")); replacements << qMakePair(QString("…"), QString("…")); replacements << qMakePair(QString("‰"), QString("‰")); replacements << qMakePair(QString("™"), QString("™")); return replacements; } QString OriginProjectParser::replaceSpecialChars(const QString& text) const { QString t = text; for (const auto& r : charReplacementList()) t.replace(r.first, r.second); return t; } /*! * converts the string with Origin's syntax for text formatting/highlighting * to a string in the richtext/html format supported by Qt. * For the supported syntax, see: * https://www.originlab.com/doc/LabTalk/ref/Label-cmd * https://www.originlab.com/doc/Origin-Help/TextOb-Prop-Text-tab * https://doc.qt.io/qt-5/richtext-html-subset.html */ QString OriginProjectParser::parseOriginTags(const QString& str) const { DEBUG("parseOriginTags()"); DEBUG(" string: " << STDSTRING(str)); QDEBUG(" UTF8 string: " << str.toUtf8()); QString line = str; //replace %(...) tags // QRegExp rxcol("\\%\\(\\d+\\)"); // replace \l(x) (plot legend tags) with \\c{x}, where x is a digit line.replace(QRegularExpression(QStringLiteral("\\\\\\s*l\\s*\\(\\s*(\\d+)\\s*\\)")), QStringLiteral("\\c{\\1}")); // replace umlauts etc. line = replaceSpecialChars(line); // replace tabs (not really supported) line.replace('\t', "        "); // In PCRE2 (which is what QRegularExpression uses) variable-length lookbehind is supposed to be // exprimental in Perl 5.30; which means it doesn't work at the moment, i.e. using a variable-length // negative lookbehind isn't valid syntax from QRegularExpression POV. // Ultimately we have to reverse the string and use a negative _lookahead_ instead. // The goal is to temporatily replace '(' and ')' that don't denote tags; this is so that we // can handle parenthesis that are inside the tag, e.g. '\b(bold (cf))', we want the '(cf)' part // to remain as is. const QRegularExpression nonTagsRe("\\)([^)(]*)\\((?!\\s*([buigs\\+\\-]|\\d{1,3}\\s*[pc]|[\\w ]+\\s*:\\s*f)\\s*\\\\)"); QString linerev = strreverse(line); const QString lBracket = strreverse("&lbracket;"); const QString rBracket = strreverse("&rbracket;"); linerev.replace(nonTagsRe, rBracket + QStringLiteral("\\1") + lBracket); // change the line back to normal line = strreverse(linerev); //replace \-(...), \+(...), \b(...), \i(...), \u(...), \s(....), \g(...), \f:font(...), // \c'number'(...), \p'size'(...) tags with equivalent supported HTML syntax const QRegularExpression tagsRe(QStringLiteral("\\\\\\s*([-+bgisu]|f:(\\w[\\w ]+)|[pc]\\s*(\\d+))\\s*\\(([^()]+?)\\)")); QRegularExpressionMatch rmatch; while (line.contains(tagsRe, &rmatch)) { QString rep; const QString tagText = rmatch.captured(4); const QString marker = rmatch.captured(1); if (marker.startsWith(QLatin1Char('-'))) { rep = QStringLiteral("%1").arg(tagText); } else if (marker.startsWith(QLatin1Char('+'))) { rep = QStringLiteral("%1").arg(tagText); } else if (marker.startsWith(QLatin1Char('b'))) { rep = QStringLiteral("%1").arg(tagText); } else if (marker.startsWith(QLatin1Char('g'))) { // greek symbols e.g. α φ rep = QStringLiteral("%1").arg(tagText); } else if (marker.startsWith(QLatin1Char('i'))) { rep = QStringLiteral("%1").arg(tagText); } else if (marker.startsWith(QLatin1Char('s'))) { rep = QStringLiteral("%1").arg(tagText); } else if (marker.startsWith(QLatin1Char('u'))) { rep = QStringLiteral("%1").arg(tagText); } else if (marker.startsWith(QLatin1Char('f'))) { rep = QStringLiteral("%2").arg(rmatch.captured(2).trimmed(), tagText); } else if (marker.startsWith(QLatin1Char('p'))) { // e.g. \p200(...), means use font-size 200% rep = QStringLiteral("%2").arg(rmatch.captured(3), tagText); } else if (marker.startsWith(QLatin1Char('c'))) { // e.g. \c12(...), set the text color to the corresponding color from // the color drop-down list in OriginLab const int colorIndex = rmatch.captured(3).toInt(); Origin::Color c; c.type = Origin::Color::ColorType::Regular; c.regular = colorIndex <= 23 ? static_cast(colorIndex) : Origin::Color::RegularColor::Black; QColor color = OriginProjectParser::color(c); rep = QStringLiteral("%2").arg(color.name(), tagText); } line.replace(rmatch.capturedStart(0), rmatch.capturedLength(0), rep); } // put non-tag '(' and ')' back in their places line.replace("&lbracket;", "("); line.replace("&rbracket;", ")"); // special characters line.replace(QRegularExpression(QStringLiteral("\\\\\\((\\d+)\\)")), "&#\\1;"); DEBUG(" result: " << STDSTRING(line)); return line; } diff --git a/src/backend/spreadsheet/Spreadsheet.cpp b/src/backend/spreadsheet/Spreadsheet.cpp index c4d2758c3..611a89412 100644 --- a/src/backend/spreadsheet/Spreadsheet.cpp +++ b/src/backend/spreadsheet/Spreadsheet.cpp @@ -1,1071 +1,1071 @@ /*************************************************************************** File : Spreadsheet.cpp Project : LabPlot Description : Aspect providing a spreadsheet table with column logic -------------------------------------------------------------------- Copyright : (C) 2006-2008 Tilman Benkert (thzs@gmx.net) Copyright : (C) 2006-2009 Knut Franke (knut.franke@gmx.de) Copyright : (C) 2012-2019 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2017-2020 Stefan Gerlach (stefan.gerlach@uni.kn) ***************************************************************************/ /*************************************************************************** * * * 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 "Spreadsheet.h" #include "SpreadsheetModel.h" #include "backend/core/AspectPrivate.h" #include "backend/core/AbstractAspect.h" #include "backend/core/column/ColumnStringIO.h" #include "backend/core/datatypes/DateTime2StringFilter.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "commonfrontend/spreadsheet/SpreadsheetView.h" #include #include #include #include #include /*! \class Spreadsheet \brief Aspect providing a spreadsheet table with column logic. Spreadsheet is a container object for columns with no data of its own. By definition, it's columns are all of its children inheriting from class Column. Thus, the basic API is already defined by AbstractAspect (managing the list of columns, notification of column insertion/removal) and Column (changing and monitoring state of the actual data). Spreadsheet stores a pointer to its primary view of class SpreadsheetView. SpreadsheetView calls the Spreadsheet API but Spreadsheet only notifies SpreadsheetView by signals without calling its API directly. This ensures a maximum independence of UI and backend. SpreadsheetView can be easily replaced by a different class. User interaction is completely handled in SpreadsheetView and translated into Spreadsheet API calls (e.g., when a user edits a cell this will be handled by the delegate of SpreadsheetView and Spreadsheet will not know whether a script or a user changed the data.). All actions, menus etc. for the user interaction are handled SpreadsheetView, e.g., via a context menu. Selections are also handled by SpreadsheetView. The view itself is created by the first call to view(); \ingroup backend */ Spreadsheet::Spreadsheet(const QString& name, bool loading, AspectType type) : AbstractDataSource(name, type) { if (!loading) init(); } /*! initializes the spreadsheet with the default number of columns and rows */ void Spreadsheet::init() { KConfigGroup group = KSharedConfig::openConfig()->group(QLatin1String("Spreadsheet")); const int columns = group.readEntry(QLatin1String("ColumnCount"), 2); const int rows = group.readEntry(QLatin1String("RowCount"), 100); for (int i = 0; i < columns; i++) { Column* new_col = new Column(QString::number(i+1), AbstractColumn::Numeric); new_col->setPlotDesignation(i == 0 ? AbstractColumn::X : AbstractColumn::Y); addChild(new_col); } setRowCount(rows); } void Spreadsheet::setModel(SpreadsheetModel* model) { m_model = model; } SpreadsheetModel* Spreadsheet::model() { return m_model; } /*! Constructs a primary view on me. This method may be called multiple times during the life time of an Aspect, or it might not get called at all. Aspects must not depend on the existence of a view for their operation. */ QWidget* Spreadsheet::view() const { if (!m_partView) { bool readOnly = (this->parentAspect()->type() == AspectType::DatapickerCurve); m_view = new SpreadsheetView(const_cast(this), readOnly); m_partView = m_view; } return m_partView; } bool Spreadsheet::exportView() const { return m_view->exportView(); } bool Spreadsheet::printView() { return m_view->printView(); } bool Spreadsheet::printPreview() const { return m_view->printPreview(); } /*! Returns the maximum number of rows in the spreadsheet. */ int Spreadsheet::rowCount() const { int result = 0; for (auto* col : children()) { const int col_rows = col->rowCount(); if ( col_rows > result) result = col_rows; } return result; } void Spreadsheet::removeRows(int first, int count) { if ( count < 1 || first < 0 || first+count > rowCount()) return; WAIT_CURSOR; beginMacro( i18np("%1: remove 1 row", "%1: remove %2 rows", name(), count) ); for (auto* col : children()) col->removeRows(first, count); endMacro(); RESET_CURSOR; } void Spreadsheet::insertRows(int before, int count) { if ( count < 1 || before < 0 || before > rowCount()) return; WAIT_CURSOR; beginMacro( i18np("%1: insert 1 row", "%1: insert %2 rows", name(), count) ); for (auto* col : children()) col->insertRows(before, count); endMacro(); RESET_CURSOR; } void Spreadsheet::appendRows(int count) { insertRows(rowCount(), count); } void Spreadsheet::appendRow() { insertRows(rowCount(), 1); } void Spreadsheet::appendColumns(int count) { insertColumns(columnCount(), count); } void Spreadsheet::appendColumn() { insertColumns(columnCount(), 1); } void Spreadsheet::prependColumns(int count) { insertColumns(0, count); } /*! Sets the number of rows of the spreadsheet to \c new_size */ void Spreadsheet::setRowCount(int new_size) { int current_size = rowCount(); if (new_size > current_size) insertRows(current_size, new_size-current_size); if (new_size < current_size && new_size >= 0) removeRows(new_size, current_size-new_size); } /*! Returns the column with the number \c index. Shallow wrapper around \sa AbstractAspect::child() - see there for caveat. */ Column* Spreadsheet::column(int index) const { return child(index); } /*! Returns the column with the name \c name. */ Column* Spreadsheet::column(const QString &name) const { return child(name); } /*! Returns the total number of columns in the spreadsheet. */ int Spreadsheet::columnCount() const { return childCount(); } /*! Returns the number of columns matching the given designation. */ int Spreadsheet::columnCount(AbstractColumn::PlotDesignation pd) const { int count = 0; for (auto* col : children()) if (col->plotDesignation() == pd) count++; return count; } void Spreadsheet::removeColumns(int first, int count) { if ( count < 1 || first < 0 || first+count > columnCount()) return; WAIT_CURSOR; beginMacro( i18np("%1: remove 1 column", "%1: remove %2 columns", name(), count) ); for (int i = 0; i < count; i++) child(first)->remove(); endMacro(); RESET_CURSOR; } void Spreadsheet::insertColumns(int before, int count) { WAIT_CURSOR; beginMacro( i18np("%1: insert 1 column", "%1: insert %2 columns", name(), count) ); Column * before_col = column(before); int rows = rowCount(); for (int i = 0; i < count; i++) { Column * new_col = new Column(QString::number(i+1), AbstractColumn::Numeric); new_col->setPlotDesignation(AbstractColumn::Y); new_col->insertRows(0, rows); insertChildBefore(new_col, before_col); } endMacro(); RESET_CURSOR; } /*! Sets the number of columns to \c new_size */ void Spreadsheet::setColumnCount(int new_size) { int old_size = columnCount(); if ( old_size == new_size || new_size < 0 ) return; if (new_size < old_size) removeColumns(new_size, old_size-new_size); else insertColumns(old_size, new_size-old_size); } /*! Clears the whole spreadsheet. */ void Spreadsheet::clear() { WAIT_CURSOR; beginMacro(i18n("%1: clear", name())); for (auto* col : children()) col->clear(); endMacro(); RESET_CURSOR; } /*! Clears all mask in the spreadsheet. */ void Spreadsheet::clearMasks() { WAIT_CURSOR; beginMacro(i18n("%1: clear all masks", name())); for (auto* col : children()) col->clearMasks(); endMacro(); RESET_CURSOR; } /*! Returns a new context menu. The caller takes ownership of the menu. */ QMenu* Spreadsheet::createContextMenu() { QMenu* menu = AbstractPart::createContextMenu(); Q_ASSERT(menu); emit requestProjectContextMenu(menu); return menu; } void Spreadsheet::moveColumn(int from, int to) { Column* col = child(from); beginMacro(i18n("%1: move column %2 from position %3 to %4.", name(), col->name(), from+1, to+1)); col->remove(); insertChildBefore(col, child(to)); endMacro(); } void Spreadsheet::copy(Spreadsheet* other) { WAIT_CURSOR; beginMacro(i18n("%1: copy %2", name(), other->name())); for (auto* col : children()) col->remove(); for (auto* src_col : other->children()) { Column * new_col = new Column(src_col->name(), src_col->columnMode()); new_col->copy(src_col); new_col->setPlotDesignation(src_col->plotDesignation()); QVector< Interval > masks = src_col->maskedIntervals(); for (const auto& iv : masks) new_col->setMasked(iv); QVector< Interval > formulas = src_col->formulaIntervals(); for (const auto& iv : formulas) new_col->setFormula(iv, src_col->formula(iv.start())); new_col->setWidth(src_col->width()); addChild(new_col); } setComment(other->comment()); endMacro(); RESET_CURSOR; } // FIXME: replace index-based API with Column*-based one /*! Determines the corresponding X column. */ int Spreadsheet::colX(int col) { for (int i = col-1; i >= 0; i--) { if (column(i)->plotDesignation() == AbstractColumn::X) return i; } int cols = columnCount(); for (int i = col+1; i < cols; i++) { if (column(i)->plotDesignation() == AbstractColumn::X) return i; } return -1; } /*! Determines the corresponding Y column. */ int Spreadsheet::colY(int col) { int cols = columnCount(); if (column(col)->plotDesignation() == AbstractColumn::XError || column(col)->plotDesignation() == AbstractColumn::YError) { // look to the left first for (int i = col-1; i >= 0; i--) { if (column(i)->plotDesignation() == AbstractColumn::Y) return i; } for (int i = col+1; i < cols; i++) { if (column(i)->plotDesignation() == AbstractColumn::Y) return i; } } else { // look to the right first for (int i = col+1; i < cols; i++) { if (column(i)->plotDesignation() == AbstractColumn::Y) return i; } for (int i = col-1; i >= 0; i--) { if (column(i)->plotDesignation() == AbstractColumn::Y) return i; } } return -1; } /*! Sorts the given list of column. If 'leading' is a null pointer, each column is sorted separately. */ void Spreadsheet::sortColumns(Column* leading, const QVector& cols, bool ascending) { if (cols.isEmpty()) return; // the normal QPair comparison does not work properly with descending sorting // therefore we use our own compare functions class CompareFunctions { public: static bool doubleLess(QPair a, QPair b) { return a.first < b.first; } static bool doubleGreater(QPair a, QPair b) { return a.first > b.first; } static bool integerLess(QPair a, QPair b) { return a.first < b.first; } static bool integerGreater(QPair a, QPair b) { return a.first > b.first; } static bool bigIntLess(QPair a, QPair b) { return a.first < b.first; } static bool bigIntGreater(QPair a, QPair b) { return a.first > b.first; } static bool QStringLess(const QPair& a, const QPair& b) { return a < b; } static bool QStringGreater(const QPair& a, const QPair& b) { return a > b; } static bool QDateTimeLess(const QPair& a, const QPair& b) { return a < b; } static bool QDateTimeGreater(const QPair& a, const QPair& b) { return a > b; } }; WAIT_CURSOR; beginMacro(i18n("%1: sort columns", name())); if (leading == nullptr) { // sort separately for (auto* col : cols) { switch (col->columnMode()) { case AbstractColumn::Numeric: { int rows = col->rowCount(); QVector< QPair > map; for (int j = 0; j < rows; j++) map.append(QPair(col->valueAt(j), j)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::doubleLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::doubleGreater); QVectorIterator< QPair > it(map); Column *temp_col = new Column("temp", col->columnMode()); int k = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, k, 1); temp_col->setMasked(col->isMasked(it.next().second)); k++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; break; } case AbstractColumn::Integer: { int rows = col->rowCount(); QVector< QPair > map; for (int j = 0; j < rows; j++) map.append(QPair(col->valueAt(j), j)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::doubleLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::doubleGreater); QVectorIterator> it(map); Column* temp_col = new Column("temp", col->columnMode()); int k = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, k, 1); temp_col->setMasked(col->isMasked(it.next().second)); k++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; break; } case AbstractColumn::BigInt: { int rows = col->rowCount(); QVector< QPair > map; for (int j = 0; j < rows; j++) map.append(QPair(col->valueAt(j), j)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::doubleLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::doubleGreater); QVectorIterator> it(map); Column* temp_col = new Column("temp", col->columnMode()); int k = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, k, 1); temp_col->setMasked(col->isMasked(it.next().second)); k++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; break; } case AbstractColumn::Text: { int rows = col->rowCount(); QVector> map; for (int j = 0; j < rows; j++) map.append(QPair(col->textAt(j), j)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::QStringLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::QStringGreater); QVectorIterator< QPair > it(map); Column* temp_col = new Column("temp", col->columnMode()); int k = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, k, 1); temp_col->setMasked(col->isMasked(it.next().second)); k++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; break; } case AbstractColumn::DateTime: case AbstractColumn::Month: case AbstractColumn::Day: { int rows = col->rowCount(); QVector< QPair > map; for (int j = 0; j < rows; j++) map.append(QPair(col->dateTimeAt(j), j)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::QDateTimeLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::QDateTimeGreater); QVectorIterator< QPair > it(map); Column *temp_col = new Column("temp", col->columnMode()); int k = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, k, 1); temp_col->setMasked(col->isMasked(it.next().second)); k++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; break; } } } } else { // sort with leading column switch (leading->columnMode()) { case AbstractColumn::Numeric: { QVector> map; int rows = leading->rowCount(); for (int i = 0; i < rows; i++) map.append(QPair(leading->valueAt(i), i)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::doubleLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::doubleGreater); QVectorIterator> it(map); for (auto* col : cols) { Column *temp_col = new Column("temp", col->columnMode()); it.toFront(); int j = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, j, 1); temp_col->setMasked(col->isMasked(it.next().second)); j++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; } break; } case AbstractColumn::Integer: { QVector> map; int rows = leading->rowCount(); for (int i = 0; i < rows; i++) map.append(QPair(leading->valueAt(i), i)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::integerLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::integerGreater); QVectorIterator> it(map); for (auto* col : cols) { Column *temp_col = new Column("temp", col->columnMode()); it.toFront(); int j = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, j, 1); temp_col->setMasked(col->isMasked(it.next().second)); j++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; } break; } case AbstractColumn::BigInt: { QVector> map; int rows = leading->rowCount(); for (int i = 0; i < rows; i++) map.append(QPair(leading->valueAt(i), i)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::bigIntLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::bigIntGreater); QVectorIterator> it(map); for (auto* col : cols) { Column *temp_col = new Column("temp", col->columnMode()); it.toFront(); int j = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, j, 1); temp_col->setMasked(col->isMasked(it.next().second)); j++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; } break; } case AbstractColumn::Text: { QVector> map; int rows = leading->rowCount(); for (int i = 0; i < rows; i++) map.append(QPair(leading->textAt(i), i)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::QStringLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::QStringGreater); QVectorIterator> it(map); for (auto* col : cols) { Column *temp_col = new Column("temp", col->columnMode()); it.toFront(); int j = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, j, 1); temp_col->setMasked(col->isMasked(it.next().second)); j++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; } break; } case AbstractColumn::DateTime: case AbstractColumn::Month: case AbstractColumn::Day: { QVector> map; int rows = leading->rowCount(); for (int i = 0; i < rows; i++) map.append(QPair(leading->dateTimeAt(i), i)); if (ascending) std::stable_sort(map.begin(), map.end(), CompareFunctions::QDateTimeLess); else std::stable_sort(map.begin(), map.end(), CompareFunctions::QDateTimeGreater); QVectorIterator> it(map); for (auto* col : cols) { Column *temp_col = new Column("temp", col->columnMode()); it.toFront(); int j = 0; // put the values in the right order into temp_col while (it.hasNext()) { temp_col->copy(col, it.peekNext().second, j, 1); temp_col->setMasked(col->isMasked(it.next().second)); j++; } // copy the sorted column col->copy(temp_col, 0, 0, rows); delete temp_col; } break; } } } endMacro(); RESET_CURSOR; } // end of sortColumns() /*! Returns an icon to be used for decorating my views. */ QIcon Spreadsheet::icon() const { return QIcon::fromTheme("labplot-spreadsheet"); } /*! Returns the text displayed in the given cell. */ QString Spreadsheet::text(int row, int col) const { Column* c = column(col); if (!c) return QString(); return c->asStringColumn()->textAt(row); } /*! * This slot is, indirectly, called when a child of \c Spreadsheet (i.e. column) was selected in \c ProjectExplorer. * Emits the signal \c columnSelected that is handled in \c SpreadsheetView. */ void Spreadsheet::childSelected(const AbstractAspect* aspect) { const Column* column = qobject_cast(aspect); if (column) { int index = indexOfChild(column); emit columnSelected(index); } } /*! * This slot is, indirectly, called when a child of \c Spreadsheet (i.e. column) was deselected in \c ProjectExplorer. * Emits the signal \c columnDeselected that is handled in \c SpreadsheetView. */ void Spreadsheet::childDeselected(const AbstractAspect* aspect) { const Column* column = qobject_cast(aspect); if (column) { int index = indexOfChild(column); emit columnDeselected(index); } } /*! * Emits the signal to select or to deselect the column number \c index in the project explorer, * if \c selected=true or \c selected=false, respectively. * The signal is handled in \c AspectTreeModel and forwarded to the tree view in \c ProjectExplorer. * This function is called in \c SpreadsheetView upon selection changes. */ void Spreadsheet::setColumnSelectedInView(int index, bool selected) { if (selected) { emit childAspectSelectedInView(child(index)); //deselect the spreadsheet in the project explorer, if a child (column) was selected //and also all possible parents like folder, workbook, datapicker curve, datapicker //to prevents unwanted multiple selection in the project explorer //if one of the parents of the selected column was also selected before. AbstractAspect* parent = this; while (parent) { emit childAspectDeselectedInView(parent); parent = parent->parentAspect(); } } else emit childAspectDeselectedInView(child(index)); } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## /*! Saves as XML. */ void Spreadsheet::save(QXmlStreamWriter* writer) const { writer->writeStartElement("spreadsheet"); writeBasicAttributes(writer); writeCommentElement(writer); //columns - for (auto* col : children(IncludeHidden)) + for (auto* col : children(ChildIndexFlag::IncludeHidden)) col->save(writer); writer->writeEndElement(); // "spreadsheet" } /*! Loads from XML. */ bool Spreadsheet::load(XmlStreamReader* reader, bool preview) { if (!readBasicAttributes(reader)) return false; // read child elements while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement()) break; if (reader->isStartElement()) { if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (reader->name() == "column") { Column* column = new Column(QString()); if (!column->load(reader, preview)) { delete column; setColumnCount(0); return false; } addChildFast(column); } else { // unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } } return !reader->hasError(); } void Spreadsheet::registerShortcuts() { //TODO: when we create a live-data source we don't have the view here yet. why? if (m_view) m_view->registerShortcuts(); } void Spreadsheet::unregisterShortcuts() { if (m_view) m_view->unregisterShortcuts(); } //############################################################################## //######################## Data Import ####################################### //############################################################################## int Spreadsheet::prepareImport(std::vector& dataContainer, AbstractFileFilter::ImportMode importMode, int actualRows, int actualCols, QStringList colNameList, QVector columnMode) { DEBUG("Spreadsheet::prepareImport()") DEBUG(" resize spreadsheet to rows = " << actualRows << " and cols = " << actualCols) QDEBUG(" column name list = " << colNameList) int columnOffset = 0; setUndoAware(false); if (m_model != nullptr) m_model->suppressSignals(true); //make the available columns undo unaware before we resize and rename them below, //the same will be done for new columns in this->resize(). for (int i = 0; i < childCount(); i++) child(i)->setUndoAware(false); columnOffset = this->resize(importMode, colNameList, actualCols); // resize the spreadsheet if (importMode == AbstractFileFilter::Replace) { clear(); setRowCount(actualRows); } else { if (rowCount() < actualRows) setRowCount(actualRows); } if (columnMode.size() < actualCols) { qWarning("columnMode[] size is too small! Giving up."); return -1; } dataContainer.resize(actualCols); for (int n = 0; n < actualCols; n++) { // data() returns a void* which is a pointer to any data type (see ColumnPrivate.cpp) Column* column = this->child(columnOffset+n); DEBUG(" column " << n << " columnMode = " << columnMode[n]); column->setColumnModeFast(columnMode[n]); //in the most cases the first imported column is meant to be used as x-data. //Other columns provide mostly y-data or errors. //TODO: this has to be configurable for the user in the import widget, //it should be possible to specify x-error plot designation, etc. AbstractColumn::PlotDesignation desig = (n == 0) ? AbstractColumn::X : AbstractColumn::Y; column->setPlotDesignation(desig); switch (columnMode[n]) { case AbstractColumn::Numeric: { auto* vector = static_cast*>(column->data()); vector->resize(actualRows); dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Integer: { auto* vector = static_cast*>(column->data()); vector->resize(actualRows); dataContainer[n] = static_cast(vector); break; } case AbstractColumn::BigInt: { auto* vector = static_cast*>(column->data()); vector->resize(actualRows); dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Text: { auto* vector = static_cast*>(column->data()); vector->resize(actualRows); dataContainer[n] = static_cast(vector); break; } case AbstractColumn::Month: case AbstractColumn::Day: case AbstractColumn::DateTime: { auto* vector = static_cast* >(column->data()); vector->resize(actualRows); dataContainer[n] = static_cast(vector); break; } } } // QDEBUG("dataPointers =" << dataPointers); DEBUG("Spreadsheet::prepareImport() DONE"); return columnOffset; } /*! resize data source to cols columns returns column offset depending on import mode */ int Spreadsheet::resize(AbstractFileFilter::ImportMode mode, QStringList colNameList, int cols) { DEBUG("Spreadsheet::resize()") QDEBUG(" column name list = " << colNameList) // name additional columns for (int k = colNameList.size(); k < cols; k++ ) colNameList.append( "Column " + QString::number(k+1) ); int columnOffset = 0; //indexes the "start column" in the spreadsheet. Starting from this column the data will be imported. Column* newColumn = nullptr; if (mode == AbstractFileFilter::Append) { columnOffset = childCount(); for (int n = 0; n < cols; n++ ) { newColumn = new Column(colNameList.at(n), AbstractColumn::Numeric); newColumn->setUndoAware(false); addChildFast(newColumn); } } else if (mode == AbstractFileFilter::Prepend) { Column* firstColumn = child(0); for (int n = 0; n < cols; n++ ) { newColumn = new Column(colNameList.at(n), AbstractColumn::Numeric); newColumn->setUndoAware(false); insertChildBeforeFast(newColumn, firstColumn); } } else if (mode == AbstractFileFilter::Replace) { //replace completely the previous content of the data source with the content to be imported. int columns = childCount(); if (columns > cols) { //there're more columns in the data source then required -> remove the superfluous columns for (int i = 0; i < columns-cols; i++) removeChild(child(0)); } else { //create additional columns if needed for (int i = columns; i < cols; i++) { newColumn = new Column(colNameList.at(i), AbstractColumn::Numeric); newColumn->setUndoAware(false); addChildFast(newColumn); } } // 1. rename the columns that were already available // 2. suppress the dataChanged signal for all columns // 3. send aspectDescriptionChanged because otherwise the column // will not be connected again to the curves (project.cpp, descriptionChanged) for (int i = 0; i < childCount(); i++) { child(i)->setSuppressDataChangedSignal(true); emit child(i)->reset(child(i)); child(i)->setName(colNameList.at(i)); child(i)->aspectDescriptionChanged(child(i)); } } return columnOffset; } void Spreadsheet::finalizeImport(int columnOffset, int startColumn, int endColumn, const QString& dateTimeFormat, AbstractFileFilter::ImportMode importMode) { DEBUG("Spreadsheet::finalizeImport()"); //determine the dependent plots QVector plots; if (importMode == AbstractFileFilter::Replace) { for (int n = startColumn; n <= endColumn; n++) { Column* column = this->column(columnOffset + n - startColumn); column->addUsedInPlots(plots); } //suppress retransform in the dependent plots for (auto* plot : plots) plot->setSuppressDataChangedSignal(true); } // set the comments for each of the columns if datasource is a spreadsheet const int rows = rowCount(); for (int n = startColumn; n <= endColumn; n++) { Column* column = this->column(columnOffset + n - startColumn); DEBUG(" column " << n << " of type " << column->columnMode()); QString comment; switch (column->columnMode()) { case AbstractColumn::Numeric: comment = i18np("numerical data, %1 element", "numerical data, %1 elements", rows); break; case AbstractColumn::Integer: comment = i18np("integer data, %1 element", "integer data, %1 elements", rows); break; case AbstractColumn::BigInt: comment = i18np("big integer data, %1 element", "big integer data, %1 elements", rows); break; case AbstractColumn::Text: comment = i18np("text data, %1 element", "text data, %1 elements", rows); break; case AbstractColumn::Month: comment = i18np("month data, %1 element", "month data, %1 elements", rows); break; case AbstractColumn::Day: comment = i18np("day data, %1 element", "day data, %1 elements", rows); break; case AbstractColumn::DateTime: comment = i18np("date and time data, %1 element", "date and time data, %1 elements", rows); // set same datetime format in column auto* filter = static_cast(column->outputFilter()); filter->setFormat(dateTimeFormat); } column->setComment(comment); if (importMode == AbstractFileFilter::Replace) { column->setSuppressDataChangedSignal(false); column->setChanged(); } } if (importMode == AbstractFileFilter::Replace) { //retransform the dependent plots for (auto* plot : plots) { plot->setSuppressDataChangedSignal(false); plot->dataChanged(); } } //make the spreadsheet and all its children undo aware again setUndoAware(true); for (int i = 0; i < childCount(); i++) child(i)->setUndoAware(true); if (m_model != nullptr) m_model->suppressSignals(false); if (m_partView != nullptr && m_view != nullptr) m_view->resizeHeader(); DEBUG("Spreadsheet::finalizeImport() DONE"); } diff --git a/src/backend/worksheet/Worksheet.cpp b/src/backend/worksheet/Worksheet.cpp index 1e9a3b761..b832a0ef8 100644 --- a/src/backend/worksheet/Worksheet.cpp +++ b/src/backend/worksheet/Worksheet.cpp @@ -1,1579 +1,1579 @@ /*************************************************************************** File : Worksheet.cpp Project : LabPlot Description : Worksheet -------------------------------------------------------------------- Copyright : (C) 2009 Tilman Benkert (thzs@gmx.net) Copyright : (C) 2011-2019 by 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 "Worksheet.h" #include "WorksheetPrivate.h" #include "WorksheetElement.h" #include "commonfrontend/worksheet/WorksheetView.h" #include "backend/core/Project.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/worksheet/Image.h" #include "backend/worksheet/TreeModel.h" #include "backend/worksheet/TextLabel.h" #include "backend/lib/commandtemplates.h" #include "backend/lib/XmlStreamReader.h" #include "kdefrontend/worksheet/ExportWorksheetDialog.h" #include "kdefrontend/ThemeHandler.h" #include #include #include #include #include #include #include #include #include #include #include /** * \class Worksheet * \brief Top-level container for worksheet elements like plot, labels, etc. * * The worksheet is, besides the data containers \c Spreadsheet and \c Matrix, another central part of the application * and provides an area for showing and grouping together different kinds of worksheet objects - plots, labels &etc; * * * \ingroup worksheet */ Worksheet::Worksheet(const QString& name, bool loading) : AbstractPart(name, AspectType::Worksheet), d(new WorksheetPrivate(this)) { connect(this, &Worksheet::aspectAdded, this, &Worksheet::handleAspectAdded); connect(this, &Worksheet::aspectAboutToBeRemoved, this, &Worksheet::handleAspectAboutToBeRemoved); connect(this, &Worksheet::aspectRemoved, this, &Worksheet::handleAspectRemoved); if (!loading) init(); } Worksheet::~Worksheet() { delete d; } void Worksheet::init() { KConfig config; KConfigGroup group = config.group("Worksheet"); //size d->scaleContent = group.readEntry("ScaleContent", false); d->useViewSize = group.readEntry("UseViewSize", false); d->pageRect.setX(0); d->pageRect.setY(0); d->pageRect.setWidth(group.readEntry("Width", 1000)); d->pageRect.setHeight(group.readEntry("Height", 1000)); d->m_scene->setSceneRect(d->pageRect); //background d->backgroundType = (PlotArea::BackgroundType) group.readEntry("BackgroundType", (int) PlotArea::Color); d->backgroundColorStyle = (PlotArea::BackgroundColorStyle) group.readEntry("BackgroundColorStyle", (int) PlotArea::SingleColor); d->backgroundImageStyle = (PlotArea::BackgroundImageStyle) group.readEntry("BackgroundImageStyle", (int) PlotArea::Scaled); d->backgroundBrushStyle = (Qt::BrushStyle) group.readEntry("BackgroundBrushStyle", (int) Qt::SolidPattern); d->backgroundFileName = group.readEntry("BackgroundFileName", QString()); d->backgroundFirstColor = group.readEntry("BackgroundFirstColor", QColor(Qt::white)); d->backgroundSecondColor = group.readEntry("BackgroundSecondColor", QColor(Qt::black)); d->backgroundOpacity = group.readEntry("BackgroundOpacity", 1.0); //layout d->layout = (Worksheet::Layout) group.readEntry("Layout", (int) Worksheet::VerticalLayout); d->layoutTopMargin = group.readEntry("LayoutTopMargin", convertToSceneUnits(0.5, Centimeter)); d->layoutBottomMargin = group.readEntry("LayoutBottomMargin", convertToSceneUnits(0.5, Centimeter)); d->layoutLeftMargin = group.readEntry("LayoutLeftMargin", convertToSceneUnits(0.5, Centimeter)); d->layoutRightMargin = group.readEntry("LayoutRightMargin", convertToSceneUnits(0.5, Centimeter)); d->layoutVerticalSpacing = group.readEntry("LayoutVerticalSpacing", convertToSceneUnits(0.5, Centimeter)); d->layoutHorizontalSpacing = group.readEntry("LayoutHorizontalSpacing", convertToSceneUnits(0.5, Centimeter)); d->layoutRowCount = group.readEntry("LayoutRowCount", 2); d->layoutColumnCount = group.readEntry("LayoutColumnCount", 2); //default theme KConfigGroup settings = KSharedConfig::openConfig()->group(QLatin1String("Settings_Worksheet")); d->theme = settings.readEntry(QStringLiteral("Theme"), QString()); if (!d->theme.isEmpty()) loadTheme(d->theme); } /*! converts from \c unit to the scene units. At the moment, 1 scene unit corresponds to 1/10 mm. */ float Worksheet::convertToSceneUnits(const float value, const Worksheet::Unit unit) { switch (unit) { case Worksheet::Millimeter: return value*10.0; case Worksheet::Centimeter: return value*100.0; case Worksheet::Inch: return value*25.4*10.; case Worksheet::Point: return value*25.4/72.*10.; } return 0; } /*! converts from the scene units to \c unit . At the moment, 1 scene unit corresponds to 1/10 mm. */ float Worksheet::convertFromSceneUnits(const float value, const Worksheet::Unit unit) { switch (unit) { case Worksheet::Millimeter: return value/10.0; case Worksheet::Centimeter: return value/100.0; case Worksheet::Inch: return value/25.4/10.; case Worksheet::Point: return value/25.4/10.*72.; } return 0; } QIcon Worksheet::icon() const { return QIcon::fromTheme("labplot-worksheet"); } /** * Return a new context menu. The caller takes ownership of the menu. */ QMenu* Worksheet::createContextMenu() { QMenu* menu = AbstractPart::createContextMenu(); Q_ASSERT(menu); emit requestProjectContextMenu(menu); return menu; } //! Construct a primary view on me. /** * This method may be called multiple times during the life time of an Aspect, or it might not get * called at all. Aspects must not depend on the existence of a view for their operation. */ QWidget* Worksheet::view() const { if (!m_partView) { m_view = new WorksheetView(const_cast(this)); m_partView = m_view; connect(m_view, &WorksheetView::statusInfo, this, &Worksheet::statusInfo); connect(this, &Worksheet::cartesianPlotMouseModeChanged, m_view, &WorksheetView::cartesianPlotMouseModeChangedSlot); } return m_partView; } /*! * returns the list of all parent aspects (folders and sub-folders) * together with all the data containers required to plot the data in the worksheet */ QVector Worksheet::dependsOn() const { //add all parent aspects (folders and sub-folders) QVector aspects = AbstractAspect::dependsOn(); //traverse all plots and add all data containers they depend on for (const auto* plot : children()) aspects << plot->dependsOn(); return aspects; } bool Worksheet::exportView() const { auto* dlg = new ExportWorksheetDialog(m_view); dlg->setFileName(name()); bool ret; if ( (ret = (dlg->exec() == QDialog::Accepted)) ) { QString path = dlg->path(); const WorksheetView::ExportFormat format = dlg->exportFormat(); const WorksheetView::ExportArea area = dlg->exportArea(); const bool background = dlg->exportBackground(); const int resolution = dlg->exportResolution(); WAIT_CURSOR; m_view->exportToFile(path, format, area, background, resolution); RESET_CURSOR; } delete dlg; return ret; } bool Worksheet::printView() { QPrinter printer; auto* dlg = new QPrintDialog(&printer, m_view); dlg->setWindowTitle(i18nc("@title:window", "Print Worksheet")); bool ret; if ( (ret = (dlg->exec() == QDialog::Accepted)) ) m_view->print(&printer); delete dlg; return ret; } bool Worksheet::printPreview() const { auto* dlg = new QPrintPreviewDialog(m_view); connect(dlg, &QPrintPreviewDialog::paintRequested, m_view, &WorksheetView::print); return dlg->exec(); } void Worksheet::handleAspectAdded(const AbstractAspect* aspect) { const auto* addedElement = qobject_cast(aspect); if (!addedElement) return; if (aspect->parentAspect() != this) return; //add the GraphicsItem of the added child to the scene QGraphicsItem* item = addedElement->graphicsItem(); d->m_scene->addItem(item); const CartesianPlot* plot = dynamic_cast(aspect); if (plot) { connect(plot, &CartesianPlot::mouseMoveCursorModeSignal, this, &Worksheet::cartesianPlotMouseMoveCursorMode); connect(plot, &CartesianPlot::mouseMoveZoomSelectionModeSignal, this, &Worksheet::cartesianPlotMouseMoveZoomSelectionMode); connect(plot, &CartesianPlot::mousePressCursorModeSignal, this, &Worksheet::cartesianPlotMousePressCursorMode); connect(plot, &CartesianPlot::mousePressZoomSelectionModeSignal, this, &Worksheet::cartesianPlotMousePressZoomSelectionMode); connect(plot, &CartesianPlot::mouseReleaseZoomSelectionModeSignal, this, &Worksheet::cartesianPlotMouseReleaseZoomSelectionMode); connect(plot, &CartesianPlot::mouseHoverZoomSelectionModeSignal, this, &Worksheet::cartesianPlotMouseHoverZoomSelectionMode); connect(plot, &CartesianPlot::mouseHoverOutsideDataRectSignal, this, &Worksheet::cartesianPlotMouseHoverOutsideDataRect); connect(plot, &CartesianPlot::aspectDescriptionChanged, this, &Worksheet::updateCompleteCursorTreeModel); connect(plot, &CartesianPlot::curveNameChanged, this, &Worksheet::updateCompleteCursorTreeModel); connect(plot, &CartesianPlot::curveRemoved, this, &Worksheet::curveRemoved); connect(plot, &CartesianPlot::curveAdded, this, &Worksheet::curveAdded); connect(plot, &CartesianPlot::visibleChanged, this, &Worksheet::updateCompleteCursorTreeModel); connect(plot, &CartesianPlot::curveVisibilityChangedSignal, this, &Worksheet::updateCompleteCursorTreeModel); connect(plot, &CartesianPlot::curveDataChanged, this, &Worksheet::curveDataChanged); connect(plot, static_cast(&CartesianPlot::curveLinePenChanged), this, &Worksheet::updateCurveBackground); connect(plot, &CartesianPlot::mouseModeChanged, this, &Worksheet::cartesianPlotMouseModeChangedSlot); auto* p = const_cast(plot); p->setLocked(d->plotsLocked); cursorModelPlotAdded(p->name()); } qreal zVal = 0; - for (auto* child : children(IncludeHidden)) + for (auto* child : children(ChildIndexFlag::IncludeHidden)) child->graphicsItem()->setZValue(zVal++); //if a theme was selected in the worksheet, apply this theme for newly added children if (!d->theme.isEmpty() && !isLoading()) { KConfig config(ThemeHandler::themeFilePath(d->theme), KConfig::SimpleConfig); const_cast(addedElement)->loadThemeConfig(config); } //recalculated the layout if (!isLoading()) { if (d->layout != Worksheet::NoLayout) d->updateLayout(false); } } void Worksheet::handleAspectAboutToBeRemoved(const AbstractAspect* aspect) { const auto* removedElement = qobject_cast(aspect); if (removedElement) { QGraphicsItem* item = removedElement->graphicsItem(); d->m_scene->removeItem(item); } } void Worksheet::handleAspectRemoved(const AbstractAspect* parent, const AbstractAspect* before, const AbstractAspect* child) { Q_UNUSED(parent); Q_UNUSED(before); if (d->layout != Worksheet::NoLayout) d->updateLayout(false); auto* plot = dynamic_cast(child); if (plot) cursorModelPlotRemoved(plot->name()); } QGraphicsScene* Worksheet::scene() const { return d->m_scene; } QRectF Worksheet::pageRect() const { return d->m_scene->sceneRect(); } /*! this slot is called when a worksheet element is selected in the project explorer. emits \c itemSelected() which forwards this event to the \c WorksheetView in order to select the corresponding \c QGraphicsItem. */ void Worksheet::childSelected(const AbstractAspect* aspect) { auto* element = qobject_cast(const_cast(aspect)); if (element) emit itemSelected(element->graphicsItem()); } /*! this slot is called when a worksheet element is deselected in the project explorer. emits \c itemDeselected() which forwards this event to \c WorksheetView in order to deselect the corresponding \c QGraphicsItem. */ void Worksheet::childDeselected(const AbstractAspect* aspect) { auto* element = qobject_cast(const_cast(aspect)); if (element) emit itemDeselected(element->graphicsItem()); } /*! * Emits the signal to select or to deselect the aspect corresponding to \c QGraphicsItem \c item in the project explorer, * if \c selected=true or \c selected=false, respectively. * The signal is handled in \c AspectTreeModel and forwarded to the tree view in \c ProjectExplorer. * This function is called in \c WorksheetView upon selection changes. */ void Worksheet::setItemSelectedInView(const QGraphicsItem* item, const bool b) { //determine the corresponding aspect const AbstractAspect* aspect(nullptr); - for (const auto* child : children(IncludeHidden) ) { + for (const auto* child : children(ChildIndexFlag::IncludeHidden) ) { aspect = this->aspectFromGraphicsItem(child, item); if (aspect) break; } if (!aspect) return; //forward selection/deselection to AbstractTreeModel if (b) emit childAspectSelectedInView(aspect); else emit childAspectDeselectedInView(aspect); } /*! * helper function: checks whether \c aspect or one of its children has the \c GraphicsItem \c item * Returns a pointer to \c WorksheetElement having this item. */ WorksheetElement* Worksheet::aspectFromGraphicsItem(const WorksheetElement* aspect, const QGraphicsItem* item) const { if ( aspect->graphicsItem() == item ) return const_cast(aspect); else { - for (const auto* child : aspect->children(AbstractAspect::IncludeHidden) ) { + for (const auto* child : aspect->children(AbstractAspect::ChildIndexFlag::IncludeHidden) ) { WorksheetElement* a = this->aspectFromGraphicsItem(child, item); if (a) return a; } return nullptr; } } /*! Selects or deselects the worksheet in the project explorer. This function is called in \c WorksheetView. The worksheet gets deselected if there are selected items in the view, and selected if there are no selected items in the view. */ void Worksheet::setSelectedInView(const bool b) { if (b) emit childAspectSelectedInView(this); else emit childAspectDeselectedInView(this); } void Worksheet::deleteAspectFromGraphicsItem(const QGraphicsItem* item) { Q_ASSERT(item); //determine the corresponding aspect AbstractAspect* aspect(nullptr); - for (const auto* child : children(IncludeHidden) ) { + for (const auto* child : children(ChildIndexFlag::IncludeHidden) ) { aspect = this->aspectFromGraphicsItem(child, item); if (aspect) break; } if (!aspect) return; if (aspect->parentAspect()) aspect->parentAspect()->removeChild(aspect); else this->removeChild(aspect); } void Worksheet::setIsClosing() { if (m_view) m_view->setIsClosing(); } /*! * \brief Worksheet::getPlotCount * \return number of CartesianPlot's in the Worksheet */ int Worksheet::getPlotCount() { return children().length(); } /*! * \brief Worksheet::getPlot * \param index Number of plot which should be returned * \return Pointer to the CartesianPlot which was searched with index */ CartesianPlot* Worksheet::getPlot(int index) { auto plots = children(); if (plots.length() - 1 >= index) return plots.at(index); return nullptr; } TreeModel* Worksheet::cursorModel() { return d->cursorData; } void Worksheet::update() { emit requestUpdate(); } void Worksheet::setSuppressLayoutUpdate(bool value) { d->suppressLayoutUpdate = value; } void Worksheet::updateLayout() { d->updateLayout(); } Worksheet::CartesianPlotActionMode Worksheet::cartesianPlotActionMode() { return d->cartesianPlotActionMode; } Worksheet::CartesianPlotActionMode Worksheet::cartesianPlotCursorMode() { return d->cartesianPlotCursorMode; } bool Worksheet::plotsLocked() { return d->plotsLocked; } void Worksheet::setCartesianPlotActionMode(Worksheet::CartesianPlotActionMode mode) { if (d->cartesianPlotActionMode == mode) return; d->cartesianPlotActionMode = mode; project()->setChanged(true); } void Worksheet::setCartesianPlotCursorMode(Worksheet::CartesianPlotActionMode mode) { if (d->cartesianPlotCursorMode == mode) return; d->cartesianPlotCursorMode = mode; if (mode == Worksheet::CartesianPlotActionMode::ApplyActionToAll) { d->suppressCursorPosChanged = true; QVector plots = children(); QPointF logicPos; if (!plots.isEmpty()) { for (int i = 0; i < 2; i++) { logicPos = QPointF(plots[0]->cursorPos(i), 0); // y value does not matter cartesianPlotMousePressCursorMode(i, logicPos); } } d->suppressCursorPosChanged = false; } updateCompleteCursorTreeModel(); project()->setChanged(true); } void Worksheet::setPlotsLocked(bool lock) { if (d->plotsLocked == lock) return; d->plotsLocked = lock; for (auto* plot: children()) plot->setLocked(lock); project()->setChanged(true); } void Worksheet::registerShortcuts() { m_view->registerShortcuts(); } void Worksheet::unregisterShortcuts() { m_view->unregisterShortcuts(); } /* =============================== getter methods for general options ==================================== */ BASIC_D_READER_IMPL(Worksheet, bool, scaleContent, scaleContent) BASIC_D_READER_IMPL(Worksheet, bool, useViewSize, useViewSize) /* =============================== getter methods for background options ================================= */ BASIC_D_READER_IMPL(Worksheet, PlotArea::BackgroundType, backgroundType, backgroundType) BASIC_D_READER_IMPL(Worksheet, PlotArea::BackgroundColorStyle, backgroundColorStyle, backgroundColorStyle) BASIC_D_READER_IMPL(Worksheet, PlotArea::BackgroundImageStyle, backgroundImageStyle, backgroundImageStyle) BASIC_D_READER_IMPL(Worksheet, Qt::BrushStyle, backgroundBrushStyle, backgroundBrushStyle) CLASS_D_READER_IMPL(Worksheet, QColor, backgroundFirstColor, backgroundFirstColor) CLASS_D_READER_IMPL(Worksheet, QColor, backgroundSecondColor, backgroundSecondColor) CLASS_D_READER_IMPL(Worksheet, QString, backgroundFileName, backgroundFileName) BASIC_D_READER_IMPL(Worksheet, float, backgroundOpacity, backgroundOpacity) /* =============================== getter methods for layout options ====================================== */ BASIC_D_READER_IMPL(Worksheet, Worksheet::Layout, layout, layout) BASIC_D_READER_IMPL(Worksheet, float, layoutTopMargin, layoutTopMargin) BASIC_D_READER_IMPL(Worksheet, float, layoutBottomMargin, layoutBottomMargin) BASIC_D_READER_IMPL(Worksheet, float, layoutLeftMargin, layoutLeftMargin) BASIC_D_READER_IMPL(Worksheet, float, layoutRightMargin, layoutRightMargin) BASIC_D_READER_IMPL(Worksheet, float, layoutHorizontalSpacing, layoutHorizontalSpacing) BASIC_D_READER_IMPL(Worksheet, float, layoutVerticalSpacing, layoutVerticalSpacing) BASIC_D_READER_IMPL(Worksheet, int, layoutRowCount, layoutRowCount) BASIC_D_READER_IMPL(Worksheet, int, layoutColumnCount, layoutColumnCount) CLASS_D_READER_IMPL(Worksheet, QString, theme, theme) /* ============================ setter methods and undo commands for general options ===================== */ void Worksheet::setUseViewSize(bool useViewSize) { if (useViewSize != d->useViewSize) { d->useViewSize = useViewSize; emit useViewSizeRequested(); } } STD_SETTER_CMD_IMPL_S(Worksheet, SetScaleContent, bool, scaleContent) void Worksheet::setScaleContent(bool scaleContent) { if (scaleContent != d->scaleContent) exec(new WorksheetSetScaleContentCmd(d, scaleContent, ki18n("%1: change \"rescale the content\" property"))); } /* ============================ setter methods and undo commands for background options ================= */ STD_SETTER_CMD_IMPL_F_S(Worksheet, SetBackgroundType, PlotArea::BackgroundType, backgroundType, update) void Worksheet::setBackgroundType(PlotArea::BackgroundType type) { if (type != d->backgroundType) exec(new WorksheetSetBackgroundTypeCmd(d, type, ki18n("%1: background type changed"))); } STD_SETTER_CMD_IMPL_F_S(Worksheet, SetBackgroundColorStyle, PlotArea::BackgroundColorStyle, backgroundColorStyle, update) void Worksheet::setBackgroundColorStyle(PlotArea::BackgroundColorStyle style) { if (style != d->backgroundColorStyle) exec(new WorksheetSetBackgroundColorStyleCmd(d, style, ki18n("%1: background color style changed"))); } STD_SETTER_CMD_IMPL_F_S(Worksheet, SetBackgroundImageStyle, PlotArea::BackgroundImageStyle, backgroundImageStyle, update) void Worksheet::setBackgroundImageStyle(PlotArea::BackgroundImageStyle style) { if (style != d->backgroundImageStyle) exec(new WorksheetSetBackgroundImageStyleCmd(d, style, ki18n("%1: background image style changed"))); } STD_SETTER_CMD_IMPL_F_S(Worksheet, SetBackgroundBrushStyle, Qt::BrushStyle, backgroundBrushStyle, update) void Worksheet::setBackgroundBrushStyle(Qt::BrushStyle style) { if (style != d->backgroundBrushStyle) exec(new WorksheetSetBackgroundBrushStyleCmd(d, style, ki18n("%1: background brush style changed"))); } STD_SETTER_CMD_IMPL_F_S(Worksheet, SetBackgroundFirstColor, QColor, backgroundFirstColor, update) void Worksheet::setBackgroundFirstColor(const QColor &color) { if (color!= d->backgroundFirstColor) exec(new WorksheetSetBackgroundFirstColorCmd(d, color, ki18n("%1: set background first color"))); } STD_SETTER_CMD_IMPL_F_S(Worksheet, SetBackgroundSecondColor, QColor, backgroundSecondColor, update) void Worksheet::setBackgroundSecondColor(const QColor &color) { if (color!= d->backgroundSecondColor) exec(new WorksheetSetBackgroundSecondColorCmd(d, color, ki18n("%1: set background second color"))); } STD_SETTER_CMD_IMPL_F_S(Worksheet, SetBackgroundFileName, QString, backgroundFileName, update) void Worksheet::setBackgroundFileName(const QString& fileName) { if (fileName!= d->backgroundFileName) exec(new WorksheetSetBackgroundFileNameCmd(d, fileName, ki18n("%1: set background image"))); } STD_SETTER_CMD_IMPL_F_S(Worksheet, SetBackgroundOpacity, float, backgroundOpacity, update) void Worksheet::setBackgroundOpacity(float opacity) { if (opacity != d->backgroundOpacity) exec(new WorksheetSetBackgroundOpacityCmd(d, opacity, ki18n("%1: set opacity"))); } /* ============================ setter methods and undo commands for layout options ================= */ STD_SETTER_CMD_IMPL_F_S(Worksheet, SetLayout, Worksheet::Layout, layout, updateLayout) void Worksheet::setLayout(Worksheet::Layout layout) { if (layout != d->layout) { beginMacro(i18n("%1: set layout", name())); exec(new WorksheetSetLayoutCmd(d, layout, ki18n("%1: set layout"))); endMacro(); } } STD_SETTER_CMD_IMPL_M_F_S(Worksheet, SetLayoutTopMargin, float, layoutTopMargin, updateLayout) void Worksheet::setLayoutTopMargin(float margin) { if (margin != d->layoutTopMargin) { beginMacro(i18n("%1: set layout top margin", name())); exec(new WorksheetSetLayoutTopMarginCmd(d, margin, ki18n("%1: set layout top margin"))); endMacro(); } } STD_SETTER_CMD_IMPL_M_F_S(Worksheet, SetLayoutBottomMargin, float, layoutBottomMargin, updateLayout) void Worksheet::setLayoutBottomMargin(float margin) { if (margin != d->layoutBottomMargin) { beginMacro(i18n("%1: set layout bottom margin", name())); exec(new WorksheetSetLayoutBottomMarginCmd(d, margin, ki18n("%1: set layout bottom margin"))); endMacro(); } } STD_SETTER_CMD_IMPL_M_F_S(Worksheet, SetLayoutLeftMargin, float, layoutLeftMargin, updateLayout) void Worksheet::setLayoutLeftMargin(float margin) { if (margin != d->layoutLeftMargin) { beginMacro(i18n("%1: set layout left margin", name())); exec(new WorksheetSetLayoutLeftMarginCmd(d, margin, ki18n("%1: set layout left margin"))); endMacro(); } } STD_SETTER_CMD_IMPL_M_F_S(Worksheet, SetLayoutRightMargin, float, layoutRightMargin, updateLayout) void Worksheet::setLayoutRightMargin(float margin) { if (margin != d->layoutRightMargin) { beginMacro(i18n("%1: set layout right margin", name())); exec(new WorksheetSetLayoutRightMarginCmd(d, margin, ki18n("%1: set layout right margin"))); endMacro(); } } STD_SETTER_CMD_IMPL_M_F_S(Worksheet, SetLayoutVerticalSpacing, float, layoutVerticalSpacing, updateLayout) void Worksheet::setLayoutVerticalSpacing(float spacing) { if (spacing != d->layoutVerticalSpacing) { beginMacro(i18n("%1: set layout vertical spacing", name())); exec(new WorksheetSetLayoutVerticalSpacingCmd(d, spacing, ki18n("%1: set layout vertical spacing"))); endMacro(); } } STD_SETTER_CMD_IMPL_M_F_S(Worksheet, SetLayoutHorizontalSpacing, float, layoutHorizontalSpacing, updateLayout) void Worksheet::setLayoutHorizontalSpacing(float spacing) { if (spacing != d->layoutHorizontalSpacing) { beginMacro(i18n("%1: set layout horizontal spacing", name())); exec(new WorksheetSetLayoutHorizontalSpacingCmd(d, spacing, ki18n("%1: set layout horizontal spacing"))); endMacro(); } } STD_SETTER_CMD_IMPL_M_F_S(Worksheet, SetLayoutRowCount, int, layoutRowCount, updateLayout) void Worksheet::setLayoutRowCount(int count) { if (count != d->layoutRowCount) { beginMacro(i18n("%1: set layout row count", name())); exec(new WorksheetSetLayoutRowCountCmd(d, count, ki18n("%1: set layout row count"))); endMacro(); } } STD_SETTER_CMD_IMPL_M_F_S(Worksheet, SetLayoutColumnCount, int, layoutColumnCount, updateLayout) void Worksheet::setLayoutColumnCount(int count) { if (count != d->layoutColumnCount) { beginMacro(i18n("%1: set layout column count", name())); exec(new WorksheetSetLayoutColumnCountCmd(d, count, ki18n("%1: set layout column count"))); endMacro(); } } class WorksheetSetPageRectCmd : public StandardMacroSetterCmd { public: WorksheetSetPageRectCmd(Worksheet::Private* target, QRectF newValue, const KLocalizedString& description) : StandardMacroSetterCmd(target, &Worksheet::Private::pageRect, newValue, description) {} void finalize() override { m_target->updatePageRect(); emit m_target->q->pageRectChanged(m_target->*m_field); } void finalizeUndo() override { m_target->m_scene->setSceneRect(m_target->*m_field); emit m_target->q->pageRectChanged(m_target->*m_field); } }; void Worksheet::setPageRect(const QRectF& rect) { //don't allow any rectangulars of width/height equal to zero if (qFuzzyCompare(rect.width(), 0.) || qFuzzyCompare(rect.height(), 0.)) { emit pageRectChanged(d->pageRect); return; } if (rect != d->pageRect) { if (!d->useViewSize) { beginMacro(i18n("%1: set page size", name())); exec(new WorksheetSetPageRectCmd(d, rect, ki18n("%1: set page size"))); endMacro(); } else { d->pageRect = rect; d->updatePageRect(); emit pageRectChanged(d->pageRect); } } } void Worksheet::setPrinting(bool on) const { - QVector childElements = children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + QVector childElements = children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* child : childElements) child->setPrinting(on); } STD_SETTER_CMD_IMPL_S(Worksheet, SetTheme, QString, theme) void Worksheet::setTheme(const QString& theme) { if (theme != d->theme) { if (!theme.isEmpty()) { beginMacro( i18n("%1: load theme %2", name(), theme) ); exec(new WorksheetSetThemeCmd(d, theme, ki18n("%1: set theme"))); loadTheme(theme); endMacro(); } else { exec(new WorksheetSetThemeCmd(d, theme, ki18n("%1: disable theming"))); } } } void Worksheet::cartesianPlotMousePressZoomSelectionMode(QPointF logicPos) { if (cartesianPlotActionMode() == Worksheet::ApplyActionToAll) { - auto plots = children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + auto plots = children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* plot : plots) plot->mousePressZoomSelectionMode(logicPos); } else { CartesianPlot* plot = static_cast(QObject::sender()); plot->mousePressZoomSelectionMode(logicPos); } } void Worksheet::cartesianPlotMouseReleaseZoomSelectionMode() { if (cartesianPlotActionMode() == Worksheet::ApplyActionToAll) { - auto plots = children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + auto plots = children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* plot : plots) plot->mouseReleaseZoomSelectionMode(); } else { CartesianPlot* plot = static_cast(QObject::sender()); plot->mouseReleaseZoomSelectionMode(); } } void Worksheet::cartesianPlotMousePressCursorMode(int cursorNumber, QPointF logicPos) { if (cartesianPlotCursorMode() == Worksheet::ApplyActionToAll) { - auto plots = children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + auto plots = children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* plot : plots) plot->mousePressCursorMode(cursorNumber, logicPos); } else { CartesianPlot* plot = static_cast(QObject::sender()); plot->mousePressCursorMode(cursorNumber, logicPos); } cursorPosChanged(cursorNumber, logicPos.x()); } void Worksheet::cartesianPlotMouseMoveZoomSelectionMode(QPointF logicPos) { if (cartesianPlotActionMode() == Worksheet::ApplyActionToAll) { - QVector plots = children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + QVector plots = children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* plot : plots) plot->mouseMoveZoomSelectionMode(logicPos); } else { CartesianPlot* plot = static_cast(QObject::sender()); plot->mouseMoveZoomSelectionMode(logicPos); } } void Worksheet::cartesianPlotMouseHoverZoomSelectionMode(QPointF logicPos) { if (cartesianPlotActionMode() == Worksheet::ApplyActionToAll) { - auto plots = children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + auto plots = children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* plot : plots) plot->mouseHoverZoomSelectionMode(logicPos); } else { CartesianPlot* plot = static_cast(QObject::sender()); plot->mouseHoverZoomSelectionMode(logicPos); } } void Worksheet::cartesianPlotMouseHoverOutsideDataRect() { if (cartesianPlotActionMode() == Worksheet::ApplyActionToAll) { - auto plots = children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + auto plots = children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* plot : plots) plot->mouseHoverOutsideDataRect(); } else { CartesianPlot* plot = static_cast(QObject::sender()); plot->mouseHoverOutsideDataRect(); } } void Worksheet::cartesianPlotMouseMoveCursorMode(int cursorNumber, QPointF logicPos) { if (cartesianPlotCursorMode() == Worksheet::ApplyActionToAll) { - auto plots = children(AbstractAspect::Recursive | AbstractAspect::IncludeHidden); + auto plots = children(AbstractAspect::ChildIndexFlag::Recursive | AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* plot : plots) plot->mouseMoveCursorMode(cursorNumber, logicPos); } else { CartesianPlot* plot = static_cast(QObject::sender()); plot->mouseMoveCursorMode(cursorNumber, logicPos); } cursorPosChanged(cursorNumber, logicPos.x()); } /*! * \brief Worksheet::cursorPosChanged * Updates the cursor treemodel with the new data * \param xPos: new position of the cursor * It is assumed, that the plots/curves are in the same order than receiving from * the children() function. It's not checked if the names are the same */ void Worksheet::cursorPosChanged(int cursorNumber, double xPos) { if (d->suppressCursorPosChanged) return; auto* sender = dynamic_cast(QObject::sender()); if (!sender) return; TreeModel* treeModel = cursorModel(); // if ApplyActionToSelection, each plot has it's own x value if (cartesianPlotCursorMode() == Worksheet::ApplyActionToAll) { // x values int rowPlot = 1; QModelIndex xName = treeModel->index(0, WorksheetPrivate::TreeModelColumn::SIGNALNAME); treeModel->setData(xName, QVariant("X")); double valueCursor[2]; for (int i = 0; i < 2; i++) { // need both cursors to calculate diff valueCursor[i] = sender->cursorPos(i); treeModel->setTreeData(QVariant(valueCursor[i]), 0, WorksheetPrivate::TreeModelColumn::CURSOR0+i); } treeModel->setTreeData(QVariant(valueCursor[1] - valueCursor[0]), 0, WorksheetPrivate::TreeModelColumn::CURSORDIFF); // y values for (int i = 0; i < getPlotCount(); i++) { // i=0 is the x Axis auto* plot = getPlot(i); if (!plot || !plot->isVisible()) continue; QModelIndex plotIndex = treeModel->index(rowPlot, WorksheetPrivate::TreeModelColumn::PLOTNAME); // curves int rowCurve = 0; for (int j = 0; j < plot->curveCount(); j++) { // assumption: index of signals in model is the same than the index of the signal in the plot bool valueFound; const XYCurve* curve = plot->getCurve(j); if (!curve->isVisible()) continue; double value = curve->y(xPos, valueFound); if (cursorNumber == 0) { treeModel->setTreeData(QVariant(value), rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR0, plotIndex); double valueCursor1 = treeModel->treeData(rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR1, plotIndex).toDouble(); treeModel->setTreeData(QVariant(valueCursor1 - value), rowCurve, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotIndex); } else { treeModel->setTreeData(QVariant(value), rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR1, plotIndex); double valueCursor0 = treeModel->treeData(rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR0, plotIndex).toDouble(); treeModel->setTreeData(QVariant(value - valueCursor0), rowCurve, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotIndex); } rowCurve++; } rowPlot++; } } else { // apply to selection // assumption: plot is visible int rowCount = treeModel->rowCount(); for (int i = 0; i < rowCount; i++) { QModelIndex plotIndex = treeModel->index(i, WorksheetPrivate::TreeModelColumn::PLOTNAME); if (plotIndex.data().toString().compare(sender->name()) != 0) continue; // x values (first row always exist) treeModel->setTreeData(QVariant("X"), 0, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotIndex); double valueCursor[2]; for (int i = 0; i < 2; i++) { // need both cursors to calculate diff valueCursor[i] = sender->cursorPos(i); treeModel->setTreeData(QVariant(valueCursor[i]), 0, WorksheetPrivate::TreeModelColumn::CURSOR0+i, plotIndex); } treeModel->setTreeData(QVariant(valueCursor[1]-valueCursor[0]), 0, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotIndex); // y values int rowCurve = 1; // first is x value for (int j = 0; j< sender->curveCount(); j++) { // j=0 are the x values const XYCurve* curve = sender->getCurve(j); // -1 because we start with 1 for the x axis if (!curve->isVisible()) continue; // assumption: index of signals in model is the same than the index of the signal in the plot bool valueFound; double value = curve->y(xPos, valueFound); if (cursorNumber == 0) { treeModel->setTreeData(QVariant(value), rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR0, plotIndex); double valueCursor1 = treeModel->treeData(rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR1, plotIndex).toDouble(); treeModel->setTreeData(QVariant(valueCursor1 - value), rowCurve, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotIndex); } else { treeModel->setTreeData(QVariant(value), rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR1, plotIndex); double valueCursor0 = treeModel->treeData(rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR0, plotIndex).toDouble(); treeModel->setTreeData(QVariant(value - valueCursor0), rowCurve, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotIndex); } rowCurve++; } } } } void Worksheet::cursorModelPlotAdded(const QString& name) { Q_UNUSED(name); // TreeModel* treeModel = cursorModel(); // int rowCount = treeModel->rowCount(); // // add plot at the end // treeModel->insertRows(rowCount, 1); // add empty rows. Then they become filled // treeModel->setTreeData(QVariant(name), rowCount, WorksheetPrivate::TreeModelColumn::PLOTNAME); // rowCount instead of rowCount -1 because first row is the x value updateCompleteCursorTreeModel(); } void Worksheet::cursorModelPlotRemoved(const QString& name) { TreeModel* treeModel = cursorModel(); int rowCount = treeModel->rowCount(); // first is x Axis for (int i = 1; i < rowCount; i++) { QModelIndex plotIndex = treeModel->index(i, WorksheetPrivate::TreeModelColumn::PLOTNAME); if (plotIndex.data().toString().compare(name) != 0) continue; treeModel->removeRows(plotIndex.row(), 1); return; } } void Worksheet::cartesianPlotMouseModeChangedSlot(CartesianPlot::MouseMode mode) { if (d->updateCompleteCursorModel) { updateCompleteCursorTreeModel(); d->updateCompleteCursorModel = false; } emit cartesianPlotMouseModeChanged(mode); } void Worksheet::curveDataChanged(const XYCurve* curve) { auto* plot = dynamic_cast(QObject::sender()); if (!plot) return; TreeModel* treeModel = cursorModel(); int rowCount = treeModel->rowCount(); for (int i = 0; i < rowCount; i++) { QModelIndex plotIndex = treeModel->index(i, WorksheetPrivate::TreeModelColumn::PLOTNAME); if (plotIndex.data().toString().compare(plot->name()) != 0) continue; for (int j = 0; j < plot->curveCount(); j++) { if (plot->getCurve(j)->name().compare(curve->name()) != 0) continue; treeModel->setTreeData(QVariant(curve->name()), j, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotIndex); bool valueFound; double valueCursor0 = curve->y(plot->cursorPos(0), valueFound); treeModel->setTreeData(QVariant(valueCursor0), j, WorksheetPrivate::TreeModelColumn::CURSOR0, plotIndex); double valueCursor1 = curve->y(plot->cursorPos(1), valueFound); treeModel->setTreeData(QVariant(valueCursor1), j, WorksheetPrivate::TreeModelColumn::CURSOR1, plotIndex); treeModel->setTreeData(QVariant(valueCursor1-valueCursor0), j, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotIndex); break; } break; } } void Worksheet::curveAdded(const XYCurve* curve) { auto* plot = dynamic_cast(QObject::sender()); if (!plot) return; TreeModel* treeModel = cursorModel(); int rowCount = treeModel->rowCount(); int i = 0; // first row is the x axis when applied to all plots. Starting at the second row if (cartesianPlotCursorMode() == Worksheet::ApplyActionToAll) i = 1; for (; i < rowCount; i++) { QModelIndex plotIndex = treeModel->index(i, WorksheetPrivate::TreeModelColumn::PLOTNAME); if (plotIndex.data().toString().compare(plot->name()) != 0) continue; int row = 0; for (int j = 0; j < plot->curveCount(); j++) { if (plot->getCurve(j)->name().compare(curve->name()) != 0) { if (plot->getCurve(j)->isVisible()) row ++; continue; } treeModel->insertRow(row, plotIndex); treeModel->setTreeData(QVariant(curve->name()), row, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotIndex); QColor curveColor = curve->linePen().color(); curveColor.setAlpha(50); treeModel->setTreeData(QVariant(curveColor),row, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotIndex, Qt::BackgroundRole); bool valueFound; double valueCursor0 = curve->y(plot->cursorPos(0), valueFound); treeModel->setTreeData(QVariant(valueCursor0), row, WorksheetPrivate::TreeModelColumn::CURSOR0, plotIndex); double valueCursor1 = curve->y(plot->cursorPos(1), valueFound); treeModel->setTreeData(QVariant(valueCursor1), row, WorksheetPrivate::TreeModelColumn::CURSOR1, plotIndex); treeModel->setTreeData(QVariant(valueCursor1-valueCursor0), row, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotIndex); break; } break; } } void Worksheet::curveRemoved(const XYCurve* curve) { auto* plot = dynamic_cast(QObject::sender()); if (!plot) return; TreeModel* treeModel = cursorModel(); int rowCount = treeModel->rowCount(); for (int i = 0; i < rowCount; i++) { QModelIndex plotIndex = treeModel->index(i, WorksheetPrivate::TreeModelColumn::PLOTNAME); if (plotIndex.data().toString().compare(plot->name()) != 0) continue; int curveCount = treeModel->rowCount(plotIndex); for (int j = 0; j < curveCount; j++) { QModelIndex curveIndex = treeModel->index(j, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotIndex); if (curveIndex.data().toString().compare(curve->name()) != 0) continue; treeModel->removeRow(j, plotIndex); break; } break; } } /*! * Updates the background of the cuves entry in the treeview * @param pen Pen of the curve * @param curveName Curve name to find in treemodel */ void Worksheet::updateCurveBackground(const QPen& pen, const QString& curveName) { const CartesianPlot* plot = static_cast(QObject::sender()); TreeModel* treeModel = cursorModel(); int rowCount = treeModel->rowCount(); for (int i = 0; i < rowCount; i++) { QModelIndex plotIndex = treeModel->index(i, WorksheetPrivate::TreeModelColumn::PLOTNAME); if (plotIndex.data().toString().compare(plot->name()) != 0) continue; int curveCount = treeModel->rowCount(plotIndex); for (int j = 0; j < curveCount; j++) { QModelIndex curveIndex = treeModel->index(j, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotIndex); if (curveIndex.data().toString().compare(curveName) != 0) continue; QColor curveColor = pen.color(); curveColor.setAlpha(50); treeModel->setTreeData(QVariant(curveColor), j, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotIndex, Qt::BackgroundRole); return; } return; } } /** * @brief Worksheet::updateCompleteCursorTreeModel * If the plot or the curve are not available, the plot/curve is not in the treemodel! */ void Worksheet::updateCompleteCursorTreeModel() { if (isLoading()) return; TreeModel* treeModel = cursorModel(); if (treeModel->rowCount() > 0) treeModel->removeRows(0, treeModel->rowCount()); // remove all data int plotCount = getPlotCount(); if (plotCount < 1) return; if (cartesianPlotCursorMode() == Worksheet::CartesianPlotActionMode::ApplyActionToAll) { // 1 because of the X data treeModel->insertRows(0, 1); //, treeModel->index(0,0)); // add empty rows. Then they become filled // set X data QModelIndex xName = treeModel->index(0, WorksheetPrivate::TreeModelColumn::SIGNALNAME); treeModel->setData(xName, QVariant("X")); auto* plot0 = getPlot(0); if (plot0) { double valueCursor[2]; for (int i = 0; i < 2; i++) { valueCursor[i] = plot0->cursorPos(i); QModelIndex cursor = treeModel->index(0,WorksheetPrivate::TreeModelColumn::CURSOR0+i); treeModel->setData(cursor, QVariant(valueCursor[i])); } QModelIndex diff = treeModel->index(0,WorksheetPrivate::TreeModelColumn::CURSORDIFF); treeModel->setData(diff, QVariant(valueCursor[1]-valueCursor[0])); } } else { //treeModel->insertRows(0, plotCount, treeModel->index(0,0)); // add empty rows. Then they become filled } // set plot name, y value, background for (int i = 0; i < plotCount; i++) { auto* plot = getPlot(i); QModelIndex plotName; int addOne = 0; if (!plot || !plot->isVisible()) continue; // add new entry for the plot treeModel->insertRows(treeModel->rowCount(), 1); //, treeModel->index(0, 0)); // add plot name and X row if needed if (cartesianPlotCursorMode() == Worksheet::CartesianPlotActionMode::ApplyActionToAll) { plotName = treeModel->index(i + 1, WorksheetPrivate::TreeModelColumn::PLOTNAME); // plus one because first row are the x values treeModel->setData(plotName, QVariant(plot->name())); } else { addOne = 1; plotName = treeModel->index(i, WorksheetPrivate::TreeModelColumn::PLOTNAME); treeModel->setData(plotName, QVariant(plot->name())); treeModel->insertRows(0, 1, plotName); // one, because the first row are the x values QModelIndex xName = treeModel->index(0, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotName); treeModel->setData(xName, QVariant("X")); double valueCursor[2]; for (int i = 0; i < 2; i++) { valueCursor[i] = plot->cursorPos(i); QModelIndex cursor = treeModel->index(0, WorksheetPrivate::TreeModelColumn::CURSOR0+i, plotName); treeModel->setData(cursor, QVariant(valueCursor[i])); } QModelIndex diff = treeModel->index(0, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotName); treeModel->setData(diff, QVariant(valueCursor[1]-valueCursor[0])); } int rowCurve = addOne; for (int j = 0; j < plot->curveCount(); j++) { double cursorValue[2] = {NAN, NAN}; const XYCurve* curve = plot->getCurve(j); if (!curve->isVisible()) continue; for (int k = 0; k < 2; k++) { double xPos = plot->cursorPos(k); bool valueFound; cursorValue[k] = curve->y(xPos,valueFound); } treeModel->insertRows(rowCurve, 1, plotName); QColor curveColor = curve->linePen().color(); curveColor.setAlpha(50); treeModel->setTreeData(QVariant(curveColor), rowCurve, 0, plotName, Qt::BackgroundRole); treeModel->setTreeData(QVariant(curve->name()), rowCurve, WorksheetPrivate::TreeModelColumn::SIGNALNAME, plotName); treeModel->setTreeData(QVariant(cursorValue[0]), rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR0, plotName); treeModel->setTreeData(QVariant(cursorValue[1]), rowCurve, WorksheetPrivate::TreeModelColumn::CURSOR1, plotName); treeModel->setTreeData(QVariant(cursorValue[1]-cursorValue[0]), rowCurve, WorksheetPrivate::TreeModelColumn::CURSORDIFF, plotName); rowCurve++; } } } //############################################################################## //###################### Private implementation ############################### //############################################################################## WorksheetPrivate::WorksheetPrivate(Worksheet* owner) : q(owner), m_scene(new QGraphicsScene()) { QStringList headers = {i18n("Curves"), "V1", "V2", "V2-V1"}; cursorData = new TreeModel(headers, nullptr); } QString WorksheetPrivate::name() const { return q->name(); } /*! * called if the worksheet page (the actual size of worksheet's rectangular) was changed. * if a layout is active, it is is updated - this adjusts the sizes of the elements in the layout to the new page size. * if no layout is active and the option "scale content" is active, \c handleResize() is called to adjust zhe properties. */ void WorksheetPrivate::updatePageRect() { if (q->isLoading()) return; QRectF oldRect = m_scene->sceneRect(); m_scene->setSceneRect(pageRect); if (layout != Worksheet::NoLayout) updateLayout(); else { if (scaleContent) { qreal horizontalRatio = pageRect.width() / oldRect.width(); qreal verticalRatio = pageRect.height() / oldRect.height(); - QVector childElements = q->children(AbstractAspect::IncludeHidden); + QVector childElements = q->children(AbstractAspect::ChildIndexFlag::IncludeHidden); if (useViewSize) { //don't make the change of the geometry undoable/redoable if the view size is used. for (auto* elem : childElements) { elem->setUndoAware(false); elem->handleResize(horizontalRatio, verticalRatio, true); elem->setUndoAware(true); } } else { // for (auto* child : childElements) // child->handleResize(horizontalRatio, verticalRatio, true); } } } } void WorksheetPrivate::update() { q->update(); } WorksheetPrivate::~WorksheetPrivate() { delete m_scene; } void WorksheetPrivate::updateLayout(bool undoable) { if (suppressLayoutUpdate) return; QVector list = q->children(); if (layout == Worksheet::NoLayout) { for (auto* elem : list) elem->graphicsItem()->setFlag(QGraphicsItem::ItemIsMovable, true); return; } float x = layoutLeftMargin; float y = layoutTopMargin; float w, h; int count = list.count(); if (layout == Worksheet::VerticalLayout) { w = m_scene->sceneRect().width() - layoutLeftMargin - layoutRightMargin; h = (m_scene->sceneRect().height()-layoutTopMargin-layoutBottomMargin- (count-1)*layoutVerticalSpacing)/count; for (auto* elem : list) { setContainerRect(elem, x, y, h, w, undoable); y += h + layoutVerticalSpacing; } } else if (layout == Worksheet::HorizontalLayout) { w = (m_scene->sceneRect().width()-layoutLeftMargin-layoutRightMargin- (count-1)*layoutHorizontalSpacing)/count; h = m_scene->sceneRect().height() - layoutTopMargin-layoutBottomMargin; for (auto* elem : list) { setContainerRect(elem, x, y, h, w, undoable); x += w + layoutHorizontalSpacing; } } else { //GridLayout //add new rows, if not sufficient if (count > layoutRowCount*layoutColumnCount) { layoutRowCount = floor( (float)count/layoutColumnCount + 0.5); emit q->layoutRowCountChanged(layoutRowCount); } w = (m_scene->sceneRect().width()-layoutLeftMargin-layoutRightMargin- (layoutColumnCount-1)*layoutHorizontalSpacing)/layoutColumnCount; h = (m_scene->sceneRect().height()-layoutTopMargin-layoutBottomMargin- (layoutRowCount-1)*layoutVerticalSpacing)/layoutRowCount; int columnIndex = 0; //counts the columns in a row for (auto* elem : list) { setContainerRect(elem, x, y, h, w, undoable); x += w + layoutHorizontalSpacing; columnIndex++; if (columnIndex == layoutColumnCount) { columnIndex = 0; x = layoutLeftMargin; y += h + layoutVerticalSpacing; } } } } void WorksheetPrivate::setContainerRect(WorksheetElementContainer* elem, float x, float y, float h, float w, bool undoable) { if (useViewSize) { //when using the view size, no need to put rect changes onto the undo-stack elem->setUndoAware(false); elem->setRect(QRectF(x,y,w,h)); elem->setUndoAware(true); } else { //don't put rect changed onto the undo-stack if undoable-flag is set to true, //e.g. when new child is added or removed (the layout and the childrend rects will be updated anyway) if (!undoable) { elem->setUndoAware(false); elem->setRect(QRectF(x,y,w,h)); elem->setUndoAware(true); } else elem->setRect(QRectF(x,y,w,h)); } elem->graphicsItem()->setFlag(QGraphicsItem::ItemIsMovable, false); } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## //! Save as XML void Worksheet::save(QXmlStreamWriter* writer) const { writer->writeStartElement( "worksheet" ); writeBasicAttributes(writer); writeCommentElement(writer); //applied theme if (!d->theme.isEmpty()) { writer->writeStartElement( "theme" ); writer->writeAttribute("name", d->theme); writer->writeEndElement(); } //geometry writer->writeStartElement( "geometry" ); QRectF rect = d->m_scene->sceneRect(); writer->writeAttribute( "x", QString::number(rect.x()) ); writer->writeAttribute( "y", QString::number(rect.y()) ); writer->writeAttribute( "width", QString::number(rect.width()) ); writer->writeAttribute( "height", QString::number(rect.height()) ); writer->writeAttribute( "useViewSize", QString::number(d->useViewSize) ); writer->writeEndElement(); //layout writer->writeStartElement( "layout" ); writer->writeAttribute( "layout", QString::number(d->layout) ); writer->writeAttribute( "topMargin", QString::number(d->layoutTopMargin) ); writer->writeAttribute( "bottomMargin", QString::number(d->layoutBottomMargin) ); writer->writeAttribute( "leftMargin", QString::number(d->layoutLeftMargin) ); writer->writeAttribute( "rightMargin", QString::number(d->layoutRightMargin) ); writer->writeAttribute( "verticalSpacing", QString::number(d->layoutVerticalSpacing) ); writer->writeAttribute( "horizontalSpacing", QString::number(d->layoutHorizontalSpacing) ); writer->writeAttribute( "columnCount", QString::number(d->layoutColumnCount) ); writer->writeAttribute( "rowCount", QString::number(d->layoutRowCount) ); writer->writeEndElement(); //background properties writer->writeStartElement( "background" ); writer->writeAttribute( "type", QString::number(d->backgroundType) ); writer->writeAttribute( "colorStyle", QString::number(d->backgroundColorStyle) ); writer->writeAttribute( "imageStyle", QString::number(d->backgroundImageStyle) ); writer->writeAttribute( "brushStyle", QString::number(d->backgroundBrushStyle) ); writer->writeAttribute( "firstColor_r", QString::number(d->backgroundFirstColor.red()) ); writer->writeAttribute( "firstColor_g", QString::number(d->backgroundFirstColor.green()) ); writer->writeAttribute( "firstColor_b", QString::number(d->backgroundFirstColor.blue()) ); writer->writeAttribute( "secondColor_r", QString::number(d->backgroundSecondColor.red()) ); writer->writeAttribute( "secondColor_g", QString::number(d->backgroundSecondColor.green()) ); writer->writeAttribute( "secondColor_b", QString::number(d->backgroundSecondColor.blue()) ); writer->writeAttribute( "fileName", d->backgroundFileName ); writer->writeAttribute( "opacity", QString::number(d->backgroundOpacity) ); writer->writeEndElement(); // cartesian properties writer->writeStartElement( "plotProperties" ); writer->writeAttribute( "plotsLocked", QString::number(d->plotsLocked) ); writer->writeAttribute( "cartesianPlotActionMode", QString::number(d->cartesianPlotActionMode)); writer->writeAttribute( "cartesianPlotCursorMode", QString::number(d->cartesianPlotCursorMode)); writer->writeEndElement(); //serialize all children - for (auto* child : children(IncludeHidden)) + for (auto* child : children(ChildIndexFlag::IncludeHidden)) child->save(writer); writer->writeEndElement(); // close "worksheet" section } //! Load from XML bool Worksheet::load(XmlStreamReader* reader, bool preview) { if (!readBasicAttributes(reader)) return false; //clear the theme that was potentially set in init() in order to correctly load here the worksheets without any theme used d->theme.clear(); KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs; QString str; QRectF rect; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "worksheet") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (!preview && reader->name() == "theme") { attribs = reader->attributes(); d->theme = attribs.value("name").toString(); } else if (!preview && reader->name() == "geometry") { attribs = reader->attributes(); str = attribs.value("x").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("x").toString()); else rect.setX(str.toDouble()); str = attribs.value("y").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("y").toString()); else rect.setY(str.toDouble()); str = attribs.value("width").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("width").toString()); else rect.setWidth(str.toDouble()); str = attribs.value("height").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("height").toString()); else rect.setHeight(str.toDouble()); READ_INT_VALUE("useViewSize", useViewSize, int); } else if (!preview && reader->name() == "layout") { attribs = reader->attributes(); READ_INT_VALUE("layout", layout, Worksheet::Layout); READ_DOUBLE_VALUE("topMargin", layoutTopMargin); READ_DOUBLE_VALUE("bottomMargin", layoutBottomMargin); READ_DOUBLE_VALUE("leftMargin", layoutLeftMargin); READ_DOUBLE_VALUE("rightMargin", layoutRightMargin); READ_DOUBLE_VALUE("verticalSpacing", layoutVerticalSpacing); READ_DOUBLE_VALUE("horizontalSpacing", layoutHorizontalSpacing); READ_INT_VALUE("columnCount", layoutColumnCount, int); READ_INT_VALUE("rowCount", layoutRowCount, int); } else if (!preview && reader->name() == "background") { attribs = reader->attributes(); READ_INT_VALUE("type", backgroundType, PlotArea::BackgroundType); READ_INT_VALUE("colorStyle", backgroundColorStyle, PlotArea::BackgroundColorStyle); READ_INT_VALUE("imageStyle", backgroundImageStyle, PlotArea::BackgroundImageStyle); READ_INT_VALUE("brushStyle", backgroundBrushStyle, Qt::BrushStyle); str = attribs.value("firstColor_r").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("firstColor_r").toString()); else d->backgroundFirstColor.setRed(str.toInt()); str = attribs.value("firstColor_g").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("firstColor_g").toString()); else d->backgroundFirstColor.setGreen(str.toInt()); str = attribs.value("firstColor_b").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("firstColor_b").toString()); else d->backgroundFirstColor.setBlue(str.toInt()); str = attribs.value("secondColor_r").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("secondColor_r").toString()); else d->backgroundSecondColor.setRed(str.toInt()); str = attribs.value("secondColor_g").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("secondColor_g").toString()); else d->backgroundSecondColor.setGreen(str.toInt()); str = attribs.value("secondColor_b").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("secondColor_b").toString()); else d->backgroundSecondColor.setBlue(str.toInt()); str = attribs.value("fileName").toString(); d->backgroundFileName = str; READ_DOUBLE_VALUE("opacity", backgroundOpacity); } else if(!preview && reader->name() == "plotProperties") { attribs = reader->attributes(); READ_INT_VALUE("plotsLocked", plotsLocked, bool); READ_INT_VALUE("cartesianPlotActionMode", cartesianPlotActionMode, Worksheet::CartesianPlotActionMode); READ_INT_VALUE("cartesianPlotCursorMode", cartesianPlotCursorMode, Worksheet::CartesianPlotActionMode); } else if (reader->name() == "cartesianPlot") { CartesianPlot* plot = new CartesianPlot(QString()); plot->setIsLoading(true); if (!plot->load(reader, preview)) { delete plot; return false; } else addChildFast(plot); } else if (reader->name() == "textLabel") { TextLabel* label = new TextLabel(QString()); if (!label->load(reader, preview)) { delete label; return false; } else addChildFast(label); } else if (reader->name() == "image") { Image* image = new Image(QString()); if (!image->load(reader, preview)) { delete image; return false; } else addChildFast(image); } else { // unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } if (!preview) { d->m_scene->setSceneRect(rect); d->updateLayout(); updateCompleteCursorTreeModel(); } return true; } //############################################################################## //######################### Theme management ################################## //############################################################################## void Worksheet::loadTheme(const QString& theme) { KConfig config(ThemeHandler::themeFilePath(theme), KConfig::SimpleConfig); //apply the same background color for Worksheet as for the CartesianPlot const KConfigGroup group = config.group("CartesianPlot"); this->setBackgroundBrushStyle((Qt::BrushStyle)group.readEntry("BackgroundBrushStyle",(int) this->backgroundBrushStyle())); this->setBackgroundColorStyle((PlotArea::BackgroundColorStyle)(group.readEntry("BackgroundColorStyle",(int) this->backgroundColorStyle()))); this->setBackgroundFirstColor(group.readEntry("BackgroundFirstColor",(QColor) this->backgroundFirstColor())); this->setBackgroundImageStyle((PlotArea::BackgroundImageStyle)group.readEntry("BackgroundImageStyle",(int) this->backgroundImageStyle())); this->setBackgroundOpacity(group.readEntry("BackgroundOpacity", this->backgroundOpacity())); this->setBackgroundSecondColor(group.readEntry("BackgroundSecondColor",(QColor) this->backgroundSecondColor())); this->setBackgroundType((PlotArea::BackgroundType)(group.readEntry("BackgroundType",(int) this->backgroundType()))); //load the theme for all the children - const QVector& childElements = children(AbstractAspect::IncludeHidden); + const QVector& childElements = children(AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* child : childElements) child->loadThemeConfig(config); } diff --git a/src/backend/worksheet/WorksheetElementContainer.cpp b/src/backend/worksheet/WorksheetElementContainer.cpp index d6aef598f..d6be68f25 100644 --- a/src/backend/worksheet/WorksheetElementContainer.cpp +++ b/src/backend/worksheet/WorksheetElementContainer.cpp @@ -1,286 +1,286 @@ /*************************************************************************** File : WorksheetElementContainer.cpp Project : LabPlot Description : Worksheet element container - parent of multiple elements -------------------------------------------------------------------- Copyright : (C) 2009 Tilman Benkert (thzs@gmx.net) Copyright : (C) 2012-2015 by 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 "backend/worksheet/WorksheetElementContainer.h" #include "backend/worksheet/WorksheetElementContainerPrivate.h" #include "backend/worksheet/plots/cartesian/Axis.h" #include "backend/lib/commandtemplates.h" #include "backend/lib/macros.h" #include "backend/lib/trace.h" #include #include #include #include #include /** * \class WorksheetElementContainer * \ingroup worksheet * \brief Worksheet element container - parent of multiple elements * This class provides the functionality for a containers of multiple * worksheet elements. Such a container can be a plot or group of elements. */ WorksheetElementContainer::WorksheetElementContainer(const QString& name, AspectType type) : WorksheetElement(name, type), d_ptr(new WorksheetElementContainerPrivate(this)) { connect(this, &WorksheetElementContainer::aspectAdded, this, &WorksheetElementContainer::handleAspectAdded); } WorksheetElementContainer::WorksheetElementContainer(const QString& name, WorksheetElementContainerPrivate* dd, AspectType type) : WorksheetElement(name, type), d_ptr(dd) { connect(this, &WorksheetElementContainer::aspectAdded, this, &WorksheetElementContainer::handleAspectAdded); } //no need to delete the d-pointer here - it inherits from QGraphicsItem //and is deleted during the cleanup in QGraphicsScene WorksheetElementContainer::~WorksheetElementContainer() = default; QGraphicsItem* WorksheetElementContainer::graphicsItem() const { return const_cast(static_cast(d_ptr)); } QRectF WorksheetElementContainer::rect() const { Q_D(const WorksheetElementContainer); return d->rect; } STD_SWAP_METHOD_SETTER_CMD_IMPL(WorksheetElementContainer, SetVisible, bool, swapVisible) void WorksheetElementContainer::setVisible(bool on) { Q_D(WorksheetElementContainer); //take care of proper ordering on the undo-stack, //when making the container and all its children visible/invisible. //if visible is set true, change the visibility of the container first if (on) { beginMacro( i18n("%1: set visible", name()) ); exec( new WorksheetElementContainerSetVisibleCmd(d, on, ki18n("%1: set visible")) ); } else { beginMacro( i18n("%1: set invisible", name()) ); } //change the visibility of all children - QVector childList = children(AbstractAspect::IncludeHidden | AbstractAspect::Compress); + QVector childList = children(AbstractAspect::ChildIndexFlag::IncludeHidden | AbstractAspect::ChildIndexFlag::Compress); for (auto* elem : childList) elem->setVisible(on); //if visible is set false, change the visibility of the container last if (!on) exec(new WorksheetElementContainerSetVisibleCmd(d, false, ki18n("%1: set invisible"))); endMacro(); } bool WorksheetElementContainer::isVisible() const { Q_D(const WorksheetElementContainer); return d->isVisible(); } bool WorksheetElementContainer::isFullyVisible() const { - QVector childList = children(AbstractAspect::IncludeHidden | AbstractAspect::Compress); + QVector childList = children(AbstractAspect::ChildIndexFlag::IncludeHidden | AbstractAspect::ChildIndexFlag::Compress); for (const auto* elem : childList) { if (!elem->isVisible()) return false; } return true; } void WorksheetElementContainer::setPrinting(bool on) { Q_D(WorksheetElementContainer); d->m_printing = on; } void WorksheetElementContainer::retransform() { // if (isLoading()) // return; PERFTRACE("WorksheetElementContainer::retransform()"); Q_D(WorksheetElementContainer); - QVector childList = children(AbstractAspect::IncludeHidden | AbstractAspect::Compress); + QVector childList = children(AbstractAspect::ChildIndexFlag::IncludeHidden | AbstractAspect::ChildIndexFlag::Compress); for (auto* child : childList) child->retransform(); d->recalcShapeAndBoundingRect(); } /*! * called if the size of the worksheet page was changed and the content has to be adjusted/resized (\c pageResize = true) * or if a new rectangular for the element container was set (\c pageResize = false). * In the second case, \c WorksheetElement::handleResize() is called for every worksheet child to adjuste the content to the new size. * In the first case, a new rectangular for the container is calculated and set first, which on the other hand, triggers the content adjustments * in the container children. */ void WorksheetElementContainer::handleResize(double horizontalRatio, double verticalRatio, bool pageResize) { DEBUG("WorksheetElementContainer::handleResize()"); Q_D(const WorksheetElementContainer); if (pageResize) { QRectF rect(d->rect); rect.setWidth(d->rect.width()*horizontalRatio); rect.setHeight(d->rect.height()*verticalRatio); setRect(rect); } else { // for (auto* elem : children(IncludeHidden)) // elem->handleResize(horizontalRatio, verticalRatio); } } void WorksheetElementContainer::handleAspectAdded(const AbstractAspect* aspect) { Q_D(WorksheetElementContainer); const auto* element = qobject_cast(aspect); if (element && (aspect->parentAspect() == this)) { connect(element, &WorksheetElement::hovered, this, &WorksheetElementContainer::childHovered); connect(element, &WorksheetElement::unhovered, this, &WorksheetElementContainer::childUnhovered); element->graphicsItem()->setParentItem(d); qreal zVal = 0; - for (auto* child : children(IncludeHidden)) + for (auto* child : children(ChildIndexFlag::IncludeHidden)) child->setZValue(zVal++); } if (!isLoading()) d->recalcShapeAndBoundingRect(); } void WorksheetElementContainer::childHovered() { Q_D(WorksheetElementContainer); if (!d->isSelected()) { if (d->m_hovered) d->m_hovered = false; d->update(); } } void WorksheetElementContainer::childUnhovered() { Q_D(WorksheetElementContainer); if (!d->isSelected()) { d->m_hovered = true; d->update(); } } void WorksheetElementContainer::prepareGeometryChange() { Q_D(WorksheetElementContainer); d->prepareGeometryChangeRequested(); } //################################################################ //################### Private implementation ########################## //################################################################ WorksheetElementContainerPrivate::WorksheetElementContainerPrivate(WorksheetElementContainer *owner) : q(owner) { setAcceptHoverEvents(true); } QString WorksheetElementContainerPrivate::name() const { return q->name(); } void WorksheetElementContainerPrivate::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { scene()->clearSelection(); setSelected(true); QMenu* menu = q->createContextMenu(); menu->exec(event->screenPos()); } void WorksheetElementContainerPrivate::hoverEnterEvent(QGraphicsSceneHoverEvent*) { if (!isSelected()) { m_hovered = true; update(); } } void WorksheetElementContainerPrivate::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { if (m_hovered) { m_hovered = false; update(); } } bool WorksheetElementContainerPrivate::swapVisible(bool on) { bool oldValue = isVisible(); setVisible(on); emit q->visibleChanged(on); return oldValue; } void WorksheetElementContainerPrivate::prepareGeometryChangeRequested() { prepareGeometryChange(); recalcShapeAndBoundingRect(); } void WorksheetElementContainerPrivate::recalcShapeAndBoundingRect() { // if (q->isLoading()) // return; //old logic calculating the bounding box as as the box covering all children. //we might need this logic later once we implement something like selection of multiple plots, etc. // boundingRectangle = QRectF(); // QVector childList = q->children(AbstractAspect::IncludeHidden | AbstractAspect::Compress); // foreach (const WorksheetElement* elem, childList) // boundingRectangle |= elem->graphicsItem()->mapRectToParent(elem->graphicsItem()->boundingRect()); // float penWidth = 2.; boundingRectangle = q->rect(); boundingRectangle = QRectF(-boundingRectangle.width()/2 - penWidth / 2, -boundingRectangle.height()/2 - penWidth / 2, boundingRectangle.width() + penWidth, boundingRectangle.height() + penWidth); QPainterPath path; path.addRect(boundingRectangle); //make the shape somewhat thicker then the hoveredPen to make the selection/hovering box more visible containerShape = QPainterPath(); containerShape.addPath(WorksheetElement::shapeFromPath(path, QPen(QBrush(), penWidth))); } // Inherited from QGraphicsItem QRectF WorksheetElementContainerPrivate::boundingRect() const { return boundingRectangle; } // Inherited from QGraphicsItem void WorksheetElementContainerPrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { Q_UNUSED(option) Q_UNUSED(widget) if (!isVisible()) return; if (m_hovered && !isSelected() && !m_printing) { painter->setPen(QPen(QApplication::palette().color(QPalette::Shadow), 2, Qt::SolidLine)); painter->drawPath(containerShape); } if (isSelected() && !m_printing) { painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), 2, Qt::SolidLine)); painter->drawPath(containerShape); } } diff --git a/src/backend/worksheet/plots/cartesian/Axis.cpp b/src/backend/worksheet/plots/cartesian/Axis.cpp index 4e1885d1a..361836c12 100644 --- a/src/backend/worksheet/plots/cartesian/Axis.cpp +++ b/src/backend/worksheet/plots/cartesian/Axis.cpp @@ -1,2355 +1,2355 @@ /*************************************************************************** File : Axis.cpp Project : LabPlot Description : Axis for cartesian coordinate systems. -------------------------------------------------------------------- Copyright : (C) 2011-2018 Alexander Semke (alexander.semke@web.de) Copyright : (C) 2013-2020 Stefan Gerlach (stefan.gerlach@uni.kn) ***************************************************************************/ /*************************************************************************** * * * 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/worksheet/plots/cartesian/Axis.h" #include "backend/worksheet/plots/cartesian/AxisPrivate.h" #include "backend/worksheet/Worksheet.h" #include "backend/worksheet/TextLabel.h" #include "backend/worksheet/plots/cartesian/CartesianCoordinateSystem.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/core/AbstractColumn.h" #include "backend/lib/commandtemplates.h" #include "backend/lib/XmlStreamReader.h" #include "backend/lib/macros.h" // #include "backend/lib/trace.h" #include "kdefrontend/GuiTools.h" #include #include #include #include #include #include #include extern "C" { #include #include "backend/nsl/nsl_math.h" } /** * \class AxisGrid * \brief Helper class to get the axis grid drawn with the z-Value=0. * * The painting of the grid lines is separated from the painting of the axis itself. * This allows to use a different z-values for the grid lines (z=0, drawn below all other objects ) * and for the axis (z=FLT_MAX, drawn on top of all other objects) * * \ingroup worksheet */ class AxisGrid : public QGraphicsItem { public: explicit AxisGrid(AxisPrivate* a) { axis = a; setFlag(QGraphicsItem::ItemIsSelectable, false); setFlag(QGraphicsItem::ItemIsFocusable, false); setAcceptHoverEvents(false); } QRectF boundingRect() const override { QPainterPath gridShape; gridShape.addPath(WorksheetElement::shapeFromPath(axis->majorGridPath, axis->majorGridPen)); gridShape.addPath(WorksheetElement::shapeFromPath(axis->minorGridPath, axis->minorGridPen)); QRectF boundingRectangle = gridShape.boundingRect(); return boundingRectangle; } void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override { Q_UNUSED(option) Q_UNUSED(widget) if (!axis->isVisible()) return; if (axis->linePath.isEmpty()) return; //draw major grid if (axis->majorGridPen.style() != Qt::NoPen) { painter->setOpacity(axis->majorGridOpacity); painter->setPen(axis->majorGridPen); painter->setBrush(Qt::NoBrush); painter->drawPath(axis->majorGridPath); } //draw minor grid if (axis->minorGridPen.style() != Qt::NoPen) { painter->setOpacity(axis->minorGridOpacity); painter->setPen(axis->minorGridPen); painter->setBrush(Qt::NoBrush); painter->drawPath(axis->minorGridPath); } } private: AxisPrivate* axis; }; /** * \class Axis * \brief Axis for cartesian coordinate systems. * * \ingroup worksheet */ Axis::Axis(const QString& name, AxisOrientation orientation) : WorksheetElement(name, AspectType::Axis), d_ptr(new AxisPrivate(this)) { d_ptr->orientation = orientation; init(); } Axis::Axis(const QString& name, AxisOrientation orientation, AxisPrivate* dd) : WorksheetElement(name, AspectType::Axis), d_ptr(dd) { d_ptr->orientation = orientation; init(); } void Axis::finalizeAdd() { Q_D(Axis); d->plot = dynamic_cast(parentAspect()); Q_ASSERT(d->plot); d->cSystem = dynamic_cast(d->plot->coordinateSystem()); } void Axis::init() { Q_D(Axis); KConfig config; KConfigGroup group = config.group("Axis"); d->autoScale = true; d->position = Axis::AxisCustom; d->offset = group.readEntry("PositionOffset", 0); d->scale = (Axis::AxisScale) group.readEntry("Scale", (int) Axis::ScaleLinear); d->autoScale = group.readEntry("AutoScale", true); d->start = group.readEntry("Start", 0); d->end = group.readEntry("End", 10); d->zeroOffset = group.readEntry("ZeroOffset", 0); d->scalingFactor = group.readEntry("ScalingFactor", 1.0); d->linePen.setStyle( (Qt::PenStyle) group.readEntry("LineStyle", (int) Qt::SolidLine) ); d->linePen.setWidthF( group.readEntry("LineWidth", Worksheet::convertToSceneUnits( 1.0, Worksheet::Point ) ) ); d->lineOpacity = group.readEntry("LineOpacity", 1.0); d->arrowType = (Axis::ArrowType) group.readEntry("ArrowType", (int)Axis::NoArrow); d->arrowPosition = (Axis::ArrowPosition) group.readEntry("ArrowPosition", (int)Axis::ArrowRight); d->arrowSize = group.readEntry("ArrowSize", Worksheet::convertToSceneUnits(10, Worksheet::Point)); // axis title d->title = new TextLabel(this->name(), TextLabel::AxisTitle); connect( d->title, &TextLabel::changed, this, &Axis::labelChanged); addChild(d->title); d->title->setHidden(true); d->title->graphicsItem()->setParentItem(d); d->title->graphicsItem()->setFlag(QGraphicsItem::ItemIsMovable, false); d->title->graphicsItem()->setFlag(QGraphicsItem::ItemIsFocusable, false); d->title->graphicsItem()->setAcceptHoverEvents(false); d->title->setText(this->name()); if (d->orientation == AxisVertical) d->title->setRotationAngle(90); d->titleOffsetX = Worksheet::convertToSceneUnits(2, Worksheet::Point); //distance to the axis tick labels d->titleOffsetY = Worksheet::convertToSceneUnits(2, Worksheet::Point); //distance to the axis tick labels d->majorTicksDirection = (Axis::TicksDirection) group.readEntry("MajorTicksDirection", (int) Axis::ticksOut); d->majorTicksType = (Axis::TicksType) group.readEntry("MajorTicksType", (int) Axis::TicksTotalNumber); d->majorTicksNumber = group.readEntry("MajorTicksNumber", 11); d->majorTicksSpacing = group.readEntry("MajorTicksIncrement", 0.0); // set to 0, so axisdock determines the value to not have to many labels the first time switched to Spacing d->majorTicksPen.setStyle((Qt::PenStyle) group.readEntry("MajorTicksLineStyle", (int)Qt::SolidLine) ); d->majorTicksPen.setColor( group.readEntry("MajorTicksColor", QColor(Qt::black) ) ); d->majorTicksPen.setWidthF( group.readEntry("MajorTicksWidth", Worksheet::convertToSceneUnits(1.0, Worksheet::Point) ) ); d->majorTicksLength = group.readEntry("MajorTicksLength", Worksheet::convertToSceneUnits(6.0, Worksheet::Point)); d->majorTicksOpacity = group.readEntry("MajorTicksOpacity", 1.0); d->minorTicksDirection = (Axis::TicksDirection) group.readEntry("MinorTicksDirection", (int) Axis::ticksOut); d->minorTicksType = (Axis::TicksType) group.readEntry("MinorTicksType", (int) Axis::TicksTotalNumber); d->minorTicksNumber = group.readEntry("MinorTicksNumber", 1); d->minorTicksIncrement = group.readEntry("MinorTicksIncrement", 0.0); // see MajorTicksIncrement d->minorTicksPen.setStyle((Qt::PenStyle) group.readEntry("MinorTicksLineStyle", (int)Qt::SolidLine) ); d->minorTicksPen.setColor( group.readEntry("MinorTicksColor", QColor(Qt::black) ) ); d->minorTicksPen.setWidthF( group.readEntry("MinorTicksWidth", Worksheet::convertToSceneUnits(1.0, Worksheet::Point) ) ); d->minorTicksLength = group.readEntry("MinorTicksLength", Worksheet::convertToSceneUnits(3.0, Worksheet::Point)); d->minorTicksOpacity = group.readEntry("MinorTicksOpacity", 1.0); //Labels d->labelsFormat = (Axis::LabelsFormat) group.readEntry("LabelsFormat", (int)Axis::FormatDecimal); d->labelsAutoPrecision = group.readEntry("LabelsAutoPrecision", true); d->labelsPrecision = group.readEntry("LabelsPrecision", 1); d->labelsDateTimeFormat = group.readEntry("LabelsDateTimeFormat", "yyyy-MM-dd hh:mm:ss"); d->labelsPosition = (Axis::LabelsPosition) group.readEntry("LabelsPosition", (int) Axis::LabelsOut); d->labelsOffset = group.readEntry("LabelsOffset", Worksheet::convertToSceneUnits( 5.0, Worksheet::Point )); d->labelsRotationAngle = group.readEntry("LabelsRotation", 0); d->labelsFont = group.readEntry("LabelsFont", QFont()); d->labelsFont.setPixelSize( Worksheet::convertToSceneUnits( 10.0, Worksheet::Point ) ); d->labelsColor = group.readEntry("LabelsFontColor", QColor(Qt::black)); d->labelsPrefix = group.readEntry("LabelsPrefix", "" ); d->labelsSuffix = group.readEntry("LabelsSuffix", "" ); d->labelsOpacity = group.readEntry("LabelsOpacity", 1.0); //major grid d->majorGridPen.setStyle( (Qt::PenStyle) group.readEntry("MajorGridStyle", (int) Qt::NoPen) ); d->majorGridPen.setColor(group.readEntry("MajorGridColor", QColor(Qt::gray)) ); d->majorGridPen.setWidthF( group.readEntry("MajorGridWidth", Worksheet::convertToSceneUnits( 1.0, Worksheet::Point ) ) ); d->majorGridOpacity = group.readEntry("MajorGridOpacity", 1.0); //minor grid d->minorGridPen.setStyle( (Qt::PenStyle) group.readEntry("MinorGridStyle", (int) Qt::NoPen) ); d->minorGridPen.setColor(group.readEntry("MinorGridColor", QColor(Qt::gray)) ); d->minorGridPen.setWidthF( group.readEntry("MinorGridWidth", Worksheet::convertToSceneUnits( 1.0, Worksheet::Point ) ) ); d->minorGridOpacity = group.readEntry("MinorGridOpacity", 1.0); } /*! * For the most frequently edited properties, create Actions and ActionGroups for the context menu. * For some ActionGroups the actual actions are created in \c GuiTool, */ void Axis::initActions() { visibilityAction = new QAction(QIcon::fromTheme("view-visible"), i18n("Visible"), this); visibilityAction->setCheckable(true); connect(visibilityAction, &QAction::triggered, this, &Axis::visibilityChangedSlot); //Orientation orientationActionGroup = new QActionGroup(this); orientationActionGroup->setExclusive(true); connect(orientationActionGroup, &QActionGroup::triggered, this, &Axis::orientationChangedSlot); orientationHorizontalAction = new QAction(QIcon::fromTheme("labplot-axis-horizontal"), i18n("Horizontal"), orientationActionGroup); orientationHorizontalAction->setCheckable(true); orientationVerticalAction = new QAction(QIcon::fromTheme("labplot-axis-vertical"), i18n("Vertical"), orientationActionGroup); orientationVerticalAction->setCheckable(true); //Line lineStyleActionGroup = new QActionGroup(this); lineStyleActionGroup->setExclusive(true); connect(lineStyleActionGroup, &QActionGroup::triggered, this, &Axis::lineStyleChanged); lineColorActionGroup = new QActionGroup(this); lineColorActionGroup->setExclusive(true); connect(lineColorActionGroup, &QActionGroup::triggered, this, &Axis::lineColorChanged); //Ticks //TODO } void Axis::initMenus() { this->initActions(); //Orientation orientationMenu = new QMenu(i18n("Orientation")); orientationMenu->setIcon(QIcon::fromTheme("labplot-axis-horizontal")); orientationMenu->addAction(orientationHorizontalAction); orientationMenu->addAction(orientationVerticalAction); //Line lineMenu = new QMenu(i18n("Line")); lineMenu->setIcon(QIcon::fromTheme("draw-line")); lineStyleMenu = new QMenu(i18n("Style"), lineMenu); lineStyleMenu->setIcon(QIcon::fromTheme("object-stroke-style")); lineMenu->setIcon(QIcon::fromTheme("draw-line")); lineMenu->addMenu( lineStyleMenu ); lineColorMenu = new QMenu(i18n("Color"), lineMenu); lineColorMenu->setIcon(QIcon::fromTheme("fill-color")); GuiTools::fillColorMenu( lineColorMenu, lineColorActionGroup ); lineMenu->addMenu( lineColorMenu ); } QMenu* Axis::createContextMenu() { if (!orientationMenu) initMenus(); Q_D(const Axis); 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); //Orientation if ( d->orientation == AxisHorizontal ) orientationHorizontalAction->setChecked(true); else orientationVerticalAction->setChecked(true); menu->insertMenu(firstAction, orientationMenu); //Line styles GuiTools::updatePenStyles( lineStyleMenu, lineStyleActionGroup, d->linePen.color() ); GuiTools::selectPenStyleAction(lineStyleActionGroup, d->linePen.style() ); GuiTools::selectColorAction(lineColorActionGroup, d->linePen.color() ); menu->insertMenu(firstAction, lineMenu); menu->insertSeparator(firstAction); return menu; } /*! Returns an icon to be used in the project explorer. */ QIcon Axis::icon() const{ Q_D(const Axis); QIcon ico; if (d->orientation == Axis::AxisHorizontal) ico = QIcon::fromTheme("labplot-axis-horizontal"); else ico = QIcon::fromTheme("labplot-axis-vertical"); return ico; } Axis::~Axis() { if (orientationMenu) { delete orientationMenu; delete lineMenu; } //no need to delete d->title, since it was added with addChild in init(); //no need to delete the d-pointer here - it inherits from QGraphicsItem //and is deleted during the cleanup in QGraphicsScene } QGraphicsItem *Axis::graphicsItem() const { return d_ptr; } /*! * overrides the implementation in WorksheetElement and sets the z-value to the maximal possible, * axes are drawn on top of all other object in the plot. */ void Axis::setZValue(qreal) { Q_D(Axis); d->setZValue(std::numeric_limits::max()); d->gridItem->setParentItem(d->parentItem()); d->gridItem->setZValue(0); } void Axis::retransform() { Q_D(Axis); d->retransform(); } void Axis::retransformTickLabelStrings() { Q_D(Axis); d->retransformTickLabelStrings(); } void Axis::setSuppressRetransform(bool value) { Q_D(Axis); d->suppressRetransform = value; } void Axis::handleResize(double horizontalRatio, double verticalRatio, bool pageResize) { Q_D(Axis); Q_UNUSED(pageResize); double ratio = 0; if (horizontalRatio > 1.0 || verticalRatio > 1.0) ratio = qMax(horizontalRatio, verticalRatio); else ratio = qMin(horizontalRatio, verticalRatio); QPen pen = d->linePen; pen.setWidthF(pen.widthF() * ratio); d->linePen = pen; d->majorTicksLength *= ratio; // ticks are perpendicular to axis line -> verticalRatio relevant d->minorTicksLength *= ratio; d->labelsFont.setPixelSize( d->labelsFont.pixelSize() * ratio ); //TODO: take into account rotated labels d->labelsOffset *= ratio; d->title->handleResize(horizontalRatio, verticalRatio, pageResize); } /* ============================ getter methods ================= */ BASIC_SHARED_D_READER_IMPL(Axis, bool, autoScale, autoScale) BASIC_SHARED_D_READER_IMPL(Axis, Axis::AxisOrientation, orientation, orientation) BASIC_SHARED_D_READER_IMPL(Axis, Axis::AxisPosition, position, position) BASIC_SHARED_D_READER_IMPL(Axis, Axis::AxisScale, scale, scale) BASIC_SHARED_D_READER_IMPL(Axis, double, offset, offset) BASIC_SHARED_D_READER_IMPL(Axis, double, start, start) BASIC_SHARED_D_READER_IMPL(Axis, double, end, end) BASIC_SHARED_D_READER_IMPL(Axis, qreal, scalingFactor, scalingFactor) BASIC_SHARED_D_READER_IMPL(Axis, qreal, zeroOffset, zeroOffset) BASIC_SHARED_D_READER_IMPL(Axis, TextLabel*, title, title) BASIC_SHARED_D_READER_IMPL(Axis, qreal, titleOffsetX, titleOffsetX) BASIC_SHARED_D_READER_IMPL(Axis, qreal, titleOffsetY, titleOffsetY) CLASS_SHARED_D_READER_IMPL(Axis, QPen, linePen, linePen) BASIC_SHARED_D_READER_IMPL(Axis, qreal, lineOpacity, lineOpacity) BASIC_SHARED_D_READER_IMPL(Axis, Axis::ArrowType, arrowType, arrowType) BASIC_SHARED_D_READER_IMPL(Axis, Axis::ArrowPosition, arrowPosition, arrowPosition) BASIC_SHARED_D_READER_IMPL(Axis, qreal, arrowSize, arrowSize) BASIC_SHARED_D_READER_IMPL(Axis, Axis::TicksDirection, majorTicksDirection, majorTicksDirection) BASIC_SHARED_D_READER_IMPL(Axis, Axis::TicksType, majorTicksType, majorTicksType) BASIC_SHARED_D_READER_IMPL(Axis, int, majorTicksNumber, majorTicksNumber) BASIC_SHARED_D_READER_IMPL(Axis, qreal, majorTicksSpacing, majorTicksSpacing) BASIC_SHARED_D_READER_IMPL(Axis, const AbstractColumn*, majorTicksColumn, majorTicksColumn) QString& Axis::majorTicksColumnPath() const { return d_ptr->majorTicksColumnPath; } BASIC_SHARED_D_READER_IMPL(Axis, qreal, majorTicksLength, majorTicksLength) CLASS_SHARED_D_READER_IMPL(Axis, QPen, majorTicksPen, majorTicksPen) BASIC_SHARED_D_READER_IMPL(Axis, qreal, majorTicksOpacity, majorTicksOpacity) BASIC_SHARED_D_READER_IMPL(Axis, Axis::TicksDirection, minorTicksDirection, minorTicksDirection) BASIC_SHARED_D_READER_IMPL(Axis, Axis::TicksType, minorTicksType, minorTicksType) BASIC_SHARED_D_READER_IMPL(Axis, int, minorTicksNumber, minorTicksNumber) BASIC_SHARED_D_READER_IMPL(Axis, qreal, minorTicksSpacing, minorTicksIncrement) BASIC_SHARED_D_READER_IMPL(Axis, const AbstractColumn*, minorTicksColumn, minorTicksColumn) QString& Axis::minorTicksColumnPath() const { return d_ptr->minorTicksColumnPath; } BASIC_SHARED_D_READER_IMPL(Axis, qreal, minorTicksLength, minorTicksLength) CLASS_SHARED_D_READER_IMPL(Axis, QPen, minorTicksPen, minorTicksPen) BASIC_SHARED_D_READER_IMPL(Axis, qreal, minorTicksOpacity, minorTicksOpacity) BASIC_SHARED_D_READER_IMPL(Axis, Axis::LabelsFormat, labelsFormat, labelsFormat); BASIC_SHARED_D_READER_IMPL(Axis, bool, labelsAutoPrecision, labelsAutoPrecision); BASIC_SHARED_D_READER_IMPL(Axis, int, labelsPrecision, labelsPrecision); BASIC_SHARED_D_READER_IMPL(Axis, QString, labelsDateTimeFormat, labelsDateTimeFormat); BASIC_SHARED_D_READER_IMPL(Axis, Axis::LabelsPosition, labelsPosition, labelsPosition); BASIC_SHARED_D_READER_IMPL(Axis, qreal, labelsOffset, labelsOffset); BASIC_SHARED_D_READER_IMPL(Axis, qreal, labelsRotationAngle, labelsRotationAngle); CLASS_SHARED_D_READER_IMPL(Axis, QColor, labelsColor, labelsColor); CLASS_SHARED_D_READER_IMPL(Axis, QFont, labelsFont, labelsFont); CLASS_SHARED_D_READER_IMPL(Axis, QString, labelsPrefix, labelsPrefix); CLASS_SHARED_D_READER_IMPL(Axis, QString, labelsSuffix, labelsSuffix); BASIC_SHARED_D_READER_IMPL(Axis, qreal, labelsOpacity, labelsOpacity); CLASS_SHARED_D_READER_IMPL(Axis, QPen, majorGridPen, majorGridPen) BASIC_SHARED_D_READER_IMPL(Axis, qreal, majorGridOpacity, majorGridOpacity) CLASS_SHARED_D_READER_IMPL(Axis, QPen, minorGridPen, minorGridPen) BASIC_SHARED_D_READER_IMPL(Axis, qreal, minorGridOpacity, minorGridOpacity) /* ============================ setter methods and undo commands ================= */ STD_SETTER_CMD_IMPL_F_S(Axis, SetAutoScale, bool, autoScale, retransform); void Axis::setAutoScale(bool autoScale) { Q_D(Axis); if (autoScale != d->autoScale) { exec(new AxisSetAutoScaleCmd(d, autoScale, ki18n("%1: set axis auto scaling"))); if (autoScale) { auto* plot = qobject_cast(parentAspect()); if (!plot) return; if (d->orientation == Axis::AxisHorizontal) { d->end = plot->xMax(); d->start = plot->xMin(); } else { d->end = plot->yMax(); d->start = plot->yMin(); } retransform(); emit endChanged(d->end); emit startChanged(d->start); } } } STD_SWAP_METHOD_SETTER_CMD_IMPL(Axis, SetVisible, bool, swapVisible); void Axis::setVisible(bool on) { Q_D(Axis); exec(new AxisSetVisibleCmd(d, on, on ? ki18n("%1: set visible") : ki18n("%1: set invisible"))); } bool Axis::isVisible() const { Q_D(const Axis); return d->isVisible(); } void Axis::setPrinting(bool on) { Q_D(Axis); d->setPrinting(on); } STD_SETTER_CMD_IMPL_F_S(Axis, SetOrientation, Axis::AxisOrientation, orientation, retransform); void Axis::setOrientation( AxisOrientation orientation) { Q_D(Axis); if (orientation != d->orientation) exec(new AxisSetOrientationCmd(d, orientation, ki18n("%1: set axis orientation"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetPosition, Axis::AxisPosition, position, retransform); void Axis::setPosition(AxisPosition position) { Q_D(Axis); if (position != d->position) exec(new AxisSetPositionCmd(d, position, ki18n("%1: set axis position"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetScaling, Axis::AxisScale, scale, retransformTicks); void Axis::setScale(AxisScale scale) { Q_D(Axis); if (scale != d->scale) exec(new AxisSetScalingCmd(d, scale, ki18n("%1: set axis scale"))); } STD_SETTER_CMD_IMPL_F(Axis, SetOffset, double, offset, retransform); void Axis::setOffset(double offset, bool undo) { Q_D(Axis); if (offset != d->offset) { if (undo) { exec(new AxisSetOffsetCmd(d, offset, ki18n("%1: set axis offset"))); } else { d->offset = offset; //don't need to call retransform() afterward //since the only usage of this call is in CartesianPlot, where retransform is called for all children anyway. } emit positionChanged(offset); } } STD_SETTER_CMD_IMPL_F_S(Axis, SetStart, double, start, retransform); void Axis::setStart(double start) { Q_D(Axis); if (start != d->start) exec(new AxisSetStartCmd(d, start, ki18n("%1: set axis start"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetEnd, double, end, retransform); void Axis::setEnd(double end) { Q_D(Axis); if (end != d->end) exec(new AxisSetEndCmd(d, end, ki18n("%1: set axis end"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetZeroOffset, qreal, zeroOffset, retransform); void Axis::setZeroOffset(qreal zeroOffset) { Q_D(Axis); if (zeroOffset != d->zeroOffset) exec(new AxisSetZeroOffsetCmd(d, zeroOffset, ki18n("%1: set axis zero offset"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetScalingFactor, qreal, scalingFactor, retransform); void Axis::setScalingFactor(qreal scalingFactor) { Q_D(Axis); if (scalingFactor != d->scalingFactor) exec(new AxisSetScalingFactorCmd(d, scalingFactor, ki18n("%1: set axis scaling factor"))); } //Title STD_SETTER_CMD_IMPL_F_S(Axis, SetTitleOffsetX, qreal, titleOffsetX, retransform); void Axis::setTitleOffsetX(qreal offset) { Q_D(Axis); if (offset != d->titleOffsetX) exec(new AxisSetTitleOffsetXCmd(d, offset, ki18n("%1: set title offset"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetTitleOffsetY, qreal, titleOffsetY, retransform); void Axis::setTitleOffsetY(qreal offset) { Q_D(Axis); if (offset != d->titleOffsetY) exec(new AxisSetTitleOffsetYCmd(d, offset, ki18n("%1: set title offset"))); } //Line STD_SETTER_CMD_IMPL_F_S(Axis, SetLinePen, QPen, linePen, recalcShapeAndBoundingRect); void Axis::setLinePen(const QPen &pen) { Q_D(Axis); if (pen != d->linePen) exec(new AxisSetLinePenCmd(d, pen, ki18n("%1: set line style"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLineOpacity, qreal, lineOpacity, update); void Axis::setLineOpacity(qreal opacity) { Q_D(Axis); if (opacity != d->lineOpacity) exec(new AxisSetLineOpacityCmd(d, opacity, ki18n("%1: set line opacity"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetArrowType, Axis::ArrowType, arrowType, retransformArrow); void Axis::setArrowType(ArrowType type) { Q_D(Axis); if (type != d->arrowType) exec(new AxisSetArrowTypeCmd(d, type, ki18n("%1: set arrow type"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetArrowPosition, Axis::ArrowPosition, arrowPosition, retransformArrow); void Axis::setArrowPosition(ArrowPosition position) { Q_D(Axis); if (position != d->arrowPosition) exec(new AxisSetArrowPositionCmd(d, position, ki18n("%1: set arrow position"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetArrowSize, qreal, arrowSize, retransformArrow); void Axis::setArrowSize(qreal arrowSize) { Q_D(Axis); if (arrowSize != d->arrowSize) exec(new AxisSetArrowSizeCmd(d, arrowSize, ki18n("%1: set arrow size"))); } //Major ticks STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorTicksDirection, Axis::TicksDirection, majorTicksDirection, retransformTicks); void Axis::setMajorTicksDirection(TicksDirection majorTicksDirection) { Q_D(Axis); if (majorTicksDirection != d->majorTicksDirection) exec(new AxisSetMajorTicksDirectionCmd(d, majorTicksDirection, ki18n("%1: set major ticks direction"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorTicksType, Axis::TicksType, majorTicksType, retransformTicks); void Axis::setMajorTicksType(TicksType majorTicksType) { Q_D(Axis); if (majorTicksType!= d->majorTicksType) exec(new AxisSetMajorTicksTypeCmd(d, majorTicksType, ki18n("%1: set major ticks type"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorTicksNumber, int, majorTicksNumber, retransformTicks); void Axis::setMajorTicksNumber(int majorTicksNumber) { Q_D(Axis); if (majorTicksNumber != d->majorTicksNumber) exec(new AxisSetMajorTicksNumberCmd(d, majorTicksNumber, ki18n("%1: set the total number of the major ticks"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorTicksSpacing, qreal, majorTicksSpacing, retransformTicks); void Axis::setMajorTicksSpacing(qreal majorTicksSpacing) { Q_D(Axis); if (majorTicksSpacing != d->majorTicksSpacing) exec(new AxisSetMajorTicksSpacingCmd(d, majorTicksSpacing, ki18n("%1: set the spacing of the major ticks"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorTicksColumn, const AbstractColumn*, majorTicksColumn, retransformTicks) void Axis::setMajorTicksColumn(const AbstractColumn* column) { Q_D(Axis); if (column != d->majorTicksColumn) { exec(new AxisSetMajorTicksColumnCmd(d, column, ki18n("%1: assign major ticks' values"))); if (column) { connect(column, &AbstractColumn::dataChanged, this, &Axis::retransformTicks); connect(column->parentAspect(), &AbstractAspect::aspectAboutToBeRemoved, this, &Axis::majorTicksColumnAboutToBeRemoved); //TODO: add disconnect in the undo-function } } } STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorTicksPen, QPen, majorTicksPen, recalcShapeAndBoundingRect); void Axis::setMajorTicksPen(const QPen& pen) { Q_D(Axis); if (pen != d->majorTicksPen) exec(new AxisSetMajorTicksPenCmd(d, pen, ki18n("%1: set major ticks style"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorTicksLength, qreal, majorTicksLength, retransformTicks); void Axis::setMajorTicksLength(qreal majorTicksLength) { Q_D(Axis); if (majorTicksLength != d->majorTicksLength) exec(new AxisSetMajorTicksLengthCmd(d, majorTicksLength, ki18n("%1: set major ticks length"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorTicksOpacity, qreal, majorTicksOpacity, update); void Axis::setMajorTicksOpacity(qreal opacity) { Q_D(Axis); if (opacity != d->majorTicksOpacity) exec(new AxisSetMajorTicksOpacityCmd(d, opacity, ki18n("%1: set major ticks opacity"))); } //Minor ticks STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorTicksDirection, Axis::TicksDirection, minorTicksDirection, retransformTicks); void Axis::setMinorTicksDirection(TicksDirection minorTicksDirection) { Q_D(Axis); if (minorTicksDirection != d->minorTicksDirection) exec(new AxisSetMinorTicksDirectionCmd(d, minorTicksDirection, ki18n("%1: set minor ticks direction"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorTicksType, Axis::TicksType, minorTicksType, retransformTicks); void Axis::setMinorTicksType(TicksType minorTicksType) { Q_D(Axis); if (minorTicksType!= d->minorTicksType) exec(new AxisSetMinorTicksTypeCmd(d, minorTicksType, ki18n("%1: set minor ticks type"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorTicksNumber, int, minorTicksNumber, retransformTicks); void Axis::setMinorTicksNumber(int minorTicksNumber) { Q_D(Axis); if (minorTicksNumber != d->minorTicksNumber) exec(new AxisSetMinorTicksNumberCmd(d, minorTicksNumber, ki18n("%1: set the total number of the minor ticks"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorTicksSpacing, qreal, minorTicksIncrement, retransformTicks); void Axis::setMinorTicksSpacing(qreal minorTicksSpacing) { Q_D(Axis); if (minorTicksSpacing != d->minorTicksIncrement) exec(new AxisSetMinorTicksSpacingCmd(d, minorTicksSpacing, ki18n("%1: set the spacing of the minor ticks"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorTicksColumn, const AbstractColumn*, minorTicksColumn, retransformTicks) void Axis::setMinorTicksColumn(const AbstractColumn* column) { Q_D(Axis); if (column != d->minorTicksColumn) { exec(new AxisSetMinorTicksColumnCmd(d, column, ki18n("%1: assign minor ticks' values"))); if (column) { connect(column, &AbstractColumn::dataChanged, this, &Axis::retransformTicks); connect(column->parentAspect(), &AbstractAspect::aspectAboutToBeRemoved, this, &Axis::minorTicksColumnAboutToBeRemoved); //TODO: add disconnect in the undo-function } } } STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorTicksPen, QPen, minorTicksPen, recalcShapeAndBoundingRect); void Axis::setMinorTicksPen(const QPen& pen) { Q_D(Axis); if (pen != d->minorTicksPen) exec(new AxisSetMinorTicksPenCmd(d, pen, ki18n("%1: set minor ticks style"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorTicksLength, qreal, minorTicksLength, retransformTicks); void Axis::setMinorTicksLength(qreal minorTicksLength) { Q_D(Axis); if (minorTicksLength != d->minorTicksLength) exec(new AxisSetMinorTicksLengthCmd(d, minorTicksLength, ki18n("%1: set minor ticks length"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorTicksOpacity, qreal, minorTicksOpacity, update); void Axis::setMinorTicksOpacity(qreal opacity) { Q_D(Axis); if (opacity != d->minorTicksOpacity) exec(new AxisSetMinorTicksOpacityCmd(d, opacity, ki18n("%1: set minor ticks opacity"))); } //Labels STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsFormat, Axis::LabelsFormat, labelsFormat, retransformTicks); void Axis::setLabelsFormat(LabelsFormat labelsFormat) { Q_D(Axis); if (labelsFormat != d->labelsFormat) { //TODO: this part is not undo/redo-aware if (labelsFormat == Axis::FormatDecimal) d->labelsFormatDecimalOverruled = true; else d->labelsFormatDecimalOverruled = false; exec(new AxisSetLabelsFormatCmd(d, labelsFormat, ki18n("%1: set labels format"))); } } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsAutoPrecision, bool, labelsAutoPrecision, retransformTickLabelStrings); void Axis::setLabelsAutoPrecision(bool labelsAutoPrecision) { Q_D(Axis); if (labelsAutoPrecision != d->labelsAutoPrecision) exec(new AxisSetLabelsAutoPrecisionCmd(d, labelsAutoPrecision, ki18n("%1: set labels precision"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsPrecision, int, labelsPrecision, retransformTickLabelStrings); void Axis::setLabelsPrecision(int labelsPrecision) { Q_D(Axis); if (labelsPrecision != d->labelsPrecision) exec(new AxisSetLabelsPrecisionCmd(d, labelsPrecision, ki18n("%1: set labels precision"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsDateTimeFormat, QString, labelsDateTimeFormat, retransformTickLabelStrings); void Axis::setLabelsDateTimeFormat(const QString& format) { Q_D(Axis); if (format != d->labelsDateTimeFormat) exec(new AxisSetLabelsDateTimeFormatCmd(d, format, ki18n("%1: set labels datetime format"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsPosition, Axis::LabelsPosition, labelsPosition, retransformTickLabelPositions); void Axis::setLabelsPosition(LabelsPosition labelsPosition) { Q_D(Axis); if (labelsPosition != d->labelsPosition) exec(new AxisSetLabelsPositionCmd(d, labelsPosition, ki18n("%1: set labels position"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsOffset, double, labelsOffset, retransformTickLabelPositions); void Axis::setLabelsOffset(double offset) { Q_D(Axis); if (offset != d->labelsOffset) exec(new AxisSetLabelsOffsetCmd(d, offset, ki18n("%1: set label offset"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsRotationAngle, qreal, labelsRotationAngle, retransformTickLabelPositions); void Axis::setLabelsRotationAngle(qreal angle) { Q_D(Axis); if (angle != d->labelsRotationAngle) exec(new AxisSetLabelsRotationAngleCmd(d, angle, ki18n("%1: set label rotation angle"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsColor, QColor, labelsColor, update); void Axis::setLabelsColor(const QColor& color) { Q_D(Axis); if (color != d->labelsColor) exec(new AxisSetLabelsColorCmd(d, color, ki18n("%1: set label color"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsFont, QFont, labelsFont, retransformTickLabelStrings); void Axis::setLabelsFont(const QFont& font) { Q_D(Axis); if (font != d->labelsFont) exec(new AxisSetLabelsFontCmd(d, font, ki18n("%1: set label font"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsPrefix, QString, labelsPrefix, retransformTickLabelStrings); void Axis::setLabelsPrefix(const QString& prefix) { Q_D(Axis); if (prefix != d->labelsPrefix) exec(new AxisSetLabelsPrefixCmd(d, prefix, ki18n("%1: set label prefix"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsSuffix, QString, labelsSuffix, retransformTickLabelStrings); void Axis::setLabelsSuffix(const QString& suffix) { Q_D(Axis); if (suffix != d->labelsSuffix) exec(new AxisSetLabelsSuffixCmd(d, suffix, ki18n("%1: set label suffix"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetLabelsOpacity, qreal, labelsOpacity, update); void Axis::setLabelsOpacity(qreal opacity) { Q_D(Axis); if (opacity != d->labelsOpacity) exec(new AxisSetLabelsOpacityCmd(d, opacity, ki18n("%1: set labels opacity"))); } //Major grid STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorGridPen, QPen, majorGridPen, retransformMajorGrid); void Axis::setMajorGridPen(const QPen& pen) { Q_D(Axis); if (pen != d->majorGridPen) exec(new AxisSetMajorGridPenCmd(d, pen, ki18n("%1: set major grid style"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMajorGridOpacity, qreal, majorGridOpacity, updateGrid); void Axis::setMajorGridOpacity(qreal opacity) { Q_D(Axis); if (opacity != d->majorGridOpacity) exec(new AxisSetMajorGridOpacityCmd(d, opacity, ki18n("%1: set major grid opacity"))); } //Minor grid STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorGridPen, QPen, minorGridPen, retransformMinorGrid); void Axis::setMinorGridPen(const QPen& pen) { Q_D(Axis); if (pen != d->minorGridPen) exec(new AxisSetMinorGridPenCmd(d, pen, ki18n("%1: set minor grid style"))); } STD_SETTER_CMD_IMPL_F_S(Axis, SetMinorGridOpacity, qreal, minorGridOpacity, updateGrid); void Axis::setMinorGridOpacity(qreal opacity) { Q_D(Axis); if (opacity != d->minorGridOpacity) exec(new AxisSetMinorGridOpacityCmd(d, opacity, ki18n("%1: set minor grid opacity"))); } //############################################################################## //#################################### SLOTs ################################ //############################################################################## void Axis::labelChanged() { Q_D(Axis); d->recalcShapeAndBoundingRect(); } void Axis::retransformTicks() { Q_D(Axis); d->retransformTicks(); } void Axis::majorTicksColumnAboutToBeRemoved(const AbstractAspect* aspect) { Q_D(Axis); if (aspect == d->majorTicksColumn) { d->majorTicksColumn = nullptr; d->retransformTicks(); } } void Axis::minorTicksColumnAboutToBeRemoved(const AbstractAspect* aspect) { Q_D(Axis); if (aspect == d->minorTicksColumn) { d->minorTicksColumn = nullptr; d->retransformTicks(); } } //############################################################################## //###### SLOTs for changes triggered via QActions in the context menu ######## //############################################################################## void Axis::orientationChangedSlot(QAction* action) { if (action == orientationHorizontalAction) this->setOrientation(AxisHorizontal); else this->setOrientation(AxisVertical); } void Axis::lineStyleChanged(QAction* action) { Q_D(const Axis); QPen pen = d->linePen; pen.setStyle(GuiTools::penStyleFromAction(lineStyleActionGroup, action)); this->setLinePen(pen); } void Axis::lineColorChanged(QAction* action) { Q_D(const Axis); QPen pen = d->linePen; pen.setColor(GuiTools::colorFromAction(lineColorActionGroup, action)); this->setLinePen(pen); } void Axis::visibilityChangedSlot() { Q_D(const Axis); this->setVisible(!d->isVisible()); } //##################################################################### //################### Private implementation ########################## //##################################################################### AxisPrivate::AxisPrivate(Axis* owner) : gridItem(new AxisGrid(this)), q(owner) { setFlag(QGraphicsItem::ItemIsSelectable, true); setFlag(QGraphicsItem::ItemIsFocusable, true); setAcceptHoverEvents(true); } QString AxisPrivate::name() const{ return q->name(); } bool AxisPrivate::swapVisible(bool on) { bool oldValue = isVisible(); setVisible(on); emit q->visibilityChanged(on); return oldValue; } QRectF AxisPrivate::boundingRect() const{ return boundingRectangle; } /*! Returns the shape of the XYCurve as a QPainterPath in local coordinates */ QPainterPath AxisPrivate::shape() const{ return axisShape; } /*! recalculates the position of the axis on the worksheet */ void AxisPrivate::retransform() { if (suppressRetransform || !plot) return; // PERFTRACE(name().toLatin1() + ", AxisPrivate::retransform()"); m_suppressRecalc = true; retransformLine(); m_suppressRecalc = false; recalcShapeAndBoundingRect(); } void AxisPrivate::retransformLine() { if (suppressRetransform) return; linePath = QPainterPath(); lines.clear(); QPointF startPoint; QPointF endPoint; if (orientation == Axis::AxisHorizontal) { if (position == Axis::AxisTop) offset = plot->yMax(); else if (position == Axis::AxisBottom) offset = plot->yMin(); else if (position == Axis::AxisCentered) offset = plot->yMin() + (plot->yMax()-plot->yMin())/2; startPoint.setX(start); startPoint.setY(offset); endPoint.setX(end); endPoint.setY(offset); } else { // vertical if (position == Axis::AxisLeft) offset = plot->xMin(); else if (position == Axis::AxisRight) offset = plot->xMax(); else if (position == Axis::AxisCentered) offset = plot->xMin() + (plot->xMax()-plot->xMin())/2; startPoint.setX(offset); startPoint.setY(start); endPoint.setY(end); endPoint.setX(offset); } lines.append(QLineF(startPoint, endPoint)); lines = cSystem->mapLogicalToScene(lines, AbstractCoordinateSystem::MarkGaps); for (const auto& line : lines) { linePath.moveTo(line.p1()); linePath.lineTo(line.p2()); } if (linePath.isEmpty()) { recalcShapeAndBoundingRect(); return; } else { retransformArrow(); retransformTicks(); } } void AxisPrivate::retransformArrow() { if (suppressRetransform) return; arrowPath = QPainterPath(); if (arrowType == Axis::NoArrow || lines.isEmpty()) { recalcShapeAndBoundingRect(); return; } if (arrowPosition == Axis::ArrowRight || arrowPosition == Axis::ArrowBoth) { const QPointF& endPoint = lines.at(lines.size()-1).p2(); this->addArrow(endPoint, 1); } if (arrowPosition == Axis::ArrowLeft || arrowPosition == Axis::ArrowBoth) { const QPointF& endPoint = lines.at(0).p1(); this->addArrow(endPoint, -1); } recalcShapeAndBoundingRect(); } void AxisPrivate::addArrow(QPointF startPoint, int direction) { static const double cos_phi = cos(M_PI/6.); if (orientation == Axis::AxisHorizontal) { QPointF endPoint = QPointF(startPoint.x() + direction*arrowSize, startPoint.y()); arrowPath.moveTo(startPoint); arrowPath.lineTo(endPoint); switch (arrowType) { case Axis::NoArrow: break; case Axis::SimpleArrowSmall: arrowPath.moveTo(endPoint); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/4, endPoint.y()-arrowSize/4*cos_phi)); arrowPath.moveTo(endPoint); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/4, endPoint.y()+arrowSize/4*cos_phi)); break; case Axis::SimpleArrowBig: arrowPath.moveTo(endPoint); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/2, endPoint.y()-arrowSize/2*cos_phi)); arrowPath.moveTo(endPoint); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/2, endPoint.y()+arrowSize/2*cos_phi)); break; case Axis::FilledArrowSmall: arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/4, endPoint.y()-arrowSize/4*cos_phi)); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/4, endPoint.y()+arrowSize/4*cos_phi)); arrowPath.lineTo(endPoint); break; case Axis::FilledArrowBig: arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/2, endPoint.y()-arrowSize/2*cos_phi)); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/2, endPoint.y()+arrowSize/2*cos_phi)); arrowPath.lineTo(endPoint); break; case Axis::SemiFilledArrowSmall: arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/4, endPoint.y()-arrowSize/4*cos_phi)); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/8, endPoint.y())); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/4, endPoint.y()+arrowSize/4*cos_phi)); arrowPath.lineTo(endPoint); break; case Axis::SemiFilledArrowBig: arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/2, endPoint.y()-arrowSize/2*cos_phi)); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/4, endPoint.y())); arrowPath.lineTo(QPointF(endPoint.x()-direction*arrowSize/2, endPoint.y()+arrowSize/2*cos_phi)); arrowPath.lineTo(endPoint); break; } } else { //vertical orientation QPointF endPoint = QPointF(startPoint.x(), startPoint.y()-direction*arrowSize); arrowPath.moveTo(startPoint); arrowPath.lineTo(endPoint); switch (arrowType) { case Axis::NoArrow: break; case Axis::SimpleArrowSmall: arrowPath.moveTo(endPoint); arrowPath.lineTo(QPointF(endPoint.x()-arrowSize/4*cos_phi, endPoint.y()+direction*arrowSize/4)); arrowPath.moveTo(endPoint); arrowPath.lineTo(QPointF(endPoint.x()+arrowSize/4*cos_phi, endPoint.y()+direction*arrowSize/4)); break; case Axis::SimpleArrowBig: arrowPath.moveTo(endPoint); arrowPath.lineTo(QPointF(endPoint.x()-arrowSize/2*cos_phi, endPoint.y()+direction*arrowSize/2)); arrowPath.moveTo(endPoint); arrowPath.lineTo(QPointF(endPoint.x()+arrowSize/2*cos_phi, endPoint.y()+direction*arrowSize/2)); break; case Axis::FilledArrowSmall: arrowPath.lineTo(QPointF(endPoint.x()-arrowSize/4*cos_phi, endPoint.y()+direction*arrowSize/4)); arrowPath.lineTo(QPointF(endPoint.x()+arrowSize/4*cos_phi, endPoint.y()+direction*arrowSize/4)); arrowPath.lineTo(endPoint); break; case Axis::FilledArrowBig: arrowPath.lineTo(QPointF(endPoint.x()-arrowSize/2*cos_phi, endPoint.y()+direction*arrowSize/2)); arrowPath.lineTo(QPointF(endPoint.x()+arrowSize/2*cos_phi, endPoint.y()+direction*arrowSize/2)); arrowPath.lineTo(endPoint); break; case Axis::SemiFilledArrowSmall: arrowPath.lineTo(QPointF(endPoint.x()-arrowSize/4*cos_phi, endPoint.y()+direction*arrowSize/4)); arrowPath.lineTo(QPointF(endPoint.x(), endPoint.y()+direction*arrowSize/8)); arrowPath.lineTo(QPointF(endPoint.x()+arrowSize/4*cos_phi, endPoint.y()+direction*arrowSize/4)); arrowPath.lineTo(endPoint); break; case Axis::SemiFilledArrowBig: arrowPath.lineTo(QPointF(endPoint.x()-arrowSize/2*cos_phi, endPoint.y()+direction*arrowSize/2)); arrowPath.lineTo(QPointF(endPoint.x(), endPoint.y()+direction*arrowSize/4)); arrowPath.lineTo(QPointF(endPoint.x()+arrowSize/2*cos_phi, endPoint.y()+direction*arrowSize/2)); arrowPath.lineTo(endPoint); break; } } } //! helper function for retransformTicks() bool AxisPrivate::transformAnchor(QPointF* anchorPoint) { QVector points; points.append(*anchorPoint); points = cSystem->mapLogicalToScene(points); if (points.count() != 1) { // point is not mappable or in a coordinate gap return false; } else { *anchorPoint = points.at(0); return true; } } /*! recalculates the position of the axis ticks. */ void AxisPrivate::retransformTicks() { if (suppressRetransform) return; //TODO: check that start and end are > 0 for log and >=0 for sqrt, etc. majorTicksPath = QPainterPath(); minorTicksPath = QPainterPath(); majorTickPoints.clear(); minorTickPoints.clear(); tickLabelValues.clear(); if ( majorTicksNumber < 1 || (majorTicksDirection == Axis::noTicks && minorTicksDirection == Axis::noTicks) ) { retransformTickLabelPositions(); //this calls recalcShapeAndBoundingRect() return; } //determine the increment for the major ticks double majorTicksIncrement = 0; int tmpMajorTicksNumber = 0; if (majorTicksType == Axis::TicksTotalNumber) { //the total number of major ticks is given - > determine the increment tmpMajorTicksNumber = majorTicksNumber; switch (scale) { case Axis::ScaleLinear: majorTicksIncrement = (end-start)/(majorTicksNumber-1); break; case Axis::ScaleLog10: majorTicksIncrement = (log10(end)-log10(start))/(majorTicksNumber-1); break; case Axis::ScaleLog2: majorTicksIncrement = (log(end)-log(start))/log(2)/(majorTicksNumber-1); break; case Axis::ScaleLn: majorTicksIncrement = (log(end)-log(start))/(majorTicksNumber-1); break; case Axis::ScaleSqrt: majorTicksIncrement = (sqrt(end)-sqrt(start))/(majorTicksNumber-1); break; case Axis::ScaleX2: majorTicksIncrement = (end*end - start*start)/(majorTicksNumber-1); } } else if (majorTicksType == Axis::TicksSpacing) { //the increment of the major ticks is given -> determine the number majorTicksIncrement = majorTicksSpacing * GSL_SIGN(end-start); switch (scale) { case Axis::ScaleLinear: tmpMajorTicksNumber = qRound((end-start)/majorTicksIncrement + 1); break; case Axis::ScaleLog10: tmpMajorTicksNumber = qRound((log10(end)-log10(start))/majorTicksIncrement + 1); break; case Axis::ScaleLog2: tmpMajorTicksNumber = qRound((log(end)-log(start))/log(2)/majorTicksIncrement + 1); break; case Axis::ScaleLn: tmpMajorTicksNumber = qRound((log(end)-log(start))/majorTicksIncrement + 1); break; case Axis::ScaleSqrt: tmpMajorTicksNumber = qRound((sqrt(end)-sqrt(start))/majorTicksIncrement + 1); break; case Axis::ScaleX2: tmpMajorTicksNumber = qRound((end*end - start*start)/majorTicksIncrement + 1); } } else { //custom column was provided if (majorTicksColumn) { tmpMajorTicksNumber = majorTicksColumn->rowCount(); } else { retransformTickLabelPositions(); //this calls recalcShapeAndBoundingRect() return; } } int tmpMinorTicksNumber; if (minorTicksType == Axis::TicksTotalNumber) tmpMinorTicksNumber = minorTicksNumber; else if (minorTicksType == Axis::TicksSpacing) tmpMinorTicksNumber = fabs(end - start)/ (majorTicksNumber - 1)/minorTicksIncrement - 1; else (minorTicksColumn) ? tmpMinorTicksNumber = minorTicksColumn->rowCount() : tmpMinorTicksNumber = 0; QPointF anchorPoint; QPointF startPoint; QPointF endPoint; qreal majorTickPos = 0.0; qreal minorTickPos; qreal nextMajorTickPos = 0.0; const int xDirection = cSystem->xDirection(); const int yDirection = cSystem->yDirection(); const double middleX = plot->xMin() + (plot->xMax() - plot->xMin())/2; const double middleY = plot->yMin() + (plot->yMax() - plot->yMin())/2; bool valid; //DEBUG("tmpMajorTicksNumber = " << tmpMajorTicksNumber) for (int iMajor = 0; iMajor < tmpMajorTicksNumber; iMajor++) { //DEBUG("major tick " << iMajor) //calculate major tick's position if (majorTicksType != Axis::TicksCustomColumn) { switch (scale) { case Axis::ScaleLinear: majorTickPos = start + majorTicksIncrement * iMajor; nextMajorTickPos = majorTickPos + majorTicksIncrement; break; case Axis::ScaleLog10: majorTickPos = start * pow(10, majorTicksIncrement*iMajor); nextMajorTickPos = majorTickPos * pow(10, majorTicksIncrement); break; case Axis::ScaleLog2: majorTickPos = start * pow(2, majorTicksIncrement*iMajor); nextMajorTickPos = majorTickPos * pow(2, majorTicksIncrement); break; case Axis::ScaleLn: majorTickPos = start * exp(majorTicksIncrement*iMajor); nextMajorTickPos = majorTickPos * exp(majorTicksIncrement); break; case Axis::ScaleSqrt: majorTickPos = pow(sqrt(start) + majorTicksIncrement*iMajor, 2); nextMajorTickPos = pow(sqrt(start) + majorTicksIncrement*(iMajor+1), 2); break; case Axis::ScaleX2: majorTickPos = sqrt(start*start + majorTicksIncrement*iMajor); nextMajorTickPos = sqrt(start*start + majorTicksIncrement*(iMajor+1)); break; } } else { // custom column if (!majorTicksColumn->isValid(iMajor) || majorTicksColumn->isMasked(iMajor)) continue; majorTickPos = majorTicksColumn->valueAt(iMajor); // set next major tick pos for minor ticks if (iMajor < tmpMajorTicksNumber - 1) { if (majorTicksColumn->isValid(iMajor+1) && !majorTicksColumn->isMasked(iMajor+1)) nextMajorTickPos = majorTicksColumn->valueAt(iMajor+1); } else // last major tick tmpMinorTicksNumber = 0; } //calculate start and end points for major tick's line if (majorTicksDirection != Axis::noTicks) { if (orientation == Axis::AxisHorizontal) { anchorPoint.setX(majorTickPos); anchorPoint.setY(offset); valid = transformAnchor(&anchorPoint); if (valid) { if (offset < middleY) { startPoint = anchorPoint + QPointF(0, (majorTicksDirection & Axis::ticksIn) ? yDirection * majorTicksLength : 0); endPoint = anchorPoint + QPointF(0, (majorTicksDirection & Axis::ticksOut) ? -yDirection * majorTicksLength : 0); } else { startPoint = anchorPoint + QPointF(0, (majorTicksDirection & Axis::ticksOut) ? yDirection * majorTicksLength : 0); endPoint = anchorPoint + QPointF(0, (majorTicksDirection & Axis::ticksIn) ? -yDirection * majorTicksLength : 0); } } } else { // vertical anchorPoint.setY(majorTickPos); anchorPoint.setX(offset); valid = transformAnchor(&anchorPoint); if (valid) { if (offset < middleX) { startPoint = anchorPoint + QPointF((majorTicksDirection & Axis::ticksIn) ? xDirection * majorTicksLength : 0, 0); endPoint = anchorPoint + QPointF((majorTicksDirection & Axis::ticksOut) ? -xDirection * majorTicksLength : 0, 0); } else { startPoint = anchorPoint + QPointF((majorTicksDirection & Axis::ticksOut) ? xDirection * majorTicksLength : 0, 0); endPoint = anchorPoint + QPointF((majorTicksDirection & Axis::ticksIn) ? -xDirection * majorTicksLength : 0, 0); } } } double value = scalingFactor * majorTickPos + zeroOffset; //if custom column is used, we can have duplicated values in it and we need only unique values if (majorTicksType == Axis::TicksCustomColumn && tickLabelValues.indexOf(value) != -1) valid = false; //add major tick's line to the painter path if (valid) { majorTicksPath.moveTo(startPoint); majorTicksPath.lineTo(endPoint); majorTickPoints << anchorPoint; tickLabelValues << value; } } //minor ticks //DEBUG(" tmpMinorTicksNumber = " << tmpMinorTicksNumber) if (Axis::noTicks != minorTicksDirection && tmpMajorTicksNumber > 1 && tmpMinorTicksNumber > 0 && iMajor < tmpMajorTicksNumber - 1 && nextMajorTickPos != majorTickPos) { //minor ticks are placed at equidistant positions independent of the selected scaling for the major ticks positions double minorTicksIncrement = (nextMajorTickPos - majorTickPos)/(tmpMinorTicksNumber + 1); //DEBUG(" nextMajorTickPos = " << nextMajorTickPos) //DEBUG(" majorTickPos = " << majorTickPos) //DEBUG(" minorTicksIncrement = " << minorTicksIncrement) for (int iMinor = 0; iMinor < tmpMinorTicksNumber; iMinor++) { //calculate minor tick's position if (minorTicksType != Axis::TicksCustomColumn) { minorTickPos = majorTickPos + (iMinor + 1) * minorTicksIncrement; } else { if (!minorTicksColumn->isValid(iMinor) || minorTicksColumn->isMasked(iMinor)) continue; minorTickPos = minorTicksColumn->valueAt(iMinor); //in the case a custom column is used for the minor ticks, we draw them _once_ for the whole range of the axis. //execute the minor ticks loop only once. if (iMajor > 0) break; } //DEBUG(" minorTickPos = " << minorTickPos) //calculate start and end points for minor tick's line if (orientation == Axis::AxisHorizontal) { anchorPoint.setX(minorTickPos); anchorPoint.setY(offset); valid = transformAnchor(&anchorPoint); if (valid) { if (offset < middleY) { startPoint = anchorPoint + QPointF(0, (minorTicksDirection & Axis::ticksIn) ? yDirection * minorTicksLength : 0); endPoint = anchorPoint + QPointF(0, (minorTicksDirection & Axis::ticksOut) ? -yDirection * minorTicksLength : 0); } else { startPoint = anchorPoint + QPointF(0, (minorTicksDirection & Axis::ticksOut) ? yDirection * minorTicksLength : 0); endPoint = anchorPoint + QPointF(0, (minorTicksDirection & Axis::ticksIn) ? -yDirection * minorTicksLength : 0); } } } else { // vertical anchorPoint.setY(minorTickPos); anchorPoint.setX(offset); valid = transformAnchor(&anchorPoint); if (valid) { if (offset < middleX) { startPoint = anchorPoint + QPointF((minorTicksDirection & Axis::ticksIn) ? xDirection * minorTicksLength : 0, 0); endPoint = anchorPoint + QPointF((minorTicksDirection & Axis::ticksOut) ? -xDirection * minorTicksLength : 0, 0); } else { startPoint = anchorPoint + QPointF((minorTicksDirection & Axis::ticksOut) ? xDirection * minorTicksLength : 0, 0); endPoint = anchorPoint + QPointF((minorTicksDirection & Axis::ticksIn) ? -xDirection * minorTicksLength : 0, 0); } } } //add minor tick's line to the painter path if (valid) { minorTicksPath.moveTo(startPoint); minorTicksPath.lineTo(endPoint); minorTickPoints << anchorPoint; } } } } //tick positions where changed -> update the position of the tick labels and grid lines retransformTickLabelStrings(); retransformMajorGrid(); retransformMinorGrid(); } /*! creates the tick label strings starting with the most optimal (=the smallest possible number of float digits) precision for the floats */ void AxisPrivate::retransformTickLabelStrings() { if (suppressRetransform) return; //DEBUG("AxisPrivate::retransformTickLabelStrings()"); if (labelsAutoPrecision) { //check, whether we need to increase the current precision int newPrecision = upperLabelsPrecision(labelsPrecision); if (newPrecision != labelsPrecision) { labelsPrecision = newPrecision; emit q->labelsPrecisionChanged(labelsPrecision); } else { //check, whether we can reduce the current precision newPrecision = lowerLabelsPrecision(labelsPrecision); if (newPrecision != labelsPrecision) { labelsPrecision = newPrecision; emit q->labelsPrecisionChanged(labelsPrecision); } } } //DEBUG("labelsPrecision =" << labelsPrecision); //automatically switch from 'decimal' to 'scientific' format for big numbers (>10^4) //and back to decimal when the numbers get smaller after the auto-switch again if (labelsFormat == Axis::FormatDecimal && !labelsFormatDecimalOverruled) { for (auto value : tickLabelValues) { if (std::abs(value) > 1e4) { labelsFormat = Axis::FormatScientificE; emit q->labelsFormatChanged(labelsFormat); labelsFormatAutoChanged = true; break; } } } else if (labelsFormatAutoChanged ) { //check whether we still have big numbers bool changeBack = true; for (auto value : tickLabelValues) { if (std::abs(value) > 1e4) { changeBack = false; break; } } if (changeBack) { labelsFormatAutoChanged = false; labelsFormat = Axis::FormatDecimal; emit q->labelsFormatChanged(labelsFormat); } } tickLabelStrings.clear(); QString str; if ( (orientation == Axis::AxisHorizontal && plot->xRangeFormat() == CartesianPlot::Numeric) || (orientation == Axis::AxisVertical && plot->yRangeFormat() == CartesianPlot::Numeric) ) { if (labelsFormat == Axis::FormatDecimal) { QString nullStr = QString::number(0, 'f', labelsPrecision); for (const auto value : tickLabelValues) { str = QString::number(value, 'f', labelsPrecision); if (str == "-" + nullStr) str = nullStr; str = labelsPrefix + str + labelsSuffix; tickLabelStrings << str; } } else if (labelsFormat == Axis::FormatScientificE) { QString nullStr = QString::number(0, 'e', labelsPrecision); for (const auto value : tickLabelValues) { str = QString::number(value, 'e', labelsPrecision); if (str == "-" + nullStr) str = nullStr; str = labelsPrefix + str + labelsSuffix; tickLabelStrings << str; } } else if (labelsFormat == Axis::FormatPowers10) { for (const auto value : tickLabelValues) { str = "10" + QString::number(log10(value), 'f', labelsPrecision) + ""; str = labelsPrefix + str + labelsSuffix; tickLabelStrings << str; } } else if (labelsFormat == Axis::FormatPowers2) { for (const auto value : tickLabelValues) { str = "2" + QString::number(log2(value), 'f', labelsPrecision) + ""; str = labelsPrefix + str + labelsSuffix; tickLabelStrings << str; } } else if (labelsFormat == Axis::FormatPowersE) { for (const auto value : tickLabelValues) { str = "e" + QString::number(log(value), 'f', labelsPrecision) + ""; str = labelsPrefix + str + labelsSuffix; tickLabelStrings << str; } } else if (labelsFormat == Axis::FormatMultipliesPi) { for (const auto value : tickLabelValues) { str = "" + QString::number(value / M_PI, 'f', labelsPrecision) + "" + QChar(0x03C0); str = labelsPrefix + str + labelsSuffix; tickLabelStrings << str; } } } else { for (const auto value : tickLabelValues) { QDateTime dateTime; dateTime.setMSecsSinceEpoch(value); str = dateTime.toString(labelsDateTimeFormat); str = labelsPrefix + str + labelsSuffix; tickLabelStrings << str; } } //recalculate the position of the tick labels retransformTickLabelPositions(); } /*! returns the smallest upper limit for the precision where no duplicates for the tick label float occur. */ int AxisPrivate::upperLabelsPrecision(int precision) { //DEBUG("AxisPrivate::upperLabelsPrecision() precision =" << precision); //round float to the current precision and look for duplicates. //if there are duplicates, increase the precision. QVector tempValues; for (const auto value : tickLabelValues) tempValues.append( nsl_math_round_places(value, precision) ); for (int i = 0; i < tempValues.size(); ++i) { for (int j = 0; j < tempValues.size(); ++j) { if (i == j) continue; if (tempValues.at(i) == tempValues.at(j)) { //duplicate for the current precision found, increase the precision and check again return upperLabelsPrecision(precision + 1); } } } //no duplicates for the current precision found: return the current value //DEBUG(" upper precision = " << precision); return precision; } /*! returns highest lower limit for the precision where no duplicates for the tick label float occur. */ int AxisPrivate::lowerLabelsPrecision(int precision) { // DEBUG("AxisPrivate::lowerLabelsPrecision() precision =" << precision); //round float to the current precision and look for duplicates. //if there are duplicates, decrease the precision. QVector tempValues; for (const auto value : tickLabelValues) tempValues.append( nsl_math_round_places(value, precision-1) ); for (int i = 0; i < tempValues.size(); ++i) { for (int j = 0; j < tempValues.size(); ++j) { if (i == j) continue; if (tempValues.at(i) == tempValues.at(j)) { //duplicate found for the reduced precision //-> current precision cannot be reduced, return the current value // DEBUG(" lower precision = " << precision); return precision; } } } //no duplicates found, reduce further, and check again if (precision == 0) return 0; else return lowerLabelsPrecision(precision - 1); } /*! recalculates the position of the tick labels. Called when the geometry related properties (position, offset, font size, suffix, prefix) of the labels are changed. */ void AxisPrivate::retransformTickLabelPositions() { tickLabelPoints.clear(); if (majorTicksDirection == Axis::noTicks || labelsPosition == Axis::NoLabels) { recalcShapeAndBoundingRect(); return; } QFontMetrics fm(labelsFont); double width = 0; double height = fm.ascent(); QPointF pos; const double middleX = plot->xMin() + (plot->xMax() - plot->xMin())/2; const double middleY = plot->yMin() + (plot->yMax() - plot->yMin())/2; const int xDirection = cSystem->xDirection(); const int yDirection = cSystem->yDirection(); QPointF startPoint, endPoint, anchorPoint; QTextDocument td; td.setDefaultFont(labelsFont); double cosine = cos(labelsRotationAngle * M_PI / 180.); // calculate only one time double sine = sin(labelsRotationAngle * M_PI / 180.); // calculate only one time for ( int i = 0; i < majorTickPoints.size(); i++ ) { if ((orientation == Axis::AxisHorizontal && plot->xRangeFormat() == CartesianPlot::Numeric) || (orientation == Axis::AxisVertical && plot->yRangeFormat() == CartesianPlot::Numeric)) { if (labelsFormat == Axis::FormatDecimal || labelsFormat == Axis::FormatScientificE) { width = fm.boundingRect(tickLabelStrings.at(i)).width(); } else { td.setHtml(tickLabelStrings.at(i)); width = td.size().width(); height = td.size().height(); } } else { // Datetime width = fm.boundingRect(tickLabelStrings.at(i)).width(); } double diffx = cosine * width; double diffy = sine * width; anchorPoint = majorTickPoints.at(i); //center align all labels with respect to the end point of the tick line if (orientation == Axis::AxisHorizontal) { if (offset < middleY) { startPoint = anchorPoint + QPointF(0, (majorTicksDirection & Axis::ticksIn) ? yDirection * majorTicksLength : 0); endPoint = anchorPoint + QPointF(0, (majorTicksDirection & Axis::ticksOut) ? -yDirection * majorTicksLength : 0); } else { startPoint = anchorPoint + QPointF(0, (majorTicksDirection & Axis::ticksOut) ? yDirection * majorTicksLength : 0); endPoint = anchorPoint + QPointF(0, (majorTicksDirection & Axis::ticksIn) ? -yDirection * majorTicksLength : 0); } // for rotated labels (angle is not zero), align label's corner at the position of the tick if (fabs(fabs(labelsRotationAngle) - 180.) < 1.e-2) { // +-180° if (labelsPosition == Axis::LabelsOut) { pos.setX(endPoint.x() + width/2); pos.setY(endPoint.y() + labelsOffset ); } else { pos.setX(startPoint.x() + width/2); pos.setY(startPoint.y() - height - labelsOffset); } } else if (labelsRotationAngle <= -0.01) { // [-0.01°, -180°) if (labelsPosition == Axis::LabelsOut) { pos.setX(endPoint.x() + sine * height/2); pos.setY(endPoint.y() + labelsOffset + cosine * height/2); } else { pos.setX(startPoint.x() + sine * height/2 - diffx); pos.setY(startPoint.y() - labelsOffset + cosine * height/2 + diffy); } } else if (labelsRotationAngle >= 0.01) { // [0.01°, 180°) if (labelsPosition == Axis::LabelsOut) { pos.setX(endPoint.x() - diffx + sine * height/2); pos.setY(endPoint.y() + labelsOffset + diffy + cosine * height/2); } else { pos.setX(startPoint.x() + sine * height/2); pos.setY(startPoint.y() - labelsOffset + cosine * height/2); } } else { // 0° if (labelsPosition == Axis::LabelsOut) { pos.setX(endPoint.x() - width/2); pos.setY(endPoint.y() + height + labelsOffset); } else { pos.setX(startPoint.x() - width/2); pos.setY(startPoint.y() - labelsOffset); } } // ---------------------- vertical ------------------------- } else { if (offset < middleX) { startPoint = anchorPoint + QPointF((majorTicksDirection & Axis::ticksIn) ? xDirection * majorTicksLength : 0, 0); endPoint = anchorPoint + QPointF((majorTicksDirection & Axis::ticksOut) ? -xDirection * majorTicksLength : 0, 0); } else { startPoint = anchorPoint + QPointF((majorTicksDirection & Axis::ticksOut) ? xDirection * majorTicksLength : 0, 0); endPoint = anchorPoint + QPointF((majorTicksDirection & Axis::ticksIn) ? -xDirection * majorTicksLength : 0, 0); } if (fabs(labelsRotationAngle - 90.) < 1.e-2) { // +90° if (labelsPosition == Axis::LabelsOut) { pos.setX(endPoint.x() - labelsOffset); pos.setY(endPoint.y() + width/2 ); } else { pos.setX(startPoint.x() + labelsOffset); pos.setY(startPoint.y() + width/2); } } else if (fabs(labelsRotationAngle + 90.) < 1.e-2) { // -90° if (labelsPosition == Axis::LabelsOut) { pos.setX(endPoint.x() - labelsOffset - height); pos.setY(endPoint.y() - width/2); } else { pos.setX(startPoint.x() + labelsOffset); pos.setY(startPoint.y() - width/2); } } else if (fabs(fabs(labelsRotationAngle) - 180.) < 1.e-2) { // +-180° if (labelsPosition == Axis::LabelsOut) { pos.setX(endPoint.x() - labelsOffset); pos.setY(endPoint.y() - height/2); } else { pos.setX(startPoint.x() + labelsOffset + width); pos.setY(startPoint.y() - height/2); } } else if (fabs(labelsRotationAngle) >= 0.01 && fabs(labelsRotationAngle) <= 89.99) { // [0.01°, 90°) if (labelsPosition == Axis::LabelsOut) { // left pos.setX(endPoint.x() - labelsOffset - diffx + sine * height/2); pos.setY(endPoint.y() + cosine * height/2 + diffy); } else { pos.setX(startPoint.x() + labelsOffset + sine * height/2); pos.setY(startPoint.y() + cosine * height/2); } } else if (fabs(labelsRotationAngle) >= 90.01 && fabs(labelsRotationAngle) <= 179.99) { // [90.01, 180) if (labelsPosition == Axis::LabelsOut) { // left pos.setX(endPoint.x() - labelsOffset + sine * height/2); pos.setY(endPoint.y() + cosine * height/2); } else { pos.setX(startPoint.x() + labelsOffset - diffx + sine * height/2); pos.setY(startPoint.y() + diffy + cosine * height/2); } } else { // 0° if (labelsPosition == Axis::LabelsOut) { pos.setX(endPoint.x() - width - labelsOffset); pos.setY(endPoint.y() + height/2); } else { pos.setX(startPoint.x() + labelsOffset); pos.setY(startPoint.y() + height/2); } } } tickLabelPoints << pos; } recalcShapeAndBoundingRect(); } void AxisPrivate::retransformMajorGrid() { if (suppressRetransform) return; majorGridPath = QPainterPath(); if (majorGridPen.style() == Qt::NoPen || majorTickPoints.size() == 0) { recalcShapeAndBoundingRect(); return; } //major tick points are already in scene coordinates, convert them back to logical... //TODO: mapping should work without SuppressPageClipping-flag, check float comparisons in the map-function. //Currently, grid lines disappear somtimes without this flag QVector logicalMajorTickPoints = cSystem->mapSceneToLogical(majorTickPoints, AbstractCoordinateSystem::SuppressPageClipping); if (logicalMajorTickPoints.isEmpty()) return; //TODO: //when iterating over all grid lines, skip the first and the last points for auto scaled axes, //since we don't want to paint any grid lines at the plot boundaries bool skipLowestTick, skipUpperTick; if (orientation == Axis::AxisHorizontal) { //horizontal axis skipLowestTick = qFuzzyCompare(logicalMajorTickPoints.at(0).x(), plot->xMin()); skipUpperTick = qFuzzyCompare(logicalMajorTickPoints.at(logicalMajorTickPoints.size()-1).x(), plot->xMax()); } else { skipLowestTick = qFuzzyCompare(logicalMajorTickPoints.at(0).y(), plot->yMin()); skipUpperTick = qFuzzyCompare(logicalMajorTickPoints.at(logicalMajorTickPoints.size()-1).y(), plot->yMax()); } int start, end; if (skipLowestTick) { if (logicalMajorTickPoints.size() > 1) start = 1; else start = 0; } else { start = 0; } if (skipUpperTick) { if (logicalMajorTickPoints.size() > 1) end = logicalMajorTickPoints.size() - 1; else end = 0; } else { end = logicalMajorTickPoints.size(); } QVector lines; if (orientation == Axis::AxisHorizontal) { //horizontal axis double yMin = plot->yMin(); double yMax = plot->yMax(); for (int i = start; i < end; ++i) { const QPointF& point = logicalMajorTickPoints.at(i); lines.append( QLineF(point.x(), yMin, point.x(), yMax) ); } } else { //vertical axis double xMin = plot->xMin(); double xMax = plot->xMax(); //skip the first and the last points, since we don't want to paint any grid lines at the plot boundaries for (int i = start; i < end; ++i) { const QPointF& point = logicalMajorTickPoints.at(i); lines.append( QLineF(xMin, point.y(), xMax, point.y()) ); } } lines = cSystem->mapLogicalToScene(lines, AbstractCoordinateSystem::SuppressPageClipping); for (const auto& line : lines) { majorGridPath.moveTo(line.p1()); majorGridPath.lineTo(line.p2()); } recalcShapeAndBoundingRect(); } void AxisPrivate::retransformMinorGrid() { if (suppressRetransform) return; minorGridPath = QPainterPath(); if (minorGridPen.style() == Qt::NoPen) { recalcShapeAndBoundingRect(); return; } //minor tick points are already in scene coordinates, convert them back to logical... //TODO: mapping should work without SuppressPageClipping-flag, check float comparisons in the map-function. //Currently, grid lines disappear somtimes without this flag QVector logicalMinorTickPoints = cSystem->mapSceneToLogical(minorTickPoints, AbstractCoordinateSystem::SuppressPageClipping); QVector lines; if (orientation == Axis::AxisHorizontal) { //horizontal axis double yMin = plot->yMin(); double yMax = plot->yMax(); for (const auto point : logicalMinorTickPoints) lines.append( QLineF(point.x(), yMin, point.x(), yMax) ); } else { //vertical axis double xMin = plot->xMin(); double xMax = plot->xMax(); for (const auto point: logicalMinorTickPoints) lines.append( QLineF(xMin, point.y(), xMax, point.y()) ); } lines = cSystem->mapLogicalToScene(lines, AbstractCoordinateSystem::SuppressPageClipping); for (const auto& line : lines) { minorGridPath.moveTo(line.p1()); minorGridPath.lineTo(line.p2()); } recalcShapeAndBoundingRect(); } /*! * called when the opacity of the grid was changes, update the grid graphics item */ //TODO: this function is only needed for loaded projects where update() doesn't seem to be enough //and we have to call gridItem->update() explicitly. //This is not required for newly created plots/axes. Why is this difference? void AxisPrivate::updateGrid() { gridItem->update(); } void AxisPrivate::recalcShapeAndBoundingRect() { if (m_suppressRecalc) return; prepareGeometryChange(); if (linePath.isEmpty()) { axisShape = QPainterPath(); boundingRectangle = QRectF(); title->setPositionInvalid(true); if (plot) plot->prepareGeometryChange(); return; } else { title->setPositionInvalid(false); } axisShape = WorksheetElement::shapeFromPath(linePath, linePen); axisShape.addPath(WorksheetElement::shapeFromPath(arrowPath, linePen)); axisShape.addPath(WorksheetElement::shapeFromPath(majorTicksPath, majorTicksPen)); axisShape.addPath(WorksheetElement::shapeFromPath(minorTicksPath, minorTicksPen)); QPainterPath tickLabelsPath = QPainterPath(); if (labelsPosition != Axis::NoLabels) { QTransform trafo; QPainterPath tempPath; QFontMetrics fm(labelsFont); QTextDocument td; td.setDefaultFont(labelsFont); for (int i = 0; i < tickLabelPoints.size(); i++) { tempPath = QPainterPath(); if (labelsFormat == Axis::FormatDecimal || labelsFormat == Axis::FormatScientificE) { tempPath.addRect(fm.boundingRect(tickLabelStrings.at(i))); } else { td.setHtml(tickLabelStrings.at(i)); tempPath.addRect(QRectF(0, -td.size().height(), td.size().width(), td.size().height())); } trafo.reset(); trafo.translate( tickLabelPoints.at(i).x(), tickLabelPoints.at(i).y() ); trafo.rotate(-labelsRotationAngle); tempPath = trafo.map(tempPath); tickLabelsPath.addPath(WorksheetElement::shapeFromPath(tempPath, linePen)); } axisShape.addPath(WorksheetElement::shapeFromPath(tickLabelsPath, QPen())); } //add title label, if available if ( title->isVisible() && !title->text().text.isEmpty() ) { const QRectF& titleRect = title->graphicsItem()->boundingRect(); if (titleRect.width() != 0.0 && titleRect.height() != 0.0) { //determine the new position of the title label: //we calculate the new position here and not in retransform(), //since it depends on the size and position of the tick labels, tickLabelsPath, available here. QRectF rect = linePath.boundingRect(); qreal offsetX = titleOffsetX; //the distance to the axis line qreal offsetY = titleOffsetY; //the distance to the axis line if (orientation == Axis::AxisHorizontal) { offsetY -= titleRect.height()/2; if (labelsPosition == Axis::LabelsOut) offsetY -= labelsOffset + tickLabelsPath.boundingRect().height(); title->setPosition( QPointF( (rect.topLeft().x() + rect.topRight().x())/2 + titleOffsetX, rect.bottomLeft().y() - offsetY ) ); } else { offsetX -= titleRect.width()/2; if (labelsPosition == Axis::LabelsOut) offsetX -= labelsOffset+ tickLabelsPath.boundingRect().width(); title->setPosition( QPointF( rect.topLeft().x() + offsetX, (rect.topLeft().y() + rect.bottomLeft().y())/2 - titleOffsetY) ); } axisShape.addPath(WorksheetElement::shapeFromPath(title->graphicsItem()->mapToParent(title->graphicsItem()->shape()), linePen)); } } boundingRectangle = axisShape.boundingRect(); //if the axis goes beyond the current bounding box of the plot (too high offset is used, too long labels etc.) //request a prepareGeometryChange() for the plot in order to properly keep track of geometry changes if (plot) plot->prepareGeometryChange(); } /*! paints the content of the axis. Reimplemented from \c QGraphicsItem. \sa QGraphicsItem::paint() */ void AxisPrivate::paint(QPainter *painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { Q_UNUSED(option) Q_UNUSED(widget) if (!isVisible()) return; if (linePath.isEmpty()) return; //draw the line if (linePen.style() != Qt::NoPen) { painter->setOpacity(lineOpacity); painter->setPen(linePen); painter->setBrush(Qt::SolidPattern); painter->drawPath(linePath); //draw the arrow if (arrowType != Axis::NoArrow) painter->drawPath(arrowPath); } //draw the major ticks if (majorTicksDirection != Axis::noTicks) { painter->setOpacity(majorTicksOpacity); painter->setPen(majorTicksPen); painter->setBrush(Qt::NoBrush); painter->drawPath(majorTicksPath); } //draw the minor ticks if (minorTicksDirection != Axis::noTicks) { painter->setOpacity(minorTicksOpacity); painter->setPen(minorTicksPen); painter->setBrush(Qt::NoBrush); painter->drawPath(minorTicksPath); } // draw tick labels if (labelsPosition != Axis::NoLabels) { painter->setOpacity(labelsOpacity); painter->setPen(QPen(labelsColor)); painter->setFont(labelsFont); QTextDocument td; td.setDefaultFont(labelsFont); if ((orientation == Axis::AxisHorizontal && plot->xRangeFormat() == CartesianPlot::Numeric) || (orientation == Axis::AxisVertical && plot->yRangeFormat() == CartesianPlot::Numeric)) { for (int i = 0; i < tickLabelPoints.size(); i++) { painter->translate(tickLabelPoints.at(i)); painter->save(); painter->rotate(-labelsRotationAngle); if (labelsFormat == Axis::FormatDecimal || labelsFormat == Axis::FormatScientificE) { painter->drawText(QPoint(0,0), tickLabelStrings.at(i)); } else { td.setHtml(tickLabelStrings.at(i)); painter->translate(0, -td.size().height()); td.drawContents(painter); } painter->restore(); painter->translate(-tickLabelPoints.at(i)); } } else { // datetime for (int i = 0; i < tickLabelPoints.size(); i++) { painter->translate(tickLabelPoints.at(i)); painter->save(); painter->rotate(-labelsRotationAngle); painter->drawText(QPoint(0,0), tickLabelStrings.at(i)); painter->restore(); painter->translate(-tickLabelPoints.at(i)); } } } if (m_hovered && !isSelected() && !m_printing) { painter->setPen(QPen(QApplication::palette().color(QPalette::Shadow), 2, Qt::SolidLine)); painter->drawPath(axisShape); } if (isSelected() && !m_printing) { painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), 2, Qt::SolidLine)); painter->drawPath(axisShape); } } void AxisPrivate::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { q->createContextMenu()->exec(event->screenPos()); } void AxisPrivate::hoverEnterEvent(QGraphicsSceneHoverEvent*) { if (!isSelected()) { m_hovered = true; emit q->hovered(); update(axisShape.boundingRect()); } } void AxisPrivate::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { if (m_hovered) { m_hovered = false; emit q->unhovered(); update(axisShape.boundingRect()); } } void AxisPrivate::setPrinting(bool on) { m_printing = on; } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## //! Save as XML void Axis::save(QXmlStreamWriter* writer) const{ Q_D(const Axis); writer->writeStartElement( "axis" ); writeBasicAttributes( writer ); writeCommentElement( writer ); //general writer->writeStartElement( "general" ); writer->writeAttribute( "autoScale", QString::number(d->autoScale) ); writer->writeAttribute( "orientation", QString::number(d->orientation) ); writer->writeAttribute( "position", QString::number(d->position) ); writer->writeAttribute( "scale", QString::number(d->scale) ); writer->writeAttribute( "offset", QString::number(d->offset) ); writer->writeAttribute( "start", QString::number(d->start) ); writer->writeAttribute( "end", QString::number(d->end) ); writer->writeAttribute( "scalingFactor", QString::number(d->scalingFactor) ); writer->writeAttribute( "zeroOffset", QString::number(d->zeroOffset) ); writer->writeAttribute( "titleOffsetX", QString::number(d->titleOffsetX) ); writer->writeAttribute( "titleOffsetY", QString::number(d->titleOffsetY) ); writer->writeAttribute( "visible", QString::number(d->isVisible()) ); writer->writeEndElement(); //label d->title->save( writer ); //line writer->writeStartElement( "line" ); WRITE_QPEN(d->linePen); writer->writeAttribute( "opacity", QString::number(d->lineOpacity) ); writer->writeAttribute( "arrowType", QString::number(d->arrowType) ); writer->writeAttribute( "arrowPosition", QString::number(d->arrowPosition) ); writer->writeAttribute( "arrowSize", QString::number(d->arrowSize) ); writer->writeEndElement(); //major ticks writer->writeStartElement( "majorTicks" ); writer->writeAttribute( "direction", QString::number(d->majorTicksDirection) ); writer->writeAttribute( "type", QString::number(d->majorTicksType) ); writer->writeAttribute( "number", QString::number(d->majorTicksNumber) ); writer->writeAttribute( "increment", QString::number(d->majorTicksSpacing) ); WRITE_COLUMN(d->majorTicksColumn, majorTicksColumn); writer->writeAttribute( "length", QString::number(d->majorTicksLength) ); WRITE_QPEN(d->majorTicksPen); writer->writeAttribute( "opacity", QString::number(d->majorTicksOpacity) ); writer->writeEndElement(); //minor ticks writer->writeStartElement( "minorTicks" ); writer->writeAttribute( "direction", QString::number(d->minorTicksDirection) ); writer->writeAttribute( "type", QString::number(d->minorTicksType) ); writer->writeAttribute( "number", QString::number(d->minorTicksNumber) ); writer->writeAttribute( "increment", QString::number(d->minorTicksIncrement) ); WRITE_COLUMN(d->minorTicksColumn, minorTicksColumn); writer->writeAttribute( "length", QString::number(d->minorTicksLength) ); WRITE_QPEN(d->minorTicksPen); writer->writeAttribute( "opacity", QString::number(d->minorTicksOpacity) ); writer->writeEndElement(); //extra ticks //labels writer->writeStartElement( "labels" ); writer->writeAttribute( "position", QString::number(d->labelsPosition) ); writer->writeAttribute( "offset", QString::number(d->labelsOffset) ); writer->writeAttribute( "rotation", QString::number(d->labelsRotationAngle) ); writer->writeAttribute( "format", QString::number(d->labelsFormat) ); writer->writeAttribute( "precision", QString::number(d->labelsPrecision) ); writer->writeAttribute( "autoPrecision", QString::number(d->labelsAutoPrecision) ); writer->writeAttribute( "dateTimeFormat", d->labelsDateTimeFormat ); WRITE_QCOLOR(d->labelsColor); WRITE_QFONT(d->labelsFont); writer->writeAttribute( "prefix", d->labelsPrefix ); writer->writeAttribute( "suffix", d->labelsSuffix ); writer->writeAttribute( "opacity", QString::number(d->labelsOpacity) ); writer->writeEndElement(); //grid writer->writeStartElement( "majorGrid" ); WRITE_QPEN(d->majorGridPen); writer->writeAttribute( "opacity", QString::number(d->majorGridOpacity) ); writer->writeEndElement(); writer->writeStartElement( "minorGrid" ); WRITE_QPEN(d->minorGridPen); writer->writeAttribute( "opacity", QString::number(d->minorGridOpacity) ); writer->writeEndElement(); writer->writeEndElement(); // close "axis" section } //! Load from XML bool Axis::load(XmlStreamReader* reader, bool preview) { Q_D(Axis); 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() == "axis") break; if (!reader->isStartElement()) continue; if (!preview && reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (!preview && reader->name() == "general") { attribs = reader->attributes(); READ_INT_VALUE("autoScale", autoScale, bool); READ_INT_VALUE("orientation", orientation, Axis::AxisOrientation); READ_INT_VALUE("position", position, Axis::AxisPosition); READ_INT_VALUE("scale", scale, Axis::AxisScale); READ_DOUBLE_VALUE("offset", offset); READ_DOUBLE_VALUE("start", start); READ_DOUBLE_VALUE("end", end); READ_DOUBLE_VALUE("scalingFactor", scalingFactor); READ_DOUBLE_VALUE("zeroOffset", zeroOffset); READ_DOUBLE_VALUE("titleOffsetX", titleOffsetX); READ_DOUBLE_VALUE("titleOffsetY", titleOffsetY); str = attribs.value("visible").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("visible").toString()); else d->setVisible(str.toInt()); } else if (reader->name() == "textLabel") { d->title->load(reader, preview); } else if (!preview && reader->name() == "line") { attribs = reader->attributes(); READ_QPEN(d->linePen); READ_DOUBLE_VALUE("opacity", lineOpacity); READ_INT_VALUE("arrowType", arrowType, Axis::ArrowType); READ_INT_VALUE("arrowPosition", arrowPosition, Axis::ArrowPosition); READ_DOUBLE_VALUE("arrowSize", arrowSize); } else if (!preview && reader->name() == "majorTicks") { attribs = reader->attributes(); READ_INT_VALUE("direction", majorTicksDirection, Axis::TicksDirection); READ_INT_VALUE("type", majorTicksType, Axis::TicksType); READ_INT_VALUE("number", majorTicksNumber, int); READ_DOUBLE_VALUE("increment", majorTicksSpacing); READ_COLUMN(majorTicksColumn); READ_DOUBLE_VALUE("length", majorTicksLength); READ_QPEN(d->majorTicksPen); READ_DOUBLE_VALUE("opacity", majorTicksOpacity); } else if (!preview && reader->name() == "minorTicks") { attribs = reader->attributes(); READ_INT_VALUE("direction", minorTicksDirection, Axis::TicksDirection); READ_INT_VALUE("type", minorTicksType, Axis::TicksType); READ_INT_VALUE("number", minorTicksNumber, int); READ_DOUBLE_VALUE("increment", minorTicksIncrement); READ_COLUMN(minorTicksColumn); READ_DOUBLE_VALUE("length", minorTicksLength); READ_QPEN(d->minorTicksPen); READ_DOUBLE_VALUE("opacity", minorTicksOpacity); } else if (!preview && reader->name() == "labels") { attribs = reader->attributes(); READ_INT_VALUE("position", labelsPosition, Axis::LabelsPosition); READ_DOUBLE_VALUE("offset", labelsOffset); READ_DOUBLE_VALUE("rotation", labelsRotationAngle); READ_INT_VALUE("format", labelsFormat, Axis::LabelsFormat); READ_INT_VALUE("precision", labelsPrecision, int); READ_INT_VALUE("autoPrecision", labelsAutoPrecision, bool); d->labelsDateTimeFormat = attribs.value("dateTimeFormat").toString(); READ_QCOLOR(d->labelsColor); READ_QFONT(d->labelsFont); //don't produce any warning if no prefix or suffix is set (empty string is allowed here in xml) d->labelsPrefix = attribs.value("prefix").toString(); d->labelsSuffix = attribs.value("suffix").toString(); READ_DOUBLE_VALUE("opacity", labelsOpacity); } else if (!preview && reader->name() == "majorGrid") { attribs = reader->attributes(); READ_QPEN(d->majorGridPen); READ_DOUBLE_VALUE("opacity", majorGridOpacity); } else if (!preview && reader->name() == "minorGrid") { attribs = reader->attributes(); READ_QPEN(d->minorGridPen); READ_DOUBLE_VALUE("opacity", minorGridOpacity); } else { // unknown element reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } return true; } //############################################################################## //######################### Theme management ################################## //############################################################################## void Axis::loadThemeConfig(const KConfig& config) { const KConfigGroup group = config.group("Axis"); //we don't want to show the major and minor grid lines for non-first horizontal/vertical axes //determine the index of the axis among other axes having the same orientation bool firstAxis = true; for (const auto* axis : parentAspect()->children()) { if (orientation() == axis->orientation()) { if (axis == this) { break; } else { firstAxis = false; break; } } } QPen p; // Tick label this->setLabelsColor(group.readEntry("LabelsFontColor",(QColor) this->labelsColor())); this->setLabelsOpacity(group.readEntry("LabelsOpacity",this->labelsOpacity())); //Line this->setLineOpacity(group.readEntry("LineOpacity",this->lineOpacity())); p.setColor(group.readEntry("LineColor", (QColor) this->linePen().color())); p.setStyle((Qt::PenStyle)group.readEntry("LineStyle",(int) this->linePen().style())); p.setWidthF(group.readEntry("LineWidth", this->linePen().widthF())); this->setLinePen(p); //Major ticks this->setMajorGridOpacity(group.readEntry("MajorGridOpacity", this->majorGridOpacity())); p.setColor(group.readEntry("MajorGridColor",(QColor) this->majorGridPen().color())); if (firstAxis) p.setStyle((Qt::PenStyle)group.readEntry("MajorGridStyle",(int) this->majorGridPen().style())); else p.setStyle(Qt::NoPen); p.setWidthF(group.readEntry("MajorGridWidth", this->majorGridPen().widthF())); this->setMajorGridPen(p); p.setColor(group.readEntry("MajorTicksColor",(QColor)this->majorTicksPen().color())); p.setStyle((Qt::PenStyle)group.readEntry("MajorTicksLineStyle",(int) this->majorTicksPen().style())); p.setWidthF(group.readEntry("MajorTicksWidth", this->majorTicksPen().widthF())); this->setMajorTicksPen(p); this->setMajorTicksOpacity(group.readEntry("MajorTicksOpacity",this->majorTicksOpacity())); //Minor ticks this->setMinorGridOpacity(group.readEntry("MinorGridOpacity", this->minorGridOpacity())); p.setColor(group.readEntry("MinorGridColor",(QColor) this->minorGridPen().color())); if (firstAxis) p.setStyle((Qt::PenStyle)group.readEntry("MinorGridStyle",(int) this->minorGridPen().style())); else p.setStyle(Qt::NoPen); p.setWidthF(group.readEntry("MinorGridWidth", this->minorGridPen().widthF())); this->setMinorGridPen(p); p.setColor(group.readEntry("MinorTicksColor",(QColor) this->minorTicksPen().color())); p.setWidthF(group.readEntry("MinorTicksWidth", this->minorTicksPen().widthF())); this->setMinorTicksPen(p); this->setMinorTicksOpacity(group.readEntry("MinorTicksOpacity",this->minorTicksOpacity())); - const QVector& childElements = children(AbstractAspect::IncludeHidden); + const QVector& childElements = children(AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* child : childElements) child->loadThemeConfig(config); } void Axis::saveThemeConfig(const KConfig& config) { KConfigGroup group = config.group("Axis"); // Tick label group.writeEntry("LabelsFontColor", (QColor) this->labelsColor()); group.writeEntry("LabelsOpacity", this->labelsOpacity()); //Line group.writeEntry("LineOpacity", this->lineOpacity()); group.writeEntry("LineColor", (QColor) this->linePen().color()); group.writeEntry("LineStyle", (int) this->linePen().style()); group.writeEntry("LineWidth", this->linePen().widthF()); //Major ticks group.writeEntry("MajorGridOpacity", this->majorGridOpacity()); group.writeEntry("MajorGridColor", (QColor) this->majorGridPen().color()); group.writeEntry("MajorGridStyle", (int) this->majorGridPen().style()); group.writeEntry("MajorGridWidth", this->majorGridPen().widthF()); group.writeEntry("MajorTicksColor", (QColor)this->majorTicksPen().color()); group.writeEntry("MajorTicksLineStyle", (int) this->majorTicksPen().style()); group.writeEntry("MajorTicksWidth", this->majorTicksPen().widthF()); group.writeEntry("MajorTicksOpacity", this->majorTicksOpacity()); group.writeEntry("MajorTicksType", (int)this->majorTicksType()); //Minor ticks group.writeEntry("MinorGridOpacity", this->minorGridOpacity()); group.writeEntry("MinorGridColor",(QColor) this->minorGridPen().color()); group.writeEntry("MinorGridStyle", (int) this->minorGridPen().style()); group.writeEntry("MinorGridWidth", this->minorGridPen().widthF()); group.writeEntry("MinorTicksColor", (QColor) this->minorTicksPen().color()); group.writeEntry("MinorTicksLineStyle",( int) this->minorTicksPen().style()); group.writeEntry("MinorTicksWidth", this->minorTicksPen().widthF()); group.writeEntry("MinorTicksOpacity", this->minorTicksOpacity()); group.writeEntry("MinorTicksType", (int)this->minorTicksType()); - const QVector& childElements = children(AbstractAspect::IncludeHidden); + const QVector& childElements = children(AbstractAspect::ChildIndexFlag::IncludeHidden); childElements.at(0)->saveThemeConfig(config); } diff --git a/src/backend/worksheet/plots/cartesian/CartesianPlot.cpp b/src/backend/worksheet/plots/cartesian/CartesianPlot.cpp index 668d2f3c0..206262646 100644 --- a/src/backend/worksheet/plots/cartesian/CartesianPlot.cpp +++ b/src/backend/worksheet/plots/cartesian/CartesianPlot.cpp @@ -1,4171 +1,4171 @@ /*************************************************************************** File : CartesianPlot.cpp Project : LabPlot Description : Cartesian plot -------------------------------------------------------------------- Copyright : (C) 2011-2020 by Alexander Semke (alexander.semke@web.de) Copyright : (C) 2016-2018 by Stefan Gerlach (stefan.gerlach@uni.kn) 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 * * * ***************************************************************************/ #include "CartesianPlot.h" #include "CartesianPlotPrivate.h" #include "Axis.h" #include "XYCurve.h" #include "Histogram.h" #include "XYEquationCurve.h" #include "XYDataReductionCurve.h" #include "XYDifferentiationCurve.h" #include "XYIntegrationCurve.h" #include "XYInterpolationCurve.h" #include "XYSmoothCurve.h" #include "XYFitCurve.h" #include "XYFourierFilterCurve.h" #include "XYFourierTransformCurve.h" #include "XYConvolutionCurve.h" #include "XYCorrelationCurve.h" #include "backend/core/Project.h" #include "backend/core/datatypes/DateTime2StringFilter.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/worksheet/plots/cartesian/CartesianPlotLegend.h" #include "backend/worksheet/plots/cartesian/CustomPoint.h" #include "backend/worksheet/plots/cartesian/ReferenceLine.h" #include "backend/worksheet/plots/PlotArea.h" #include "backend/worksheet/plots/AbstractPlotPrivate.h" #include "backend/worksheet/Worksheet.h" #include "backend/worksheet/plots/cartesian/Axis.h" #include "backend/worksheet/Image.h" #include "backend/worksheet/TextLabel.h" #include "backend/lib/XmlStreamReader.h" #include "backend/lib/commandtemplates.h" #include "backend/lib/macros.h" #include "backend/lib/trace.h" #include "kdefrontend/spreadsheet/PlotDataDialog.h" //for PlotDataDialog::AnalysisAction. TODO: find a better place for this enum. #include "kdefrontend/ThemeHandler.h" #include "kdefrontend/widgets/ThemesWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include /** * \class CartesianPlot * \brief A xy-plot. * * */ CartesianPlot::CartesianPlot(const QString &name) : AbstractPlot(name, new CartesianPlotPrivate(this), AspectType::CartesianPlot) { init(); } CartesianPlot::CartesianPlot(const QString &name, CartesianPlotPrivate *dd) : AbstractPlot(name, dd, AspectType::CartesianPlot) { init(); } CartesianPlot::~CartesianPlot() { if (m_menusInitialized) { delete addNewMenu; delete zoomMenu; delete themeMenu; } delete m_coordinateSystem; //don't need to delete objects added with addChild() //no need to delete the d-pointer here - it inherits from QGraphicsItem //and is deleted during the cleanup in QGraphicsScene } /*! initializes all member variables of \c CartesianPlot */ void CartesianPlot::init() { Q_D(CartesianPlot); d->cSystem = new CartesianCoordinateSystem(this); m_coordinateSystem = d->cSystem; d->rangeType = CartesianPlot::RangeFree; d->xRangeFormat = CartesianPlot::Numeric; d->yRangeFormat = CartesianPlot::Numeric; d->xRangeDateTimeFormat = "yyyy-MM-dd hh:mm:ss"; d->yRangeDateTimeFormat = "yyyy-MM-dd hh:mm:ss"; d->rangeFirstValues = 1000; d->rangeLastValues = 1000; d->autoScaleX = true; d->autoScaleY = true; d->xScale = ScaleLinear; d->yScale = ScaleLinear; d->xRangeBreakingEnabled = false; d->yRangeBreakingEnabled = false; //the following factor determines the size of the offset between the min/max points of the curves //and the coordinate system ranges, when doing auto scaling //Factor 0 corresponds to the exact match - min/max values of the curves correspond to the start/end values of the ranges. //TODO: make this factor optional. //Provide in the UI the possibility to choose between "exact" or 0% offset, 2%, 5% and 10% for the auto fit option d->autoScaleOffsetFactor = 0.0f; m_plotArea = new PlotArea(name() + " plot area", this); addChildFast(m_plotArea); //Plot title m_title = new TextLabel(this->name() + QLatin1String("- ") + i18n("Title"), TextLabel::PlotTitle); addChild(m_title); m_title->setHidden(true); m_title->setParentGraphicsItem(m_plotArea->graphicsItem()); //offset between the plot area and the area defining the coordinate system, in scene units. d->horizontalPadding = Worksheet::convertToSceneUnits(1.5, Worksheet::Centimeter); d->verticalPadding = Worksheet::convertToSceneUnits(1.5, Worksheet::Centimeter); d->rightPadding = Worksheet::convertToSceneUnits(1.5, Worksheet::Centimeter); d->bottomPadding = Worksheet::convertToSceneUnits(1.5, Worksheet::Centimeter); d->symmetricPadding = true; connect(this, &AbstractAspect::aspectAdded, this, &CartesianPlot::childAdded); connect(this, &AbstractAspect::aspectRemoved, this, &CartesianPlot::childRemoved); graphicsItem()->setFlag(QGraphicsItem::ItemIsMovable, true); graphicsItem()->setFlag(QGraphicsItem::ItemClipsChildrenToShape, true); graphicsItem()->setFlag(QGraphicsItem::ItemIsSelectable, true); graphicsItem()->setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); graphicsItem()->setFlag(QGraphicsItem::ItemIsFocusable, true); //theme is not set at this point, initialize the color palette with default colors this->setColorPalette(KConfig()); } /*! initializes all children of \c CartesianPlot and setups a default plot of type \c type with a plot title. */ void CartesianPlot::initDefault(Type type) { Q_D(CartesianPlot); switch (type) { case FourAxes: { d->xMin = 0.0; d->xMax = 1.0; d->yMin = 0.0; d->yMax = 1.0; //Axes Axis* axis = new Axis("x axis 1", Axis::AxisHorizontal); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisBottom); axis->setStart(0); axis->setEnd(1); axis->setMajorTicksDirection(Axis::ticksIn); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksIn); axis->setMinorTicksNumber(1); QPen pen = axis->majorGridPen(); pen.setStyle(Qt::SolidLine); axis->setMajorGridPen(pen); pen = axis->minorGridPen(); pen.setStyle(Qt::DotLine); axis->setMinorGridPen(pen); axis->setSuppressRetransform(false); axis = new Axis("x axis 2", Axis::AxisHorizontal); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisTop); axis->setStart(0); axis->setEnd(1); axis->setMajorTicksDirection(Axis::ticksIn); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksIn); axis->setMinorTicksNumber(1); pen = axis->minorGridPen(); pen.setStyle(Qt::NoPen); axis->setMajorGridPen(pen); pen = axis->minorGridPen(); pen.setStyle(Qt::NoPen); axis->setMinorGridPen(pen); axis->setLabelsPosition(Axis::NoLabels); axis->title()->setText(QString()); axis->setSuppressRetransform(false); axis = new Axis("y axis 1", Axis::AxisVertical); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisLeft); axis->setStart(0); axis->setEnd(1); axis->setMajorTicksDirection(Axis::ticksIn); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksIn); axis->setMinorTicksNumber(1); pen = axis->majorGridPen(); pen.setStyle(Qt::SolidLine); axis->setMajorGridPen(pen); pen = axis->minorGridPen(); pen.setStyle(Qt::DotLine); axis->setMinorGridPen(pen); axis->setSuppressRetransform(false); axis = new Axis("y axis 2", Axis::AxisVertical); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisRight); axis->setStart(0); axis->setEnd(1); axis->setOffset(1); axis->setMajorTicksDirection(Axis::ticksIn); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksIn); axis->setMinorTicksNumber(1); pen = axis->minorGridPen(); pen.setStyle(Qt::NoPen); axis->setMajorGridPen(pen); pen = axis->minorGridPen(); pen.setStyle(Qt::NoPen); axis->setMinorGridPen(pen); axis->setLabelsPosition(Axis::NoLabels); axis->title()->setText(QString()); axis->setSuppressRetransform(false); break; } case TwoAxes: { d->xMin = 0.0; d->xMax = 1.0; d->yMin = 0.0; d->yMax = 1.0; Axis* axis = new Axis("x axis 1", Axis::AxisHorizontal); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisBottom); axis->setStart(0); axis->setEnd(1); axis->setMajorTicksDirection(Axis::ticksBoth); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksBoth); axis->setMinorTicksNumber(1); axis->setArrowType(Axis::FilledArrowSmall); axis->setSuppressRetransform(false); axis = new Axis("y axis 1", Axis::AxisVertical); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisLeft); axis->setStart(0); axis->setEnd(1); axis->setMajorTicksDirection(Axis::ticksBoth); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksBoth); axis->setMinorTicksNumber(1); axis->setArrowType(Axis::FilledArrowSmall); axis->setSuppressRetransform(false); break; } case TwoAxesCentered: { d->xMin = -0.5; d->xMax = 0.5; d->yMin = -0.5; d->yMax = 0.5; d->horizontalPadding = Worksheet::convertToSceneUnits(1.0, Worksheet::Centimeter); d->verticalPadding = Worksheet::convertToSceneUnits(1.0, Worksheet::Centimeter); QPen pen = m_plotArea->borderPen(); pen.setStyle(Qt::NoPen); m_plotArea->setBorderPen(pen); Axis* axis = new Axis("x axis 1", Axis::AxisHorizontal); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisCentered); axis->setStart(-0.5); axis->setEnd(0.5); axis->setMajorTicksDirection(Axis::ticksBoth); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksBoth); axis->setMinorTicksNumber(1); axis->setArrowType(Axis::FilledArrowSmall); axis->title()->setText(QString()); axis->setSuppressRetransform(false); axis = new Axis("y axis 1", Axis::AxisVertical); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisCentered); axis->setStart(-0.5); axis->setEnd(0.5); axis->setMajorTicksDirection(Axis::ticksBoth); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksBoth); axis->setMinorTicksNumber(1); axis->setArrowType(Axis::FilledArrowSmall); axis->title()->setText(QString()); axis->setSuppressRetransform(false); break; } case TwoAxesCenteredZero: { d->xMin = -0.5; d->xMax = 0.5; d->yMin = -0.5; d->yMax = 0.5; d->horizontalPadding = Worksheet::convertToSceneUnits(1.0, Worksheet::Centimeter); d->verticalPadding = Worksheet::convertToSceneUnits(1.0, Worksheet::Centimeter); QPen pen = m_plotArea->borderPen(); pen.setStyle(Qt::NoPen); m_plotArea->setBorderPen(pen); Axis* axis = new Axis("x axis 1", Axis::AxisHorizontal); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisCustom); axis->setOffset(0); axis->setStart(-0.5); axis->setEnd(0.5); axis->setMajorTicksDirection(Axis::ticksBoth); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksBoth); axis->setMinorTicksNumber(1); axis->setArrowType(Axis::FilledArrowSmall); axis->title()->setText(QString()); axis->setSuppressRetransform(false); axis = new Axis("y axis 1", Axis::AxisVertical); axis->setSuppressRetransform(true); addChild(axis); axis->setPosition(Axis::AxisCustom); axis->setOffset(0); axis->setStart(-0.5); axis->setEnd(0.5); axis->setMajorTicksDirection(Axis::ticksBoth); axis->setMajorTicksNumber(6); axis->setMinorTicksDirection(Axis::ticksBoth); axis->setMinorTicksNumber(1); axis->setArrowType(Axis::FilledArrowSmall); axis->title()->setText(QString()); axis->setSuppressRetransform(false); break; } } d->xMinPrev = d->xMin; d->xMaxPrev = d->xMax; d->yMinPrev = d->yMin; d->yMaxPrev = d->yMax; //Geometry, specify the plot rect in scene coordinates. //TODO: Use default settings for left, top, width, height and for min/max for the coordinate system float x = Worksheet::convertToSceneUnits(2, Worksheet::Centimeter); float y = Worksheet::convertToSceneUnits(2, Worksheet::Centimeter); float w = Worksheet::convertToSceneUnits(10, Worksheet::Centimeter); float h = Worksheet::convertToSceneUnits(10, Worksheet::Centimeter); //all plot children are initialized -> set the geometry of the plot in scene coordinates. d->rect = QRectF(x,y,w,h); d->retransform(); } void CartesianPlot::initActions() { //"add new" actions addCurveAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("xy-curve"), this); addHistogramAction = new QAction(QIcon::fromTheme("view-object-histogram-linear"), i18n("Histogram"), this); addEquationCurveAction = new QAction(QIcon::fromTheme("labplot-xy-equation-curve"), i18n("xy-curve from a mathematical Equation"), this); // no icons yet addDataReductionCurveAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Data Reduction"), this); addDifferentiationCurveAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Differentiation"), this); addIntegrationCurveAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Integration"), this); addInterpolationCurveAction = new QAction(QIcon::fromTheme("labplot-xy-interpolation-curve"), i18n("Interpolation"), this); addSmoothCurveAction = new QAction(QIcon::fromTheme("labplot-xy-smoothing-curve"), i18n("Smooth"), this); addFitCurveAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Fit"), this); addFourierFilterCurveAction = new QAction(QIcon::fromTheme("labplot-xy-fourier-filter-curve"), i18n("Fourier Filter"), this); addFourierTransformCurveAction = new QAction(QIcon::fromTheme("labplot-xy-fourier-transform-curve"), i18n("Fourier Transform"), this); addConvolutionCurveAction = new QAction(QIcon::fromTheme("labplot-xy-curve"),i18n("(De-)Convolution"), this); addCorrelationCurveAction = new QAction(QIcon::fromTheme("labplot-xy-curve"),i18n("Auto-/Cross-Correlation"), this); addLegendAction = new QAction(QIcon::fromTheme("text-field"), i18n("Legend"), this); if (children().size()>0) addLegendAction->setEnabled(false); //only one legend is allowed -> disable the action addHorizontalAxisAction = new QAction(QIcon::fromTheme("labplot-axis-horizontal"), i18n("Horizontal Axis"), this); addVerticalAxisAction = new QAction(QIcon::fromTheme("labplot-axis-vertical"), i18n("Vertical Axis"), this); addTextLabelAction = new QAction(QIcon::fromTheme("draw-text"), i18n("Text Label"), this); addImageAction = new QAction(QIcon::fromTheme("viewimage"), i18n("Image"), this); addCustomPointAction = new QAction(QIcon::fromTheme("draw-cross"), i18n("Custom Point"), this); addReferenceLineAction = new QAction(QIcon::fromTheme("draw-line"), i18n("Reference Line"), this); connect(addCurveAction, &QAction::triggered, this, &CartesianPlot::addCurve); connect(addHistogramAction,&QAction::triggered, this, &CartesianPlot::addHistogram); connect(addEquationCurveAction, &QAction::triggered, this, &CartesianPlot::addEquationCurve); connect(addDataReductionCurveAction, &QAction::triggered, this, &CartesianPlot::addDataReductionCurve); connect(addDifferentiationCurveAction, &QAction::triggered, this, &CartesianPlot::addDifferentiationCurve); connect(addIntegrationCurveAction, &QAction::triggered, this, &CartesianPlot::addIntegrationCurve); connect(addInterpolationCurveAction, &QAction::triggered, this, &CartesianPlot::addInterpolationCurve); connect(addSmoothCurveAction, &QAction::triggered, this, &CartesianPlot::addSmoothCurve); connect(addFitCurveAction, &QAction::triggered, this, &CartesianPlot::addFitCurve); connect(addFourierFilterCurveAction, &QAction::triggered, this, &CartesianPlot::addFourierFilterCurve); connect(addFourierTransformCurveAction, &QAction::triggered, this, &CartesianPlot::addFourierTransformCurve); connect(addConvolutionCurveAction, &QAction::triggered, this, &CartesianPlot::addConvolutionCurve); connect(addCorrelationCurveAction, &QAction::triggered, this, &CartesianPlot::addCorrelationCurve); connect(addLegendAction, &QAction::triggered, this, static_cast(&CartesianPlot::addLegend)); connect(addHorizontalAxisAction, &QAction::triggered, this, &CartesianPlot::addHorizontalAxis); connect(addVerticalAxisAction, &QAction::triggered, this, &CartesianPlot::addVerticalAxis); connect(addTextLabelAction, &QAction::triggered, this, &CartesianPlot::addTextLabel); connect(addImageAction, &QAction::triggered, this, &CartesianPlot::addImage); connect(addCustomPointAction, &QAction::triggered, this, &CartesianPlot::addCustomPoint); connect(addReferenceLineAction, &QAction::triggered, this, &CartesianPlot::addReferenceLine); //Analysis menu actions // addDataOperationAction = new QAction(i18n("Data Operation"), this); addDataReductionAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Data Reduction"), this); addDifferentiationAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Differentiate"), this); addIntegrationAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Integrate"), this); addInterpolationAction = new QAction(QIcon::fromTheme("labplot-xy-interpolation-curve"), i18n("Interpolate"), this); addSmoothAction = new QAction(QIcon::fromTheme("labplot-xy-smoothing-curve"), i18n("Smooth"), this); addConvolutionAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Convolute/Deconvolute"), this); addCorrelationAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Auto-/Cross-Correlation"), this); QAction* fitAction = new QAction(i18n("Linear"), this); fitAction->setData(PlotDataDialog::FitLinear); addFitAction.append(fitAction); fitAction = new QAction(i18n("Power"), this); fitAction->setData(PlotDataDialog::FitPower); addFitAction.append(fitAction); fitAction = new QAction(i18n("Exponential (degree 1)"), this); fitAction->setData(PlotDataDialog::FitExp1); addFitAction.append(fitAction); fitAction = new QAction(i18n("Exponential (degree 2)"), this); fitAction->setData(PlotDataDialog::FitExp2); addFitAction.append(fitAction); fitAction = new QAction(i18n("Inverse exponential"), this); fitAction->setData(PlotDataDialog::FitInvExp); addFitAction.append(fitAction); fitAction = new QAction(i18n("Gauss"), this); fitAction->setData(PlotDataDialog::FitGauss); addFitAction.append(fitAction); fitAction = new QAction(i18n("Cauchy-Lorentz"), this); fitAction->setData(PlotDataDialog::FitCauchyLorentz); addFitAction.append(fitAction); fitAction = new QAction(i18n("Arc Tangent"), this); fitAction->setData(PlotDataDialog::FitTan); addFitAction.append(fitAction); fitAction = new QAction(i18n("Hyperbolic Tangent"), this); fitAction->setData(PlotDataDialog::FitTanh); addFitAction.append(fitAction); fitAction = new QAction(i18n("Error Function"), this); fitAction->setData(PlotDataDialog::FitErrFunc); addFitAction.append(fitAction); fitAction = new QAction(i18n("Custom"), this); fitAction->setData(PlotDataDialog::FitCustom); addFitAction.append(fitAction); addFourierFilterAction = new QAction(QIcon::fromTheme("labplot-xy-fourier-filter-curve"), i18n("Fourier Filter"), this); addFourierTransformAction = new QAction(QIcon::fromTheme("labplot-xy-fourier-transform-curve"), i18n("Fourier Transform"), this); connect(addDataReductionAction, &QAction::triggered, this, &CartesianPlot::addDataReductionCurve); connect(addDifferentiationAction, &QAction::triggered, this, &CartesianPlot::addDifferentiationCurve); connect(addIntegrationAction, &QAction::triggered, this, &CartesianPlot::addIntegrationCurve); connect(addInterpolationAction, &QAction::triggered, this, &CartesianPlot::addInterpolationCurve); connect(addSmoothAction, &QAction::triggered, this, &CartesianPlot::addSmoothCurve); connect(addConvolutionAction, &QAction::triggered, this, &CartesianPlot::addConvolutionCurve); connect(addCorrelationAction, &QAction::triggered, this, &CartesianPlot::addCorrelationCurve); for (const auto& action : addFitAction) connect(action, &QAction::triggered, this, &CartesianPlot::addFitCurve); connect(addFourierFilterAction, &QAction::triggered, this, &CartesianPlot::addFourierFilterCurve); connect(addFourierTransformAction, &QAction::triggered, this, &CartesianPlot::addFourierTransformCurve); //zoom/navigate actions scaleAutoAction = new QAction(QIcon::fromTheme("labplot-auto-scale-all"), i18n("Auto Scale"), this); scaleAutoXAction = new QAction(QIcon::fromTheme("labplot-auto-scale-x"), i18n("Auto Scale X"), this); scaleAutoYAction = new QAction(QIcon::fromTheme("labplot-auto-scale-y"), i18n("Auto Scale Y"), this); zoomInAction = new QAction(QIcon::fromTheme("zoom-in"), i18n("Zoom In"), this); zoomOutAction = new QAction(QIcon::fromTheme("zoom-out"), i18n("Zoom Out"), this); zoomInXAction = new QAction(QIcon::fromTheme("labplot-zoom-in-x"), i18n("Zoom In X"), this); zoomOutXAction = new QAction(QIcon::fromTheme("labplot-zoom-out-x"), i18n("Zoom Out X"), this); zoomInYAction = new QAction(QIcon::fromTheme("labplot-zoom-in-y"), i18n("Zoom In Y"), this); zoomOutYAction = new QAction(QIcon::fromTheme("labplot-zoom-out-y"), i18n("Zoom Out Y"), this); shiftLeftXAction = new QAction(QIcon::fromTheme("labplot-shift-left-x"), i18n("Shift Left X"), this); shiftRightXAction = new QAction(QIcon::fromTheme("labplot-shift-right-x"), i18n("Shift Right X"), this); shiftUpYAction = new QAction(QIcon::fromTheme("labplot-shift-up-y"), i18n("Shift Up Y"), this); shiftDownYAction = new QAction(QIcon::fromTheme("labplot-shift-down-y"), i18n("Shift Down Y"), this); connect(scaleAutoAction, &QAction::triggered, this, &CartesianPlot::scaleAutoTriggered); connect(scaleAutoXAction, &QAction::triggered, this, &CartesianPlot::scaleAutoTriggered); connect(scaleAutoYAction, &QAction::triggered, this, &CartesianPlot::scaleAutoTriggered); connect(zoomInAction, &QAction::triggered, this, &CartesianPlot::zoomIn); connect(zoomOutAction, &QAction::triggered, this, &CartesianPlot::zoomOut); connect(zoomInXAction, &QAction::triggered, this, &CartesianPlot::zoomInX); connect(zoomOutXAction, &QAction::triggered, this, &CartesianPlot::zoomOutX); connect(zoomInYAction, &QAction::triggered, this, &CartesianPlot::zoomInY); connect(zoomOutYAction, &QAction::triggered, this, &CartesianPlot::zoomOutY); connect(shiftLeftXAction, &QAction::triggered, this, &CartesianPlot::shiftLeftX); connect(shiftRightXAction, &QAction::triggered, this, &CartesianPlot::shiftRightX); connect(shiftUpYAction, &QAction::triggered, this, &CartesianPlot::shiftUpY); connect(shiftDownYAction, &QAction::triggered, this, &CartesianPlot::shiftDownY); //visibility action visibilityAction = new QAction(QIcon::fromTheme("view-visible"), i18n("Visible"), this); visibilityAction->setCheckable(true); connect(visibilityAction, &QAction::triggered, this, &CartesianPlot::visibilityChanged); } void CartesianPlot::initMenus() { initActions(); addNewMenu = new QMenu(i18n("Add New")); addNewMenu->setIcon(QIcon::fromTheme("list-add")); addNewMenu->addAction(addCurveAction); addNewMenu->addAction(addHistogramAction); addNewMenu->addAction(addEquationCurveAction); addNewMenu->addSeparator(); addNewAnalysisMenu = new QMenu(i18n("Analysis Curve")); addNewAnalysisMenu->addAction(addFitCurveAction); addNewAnalysisMenu->addSeparator(); addNewAnalysisMenu->addAction(addDifferentiationCurveAction); addNewAnalysisMenu->addAction(addIntegrationCurveAction); addNewAnalysisMenu->addSeparator(); addNewAnalysisMenu->addAction(addInterpolationCurveAction); addNewAnalysisMenu->addAction(addSmoothCurveAction); addNewAnalysisMenu->addSeparator(); addNewAnalysisMenu->addAction(addFourierFilterCurveAction); addNewAnalysisMenu->addAction(addFourierTransformCurveAction); addNewAnalysisMenu->addSeparator(); addNewAnalysisMenu->addAction(addConvolutionCurveAction); addNewAnalysisMenu->addAction(addCorrelationCurveAction); addNewAnalysisMenu->addSeparator(); addNewAnalysisMenu->addAction(addDataReductionCurveAction); addNewMenu->addMenu(addNewAnalysisMenu); addNewMenu->addSeparator(); addNewMenu->addAction(addLegendAction); addNewMenu->addSeparator(); addNewMenu->addAction(addHorizontalAxisAction); addNewMenu->addAction(addVerticalAxisAction); addNewMenu->addSeparator(); addNewMenu->addAction(addTextLabelAction); addNewMenu->addAction(addImageAction); addNewMenu->addSeparator(); addNewMenu->addAction(addCustomPointAction); addNewMenu->addAction(addReferenceLineAction); zoomMenu = new QMenu(i18n("Zoom/Navigate")); zoomMenu->setIcon(QIcon::fromTheme("zoom-draw")); zoomMenu->addAction(scaleAutoAction); zoomMenu->addAction(scaleAutoXAction); zoomMenu->addAction(scaleAutoYAction); zoomMenu->addSeparator(); zoomMenu->addAction(zoomInAction); zoomMenu->addAction(zoomOutAction); zoomMenu->addSeparator(); zoomMenu->addAction(zoomInXAction); zoomMenu->addAction(zoomOutXAction); zoomMenu->addSeparator(); zoomMenu->addAction(zoomInYAction); zoomMenu->addAction(zoomOutYAction); zoomMenu->addSeparator(); zoomMenu->addAction(shiftLeftXAction); zoomMenu->addAction(shiftRightXAction); zoomMenu->addSeparator(); zoomMenu->addAction(shiftUpYAction); zoomMenu->addAction(shiftDownYAction); // Data manipulation menu // QMenu* dataManipulationMenu = new QMenu(i18n("Data Manipulation")); // dataManipulationMenu->setIcon(QIcon::fromTheme("zoom-draw")); // dataManipulationMenu->addAction(addDataOperationAction); // dataManipulationMenu->addAction(addDataReductionAction); // Data fit menu QMenu* dataFitMenu = new QMenu(i18n("Fit")); dataFitMenu->setIcon(QIcon::fromTheme("labplot-xy-fit-curve")); dataFitMenu->addAction(addFitAction.at(0)); dataFitMenu->addAction(addFitAction.at(1)); dataFitMenu->addAction(addFitAction.at(2)); dataFitMenu->addAction(addFitAction.at(3)); dataFitMenu->addAction(addFitAction.at(4)); dataFitMenu->addSeparator(); dataFitMenu->addAction(addFitAction.at(5)); dataFitMenu->addAction(addFitAction.at(6)); dataFitMenu->addSeparator(); dataFitMenu->addAction(addFitAction.at(7)); dataFitMenu->addAction(addFitAction.at(8)); dataFitMenu->addAction(addFitAction.at(9)); dataFitMenu->addSeparator(); dataFitMenu->addAction(addFitAction.at(10)); //analysis menu dataAnalysisMenu = new QMenu(i18n("Analysis")); dataAnalysisMenu->addMenu(dataFitMenu); dataAnalysisMenu->addSeparator(); dataAnalysisMenu->addAction(addDifferentiationAction); dataAnalysisMenu->addAction(addIntegrationAction); dataAnalysisMenu->addSeparator(); dataAnalysisMenu->addAction(addInterpolationAction); dataAnalysisMenu->addAction(addSmoothAction); dataAnalysisMenu->addSeparator(); dataAnalysisMenu->addAction(addFourierFilterAction); dataAnalysisMenu->addAction(addFourierTransformAction); dataAnalysisMenu->addSeparator(); dataAnalysisMenu->addAction(addConvolutionAction); dataAnalysisMenu->addAction(addCorrelationAction); dataAnalysisMenu->addSeparator(); // dataAnalysisMenu->insertMenu(nullptr, dataManipulationMenu); dataAnalysisMenu->addAction(addDataReductionAction); //themes menu themeMenu = new QMenu(i18n("Apply Theme")); themeMenu->setIcon(QIcon::fromTheme("color-management")); auto* themeWidget = new ThemesWidget(nullptr); connect(themeWidget, &ThemesWidget::themeSelected, this, &CartesianPlot::loadTheme); connect(themeWidget, &ThemesWidget::themeSelected, themeMenu, &QMenu::close); auto* widgetAction = new QWidgetAction(this); widgetAction->setDefaultWidget(themeWidget); themeMenu->addAction(widgetAction); m_menusInitialized = true; } QMenu* CartesianPlot::createContextMenu() { if (!m_menusInitialized) initMenus(); QMenu* menu = WorksheetElement::createContextMenu(); QAction* firstAction = menu->actions().at(1); menu->insertMenu(firstAction, addNewMenu); menu->insertSeparator(firstAction); menu->insertMenu(firstAction, zoomMenu); menu->insertSeparator(firstAction); menu->insertMenu(firstAction, themeMenu); menu->insertSeparator(firstAction); visibilityAction->setChecked(isVisible()); menu->insertAction(firstAction, visibilityAction); return menu; } QMenu* CartesianPlot::analysisMenu() { if (!m_menusInitialized) initMenus(); return dataAnalysisMenu; } /*! Returns an icon to be used in the project explorer. */ QIcon CartesianPlot::icon() const { return QIcon::fromTheme("office-chart-line"); } QVector CartesianPlot::dependsOn() const { //aspects which the plotted data in the worksheet depends on (spreadsheets and later matrices) QVector aspects; for (const auto* curve : children()) { if (curve->xColumn() && dynamic_cast(curve->xColumn()->parentAspect()) ) aspects << curve->xColumn()->parentAspect(); if (curve->yColumn() && dynamic_cast(curve->yColumn()->parentAspect()) ) aspects << curve->yColumn()->parentAspect(); } return aspects; } void CartesianPlot::navigate(CartesianPlot::NavigationOperation op) { Q_D(CartesianPlot); if (op == ScaleAuto) { if (d->curvesXMinMaxIsDirty || d->curvesYMinMaxIsDirty || !autoScaleX() || !autoScaleY()) { d->curvesXMinMaxIsDirty = true; d->curvesYMinMaxIsDirty = true; } scaleAuto(); } else if (op == ScaleAutoX) setAutoScaleX(true); else if (op == ScaleAutoY) setAutoScaleY(true); else if (op == ZoomIn) zoomIn(); else if (op == ZoomOut) zoomOut(); else if (op == ZoomInX) zoomInX(); else if (op == ZoomOutX) zoomOutX(); else if (op == ZoomInY) zoomInY(); else if (op == ZoomOutY) zoomOutY(); else if (op == ShiftLeftX) shiftLeftX(); else if (op == ShiftRightX) shiftRightX(); else if (op == ShiftUpY) shiftUpY(); else if (op == ShiftDownY) shiftDownY(); } void CartesianPlot::setSuppressDataChangedSignal(bool value) { Q_D(CartesianPlot); d->suppressRetransform = value; } void CartesianPlot::processDropEvent(QDropEvent* event) { PERFTRACE("CartesianPlot::processDropEvent"); const QMimeData* mimeData = event->mimeData(); if (!mimeData) return; //deserialize the mime data to the vector of aspect pointers QByteArray data = mimeData->data(QLatin1String("labplot-dnd")); QVector vec; QDataStream stream(&data, QIODevice::ReadOnly); stream >> vec; QVector columns; for (auto a : vec) { auto* aspect = (AbstractAspect*)a; auto* column = dynamic_cast(aspect); if (column) columns << column; } //return if there are no columns being dropped. //TODO: extend this later when we allow to drag&drop plots, etc. if (columns.isEmpty()) return; //determine the first column with "x plot designation" as the x-data column for all curves to be created const AbstractColumn* xColumn = nullptr; for (const auto* column : columns) { if (column->plotDesignation() == AbstractColumn::X) { xColumn = column; break; } } //if no column with "x plot designation" is available, use the x-data column of the first curve in the plot, if (xColumn == nullptr) { QVector curves = children(); if (!curves.isEmpty()) xColumn = curves.at(0)->xColumn(); } //use the first dropped column if no column with "x plot designation" nor curves are available if (xColumn == nullptr) xColumn = columns.at(0); //create curves bool curvesAdded = false; for (const auto* column : columns) { if (column == xColumn) continue; XYCurve* curve = new XYCurve(column->name()); curve->suppressRetransform(true); //suppress retransform, all curved will be recalculated at the end curve->setXColumn(xColumn); curve->setYColumn(column); addChild(curve); curve->suppressRetransform(false); curvesAdded = true; } if (curvesAdded) dataChanged(); } bool CartesianPlot::isPanningActive() const { Q_D(const CartesianPlot); return d->panningStarted; } bool CartesianPlot::isHovered() const { Q_D(const CartesianPlot); return d->m_hovered; } bool CartesianPlot::isPrinted() const { Q_D(const CartesianPlot); return d->m_printing; } bool CartesianPlot::isSelected() const { Q_D(const CartesianPlot); return d->isSelected(); } //############################################################################## //################################ getter methods ############################ //############################################################################## BASIC_SHARED_D_READER_IMPL(CartesianPlot, CartesianPlot::RangeType, rangeType, rangeType) BASIC_SHARED_D_READER_IMPL(CartesianPlot, CartesianPlot::RangeFormat, xRangeFormat, xRangeFormat) BASIC_SHARED_D_READER_IMPL(CartesianPlot, CartesianPlot::RangeFormat, yRangeFormat, yRangeFormat) BASIC_SHARED_D_READER_IMPL(CartesianPlot, int, rangeLastValues, rangeLastValues) BASIC_SHARED_D_READER_IMPL(CartesianPlot, int, rangeFirstValues, rangeFirstValues) BASIC_SHARED_D_READER_IMPL(CartesianPlot, bool, autoScaleX, autoScaleX) BASIC_SHARED_D_READER_IMPL(CartesianPlot, double, xMin, xMin) BASIC_SHARED_D_READER_IMPL(CartesianPlot, double, xMax, xMax) BASIC_SHARED_D_READER_IMPL(CartesianPlot, CartesianPlot::Scale, xScale, xScale) BASIC_SHARED_D_READER_IMPL(CartesianPlot, bool, xRangeBreakingEnabled, xRangeBreakingEnabled) CLASS_SHARED_D_READER_IMPL(CartesianPlot, CartesianPlot::RangeBreaks, xRangeBreaks, xRangeBreaks) BASIC_SHARED_D_READER_IMPL(CartesianPlot, bool, autoScaleY, autoScaleY) BASIC_SHARED_D_READER_IMPL(CartesianPlot, double, yMin, yMin) BASIC_SHARED_D_READER_IMPL(CartesianPlot, double, yMax, yMax) BASIC_SHARED_D_READER_IMPL(CartesianPlot, CartesianPlot::Scale, yScale, yScale) BASIC_SHARED_D_READER_IMPL(CartesianPlot, bool, yRangeBreakingEnabled, yRangeBreakingEnabled) CLASS_SHARED_D_READER_IMPL(CartesianPlot, CartesianPlot::RangeBreaks, yRangeBreaks, yRangeBreaks) CLASS_SHARED_D_READER_IMPL(CartesianPlot, QPen, cursorPen, cursorPen); CLASS_SHARED_D_READER_IMPL(CartesianPlot, bool, cursor0Enable, cursor0Enable); CLASS_SHARED_D_READER_IMPL(CartesianPlot, bool, cursor1Enable, cursor1Enable); CLASS_SHARED_D_READER_IMPL(CartesianPlot, QString, theme, theme) /*! returns the actual bounding rectangular of the plot area showing data (plot's rectangular minus padding) in plot's coordinates */ QRectF CartesianPlot::dataRect() const { Q_D(const CartesianPlot); return d->dataRect; } CartesianPlot::MouseMode CartesianPlot::mouseMode() const { Q_D(const CartesianPlot); return d->mouseMode; } const QString& CartesianPlot::xRangeDateTimeFormat() const { Q_D(const CartesianPlot); return d->xRangeDateTimeFormat; } const QString& CartesianPlot::yRangeDateTimeFormat() const { Q_D(const CartesianPlot); return d->yRangeDateTimeFormat; } //############################################################################## //###################### setter methods and undo commands #################### //############################################################################## /*! set the rectangular, defined in scene coordinates */ class CartesianPlotSetRectCmd : public QUndoCommand { public: CartesianPlotSetRectCmd(CartesianPlotPrivate* private_obj, QRectF rect) : m_private(private_obj), m_rect(rect) { setText(i18n("%1: change geometry rect", m_private->name())); }; void redo() override { // const double horizontalRatio = m_rect.width() / m_private->rect.width(); // const double verticalRatio = m_rect.height() / m_private->rect.height(); qSwap(m_private->rect, m_rect); // m_private->q->handleResize(horizontalRatio, verticalRatio, false); m_private->retransform(); emit m_private->q->rectChanged(m_private->rect); }; void undo() override { redo(); } private: CartesianPlotPrivate* m_private; QRectF m_rect; }; void CartesianPlot::setRect(const QRectF& rect) { Q_D(CartesianPlot); if (rect != d->rect) exec(new CartesianPlotSetRectCmd(d, rect)); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetRangeType, CartesianPlot::RangeType, rangeType, rangeChanged); void CartesianPlot::setRangeType(RangeType type) { Q_D(CartesianPlot); if (type != d->rangeType) exec(new CartesianPlotSetRangeTypeCmd(d, type, ki18n("%1: set range type"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetXRangeFormat, CartesianPlot::RangeFormat, xRangeFormat, xRangeFormatChanged); void CartesianPlot::setXRangeFormat(RangeFormat format) { Q_D(CartesianPlot); if (format != d->xRangeFormat) exec(new CartesianPlotSetXRangeFormatCmd(d, format, ki18n("%1: set x-range format"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetYRangeFormat, CartesianPlot::RangeFormat, yRangeFormat, yRangeFormatChanged); void CartesianPlot::setYRangeFormat(RangeFormat format) { Q_D(CartesianPlot); if (format != d->yRangeFormat) exec(new CartesianPlotSetYRangeFormatCmd(d, format, ki18n("%1: set y-range format"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetRangeLastValues, int, rangeLastValues, rangeChanged); void CartesianPlot::setRangeLastValues(int values) { Q_D(CartesianPlot); if (values != d->rangeLastValues) exec(new CartesianPlotSetRangeLastValuesCmd(d, values, ki18n("%1: set range"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetRangeFirstValues, int, rangeFirstValues, rangeChanged); void CartesianPlot::setRangeFirstValues(int values) { Q_D(CartesianPlot); if (values != d->rangeFirstValues) exec(new CartesianPlotSetRangeFirstValuesCmd(d, values, ki18n("%1: set range"))); } class CartesianPlotSetAutoScaleXCmd : public QUndoCommand { public: CartesianPlotSetAutoScaleXCmd(CartesianPlotPrivate* private_obj, bool autoScale) : m_private(private_obj), m_autoScale(autoScale), m_autoScaleOld(false), m_minOld(0.0), m_maxOld(0.0) { setText(i18n("%1: change x-range auto scaling", m_private->name())); }; void redo() override { m_autoScaleOld = m_private->autoScaleX; if (m_autoScale) { m_minOld = m_private->xMin; m_maxOld = m_private->xMax; m_private->q->scaleAutoX(); } m_private->autoScaleX = m_autoScale; emit m_private->q->xAutoScaleChanged(m_autoScale); }; void undo() override { if (!m_autoScaleOld) { m_private->xMin = m_minOld; m_private->xMax = m_maxOld; m_private->retransformScales(); } m_private->autoScaleX = m_autoScaleOld; emit m_private->q->xAutoScaleChanged(m_autoScaleOld); } private: CartesianPlotPrivate* m_private; bool m_autoScale; bool m_autoScaleOld; double m_minOld; double m_maxOld; }; void CartesianPlot::setAutoScaleX(bool autoScaleX) { Q_D(CartesianPlot); if (autoScaleX != d->autoScaleX) exec(new CartesianPlotSetAutoScaleXCmd(d, autoScaleX)); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetXMin, double, xMin, retransformScales) void CartesianPlot::setXMin(double xMin) { Q_D(CartesianPlot); if (xMin != d->xMin && xMin != -INFINITY && xMin != INFINITY) { d->curvesYMinMaxIsDirty = true; exec(new CartesianPlotSetXMinCmd(d, xMin, ki18n("%1: set min x"))); if (d->autoScaleY) scaleAutoY(); } } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetXMax, double, xMax, retransformScales) void CartesianPlot::setXMax(double xMax) { Q_D(CartesianPlot); if (xMax != d->xMax && xMax != -INFINITY && xMax != INFINITY) { d->curvesYMinMaxIsDirty = true; exec(new CartesianPlotSetXMaxCmd(d, xMax, ki18n("%1: set max x"))); if (d->autoScaleY) scaleAutoY(); } } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetXScale, CartesianPlot::Scale, xScale, retransformScales) void CartesianPlot::setXScale(Scale scale) { Q_D(CartesianPlot); if (scale != d->xScale) exec(new CartesianPlotSetXScaleCmd(d, scale, ki18n("%1: set x scale"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetXRangeBreakingEnabled, bool, xRangeBreakingEnabled, retransformScales) void CartesianPlot::setXRangeBreakingEnabled(bool enabled) { Q_D(CartesianPlot); if (enabled != d->xRangeBreakingEnabled) exec(new CartesianPlotSetXRangeBreakingEnabledCmd(d, enabled, ki18n("%1: x-range breaking enabled"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetXRangeBreaks, CartesianPlot::RangeBreaks, xRangeBreaks, retransformScales) void CartesianPlot::setXRangeBreaks(const RangeBreaks& breakings) { Q_D(CartesianPlot); exec(new CartesianPlotSetXRangeBreaksCmd(d, breakings, ki18n("%1: x-range breaks changed"))); } class CartesianPlotSetAutoScaleYCmd : public QUndoCommand { public: CartesianPlotSetAutoScaleYCmd(CartesianPlotPrivate* private_obj, bool autoScale) : m_private(private_obj), m_autoScale(autoScale), m_autoScaleOld(false), m_minOld(0.0), m_maxOld(0.0) { setText(i18n("%1: change y-range auto scaling", m_private->name())); }; void redo() override { m_autoScaleOld = m_private->autoScaleY; if (m_autoScale) { m_minOld = m_private->yMin; m_maxOld = m_private->yMax; m_private->q->scaleAutoY(); } m_private->autoScaleY = m_autoScale; emit m_private->q->yAutoScaleChanged(m_autoScale); }; void undo() override { if (!m_autoScaleOld) { m_private->yMin = m_minOld; m_private->yMax = m_maxOld; m_private->retransformScales(); } m_private->autoScaleY = m_autoScaleOld; emit m_private->q->yAutoScaleChanged(m_autoScaleOld); } private: CartesianPlotPrivate* m_private; bool m_autoScale; bool m_autoScaleOld; double m_minOld; double m_maxOld; }; void CartesianPlot::setAutoScaleY(bool autoScaleY) { Q_D(CartesianPlot); if (autoScaleY != d->autoScaleY) exec(new CartesianPlotSetAutoScaleYCmd(d, autoScaleY)); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetYMin, double, yMin, retransformScales) void CartesianPlot::setYMin(double yMin) { Q_D(CartesianPlot); if (yMin != d->yMin) { d->curvesXMinMaxIsDirty = true; exec(new CartesianPlotSetYMinCmd(d, yMin, ki18n("%1: set min y"))); if (d->autoScaleX) scaleAutoX(); } } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetYMax, double, yMax, retransformScales) void CartesianPlot::setYMax(double yMax) { Q_D(CartesianPlot); if (yMax != d->yMax) { d->curvesXMinMaxIsDirty = true; exec(new CartesianPlotSetYMaxCmd(d, yMax, ki18n("%1: set max y"))); if (d->autoScaleX) scaleAutoX(); } } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetYScale, CartesianPlot::Scale, yScale, retransformScales) void CartesianPlot::setYScale(Scale scale) { Q_D(CartesianPlot); if (scale != d->yScale) exec(new CartesianPlotSetYScaleCmd(d, scale, ki18n("%1: set y scale"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetYRangeBreakingEnabled, bool, yRangeBreakingEnabled, retransformScales) void CartesianPlot::setYRangeBreakingEnabled(bool enabled) { Q_D(CartesianPlot); if (enabled != d->yRangeBreakingEnabled) exec(new CartesianPlotSetYRangeBreakingEnabledCmd(d, enabled, ki18n("%1: y-range breaking enabled"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetYRangeBreaks, CartesianPlot::RangeBreaks, yRangeBreaks, retransformScales) void CartesianPlot::setYRangeBreaks(const RangeBreaks& breaks) { Q_D(CartesianPlot); exec(new CartesianPlotSetYRangeBreaksCmd(d, breaks, ki18n("%1: y-range breaks changed"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetCursorPen, QPen, cursorPen, update) void CartesianPlot::setCursorPen(const QPen &pen) { Q_D(CartesianPlot); if (pen != d->cursorPen) exec(new CartesianPlotSetCursorPenCmd(d, pen, ki18n("%1: y-range breaks changed"))); } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetCursor0Enable, bool, cursor0Enable, updateCursor) void CartesianPlot::setCursor0Enable(const bool &enable) { Q_D(CartesianPlot); if (enable != d->cursor0Enable) { if (std::isnan(d->cursor0Pos.x())) { // if never set, set initial position d->cursor0Pos.setX(d->cSystem->mapSceneToLogical(QPointF(0,0)).x()); mousePressCursorModeSignal(0, d->cursor0Pos); // simulate mousePress to update values in the cursor dock } exec(new CartesianPlotSetCursor0EnableCmd(d, enable, ki18n("%1: Cursor0 enable"))); } } STD_SETTER_CMD_IMPL_F_S(CartesianPlot, SetCursor1Enable, bool, cursor1Enable, updateCursor) void CartesianPlot::setCursor1Enable(const bool &enable) { Q_D(CartesianPlot); if (enable != d->cursor1Enable) { if (std::isnan(d->cursor1Pos.x())) { // if never set, set initial position d->cursor1Pos.setX(d->cSystem->mapSceneToLogical(QPointF(0,0)).x()); mousePressCursorModeSignal(1, d->cursor1Pos); // simulate mousePress to update values in the cursor dock } exec(new CartesianPlotSetCursor1EnableCmd(d, enable, ki18n("%1: Cursor1 enable"))); } } STD_SETTER_CMD_IMPL_S(CartesianPlot, SetTheme, QString, theme) void CartesianPlot::setTheme(const QString& theme) { Q_D(CartesianPlot); if (theme != d->theme) { if (!theme.isEmpty()) { beginMacro( i18n("%1: load theme %2", name(), theme) ); exec(new CartesianPlotSetThemeCmd(d, theme, ki18n("%1: set theme"))); loadTheme(theme); endMacro(); } else exec(new CartesianPlotSetThemeCmd(d, theme, ki18n("%1: disable theming"))); } } //################################################################ //########################## Slots ############################### //################################################################ void CartesianPlot::addHorizontalAxis() { Axis* axis = new Axis("x-axis", Axis::AxisHorizontal); if (axis->autoScale()) { axis->setUndoAware(false); axis->setStart(xMin()); axis->setEnd(xMax()); axis->setUndoAware(true); } addChild(axis); } void CartesianPlot::addVerticalAxis() { Axis* axis = new Axis("y-axis", Axis::AxisVertical); if (axis->autoScale()) { axis->setUndoAware(false); axis->setStart(yMin()); axis->setEnd(yMax()); axis->setUndoAware(true); } addChild(axis); } void CartesianPlot::addCurve() { addChild(new XYCurve("xy-curve")); } void CartesianPlot::addEquationCurve() { addChild(new XYEquationCurve("f(x)")); } void CartesianPlot::addHistogram() { addChild(new Histogram("Histogram")); } /*! * returns the first selected XYCurve in the plot */ const XYCurve* CartesianPlot::currentCurve() const { for (const auto* curve : this->children()) { if (curve->graphicsItem()->isSelected()) return curve; } return nullptr; } void CartesianPlot::addDataReductionCurve() { XYDataReductionCurve* curve = new XYDataReductionCurve("Data reduction"); const XYCurve* curCurve = currentCurve(); if (curCurve) { beginMacro( i18n("%1: reduce '%2'", name(), curCurve->name()) ); curve->setName( i18n("Reduction of '%1'", curCurve->name()) ); curve->setDataSourceType(XYAnalysisCurve::DataSourceCurve); curve->setDataSourceCurve(curCurve); this->addChild(curve); curve->recalculate(); emit curve->dataReductionDataChanged(curve->dataReductionData()); } else { beginMacro(i18n("%1: add data reduction curve", name())); this->addChild(curve); } endMacro(); } void CartesianPlot::addDifferentiationCurve() { XYDifferentiationCurve* curve = new XYDifferentiationCurve("Differentiation"); const XYCurve* curCurve = currentCurve(); if (curCurve) { beginMacro( i18n("%1: differentiate '%2'", name(), curCurve->name()) ); curve->setName( i18n("Derivative of '%1'", curCurve->name()) ); curve->setDataSourceType(XYAnalysisCurve::DataSourceCurve); curve->setDataSourceCurve(curCurve); this->addChild(curve); curve->recalculate(); emit curve->differentiationDataChanged(curve->differentiationData()); } else { beginMacro(i18n("%1: add differentiation curve", name())); this->addChild(curve); } endMacro(); } void CartesianPlot::addIntegrationCurve() { XYIntegrationCurve* curve = new XYIntegrationCurve("Integration"); const XYCurve* curCurve = currentCurve(); if (curCurve) { beginMacro( i18n("%1: integrate '%2'", name(), curCurve->name()) ); curve->setName( i18n("Integral of '%1'", curCurve->name()) ); curve->setDataSourceType(XYAnalysisCurve::DataSourceCurve); curve->setDataSourceCurve(curCurve); this->addChild(curve); curve->recalculate(); emit curve->integrationDataChanged(curve->integrationData()); } else { beginMacro(i18n("%1: add integration curve", name())); this->addChild(curve); } endMacro(); } void CartesianPlot::addInterpolationCurve() { XYInterpolationCurve* curve = new XYInterpolationCurve("Interpolation"); const XYCurve* curCurve = currentCurve(); if (curCurve) { beginMacro( i18n("%1: interpolate '%2'", name(), curCurve->name()) ); curve->setName( i18n("Interpolation of '%1'", curCurve->name()) ); curve->setDataSourceType(XYAnalysisCurve::DataSourceCurve); curve->setDataSourceCurve(curCurve); curve->recalculate(); this->addChild(curve); emit curve->interpolationDataChanged(curve->interpolationData()); } else { beginMacro(i18n("%1: add interpolation curve", name())); this->addChild(curve); } endMacro(); } void CartesianPlot::addSmoothCurve() { XYSmoothCurve* curve = new XYSmoothCurve("Smooth"); const XYCurve* curCurve = currentCurve(); if (curCurve) { beginMacro( i18n("%1: smooth '%2'", name(), curCurve->name()) ); curve->setName( i18n("Smoothing of '%1'", curCurve->name()) ); curve->setDataSourceType(XYAnalysisCurve::DataSourceCurve); curve->setDataSourceCurve(curCurve); this->addChild(curve); curve->recalculate(); emit curve->smoothDataChanged(curve->smoothData()); } else { beginMacro(i18n("%1: add smoothing curve", name())); this->addChild(curve); } endMacro(); } void CartesianPlot::addFitCurve() { DEBUG("CartesianPlot::addFitCurve()"); XYFitCurve* curve = new XYFitCurve("fit"); const XYCurve* curCurve = currentCurve(); if (curCurve) { beginMacro( i18n("%1: fit to '%2'", name(), curCurve->name()) ); curve->setName( i18n("Fit to '%1'", curCurve->name()) ); curve->setDataSourceType(XYAnalysisCurve::DataSourceCurve); curve->setDataSourceCurve(curCurve); //set the fit model category and type const auto* action = qobject_cast(QObject::sender()); PlotDataDialog::AnalysisAction type = (PlotDataDialog::AnalysisAction)action->data().toInt(); curve->initFitData(type); curve->initStartValues(curCurve); //fit with weights for y if the curve has error bars for y if (curCurve->yErrorType() == XYCurve::SymmetricError && curCurve->yErrorPlusColumn()) { XYFitCurve::FitData fitData = curve->fitData(); fitData.yWeightsType = nsl_fit_weight_instrumental; curve->setFitData(fitData); curve->setYErrorColumn(curCurve->yErrorPlusColumn()); } curve->recalculate(); //add the child after the fit was calculated so the dock widgets gets the fit results //and call retransform() after this to calculate and to paint the data points of the fit-curve this->addChild(curve); curve->retransform(); } else { beginMacro(i18n("%1: add fit curve", name())); this->addChild(curve); } endMacro(); } void CartesianPlot::addFourierFilterCurve() { XYFourierFilterCurve* curve = new XYFourierFilterCurve("Fourier filter"); const XYCurve* curCurve = currentCurve(); if (curCurve) { beginMacro( i18n("%1: Fourier filtering of '%2'", name(), curCurve->name()) ); curve->setName( i18n("Fourier filtering of '%1'", curCurve->name()) ); curve->setDataSourceType(XYAnalysisCurve::DataSourceCurve); curve->setDataSourceCurve(curCurve); this->addChild(curve); } else { beginMacro(i18n("%1: add Fourier filter curve", name())); this->addChild(curve); } endMacro(); } void CartesianPlot::addFourierTransformCurve() { XYFourierTransformCurve* curve = new XYFourierTransformCurve("Fourier transform"); this->addChild(curve); } void CartesianPlot::addConvolutionCurve() { XYConvolutionCurve* curve = new XYConvolutionCurve("Convolution"); this->addChild(curve); } void CartesianPlot::addCorrelationCurve() { XYCorrelationCurve* curve = new XYCorrelationCurve("Auto-/Cross-Correlation"); this->addChild(curve); } /*! * public helper function to set a legend object created outside of CartesianPlot, e.g. in \c OriginProjectParser. */ void CartesianPlot::addLegend(CartesianPlotLegend* legend) { m_legend = legend; this->addChild(legend); } void CartesianPlot::addLegend() { //don't do anything if there's already a legend if (m_legend) return; m_legend = new CartesianPlotLegend(this, "legend"); this->addChild(m_legend); m_legend->retransform(); //only one legend is allowed -> disable the action if (m_menusInitialized) addLegendAction->setEnabled(false); } void CartesianPlot::addTextLabel() { TextLabel* label = new TextLabel("text label"); this->addChild(label); label->setParentGraphicsItem(graphicsItem()); } void CartesianPlot::addImage() { Image* image = new Image("image"); this->addChild(image); } void CartesianPlot::addCustomPoint() { CustomPoint* point = new CustomPoint(this, "custom point"); this->addChild(point); point->retransform(); } void CartesianPlot::addReferenceLine() { ReferenceLine* line = new ReferenceLine(this, "reference line"); this->addChild(line); line->retransform(); } int CartesianPlot::curveCount(){ return children().length(); } const XYCurve* CartesianPlot::getCurve(int index){ return children().at(index); } double CartesianPlot::cursorPos(int cursorNumber) { Q_D(CartesianPlot); if (cursorNumber == 0) return d->cursor0Pos.x(); else return d->cursor1Pos.x(); } void CartesianPlot::childAdded(const AbstractAspect* child) { Q_D(CartesianPlot); const auto* curve = qobject_cast(child); if (curve) { connect(curve, &XYCurve::dataChanged, this, &CartesianPlot::dataChanged); connect(curve, &XYCurve::xDataChanged, this, &CartesianPlot::xDataChanged); connect(curve, &XYCurve::xErrorTypeChanged, this, &CartesianPlot::dataChanged); connect(curve, &XYCurve::xErrorPlusColumnChanged, this, &CartesianPlot::dataChanged); connect(curve, &XYCurve::xErrorMinusColumnChanged, this, &CartesianPlot::dataChanged); connect(curve, &XYCurve::yDataChanged, this, &CartesianPlot::yDataChanged); connect(curve, &XYCurve::yErrorTypeChanged, this, &CartesianPlot::dataChanged); connect(curve, &XYCurve::yErrorPlusColumnChanged, this, &CartesianPlot::dataChanged); connect(curve, &XYCurve::yErrorMinusColumnChanged, this, &CartesianPlot::dataChanged); connect(curve, static_cast(&XYCurve::visibilityChanged), this, &CartesianPlot::curveVisibilityChanged); //update the legend on changes of the name, line and symbol styles connect(curve, &XYCurve::aspectDescriptionChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::aspectDescriptionChanged, this, &CartesianPlot::curveNameChanged); connect(curve, &XYCurve::lineTypeChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::linePenChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::linePenChanged, this, static_cast(&CartesianPlot::curveLinePenChanged)); connect(curve, &XYCurve::lineOpacityChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::symbolsStyleChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::symbolsSizeChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::symbolsRotationAngleChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::symbolsOpacityChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::symbolsBrushChanged, this, &CartesianPlot::updateLegend); connect(curve, &XYCurve::symbolsPenChanged, this, &CartesianPlot::updateLegend); updateLegend(); d->curvesXMinMaxIsDirty = true; d->curvesYMinMaxIsDirty = true; //in case the first curve is added, check whether we start plotting datetime data if (children().size() == 1) { const auto* col = dynamic_cast(curve->xColumn()); if (col) { if (col->columnMode() == AbstractColumn::DateTime) { setUndoAware(false); setXRangeFormat(CartesianPlot::DateTime); setUndoAware(true); //set column's datetime format for all horizontal axis for (auto* axis : children()) { if (axis->orientation() == Axis::AxisHorizontal) { auto* filter = static_cast(col->outputFilter()); d->xRangeDateTimeFormat = filter->format(); axis->setUndoAware(false); axis->setLabelsDateTimeFormat(d->xRangeDateTimeFormat); axis->setUndoAware(true); } } } } col = dynamic_cast(curve->yColumn()); if (col) { if (col->columnMode() == AbstractColumn::DateTime) { setUndoAware(false); setYRangeFormat(CartesianPlot::DateTime); setUndoAware(true); //set column's datetime format for all vertical axis for (auto* axis : children()) { if (axis->orientation() == Axis::AxisVertical) { auto* filter = static_cast(col->outputFilter()); d->yRangeDateTimeFormat = filter->format(); axis->setUndoAware(false); axis->setLabelsDateTimeFormat(d->yRangeDateTimeFormat); axis->setUndoAware(true); } } } } } emit curveAdded(curve); } else { const auto* hist = qobject_cast(child); if (hist) { connect(hist, &Histogram::dataChanged, this, &CartesianPlot::dataChanged); connect(hist, &Histogram::visibilityChanged, this, &CartesianPlot::curveVisibilityChanged); updateLegend(); } // if an element is hovered, the curves which are handled manually in this class // must be unhovered const auto* element = static_cast(child); connect(element, &WorksheetElement::hovered, this, &CartesianPlot::childHovered); } if (!isLoading()) { //if a theme was selected, apply the theme settings for newly added children, too if (!d->theme.isEmpty()) { const auto* elem = dynamic_cast(child); if (elem) { KConfig config(ThemeHandler::themeFilePath(d->theme), KConfig::SimpleConfig); const_cast(elem)->loadThemeConfig(config); } } else { //no theme is available, apply the default colors for curves only, s.a. XYCurve::loadThemeConfig() const auto* curve = dynamic_cast(child); if (curve) { int index = indexOfChild(curve); QColor themeColor; if (index < m_themeColorPalette.size()) themeColor = m_themeColorPalette.at(index); else { if (m_themeColorPalette.size()) themeColor = m_themeColorPalette.last(); } auto* c = const_cast(curve); //Line QPen p = curve->linePen(); p.setColor(themeColor); c->setLinePen(p); //Drop line p = curve->dropLinePen(); p.setColor(themeColor); c->setDropLinePen(p); //Symbol QBrush brush = c->symbolsBrush(); brush.setColor(themeColor); c->setSymbolsBrush(brush); p = c->symbolsPen(); p.setColor(themeColor); c->setSymbolsPen(p); //Filling c->setFillingFirstColor(themeColor); //Error bars p.setColor(themeColor); c->setErrorBarsPen(p); } } } } void CartesianPlot::childRemoved(const AbstractAspect* parent, const AbstractAspect* before, const AbstractAspect* child) { Q_UNUSED(parent); Q_UNUSED(before); if (m_legend == child) { if (m_menusInitialized) addLegendAction->setEnabled(true); m_legend = nullptr; } else { const auto* curve = qobject_cast(child); if (curve) { updateLegend(); emit curveRemoved(curve); } } } /*! * \brief CartesianPlot::childHovered * Unhover all curves, when another child is hovered. The hover handling for the curves is done in their parent (CartesianPlot), * because the hover should set when the curve is hovered and not just the bounding rect (for more see hoverMoveEvent) */ void CartesianPlot::childHovered() { Q_D(CartesianPlot); bool curveSender = dynamic_cast(QObject::sender()) != nullptr; if (!d->isSelected()) { if (d->m_hovered) d->m_hovered = false; d->update(); } if (!curveSender) { for (auto curve: children()) curve->setHover(false); } } void CartesianPlot::updateLegend() { if (m_legend) m_legend->retransform(); } /*! called when in one of the curves the data was changed. Autoscales the coordinate system and the x-axes, when "auto-scale" is active. */ void CartesianPlot::dataChanged() { if (project() && project()->isLoading()) return; Q_D(CartesianPlot); d->curvesXMinMaxIsDirty = true; d->curvesYMinMaxIsDirty = true; bool updated = false; if (d->autoScaleX && d->autoScaleY) updated = scaleAuto(); else if (d->autoScaleX) updated = scaleAutoX(); else if (d->autoScaleY) updated = scaleAutoY(); if (!updated || !QObject::sender()) { //even if the plot ranges were not changed, either no auto scale active or the new data //is within the current ranges and no change of the ranges is required, //retransform the curve in order to show the changes auto* curve = dynamic_cast(QObject::sender()); if (curve) curve->retransform(); else { auto* hist = dynamic_cast(QObject::sender()); if (hist) hist->retransform(); else { //no sender available, the function was called directly in the file filter (live data source got new data) //or in Project::load() -> retransform all available curves since we don't know which curves are affected. //TODO: this logic can be very expensive for (auto* c : children()) { c->recalcLogicalPoints(); c->retransform(); } } } } } /*! called when in one of the curves the x-data was changed. Autoscales the coordinate system and the x-axes, when "auto-scale" is active. */ void CartesianPlot::xDataChanged() { if (project() && project()->isLoading()) return; Q_D(CartesianPlot); if (d->suppressRetransform) return; d->curvesXMinMaxIsDirty = true; bool updated = false; if (d->autoScaleX) updated = this->scaleAutoX(); if (!updated) { //even if the plot ranges were not changed, either no auto scale active or the new data //is within the current ranges and no change of the ranges is required, //retransform the curve in order to show the changes auto* curve = dynamic_cast(QObject::sender()); if (curve) curve->retransform(); else { auto* hist = dynamic_cast(QObject::sender()); if (hist) hist->retransform(); } } //in case there is only one curve and its column mode was changed, check whether we start plotting datetime data if (children().size() == 1) { auto* curve = dynamic_cast(QObject::sender()); if (curve) { const AbstractColumn* col = curve->xColumn(); if (col->columnMode() == AbstractColumn::DateTime && d->xRangeFormat != CartesianPlot::DateTime) { setUndoAware(false); setXRangeFormat(CartesianPlot::DateTime); setUndoAware(true); } } } emit curveDataChanged(dynamic_cast(QObject::sender())); } /*! called when in one of the curves the x-data was changed. Autoscales the coordinate system and the x-axes, when "auto-scale" is active. */ void CartesianPlot::yDataChanged() { if (project() && project()->isLoading()) return; Q_D(CartesianPlot); if (d->suppressRetransform) return; d->curvesYMinMaxIsDirty = true; bool updated = false; if (d->autoScaleY) updated = this->scaleAutoY(); if (!updated) { //even if the plot ranges were not changed, either no auto scale active or the new data //is within the current ranges and no change of the ranges is required, //retransform the curve in order to show the changes auto* curve = dynamic_cast(QObject::sender()); if (curve) curve->retransform(); else { auto* hist = dynamic_cast(QObject::sender()); if (hist) hist->retransform(); } } //in case there is only one curve and its column mode was changed, check whether we start plotting datetime data if (children().size() == 1) { auto* curve = dynamic_cast(QObject::sender()); if (curve) { const AbstractColumn* col = curve->yColumn(); if (col->columnMode() == AbstractColumn::DateTime && d->xRangeFormat != CartesianPlot::DateTime) { setUndoAware(false); setYRangeFormat(CartesianPlot::DateTime); setUndoAware(true); } } } emit curveDataChanged(dynamic_cast(QObject::sender())); } void CartesianPlot::curveVisibilityChanged() { Q_D(CartesianPlot); d->curvesXMinMaxIsDirty = true; d->curvesYMinMaxIsDirty = true; updateLegend(); if (d->autoScaleX && d->autoScaleY) this->scaleAuto(); else if (d->autoScaleX) this->scaleAutoX(); else if (d->autoScaleY) this->scaleAutoY(); emit curveVisibilityChangedSignal(); } void CartesianPlot::curveLinePenChanged(QPen pen) { const auto* curve = qobject_cast(QObject::sender()); emit curveLinePenChanged(pen, curve->name()); } void CartesianPlot::setMouseMode(const MouseMode mouseMode) { Q_D(CartesianPlot); d->mouseMode = mouseMode; d->setHandlesChildEvents(mouseMode != CartesianPlot::SelectionMode); QList items = d->childItems(); if (d->mouseMode == CartesianPlot::SelectionMode) { d->setZoomSelectionBandShow(false); d->setCursor(Qt::ArrowCursor); for (auto* item : items) item->setFlag(QGraphicsItem::ItemStacksBehindParent, false); } else { for (auto* item : items) item->setFlag(QGraphicsItem::ItemStacksBehindParent, true); } //when doing zoom selection, prevent the graphics item from being movable //if it's currently movable (no worksheet layout available) const auto* worksheet = dynamic_cast(parentAspect()); if (worksheet) { if (mouseMode == CartesianPlot::SelectionMode) { if (worksheet->layout() != Worksheet::NoLayout) graphicsItem()->setFlag(QGraphicsItem::ItemIsMovable, false); else graphicsItem()->setFlag(QGraphicsItem::ItemIsMovable, true); } else //zoom m_selection graphicsItem()->setFlag(QGraphicsItem::ItemIsMovable, false); } emit mouseModeChanged(mouseMode); } void CartesianPlot::setLocked(bool locked) { Q_D(CartesianPlot); d->locked = locked; } bool CartesianPlot::scaleAutoX() { Q_D(CartesianPlot); if (d->curvesXMinMaxIsDirty) { calculateCurvesXMinMax(false); //loop over all histograms and determine the maximum and minimum x-values for (const auto* curve : this->children()) { if (!curve->isVisible()) continue; if (!curve->dataColumn()) continue; const double min = curve->getXMinimum(); if (min < d->curvesXMin) d->curvesXMin = min; const double max = curve->getXMaximum(); if (max > d->curvesXMax) d->curvesXMax = max; } // do it at the end, because it must be from the real min/max values double errorBarsCapSize = -1; for (auto* curve : this->children()) { if (curve->yErrorType() != XYCurve::ErrorType::NoError) { errorBarsCapSize = qMax(errorBarsCapSize, curve->errorBarsCapSize()); } } if (errorBarsCapSize > 0) { // must be done, because retransformScales uses xMin/xMax if (d->curvesXMin != d->xMin && d->curvesXMin != INFINITY) d->xMin = d->curvesXMin; if (d->curvesXMax != d->xMax && d->curvesXMax != -INFINITY) d->xMax = d->curvesXMax; // When the previous scale is completely different. The mapTo functions scale with wrong values. To prevent // this a rescale must be done. // The errorBarsCapSize is in Scene coordinates. So this value must be transformed into a logical value. Due // to nonlinear scalings it cannot only be multiplied with a scaling factor and depends on the position of the // column value // dirty hack: call setIsLoading(true) to suppress the call of retransform() in retransformScales() since a // retransform is already done at the end of this function setIsLoading(true); d->retransformScales(); setIsLoading(false); QPointF point = coordinateSystem()->mapLogicalToScene(QPointF(d->curvesXMin, 0), AbstractCoordinateSystem::SuppressPageClipping); point.setX(point.x() - errorBarsCapSize); point = coordinateSystem()->mapSceneToLogical(point, AbstractCoordinateSystem::SuppressPageClipping); // Problem is, when the scaling is not linear (for example log(x)) and the minimum is 0. In this // case mapLogicalToScene returns (0,0) which is smaller than the curves minimum if (point.x() < d->curvesXMin) d->curvesXMin = point.x(); point = coordinateSystem()->mapLogicalToScene(QPointF(d->curvesXMax, 0), AbstractCoordinateSystem::SuppressPageClipping); point.setX(point.x() + errorBarsCapSize); point = coordinateSystem()->mapSceneToLogical(point, AbstractCoordinateSystem::SuppressPageClipping); if (point.x() > d->curvesXMax) d->curvesXMax = point.x(); } d->curvesYMinMaxIsDirty = true; d->curvesXMinMaxIsDirty = false; } bool update = false; if (d->curvesXMin != d->xMin && d->curvesXMin != INFINITY) { d->xMin = d->curvesXMin; update = true; } if (d->curvesXMax != d->xMax && d->curvesXMax != -INFINITY) { d->xMax = d->curvesXMax; update = true; } if (update) { if (d->xMax == d->xMin) { //in case min and max are equal (e.g. if we plot a single point), subtract/add 10% of the value if (d->xMax != 0) { d->xMax = d->xMax*1.1; d->xMin = d->xMin*0.9; } else { d->xMax = 0.1; d->xMin = -0.1; } } else { double offset = (d->xMax - d->xMin)*d->autoScaleOffsetFactor; d->xMin -= offset; d->xMax += offset; } d->retransformScales(); } return update; } bool CartesianPlot::scaleAutoY() { Q_D(CartesianPlot); if (d->curvesYMinMaxIsDirty) { calculateCurvesYMinMax(false); // loop over all curves //loop over all histograms and determine the maximum y-value for (const auto* curve : this->children()) { if (!curve->isVisible()) continue; const double min = curve->getYMinimum(); if (d->curvesYMin > min) d->curvesYMin = min; const double max = curve->getYMaximum(); if (max > d->curvesYMax) d->curvesYMax = max; } // do it at the end, because it must be from the real min/max values double errorBarsCapSize = -1; for (auto* curve : this->children()) { if (curve->xErrorType() != XYCurve::ErrorType::NoError) { errorBarsCapSize = qMax(errorBarsCapSize, curve->errorBarsCapSize()); } } if (errorBarsCapSize > 0) { if (d->curvesYMin != d->yMin && d->curvesYMin != INFINITY) d->yMin = d->curvesYMin; if (d->curvesYMax != d->yMax && d->curvesYMax != -INFINITY) d->yMax = d->curvesYMax; setIsLoading(true); d->retransformScales(); setIsLoading(false); QPointF point = coordinateSystem()->mapLogicalToScene(QPointF(0, d->curvesYMin), AbstractCoordinateSystem::SuppressPageClipping); point.setY(point.y() + errorBarsCapSize); point = coordinateSystem()->mapSceneToLogical(point, AbstractCoordinateSystem::SuppressPageClipping); if (point.y() < d->curvesYMin) d->curvesYMin = point.y(); point = coordinateSystem()->mapLogicalToScene(QPointF(0, d->curvesYMax), AbstractCoordinateSystem::SuppressPageClipping); point.setY(point.y() - errorBarsCapSize); point = coordinateSystem()->mapSceneToLogical(point, AbstractCoordinateSystem::SuppressPageClipping); if (point.y() > d->curvesYMax) d->curvesYMax = point.y(); } d->curvesXMinMaxIsDirty = true; d->curvesYMinMaxIsDirty = false; } bool update = false; if (d->curvesYMin != d->yMin && d->curvesYMin != INFINITY) { d->yMin = d->curvesYMin; update = true; } if (d->curvesYMax != d->yMax && d->curvesYMax != -INFINITY) { d->yMax = d->curvesYMax; update = true; } if (update) { if (d->yMax == d->yMin) { //in case min and max are equal (e.g. if we plot a single point), subtract/add 10% of the value if (d->yMax != 0) { d->yMax = d->yMax*1.1; d->yMin = d->yMin*0.9; } else { d->yMax = 0.1; d->yMin = -0.1; } } else { double offset = (d->yMax - d->yMin)*d->autoScaleOffsetFactor; d->yMin -= offset; d->yMax += offset; } d->retransformScales(); } return update; } void CartesianPlot::scaleAutoTriggered() { QAction* action = dynamic_cast(QObject::sender()); if (!action) return; if (action == scaleAutoAction) scaleAuto(); else if (action == scaleAutoXAction) setAutoScaleX(true); else if (action == scaleAutoYAction) setAutoScaleY(true); } bool CartesianPlot::scaleAuto() { DEBUG("CartesianPlot::scaleAuto()"); Q_D(CartesianPlot); if (d->curvesXMinMaxIsDirty) { calculateCurvesXMinMax(); double errorBarsCapSize = -1; for (auto* curve : this->children()) { if (curve->yErrorType() != XYCurve::ErrorType::NoError) { errorBarsCapSize = qMax(errorBarsCapSize, curve->errorBarsCapSize()); } } if (errorBarsCapSize > 0) { if (d->curvesXMin != d->xMin && d->curvesXMin != INFINITY) d->xMin = d->curvesXMin; if (d->curvesXMax != d->xMax && d->curvesXMax != -INFINITY) d->xMax = d->curvesXMax; setIsLoading(true); d->retransformScales(); setIsLoading(false); QPointF point = coordinateSystem()->mapLogicalToScene(QPointF(d->curvesXMin, 0), AbstractCoordinateSystem::SuppressPageClipping); point.setX(point.x() - errorBarsCapSize); point = coordinateSystem()->mapSceneToLogical(point, AbstractCoordinateSystem::SuppressPageClipping); if (point.x() < d->curvesXMin) d->curvesXMin = point.x(); point = coordinateSystem()->mapLogicalToScene(QPointF(d->curvesXMax, 0), AbstractCoordinateSystem::SuppressPageClipping); point.setX(point.x() + errorBarsCapSize); point = coordinateSystem()->mapSceneToLogical(point, AbstractCoordinateSystem::SuppressPageClipping); if (point.x() > d->curvesXMax) d->curvesXMax = point.x(); } d->curvesXMinMaxIsDirty = false; } if (d->curvesYMinMaxIsDirty) { calculateCurvesYMinMax(); double errorBarsCapSize = -1; for (auto* curve : this->children()) { if (curve->xErrorType() != XYCurve::ErrorType::NoError) { errorBarsCapSize = qMax(errorBarsCapSize, curve->errorBarsCapSize()); } } if (errorBarsCapSize > 0) { if (d->curvesYMin != d->yMin && d->curvesYMin != INFINITY) d->yMin = d->curvesYMin; if (d->curvesYMax != d->yMax && d->curvesYMax != -INFINITY) d->yMax = d->curvesYMax; setIsLoading(true); d->retransformScales(); setIsLoading(false); QPointF point = coordinateSystem()->mapLogicalToScene(QPointF(0, d->curvesYMin), AbstractCoordinateSystem::SuppressPageClipping); point.setY(point.y() + errorBarsCapSize); point = coordinateSystem()->mapSceneToLogical(point, AbstractCoordinateSystem::SuppressPageClipping); if (point.y() < d->curvesYMin) d->curvesYMin = point.y(); point = coordinateSystem()->mapLogicalToScene(QPointF(0, d->curvesYMax), AbstractCoordinateSystem::SuppressPageClipping); point.setY(point.y() - errorBarsCapSize); point = coordinateSystem()->mapSceneToLogical(point, AbstractCoordinateSystem::SuppressPageClipping); if (point.y() > d->curvesYMax) d->curvesYMax = point.y(); } d->curvesYMinMaxIsDirty = false; } bool updateX = false; bool updateY = false; if (d->curvesXMin != d->xMin && d->curvesXMin != INFINITY) { d->xMin = d->curvesXMin; updateX = true; } if (d->curvesXMax != d->xMax && d->curvesXMax != -INFINITY) { d->xMax = d->curvesXMax; updateX = true; } if (d->curvesYMin != d->yMin && d->curvesYMin != INFINITY) { d->yMin = d->curvesYMin; updateY = true; } if (d->curvesYMax != d->yMax && d->curvesYMax != -INFINITY) { d->yMax = d->curvesYMax; updateY = true; } DEBUG(" xmin/xmax = " << d->xMin << '/' << d->xMax << ", ymin/ymax = " << d->yMin << '/' << d->yMax); if (updateX || updateY) { if (updateX) { if (d->xMax == d->xMin) { //in case min and max are equal (e.g. if we plot a single point), subtract/add 10% of the value if (d->xMax != 0) { d->xMax = d->xMax*1.1; d->xMin = d->xMin*0.9; } else { d->xMax = 0.1; d->xMin = -0.1; } } else { double offset = (d->xMax - d->xMin)*d->autoScaleOffsetFactor; d->xMin -= offset; d->xMax += offset; } setAutoScaleX(true); } if (updateY) { if (d->yMax == d->yMin) { //in case min and max are equal (e.g. if we plot a single point), subtract/add 10% of the value if (d->yMax != 0) { d->yMax = d->yMax*1.1; d->yMin = d->yMin*0.9; } else { d->yMax = 0.1; d->yMin = -0.1; } } else { double offset = (d->yMax - d->yMin)*d->autoScaleOffsetFactor; d->yMin -= offset; d->yMax += offset; } setAutoScaleY(true); } d->retransformScales(); } return (updateX || updateY); } /*! * Calculates and sets curves y min and max. This function does not respect the range * of the y axis */ void CartesianPlot::calculateCurvesXMinMax(bool completeRange) { Q_D(CartesianPlot); d->curvesXMin = INFINITY; d->curvesXMax = -INFINITY; //loop over all xy-curves and determine the maximum and minimum x-values for (const auto* curve : this->children()) { if (!curve->isVisible()) continue; auto* xColumn = curve->xColumn(); if (!xColumn) continue; double min = d->curvesXMin; double max = d->curvesXMax; int start =0; int end = 0; if (d->rangeType == CartesianPlot::RangeFree && curve->yColumn() && !completeRange) { curve->yColumn()->indicesMinMax(yMin(), yMax(), start, end); if (end < curve->yColumn()->rowCount()) end ++; } else { switch (d->rangeType) { case CartesianPlot::RangeFree: start = 0; end = xColumn->rowCount(); break; case CartesianPlot::RangeLast: start = xColumn->rowCount() - d->rangeLastValues; end = xColumn->rowCount(); break; case CartesianPlot::RangeFirst: start = 0; end = d->rangeFirstValues; break; } } curve->minMaxX(start, end, min, max, true); if (min < d->curvesXMin) d->curvesXMin = min; if (max > d->curvesXMax) d->curvesXMax = max; } //loop over all histograms and determine the maximum and minimum x-values for (const auto* curve : this->children()) { if (!curve->isVisible()) continue; if (!curve->dataColumn()) continue; const double min = curve->getXMinimum(); if (d->curvesXMin > min) d->curvesXMin = min; const double max = curve->getXMaximum(); if (max > d->curvesXMax) d->curvesXMax = max; } } /*! * Calculates and sets curves y min and max. This function does not respect the range * of the x axis */ void CartesianPlot::calculateCurvesYMinMax(bool completeRange) { Q_D(CartesianPlot); d->curvesYMin = INFINITY; d->curvesYMax = -INFINITY; double min = d->curvesYMin; double max = d->curvesYMax; //loop over all xy-curves and determine the maximum and minimum y-values for (const auto* curve : this->children()) { if (!curve->isVisible()) continue; auto* yColumn = curve->yColumn(); if (!yColumn) continue; int start =0; int end = 0; if (d->rangeType == CartesianPlot::RangeFree && curve->xColumn() && !completeRange) { curve->xColumn()->indicesMinMax(xMin(), xMax(), start, end); if (end < curve->xColumn()->rowCount()) end ++; // because minMaxY excludes indexMax } else { switch (d->rangeType) { case CartesianPlot::RangeFree: start = 0; end = yColumn->rowCount(); break; case CartesianPlot::RangeLast: start = yColumn->rowCount() - d->rangeLastValues; end = yColumn->rowCount(); break; case CartesianPlot::RangeFirst: start = 0; end = d->rangeFirstValues; break; } } curve->minMaxY(start, end, min, max, true); if (min < d->curvesYMin) d->curvesYMin = min; if (max > d->curvesYMax) d->curvesYMax = max; } //loop over all histograms and determine the maximum y-value for (const auto* curve : this->children()) { if (!curve->isVisible()) continue; const double min = curve->getYMinimum(); if (d->curvesYMin > min) d->curvesYMin = min; const double max = curve->getYMaximum(); if (max > d->curvesYMax) d->curvesYMax = max; } } void CartesianPlot::zoomIn() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleX(false); setAutoScaleY(false); setUndoAware(true); d->curvesXMinMaxIsDirty = true; d->curvesYMinMaxIsDirty = true; zoom(true, true); //zoom in x zoom(false, true); //zoom in y d->retransformScales(); } void CartesianPlot::zoomOut() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleX(false); setAutoScaleY(false); setUndoAware(true); d->curvesXMinMaxIsDirty = true; d->curvesYMinMaxIsDirty = true; zoom(true, false); //zoom out x zoom(false, false); //zoom out y d->retransformScales(); } void CartesianPlot::zoomInX() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleX(false); setUndoAware(true); d->curvesYMinMaxIsDirty = true; zoom(true, true); //zoom in x if (d->autoScaleY && autoScaleY()) return; d->retransformScales(); } void CartesianPlot::zoomOutX() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleX(false); setUndoAware(true); d->curvesYMinMaxIsDirty = true; zoom(true, false); //zoom out x if (d->autoScaleY && autoScaleY()) return; d->retransformScales(); } void CartesianPlot::zoomInY() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleY(false); setUndoAware(true); d->curvesYMinMaxIsDirty = true; zoom(false, true); //zoom in y if (d->autoScaleX && autoScaleX()) return; d->retransformScales(); } void CartesianPlot::zoomOutY() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleY(false); setUndoAware(true); d->curvesYMinMaxIsDirty = true; zoom(false, false); //zoom out y if (d->autoScaleX && autoScaleX()) return; d->retransformScales(); } /*! * helper function called in other zoom*() functions * and doing the actual change of the data ranges. * @param x if set to \true the x-range is modified, the y-range for \c false * @param in the "zoom in" is performed if set to \c \true, "zoom out" for \c false */ void CartesianPlot::zoom(bool x, bool in) { Q_D(CartesianPlot); double min; double max; CartesianPlot::Scale scale; if (x) { min = d->xMin; max = d->xMax; scale = d->xScale; } else { min = d->yMin; max = d->yMax; scale = d->yScale; } double factor = m_zoomFactor; if (in) factor = 1/factor; switch (scale) { case ScaleLinear: { double oldRange = max - min; double newRange = (max - min) * factor; max = max + (newRange - oldRange) / 2; min = min - (newRange - oldRange) / 2; break; } case ScaleLog10: case ScaleLog10Abs: { double oldRange = log10(max) - log10(min); double newRange = (log10(max) - log10(min)) * factor; max = max * pow(10, (newRange - oldRange) / 2.); min = min / pow(10, (newRange - oldRange) / 2.); break; } case ScaleLog2: case ScaleLog2Abs: { double oldRange = log2(max) - log2(min); double newRange = (log2(max) - log2(min)) * factor; max = max * pow(2, (newRange - oldRange) / 2.); min = min / pow(2, (newRange - oldRange) / 2.); break; } case ScaleLn: case ScaleLnAbs: { double oldRange = log(max) - log(min); double newRange = (log(max) - log(min)) * factor; max = max * exp((newRange - oldRange) / 2.); min = min / exp((newRange - oldRange) / 2.); break; } case ScaleSqrt: case ScaleX2: break; } if (!std::isnan(min) && !std::isnan(max) && std::isfinite(min) && std::isfinite(max)) { if (x) { d->xMin = min; d->xMax = max; } else { d->yMin = min; d->yMax = max; } } } /*! * helper function called in other shift*() functions * and doing the actual change of the data ranges. * @param x if set to \true the x-range is modified, the y-range for \c false * @param leftOrDown the "shift left" for x or "shift dows" for y is performed if set to \c \true, * "shift right" or "shift up" for \c false */ void CartesianPlot::shift(bool x, bool leftOrDown) { Q_D(CartesianPlot); double min; double max; CartesianPlot::Scale scale; double offset = 0.0; double factor = 0.1; if (x) { min = d->xMin; max = d->xMax; scale = d->xScale; } else { min = d->yMin; max = d->yMax; scale = d->yScale; } if (leftOrDown) factor *= -1.; switch (scale) { case ScaleLinear: { offset = (max - min) * factor; min += offset; max += offset; break; } case ScaleLog10: case ScaleLog10Abs: { offset = (log10(max) - log10(min)) * factor; min *= pow(10, offset); max *= pow(10, offset); break; } case ScaleLog2: case ScaleLog2Abs: { offset = (log2(max) - log2(min)) * factor; min *= pow(2, offset); max *= pow(2, offset); break; } case ScaleLn: case ScaleLnAbs: { offset = (log10(max) - log10(min)) * factor; min *= exp(offset); max *= exp(offset); break; } case ScaleSqrt: case ScaleX2: break; } if (!std::isnan(min) && !std::isnan(max) && std::isfinite(min) && std::isfinite(max)) { if (x) { d->xMin = min; d->xMax = max; } else { d->yMin = min; d->yMax = max; } } } void CartesianPlot::shiftLeftX() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleX(false); setUndoAware(true); d->curvesYMinMaxIsDirty = true; shift(true, true); if (d->autoScaleY && scaleAutoY()) return; d->retransformScales(); } void CartesianPlot::shiftRightX() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleX(false); setUndoAware(true); d->curvesYMinMaxIsDirty = true; shift(true, false); if (d->autoScaleY && scaleAutoY()) return; d->retransformScales(); } void CartesianPlot::shiftUpY() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleY(false); setUndoAware(true); d->curvesXMinMaxIsDirty = true; shift(false, false); if (d->autoScaleX && scaleAutoX()) return; d->retransformScales(); } void CartesianPlot::shiftDownY() { Q_D(CartesianPlot); setUndoAware(false); setAutoScaleY(false); setUndoAware(true); d->curvesXMinMaxIsDirty = true; shift(false, true); if (d->autoScaleX && scaleAutoX()) return; d->retransformScales(); } void CartesianPlot::cursor() { Q_D(CartesianPlot); d->retransformScales(); } void CartesianPlot::mousePressZoomSelectionMode(QPointF logicPos) { Q_D(CartesianPlot); d->mousePressZoomSelectionMode(logicPos); } void CartesianPlot::mousePressCursorMode(int cursorNumber, QPointF logicPos) { Q_D(CartesianPlot); d->mousePressCursorMode(cursorNumber, logicPos); } void CartesianPlot::mouseMoveZoomSelectionMode(QPointF logicPos) { Q_D(CartesianPlot); d->mouseMoveZoomSelectionMode(logicPos); } void CartesianPlot::mouseMoveCursorMode(int cursorNumber, QPointF logicPos) { Q_D(CartesianPlot); d->mouseMoveCursorMode(cursorNumber, logicPos); } void CartesianPlot::mouseReleaseZoomSelectionMode() { Q_D(CartesianPlot); d->mouseReleaseZoomSelectionMode(); } void CartesianPlot::mouseHoverZoomSelectionMode(QPointF logicPos) { Q_D(CartesianPlot); d->mouseHoverZoomSelectionMode(logicPos); } void CartesianPlot::mouseHoverOutsideDataRect() { Q_D(CartesianPlot); d->mouseHoverOutsideDataRect(); } //############################################################################## //###### SLOTs for changes triggered via QActions in the context menu ######## //############################################################################## void CartesianPlot::visibilityChanged() { Q_D(CartesianPlot); this->setVisible(!d->isVisible()); } //##################################################################### //################### Private implementation ########################## //##################################################################### CartesianPlotPrivate::CartesianPlotPrivate(CartesianPlot* plot) : AbstractPlotPrivate(plot), q(plot) { setData(0, WorksheetElement::NameCartesianPlot); m_cursor0Text.prepare(); m_cursor1Text.prepare(); } /*! updates the position of plot rectangular in scene coordinates to \c r and recalculates the scales. The size of the plot corresponds to the size of the plot area, the area which is filled with the background color etc. and which can pose the parent item for several sub-items (like TextLabel). Note, the size of the area used to define the coordinate system doesn't need to be equal to this plot area. Also, the size (=bounding box) of CartesianPlot can be greater than the size of the plot area. */ void CartesianPlotPrivate::retransform() { if (suppressRetransform) return; PERFTRACE("CartesianPlotPrivate::retransform()"); prepareGeometryChange(); setPos( rect.x()+rect.width()/2, rect.y()+rect.height()/2); updateDataRect(); retransformScales(); //plotArea position is always (0, 0) in parent's coordinates, don't need to update here q->plotArea()->setRect(rect); //call retransform() for the title and the legend (if available) //when a predefined position relative to the (Left, Centered etc.) is used, //the actual position needs to be updated on plot's geometry changes. if (q->title()) q->title()->retransform(); if (q->m_legend) q->m_legend->retransform(); WorksheetElementContainerPrivate::recalcShapeAndBoundingRect(); } void CartesianPlotPrivate::retransformScales() { DEBUG("CartesianPlotPrivate::retransformScales()"); DEBUG(" xmin/xmax = " << xMin << '/'<< xMax << ", ymin/ymax = " << yMin << '/' << yMax); PERFTRACE("CartesianPlotPrivate::retransformScales()"); QVector scales; //check ranges for log-scales if (xScale != CartesianPlot::ScaleLinear) checkXRange(); //check whether we have x-range breaks - the first break, if available, should be valid bool hasValidBreak = (xRangeBreakingEnabled && !xRangeBreaks.list.isEmpty() && xRangeBreaks.list.first().isValid()); static const int breakGap = 20; double sceneStart, sceneEnd, logicalStart, logicalEnd; //create x-scales int plotSceneStart = dataRect.x(); int plotSceneEnd = dataRect.x() + dataRect.width(); if (!hasValidBreak) { //no breaks available -> range goes from the plot beginning to the end of the plot sceneStart = plotSceneStart; sceneEnd = plotSceneEnd; logicalStart = xMin; logicalEnd = xMax; //TODO: how should we handle the case sceneStart == sceneEnd? //(to reproduce, create plots and adjust the spacing/pading to get zero size for the plots) if (sceneStart != sceneEnd) scales << this->createScale(xScale, sceneStart, sceneEnd, logicalStart, logicalEnd); } else { int sceneEndLast = plotSceneStart; int logicalEndLast = xMin; for (const auto& rb : xRangeBreaks.list) { if (!rb.isValid()) break; //current range goes from the end of the previous one (or from the plot beginning) to curBreak.start sceneStart = sceneEndLast; if (&rb == &xRangeBreaks.list.first()) sceneStart += breakGap; sceneEnd = plotSceneStart + (plotSceneEnd-plotSceneStart) * rb.position; logicalStart = logicalEndLast; logicalEnd = rb.start; if (sceneStart != sceneEnd) scales << this->createScale(xScale, sceneStart, sceneEnd, logicalStart, logicalEnd); sceneEndLast = sceneEnd; logicalEndLast = rb.end; } //add the remaining range going from the last available range break to the end of the plot (=end of the x-data range) sceneStart = sceneEndLast+breakGap; sceneEnd = plotSceneEnd; logicalStart = logicalEndLast; logicalEnd = xMax; if (sceneStart != sceneEnd) scales << this->createScale(xScale, sceneStart, sceneEnd, logicalStart, logicalEnd); } cSystem->setXScales(scales); //check ranges for log-scales if (yScale != CartesianPlot::ScaleLinear) checkYRange(); //check whether we have y-range breaks - the first break, if available, should be valid hasValidBreak = (yRangeBreakingEnabled && !yRangeBreaks.list.isEmpty() && yRangeBreaks.list.first().isValid()); //create y-scales scales.clear(); plotSceneStart = dataRect.y() + dataRect.height(); plotSceneEnd = dataRect.y(); if (!hasValidBreak) { //no breaks available -> range goes from the plot beginning to the end of the plot sceneStart = plotSceneStart; sceneEnd = plotSceneEnd; logicalStart = yMin; logicalEnd = yMax; if (sceneStart != sceneEnd) scales << this->createScale(yScale, sceneStart, sceneEnd, logicalStart, logicalEnd); } else { int sceneEndLast = plotSceneStart; int logicalEndLast = yMin; for (const auto& rb : yRangeBreaks.list) { if (!rb.isValid()) break; //current range goes from the end of the previous one (or from the plot beginning) to curBreak.start sceneStart = sceneEndLast; if (&rb == &yRangeBreaks.list.first()) sceneStart -= breakGap; sceneEnd = plotSceneStart + (plotSceneEnd-plotSceneStart) * rb.position; logicalStart = logicalEndLast; logicalEnd = rb.start; if (sceneStart != sceneEnd) scales << this->createScale(yScale, sceneStart, sceneEnd, logicalStart, logicalEnd); sceneEndLast = sceneEnd; logicalEndLast = rb.end; } //add the remaining range going from the last available range break to the end of the plot (=end of the y-data range) sceneStart = sceneEndLast-breakGap; sceneEnd = plotSceneEnd; logicalStart = logicalEndLast; logicalEnd = yMax; if (sceneStart != sceneEnd) scales << this->createScale(yScale, sceneStart, sceneEnd, logicalStart, logicalEnd); } cSystem->setYScales(scales); //calculate the changes in x and y and save the current values for xMin, xMax, yMin, yMax double deltaXMin = 0; double deltaXMax = 0; double deltaYMin = 0; double deltaYMax = 0; if (xMin != xMinPrev) { deltaXMin = xMin - xMinPrev; emit q->xMinChanged(xMin); } if (xMax != xMaxPrev) { deltaXMax = xMax - xMaxPrev; emit q->xMaxChanged(xMax); } if (yMin != yMinPrev) { deltaYMin = yMin - yMinPrev; emit q->yMinChanged(yMin); } if (yMax != yMaxPrev) { deltaYMax = yMax - yMaxPrev; emit q->yMaxChanged(yMax); } xMinPrev = xMin; xMaxPrev = xMax; yMinPrev = yMin; yMaxPrev = yMax; //adjust auto-scale axes for (auto* axis : q->children()) { if (!axis->autoScale()) continue; if (axis->orientation() == Axis::AxisHorizontal) { if (deltaXMax != 0) { axis->setUndoAware(false); axis->setSuppressRetransform(true); axis->setEnd(xMax); axis->setUndoAware(true); axis->setSuppressRetransform(false); } if (deltaXMin != 0) { axis->setUndoAware(false); axis->setSuppressRetransform(true); axis->setStart(xMin); axis->setUndoAware(true); axis->setSuppressRetransform(false); } //TODO; // if (axis->position() == Axis::AxisCustom && deltaYMin != 0) { // axis->setOffset(axis->offset() + deltaYMin, false); // } } else { if (deltaYMax != 0) { axis->setUndoAware(false); axis->setSuppressRetransform(true); axis->setEnd(yMax); axis->setUndoAware(true); axis->setSuppressRetransform(false); } if (deltaYMin != 0) { axis->setUndoAware(false); axis->setSuppressRetransform(true); axis->setStart(yMin); axis->setUndoAware(true); axis->setSuppressRetransform(false); } //TODO; // if (axis->position() == Axis::AxisCustom && deltaXMin != 0) { // axis->setOffset(axis->offset() + deltaXMin, false); // } } } // call retransform() on the parent to trigger the update of all axes and curves. //no need to do this on load since all plots are retransformed again after the project is loaded. if (!q->isLoading()) q->retransform(); } /* * calculates the rectangular of the are showing the actual data (plot's rect minus padding), * in plot's coordinates. */ void CartesianPlotPrivate::updateDataRect() { dataRect = mapRectFromScene(rect); double paddingLeft = horizontalPadding; double paddingRight = rightPadding; double paddingTop = verticalPadding; double paddingBottom = bottomPadding; if (symmetricPadding) { paddingRight = horizontalPadding; paddingBottom = verticalPadding; } dataRect.setX(dataRect.x() + paddingLeft); dataRect.setY(dataRect.y() + paddingTop); double newHeight = dataRect.height() - paddingBottom; if (newHeight < 0) newHeight = 0; dataRect.setHeight(newHeight); double newWidth = dataRect.width() - paddingRight; if (newWidth < 0) newWidth = 0; dataRect.setWidth(newWidth); } void CartesianPlotPrivate::rangeChanged() { curvesXMinMaxIsDirty = true; curvesYMinMaxIsDirty = true; if (autoScaleX && autoScaleY) q->scaleAuto(); else if (autoScaleX) q->scaleAutoX(); else if (autoScaleY) q->scaleAutoY(); } void CartesianPlotPrivate::xRangeFormatChanged() { for (auto* axis : q->children()) { if (axis->orientation() == Axis::AxisHorizontal) axis->retransformTickLabelStrings(); } } void CartesianPlotPrivate::yRangeFormatChanged() { for (auto* axis : q->children()) { if (axis->orientation() == Axis::AxisVertical) axis->retransformTickLabelStrings(); } } /*! * don't allow any negative values for the x range when log or sqrt scalings are used */ void CartesianPlotPrivate::checkXRange() { double min = 0.01; if (xMin <= 0.0) { (min < xMax*min) ? xMin = min : xMin = xMax*min; emit q->xMinChanged(xMin); } else if (xMax <= 0.0) { (-min > xMin*min) ? xMax = -min : xMax = xMin*min; emit q->xMaxChanged(xMax); } } /*! * don't allow any negative values for the y range when log or sqrt scalings are used */ void CartesianPlotPrivate::checkYRange() { double min = 0.01; if (yMin <= 0.0) { (min < yMax*min) ? yMin = min : yMin = yMax*min; emit q->yMinChanged(yMin); } else if (yMax <= 0.0) { (-min > yMin*min) ? yMax = -min : yMax = yMin*min; emit q->yMaxChanged(yMax); } } CartesianScale* CartesianPlotPrivate::createScale(CartesianPlot::Scale type, double sceneStart, double sceneEnd, double logicalStart, double logicalEnd) { DEBUG("CartesianPlotPrivate::createScale() scene start/end = " << sceneStart << '/' << sceneEnd << ", logical start/end = " << logicalStart << '/' << logicalEnd); // Interval interval (logicalStart-0.01, logicalEnd+0.01); //TODO: move this to CartesianScale Interval interval (std::numeric_limits::lowest(), std::numeric_limits::max()); // Interval interval (logicalStart, logicalEnd); if (type == CartesianPlot::ScaleLinear) return CartesianScale::createLinearScale(interval, sceneStart, sceneEnd, logicalStart, logicalEnd); else return CartesianScale::createLogScale(interval, sceneStart, sceneEnd, logicalStart, logicalEnd, type); } /*! * Reimplemented from QGraphicsItem. */ QVariant CartesianPlotPrivate::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemPositionChange) { const QPointF& itemPos = value.toPointF();//item's center point in parent's coordinates; const qreal x = itemPos.x(); const qreal y = itemPos.y(); //calculate the new rect and forward the changes to the frontend QRectF newRect; const qreal w = rect.width(); const qreal h = rect.height(); newRect.setX(x-w/2); newRect.setY(y-h/2); newRect.setWidth(w); newRect.setHeight(h); emit q->rectChanged(newRect); } return QGraphicsItem::itemChange(change, value); } //############################################################################## //################################## Events ################################## //############################################################################## /*! * \brief CartesianPlotPrivate::mousePressEvent * In this function only basic stuff is done. The mousePressEvent is forwarded to the Worksheet, which * has access to all cartesian plots and can apply the changes to all plots if the option "applyToAll" * is set. The worksheet calls then the corresponding mousepressZoomMode/CursorMode function in this class * This is done for mousePress, mouseMove and mouseRelease event * This function sends a signal with the logical position, because this is the only value which is the same * in all plots. Using the scene coordinates is not possible * \param event */ void CartesianPlotPrivate::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (mouseMode == CartesianPlot::ZoomSelectionMode || mouseMode == CartesianPlot::ZoomXSelectionMode || mouseMode == CartesianPlot::ZoomYSelectionMode) emit q->mousePressZoomSelectionModeSignal(cSystem->mapSceneToLogical(event->pos(), AbstractCoordinateSystem::MappingFlag::Limit)); else if (mouseMode == CartesianPlot::Cursor) { setCursor(Qt::SizeHorCursor); QPointF logicalPos = cSystem->mapSceneToLogical(event->pos(), AbstractCoordinateSystem::MappingFlag::Limit); double cursorPenWidth2 = cursorPen.width()/2.; if (cursorPenWidth2 < 10.) cursorPenWidth2 = 10.; if (cursor0Enable && qAbs(event->pos().x()-cSystem->mapLogicalToScene(QPointF(cursor0Pos.x(),yMin)).x()) < cursorPenWidth2) { selectedCursor = 0; } else if (cursor1Enable && qAbs(event->pos().x()-cSystem->mapLogicalToScene(QPointF(cursor1Pos.x(),yMin)).x()) < cursorPenWidth2) { selectedCursor = 1; } else if (QApplication::keyboardModifiers() & Qt::ControlModifier){ cursor1Enable = true; selectedCursor = 1; emit q->cursor1EnableChanged(cursor1Enable); } else { cursor0Enable = true; selectedCursor = 0; emit q->cursor0EnableChanged(cursor0Enable); } emit q->mousePressCursorModeSignal(selectedCursor, logicalPos); } else { if (!locked && dataRect.contains(event->pos())) { panningStarted = true; m_panningStart = event->pos(); setCursor(Qt::ClosedHandCursor); } } QGraphicsItem::mousePressEvent(event); } void CartesianPlotPrivate::mousePressZoomSelectionMode(QPointF logicalPos) { if (mouseMode == CartesianPlot::ZoomSelectionMode) { if (logicalPos.x() < xMin) logicalPos.setX(xMin); if (logicalPos.x() > xMax) logicalPos.setX(xMax); if (logicalPos.y() < yMin) logicalPos.setY(yMin); if (logicalPos.y() > yMax) logicalPos.setY(yMax); m_selectionStart = cSystem->mapLogicalToScene(logicalPos, CartesianCoordinateSystem::SuppressPageClipping); } else if (mouseMode == CartesianPlot::ZoomXSelectionMode) { logicalPos.setY(yMin); // must be done, because the other plots can have other ranges, value must be in the scenes m_selectionStart.setX(cSystem->mapLogicalToScene(logicalPos, CartesianCoordinateSystem::SuppressPageClipping).x()); m_selectionStart.setY(dataRect.y()); } else if (mouseMode == CartesianPlot::ZoomYSelectionMode) { logicalPos.setX(xMin); // must be done, because the other plots can have other ranges, value must be in the scenes m_selectionStart.setX(dataRect.x()); m_selectionStart.setY(cSystem->mapLogicalToScene(logicalPos, CartesianCoordinateSystem::SuppressPageClipping).y()); } m_selectionEnd = m_selectionStart; m_selectionBandIsShown = true; } void CartesianPlotPrivate::mousePressCursorMode(int cursorNumber, QPointF logicalPos) { cursorNumber == 0 ? cursor0Enable = true : cursor1Enable = true; QPointF p1(logicalPos.x(), yMin); QPointF p2(logicalPos.x(), yMax); if (cursorNumber == 0) { cursor0Pos.setX(logicalPos.x()); cursor0Pos.setY(0); } else { cursor1Pos.setX(logicalPos.x()); cursor1Pos.setY(0); } update(); } void CartesianPlotPrivate::updateCursor() { update(); } void CartesianPlotPrivate::setZoomSelectionBandShow(bool show) { m_selectionBandIsShown = show; } void CartesianPlotPrivate::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { if (mouseMode == CartesianPlot::SelectionMode) { if (panningStarted && dataRect.contains(event->pos()) ) { //don't retransform on small mouse movement deltas const int deltaXScene = (m_panningStart.x() - event->pos().x()); const int deltaYScene = (m_panningStart.y() - event->pos().y()); if (abs(deltaXScene) < 5 && abs(deltaYScene) < 5) return; const QPointF logicalEnd = cSystem->mapSceneToLogical(event->pos()); const QPointF logicalStart = cSystem->mapSceneToLogical(m_panningStart); //handle the change in x switch (xScale) { case CartesianPlot::ScaleLinear: { const float deltaX = (logicalStart.x() - logicalEnd.x()); xMax += deltaX; xMin += deltaX; break; } case CartesianPlot::ScaleLog10: case CartesianPlot::ScaleLog10Abs: { const float deltaX = log10(logicalStart.x()) - log10(logicalEnd.x()); xMin *= pow(10, deltaX); xMax *= pow(10, deltaX); break; } case CartesianPlot::ScaleLog2: case CartesianPlot::ScaleLog2Abs: { const float deltaX = log2(logicalStart.x()) - log2(logicalEnd.x()); xMin *= pow(2, deltaX); xMax *= pow(2, deltaX); break; } case CartesianPlot::ScaleLn: case CartesianPlot::ScaleLnAbs: { const float deltaX = log(logicalStart.x()) - log(logicalEnd.x()); xMin *= exp(deltaX); xMax *= exp(deltaX); break; } case CartesianPlot::ScaleSqrt: case CartesianPlot::ScaleX2: break; } //handle the change in y switch (yScale) { case CartesianPlot::ScaleLinear: { const float deltaY = (logicalStart.y() - logicalEnd.y()); yMax += deltaY; yMin += deltaY; break; } case CartesianPlot::ScaleLog10: case CartesianPlot::ScaleLog10Abs: { const float deltaY = log10(logicalStart.y()) - log10(logicalEnd.y()); yMin *= pow(10, deltaY); yMax *= pow(10, deltaY); break; } case CartesianPlot::ScaleLog2: case CartesianPlot::ScaleLog2Abs: { const float deltaY = log2(logicalStart.y()) - log2(logicalEnd.y()); yMin *= pow(2, deltaY); yMax *= pow(2, deltaY); break; } case CartesianPlot::ScaleLn: case CartesianPlot::ScaleLnAbs: { const float deltaY = log(logicalStart.y()) - log(logicalEnd.y()); yMin *= exp(deltaY); yMax *= exp(deltaY); break; } case CartesianPlot::ScaleSqrt: case CartesianPlot::ScaleX2: break; } q->setUndoAware(false); q->setAutoScaleX(false); q->setAutoScaleY(false); q->setUndoAware(true); retransformScales(); m_panningStart = event->pos(); } else QGraphicsItem::mouseMoveEvent(event); } else if (mouseMode == CartesianPlot::ZoomSelectionMode || mouseMode == CartesianPlot::ZoomXSelectionMode || mouseMode == CartesianPlot::ZoomYSelectionMode) { QGraphicsItem::mouseMoveEvent(event); if ( !boundingRect().contains(event->pos()) ) { q->info(QString()); return; } emit q->mouseMoveZoomSelectionModeSignal(cSystem->mapSceneToLogical(event->pos(), CartesianCoordinateSystem::MappingFlag::Limit)); } else if (mouseMode == CartesianPlot::Cursor) { QGraphicsItem::mouseMoveEvent(event); if (!boundingRect().contains(event->pos())) { q->info(i18n("Not inside of the bounding rect")); return; } QPointF logicalPos = cSystem->mapSceneToLogical(event->pos(), AbstractCoordinateSystem::MappingFlag::Limit); // updating treeview data and cursor position // updatign cursor position is done in Worksheet, because // multiple plots must be updated emit q->mouseMoveCursorModeSignal(selectedCursor, logicalPos); } } void CartesianPlotPrivate::mouseMoveZoomSelectionMode(QPointF logicalPos) { QString info; QPointF logicalStart = cSystem->mapSceneToLogical(m_selectionStart, CartesianCoordinateSystem::MappingFlag::SuppressPageClipping); if (mouseMode == CartesianPlot::ZoomSelectionMode) { m_selectionEnd = cSystem->mapLogicalToScene(logicalPos, CartesianCoordinateSystem::MappingFlag::SuppressPageClipping); QPointF logicalEnd = logicalPos; if (xRangeFormat == CartesianPlot::Numeric) info = QString::fromUtf8("Δx=") + QString::number(logicalEnd.x()-logicalStart.x()); else info = i18n("from x=%1 to x=%2", QDateTime::fromMSecsSinceEpoch(logicalStart.x()).toString(xRangeDateTimeFormat), QDateTime::fromMSecsSinceEpoch(logicalEnd.x()).toString(xRangeDateTimeFormat)); info += QLatin1String(", "); if (yRangeFormat == CartesianPlot::Numeric) info += QString::fromUtf8("Δy=") + QString::number(logicalEnd.y()-logicalStart.y()); else info += i18n("from y=%1 to y=%2", QDateTime::fromMSecsSinceEpoch(logicalStart.y()).toString(xRangeDateTimeFormat), QDateTime::fromMSecsSinceEpoch(logicalEnd.y()).toString(xRangeDateTimeFormat)); } else if (mouseMode == CartesianPlot::ZoomXSelectionMode) { logicalPos.setY(yMin); // must be done, because the other plots can have other ranges, value must be in the scenes m_selectionEnd.setX(cSystem->mapLogicalToScene(logicalPos, CartesianCoordinateSystem::MappingFlag::SuppressPageClipping).x());//event->pos().x()); m_selectionEnd.setY(dataRect.bottom()); QPointF logicalEnd = logicalPos; if (xRangeFormat == CartesianPlot::Numeric) info = QString::fromUtf8("Δx=") + QString::number(logicalEnd.x()-logicalStart.x()); else info = i18n("from x=%1 to x=%2", QDateTime::fromMSecsSinceEpoch(logicalStart.x()).toString(xRangeDateTimeFormat), QDateTime::fromMSecsSinceEpoch(logicalEnd.x()).toString(xRangeDateTimeFormat)); } else if (mouseMode == CartesianPlot::ZoomYSelectionMode) { m_selectionEnd.setX(dataRect.right()); logicalPos.setX(xMin); // must be done, because the other plots can have other ranges, value must be in the scenes m_selectionEnd.setY(cSystem->mapLogicalToScene(logicalPos, CartesianCoordinateSystem::MappingFlag::SuppressPageClipping).y());//event->pos().y()); QPointF logicalEnd = logicalPos; if (yRangeFormat == CartesianPlot::Numeric) info = QString::fromUtf8("Δy=") + QString::number(logicalEnd.y()-logicalStart.y()); else info = i18n("from y=%1 to y=%2", QDateTime::fromMSecsSinceEpoch(logicalStart.y()).toString(xRangeDateTimeFormat), QDateTime::fromMSecsSinceEpoch(logicalEnd.y()).toString(xRangeDateTimeFormat)); } q->info(info); update(); } void CartesianPlotPrivate::mouseMoveCursorMode(int cursorNumber, QPointF logicalPos) { QPointF p1(logicalPos.x(), 0); cursorNumber == 0 ? cursor0Pos = p1 : cursor1Pos = p1; QString info; if (xRangeFormat == CartesianPlot::Numeric) info = QString::fromUtf8("x=") + QString::number(logicalPos.x()); else info = i18n("x=%1", QDateTime::fromMSecsSinceEpoch(logicalPos.x()).toString(xRangeDateTimeFormat)); q->info(info); update(); } void CartesianPlotPrivate::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { setCursor(Qt::ArrowCursor); if (mouseMode == CartesianPlot::SelectionMode) { panningStarted = false; //TODO: why do we do this all the time?!?! const QPointF& itemPos = pos();//item's center point in parent's coordinates; const qreal x = itemPos.x(); const qreal y = itemPos.y(); //calculate the new rect and set it QRectF newRect; const qreal w = rect.width(); const qreal h = rect.height(); newRect.setX(x-w/2); newRect.setY(y-h/2); newRect.setWidth(w); newRect.setHeight(h); suppressRetransform = true; q->setRect(newRect); suppressRetransform = false; QGraphicsItem::mouseReleaseEvent(event); } else if (mouseMode == CartesianPlot::ZoomSelectionMode || mouseMode == CartesianPlot::ZoomXSelectionMode || mouseMode == CartesianPlot::ZoomYSelectionMode) { emit q->mouseReleaseZoomSelectionModeSignal(); } } void CartesianPlotPrivate::mouseReleaseZoomSelectionMode() { //don't zoom if very small region was selected, avoid occasional/unwanted zooming if ( qAbs(m_selectionEnd.x()-m_selectionStart.x()) < 20 || qAbs(m_selectionEnd.y()-m_selectionStart.y()) < 20 ) { m_selectionBandIsShown = false; return; } bool retransformPlot = true; //determine the new plot ranges QPointF logicalZoomStart = cSystem->mapSceneToLogical(m_selectionStart, AbstractCoordinateSystem::SuppressPageClipping); QPointF logicalZoomEnd = cSystem->mapSceneToLogical(m_selectionEnd, AbstractCoordinateSystem::SuppressPageClipping); if (m_selectionEnd.x() > m_selectionStart.x()) { xMin = logicalZoomStart.x(); xMax = logicalZoomEnd.x(); } else { xMin = logicalZoomEnd.x(); xMax = logicalZoomStart.x(); } if (m_selectionEnd.y() > m_selectionStart.y()) { yMin = logicalZoomEnd.y(); yMax = logicalZoomStart.y(); } else { yMin = logicalZoomStart.y(); yMax = logicalZoomEnd.y(); } if (mouseMode == CartesianPlot::ZoomSelectionMode) { curvesXMinMaxIsDirty = true; curvesYMinMaxIsDirty = true; q->setAutoScaleX(false); q->setAutoScaleY(false); } else if (mouseMode == CartesianPlot::ZoomXSelectionMode) { curvesYMinMaxIsDirty = true; q->setAutoScaleX(false); if (q->autoScaleY() && q->scaleAutoY()) retransformPlot = false; } else if (mouseMode == CartesianPlot::ZoomYSelectionMode) { curvesXMinMaxIsDirty = true; q->setAutoScaleY(false); if (q->autoScaleX() && q->scaleAutoX()) retransformPlot = false; } if (retransformPlot) retransformScales(); m_selectionBandIsShown = false; } void CartesianPlotPrivate::wheelEvent(QGraphicsSceneWheelEvent* event) { if (locked) return; //determine first, which axes are selected and zoom only in the corresponding direction. //zoom the entire plot if no axes selected. bool zoomX = false; bool zoomY = false; for (auto* axis : q->children()) { if (!axis->graphicsItem()->isSelected()) continue; if (axis->orientation() == Axis::AxisHorizontal) zoomX = true; else zoomY = true; } if (event->delta() > 0) { if (!zoomX && !zoomY) { //no special axis selected -> zoom in everything q->zoomIn(); } else { if (zoomX) q->zoomInX(); if (zoomY) q->zoomInY(); } } else { if (!zoomX && !zoomY) { //no special axis selected -> zoom in everything q->zoomOut(); } else { if (zoomX) q->zoomOutX(); if (zoomY) q->zoomOutY(); } } } void CartesianPlotPrivate::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Escape) { setCursor(Qt::ArrowCursor); q->setMouseMode(CartesianPlot::MouseMode::SelectionMode); m_selectionBandIsShown = false; } else if (event->key() == Qt::Key_Left || event->key() == Qt::Key_Right || event->key() == Qt::Key_Up ||event->key() == Qt::Key_Down) { const auto* worksheet = static_cast(q->parentAspect()); if (worksheet->layout() == Worksheet::NoLayout) { const int delta = 5; QRectF rect = q->rect(); if (event->key() == Qt::Key_Left) { rect.setX(rect.x() - delta); rect.setWidth(rect.width() - delta); } else if (event->key() == Qt::Key_Right) { rect.setX(rect.x() + delta); rect.setWidth(rect.width() + delta); } else if (event->key() == Qt::Key_Up) { rect.setY(rect.y() - delta); rect.setHeight(rect.height() - delta); } else if (event->key() == Qt::Key_Down) { rect.setY(rect.y() + delta); rect.setHeight(rect.height() + delta); } q->setRect(rect); } } QGraphicsItem::keyPressEvent(event); } void CartesianPlotPrivate::hoverMoveEvent(QGraphicsSceneHoverEvent* event) { QPointF point = event->pos(); QString info; if (dataRect.contains(point)) { QPointF logicalPoint = cSystem->mapSceneToLogical(point); if ((mouseMode == CartesianPlot::ZoomSelectionMode) || mouseMode == CartesianPlot::SelectionMode) { info = "x="; if (xRangeFormat == CartesianPlot::Numeric) info += QString::number(logicalPoint.x()); else info += QDateTime::fromMSecsSinceEpoch(logicalPoint.x()).toString(xRangeDateTimeFormat); info += ", y="; if (yRangeFormat == CartesianPlot::Numeric) info += QString::number(logicalPoint.y()); else info += QDateTime::fromMSecsSinceEpoch(logicalPoint.y()).toString(yRangeDateTimeFormat); } if (mouseMode == CartesianPlot::ZoomSelectionMode && !m_selectionBandIsShown) { emit q->mouseHoverZoomSelectionModeSignal(logicalPoint); } else if (mouseMode == CartesianPlot::ZoomXSelectionMode && !m_selectionBandIsShown) { info = "x="; if (xRangeFormat == CartesianPlot::Numeric) info += QString::number(logicalPoint.x()); else info += QDateTime::fromMSecsSinceEpoch(logicalPoint.x()).toString(xRangeDateTimeFormat); emit q->mouseHoverZoomSelectionModeSignal(logicalPoint); } else if (mouseMode == CartesianPlot::ZoomYSelectionMode && !m_selectionBandIsShown) { info = "y="; if (yRangeFormat == CartesianPlot::Numeric) info += QString::number(logicalPoint.y()); else info += QDateTime::fromMSecsSinceEpoch(logicalPoint.y()).toString(yRangeDateTimeFormat); emit q->mouseHoverZoomSelectionModeSignal(logicalPoint); } else if (mouseMode == CartesianPlot::MouseMode::SelectionMode) { // hover the nearest curve to the mousepointer // hovering curves is implemented in the parent, because no ignoreEvent() exists // for it. Checking all curves and hover the first bool curve_hovered = false; QVector curves = q->children(); for (int i=curves.count() - 1; i >= 0; i--){ // because the last curve is above the other curves if (curve_hovered){ // if a curve is already hovered, disable hover for the rest curves[i]->setHover(false); continue; } if (curves[i]->activateCurve(event->pos())){ curves[i]->setHover(true); curve_hovered = true; continue; } curves[i]->setHover(false); } } else if (mouseMode == CartesianPlot::Cursor){ info = "x="; if (yRangeFormat == CartesianPlot::Numeric) info += QString::number(logicalPoint.x()); else info += QDateTime::fromMSecsSinceEpoch(logicalPoint.x()).toString(xRangeDateTimeFormat); double cursorPenWidth2 = cursorPen.width()/2.; if (cursorPenWidth2 < 10.) cursorPenWidth2 = 10.; if ((cursor0Enable && qAbs(point.x()-cSystem->mapLogicalToScene(QPointF(cursor0Pos.x(),yMin)).x()) < cursorPenWidth2) || (cursor1Enable && qAbs(point.x()-cSystem->mapLogicalToScene(QPointF(cursor1Pos.x(),yMin)).x()) < cursorPenWidth2)) setCursor(Qt::SizeHorCursor); else setCursor(Qt::ArrowCursor); update(); } } else emit q->mouseHoverOutsideDataRectSignal(); q->info(info); QGraphicsItem::hoverMoveEvent(event); } void CartesianPlotPrivate::mouseHoverOutsideDataRect() { m_insideDataRect = false; update(); } void CartesianPlotPrivate::hoverLeaveEvent(QGraphicsSceneHoverEvent* event) { QVector curves = q->children(); for (auto* curve : curves) curve->setHover(false); m_hovered = false; QGraphicsItem::hoverLeaveEvent(event); } void CartesianPlotPrivate::mouseHoverZoomSelectionMode(QPointF logicPos) { m_insideDataRect = true; if (mouseMode == CartesianPlot::ZoomSelectionMode && !m_selectionBandIsShown) { } else if (mouseMode == CartesianPlot::ZoomXSelectionMode && !m_selectionBandIsShown) { QPointF p1(logicPos.x(), yMin); QPointF p2(logicPos.x(), yMax); m_selectionStartLine.setP1(cSystem->mapLogicalToScene(p1, CartesianCoordinateSystem::MappingFlag::Limit)); m_selectionStartLine.setP2(cSystem->mapLogicalToScene(p2, CartesianCoordinateSystem::MappingFlag::Limit)); } else if (mouseMode == CartesianPlot::ZoomYSelectionMode && !m_selectionBandIsShown) { QPointF p1(xMin, logicPos.y()); QPointF p2(xMax, logicPos.y()); m_selectionStartLine.setP1(cSystem->mapLogicalToScene(p1, CartesianCoordinateSystem::MappingFlag::Limit)); m_selectionStartLine.setP2(cSystem->mapLogicalToScene(p2, CartesianCoordinateSystem::MappingFlag::Limit)); } update(); // because if previous another selection mode was selected, the lines must be deleted } void CartesianPlotPrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { Q_UNUSED(option) Q_UNUSED(widget) if (!isVisible()) return; if (!m_printing) { painter->save(); painter->setPen(cursorPen); QFont font = painter->font(); font.setPointSize(font.pointSize() * 4); painter->setFont(font); QPointF p1 = cSystem->mapLogicalToScene(QPointF(cursor0Pos.x(),yMin)); if (cursor0Enable && p1 != QPointF(0,0)){ QPointF p2 = cSystem->mapLogicalToScene(QPointF(cursor0Pos.x(),yMax)); painter->drawLine(p1,p2); QPointF textPos = p2; textPos.setX(p2.x() - m_cursor0Text.size().width()/2); textPos.setY(p2.y() - m_cursor0Text.size().height()); if (textPos.y() < boundingRect().y()) textPos.setY(boundingRect().y()); painter->drawStaticText(textPos, m_cursor0Text); } p1 = cSystem->mapLogicalToScene(QPointF(cursor1Pos.x(),yMin)); if (cursor1Enable && p1 != QPointF(0,0)){ QPointF p2 = cSystem->mapLogicalToScene(QPointF(cursor1Pos.x(),yMax)); painter->drawLine(p1,p2); QPointF textPos = p2; // TODO: Moving this stuff into other function to not calculate it every time textPos.setX(p2.x() - m_cursor1Text.size().width()/2); textPos.setY(p2.y() - m_cursor1Text.size().height()); if (textPos.y() < boundingRect().y()) textPos.setY(boundingRect().y()); painter->drawStaticText(textPos, m_cursor1Text); } painter->restore(); } painter->setPen(QPen(Qt::black, 3)); if ((mouseMode == CartesianPlot::ZoomXSelectionMode || mouseMode == CartesianPlot::ZoomYSelectionMode) && (!m_selectionBandIsShown) && m_insideDataRect) painter->drawLine(m_selectionStartLine); if (m_selectionBandIsShown) { QPointF selectionStart = m_selectionStart; if (m_selectionStart.x() > dataRect.right()) selectionStart.setX(dataRect.right()); if (m_selectionStart.x() < dataRect.left()) selectionStart.setX(dataRect.left()); if (m_selectionStart.y() > dataRect.bottom()) selectionStart.setY(dataRect.bottom()); if (m_selectionStart.y() < dataRect.top()) selectionStart.setY(dataRect.top()); QPointF selectionEnd = m_selectionEnd; if (m_selectionEnd.x() > dataRect.right()) selectionEnd.setX(dataRect.right()); if (m_selectionEnd.x() < dataRect.left()) selectionEnd.setX(dataRect.left()); if (m_selectionEnd.y() > dataRect.bottom()) selectionEnd.setY(dataRect.bottom()); if (m_selectionEnd.y() < dataRect.top()) selectionEnd.setY(dataRect.top()); painter->save(); painter->setPen(QPen(Qt::black, 5)); painter->drawRect(QRectF(selectionStart, selectionEnd)); painter->setBrush(Qt::blue); painter->setOpacity(0.2); painter->drawRect(QRectF(selectionStart, selectionEnd)); painter->restore(); } } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## //! Save as XML void CartesianPlot::save(QXmlStreamWriter* writer) const { Q_D(const CartesianPlot); writer->writeStartElement( "cartesianPlot" ); writeBasicAttributes(writer); writeCommentElement(writer); //applied theme if (!d->theme.isEmpty()) { writer->writeStartElement( "theme" ); writer->writeAttribute("name", d->theme); writer->writeEndElement(); } //cursor writer->writeStartElement( "cursor" ); WRITE_QPEN(d->cursorPen); writer->writeEndElement(); //geometry writer->writeStartElement( "geometry" ); writer->writeAttribute( "x", QString::number(d->rect.x()) ); writer->writeAttribute( "y", QString::number(d->rect.y()) ); writer->writeAttribute( "width", QString::number(d->rect.width()) ); writer->writeAttribute( "height", QString::number(d->rect.height()) ); writer->writeAttribute( "visible", QString::number(d->isVisible()) ); writer->writeEndElement(); //coordinate system and padding writer->writeStartElement( "coordinateSystem" ); writer->writeAttribute( "autoScaleX", QString::number(d->autoScaleX) ); writer->writeAttribute( "autoScaleY", QString::number(d->autoScaleY) ); writer->writeAttribute( "xMin", QString::number(d->xMin, 'g', 16)); writer->writeAttribute( "xMax", QString::number(d->xMax, 'g', 16) ); writer->writeAttribute( "yMin", QString::number(d->yMin, 'g', 16) ); writer->writeAttribute( "yMax", QString::number(d->yMax, 'g', 16) ); writer->writeAttribute( "xScale", QString::number(d->xScale) ); writer->writeAttribute( "yScale", QString::number(d->yScale) ); writer->writeAttribute( "xRangeFormat", QString::number(d->xRangeFormat) ); writer->writeAttribute( "yRangeFormat", QString::number(d->yRangeFormat) ); writer->writeAttribute( "horizontalPadding", QString::number(d->horizontalPadding) ); writer->writeAttribute( "verticalPadding", QString::number(d->verticalPadding) ); writer->writeAttribute( "rightPadding", QString::number(d->rightPadding) ); writer->writeAttribute( "bottomPadding", QString::number(d->bottomPadding) ); writer->writeAttribute( "symmetricPadding", QString::number(d->symmetricPadding)); writer->writeEndElement(); //x-scale breaks if (d->xRangeBreakingEnabled || !d->xRangeBreaks.list.isEmpty()) { writer->writeStartElement("xRangeBreaks"); writer->writeAttribute( "enabled", QString::number(d->xRangeBreakingEnabled) ); for (const auto& rb : d->xRangeBreaks.list) { writer->writeStartElement("xRangeBreak"); writer->writeAttribute("start", QString::number(rb.start)); writer->writeAttribute("end", QString::number(rb.end)); writer->writeAttribute("position", QString::number(rb.position)); writer->writeAttribute("style", QString::number(rb.style)); writer->writeEndElement(); } writer->writeEndElement(); } //y-scale breaks if (d->yRangeBreakingEnabled || !d->yRangeBreaks.list.isEmpty()) { writer->writeStartElement("yRangeBreaks"); writer->writeAttribute( "enabled", QString::number(d->yRangeBreakingEnabled) ); for (const auto& rb : d->yRangeBreaks.list) { writer->writeStartElement("yRangeBreak"); writer->writeAttribute("start", QString::number(rb.start)); writer->writeAttribute("end", QString::number(rb.end)); writer->writeAttribute("position", QString::number(rb.position)); writer->writeAttribute("style", QString::number(rb.style)); writer->writeEndElement(); } writer->writeEndElement(); } //serialize all children (plot area, title text label, axes and curves) - for (auto* elem : children(IncludeHidden)) + for (auto* elem : children(ChildIndexFlag::IncludeHidden)) elem->save(writer); writer->writeEndElement(); // close "cartesianPlot" section } //! Load from XML bool CartesianPlot::load(XmlStreamReader* reader, bool preview) { Q_D(CartesianPlot); if (!readBasicAttributes(reader)) return false; KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used"); QXmlStreamAttributes attribs; QString str; bool titleLabelRead = false; while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "cartesianPlot") break; if (!reader->isStartElement()) continue; if (reader->name() == "comment") { if (!readCommentElement(reader)) return false; } else if (!preview && reader->name() == "theme") { attribs = reader->attributes(); d->theme = attribs.value("name").toString(); } else if (!preview && reader->name() == "cursor") { attribs = reader->attributes(); QPen pen; pen.setWidth(attribs.value("width").toInt()); pen.setStyle(static_cast(attribs.value("style").toInt())); QColor color; color.setRed(attribs.value("color_r").toInt()); color.setGreen(attribs.value("color_g").toInt()); color.setBlue(attribs.value("color_b").toInt()); pen.setColor(color); d->cursorPen = pen; } else if (!preview && reader->name() == "geometry") { attribs = reader->attributes(); str = attribs.value("x").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("x").toString()); else d->rect.setX( str.toDouble() ); str = attribs.value("y").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("y").toString()); else d->rect.setY( str.toDouble() ); str = attribs.value("width").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("width").toString()); else d->rect.setWidth( str.toDouble() ); str = attribs.value("height").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("height").toString()); else d->rect.setHeight( str.toDouble() ); str = attribs.value("visible").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("visible").toString()); else d->setVisible(str.toInt()); } else if (!preview && reader->name() == "coordinateSystem") { attribs = reader->attributes(); READ_INT_VALUE("autoScaleX", autoScaleX, bool); READ_INT_VALUE("autoScaleY", autoScaleY, bool); str = attribs.value("xMin").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("xMin").toString()); else { d->xMin = str.toDouble(); d->xMinPrev = d->xMin; } str = attribs.value("xMax").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("xMax").toString()); else { d->xMax = str.toDouble(); d->xMaxPrev = d->xMax; } str = attribs.value("yMin").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("yMin").toString()); else { d->yMin = str.toDouble(); d->yMinPrev = d->yMin; } str = attribs.value("yMax").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("yMax").toString()); else { d->yMax = str.toDouble(); d->yMaxPrev = d->yMax; } READ_INT_VALUE("xScale", xScale, CartesianPlot::Scale); READ_INT_VALUE("yScale", yScale, CartesianPlot::Scale); READ_INT_VALUE("xRangeFormat", xRangeFormat, CartesianPlot::RangeFormat); READ_INT_VALUE("yRangeFormat", yRangeFormat, CartesianPlot::RangeFormat); READ_DOUBLE_VALUE("horizontalPadding", horizontalPadding); READ_DOUBLE_VALUE("verticalPadding", verticalPadding); READ_DOUBLE_VALUE("rightPadding", rightPadding); READ_DOUBLE_VALUE("bottomPadding", bottomPadding); READ_INT_VALUE("symmetricPadding", symmetricPadding, bool); } else if (!preview && reader->name() == "xRangeBreaks") { //delete default rang break d->xRangeBreaks.list.clear(); attribs = reader->attributes(); READ_INT_VALUE("enabled", xRangeBreakingEnabled, bool); } else if (!preview && reader->name() == "xRangeBreak") { attribs = reader->attributes(); RangeBreak b; str = attribs.value("start").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("start").toString()); else b.start = str.toDouble(); str = attribs.value("end").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("end").toString()); else b.end = str.toDouble(); str = attribs.value("position").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("position").toString()); else b.position = str.toDouble(); str = attribs.value("style").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("style").toString()); else b.style = CartesianPlot::RangeBreakStyle(str.toInt()); d->xRangeBreaks.list << b; } else if (!preview && reader->name() == "yRangeBreaks") { //delete default rang break d->yRangeBreaks.list.clear(); attribs = reader->attributes(); READ_INT_VALUE("enabled", yRangeBreakingEnabled, bool); } else if (!preview && reader->name() == "yRangeBreak") { attribs = reader->attributes(); RangeBreak b; str = attribs.value("start").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("start").toString()); else b.start = str.toDouble(); str = attribs.value("end").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("end").toString()); else b.end = str.toDouble(); str = attribs.value("position").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("position").toString()); else b.position = str.toDouble(); str = attribs.value("style").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("style").toString()); else b.style = CartesianPlot::RangeBreakStyle(str.toInt()); d->yRangeBreaks.list << b; } else if (reader->name() == "textLabel") { if (!titleLabelRead) { //the first text label is always the title label m_title->load(reader, preview); titleLabelRead = true; //TODO: the name is read in m_title->load() but we overwrite it here //since the old projects don't have this " - Title" appendix yet that we add in init(). //can be removed in couple of releases m_title->setName(name() + QLatin1String(" - ") + i18n("Title")); } else { TextLabel* label = new TextLabel("text label"); if (label->load(reader, preview)) { addChildFast(label); label->setParentGraphicsItem(graphicsItem()); } else { delete label; return false; } } } else if (reader->name() == "image") { Image* image = new Image(QString()); if (!image->load(reader, preview)) { delete image; return false; } else addChildFast(image); } else if (reader->name() == "plotArea") m_plotArea->load(reader, preview); else if (reader->name() == "axis") { Axis* axis = new Axis(QString()); if (axis->load(reader, preview)) addChildFast(axis); else { delete axis; return false; } } else if (reader->name() == "xyCurve") { XYCurve* curve = new XYCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyEquationCurve") { XYEquationCurve* curve = new XYEquationCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyDataReductionCurve") { XYDataReductionCurve* curve = new XYDataReductionCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyDifferentiationCurve") { XYDifferentiationCurve* curve = new XYDifferentiationCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyIntegrationCurve") { XYIntegrationCurve* curve = new XYIntegrationCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyInterpolationCurve") { XYInterpolationCurve* curve = new XYInterpolationCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xySmoothCurve") { XYSmoothCurve* curve = new XYSmoothCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyFitCurve") { XYFitCurve* curve = new XYFitCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyFourierFilterCurve") { XYFourierFilterCurve* curve = new XYFourierFilterCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyFourierTransformCurve") { XYFourierTransformCurve* curve = new XYFourierTransformCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyConvolutionCurve") { XYConvolutionCurve* curve = new XYConvolutionCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "xyCorrelationCurve") { XYCorrelationCurve* curve = new XYCorrelationCurve(QString()); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else if (reader->name() == "cartesianPlotLegend") { m_legend = new CartesianPlotLegend(this, QString()); if (m_legend->load(reader, preview)) addChildFast(m_legend); else { delete m_legend; return false; } } else if (reader->name() == "customPoint") { CustomPoint* point = new CustomPoint(this, QString()); if (point->load(reader, preview)) addChildFast(point); else { delete point; return false; } } else if (reader->name() == "referenceLine") { ReferenceLine* line = new ReferenceLine(this, QString()); if (line->load(reader, preview)) addChildFast(line); else { delete line; return false; } } else if (reader->name() == "Histogram") { Histogram* curve = new Histogram("Histogram"); if (curve->load(reader, preview)) addChildFast(curve); else { removeChild(curve); return false; } } else { // unknown element reader->raiseWarning(i18n("unknown cartesianPlot element '%1'", reader->name().toString())); if (!reader->skipToEndElement()) return false; } } if (preview) return true; d->retransform(); //if a theme was used, initialize the color palette if (!d->theme.isEmpty()) { //TODO: check whether the theme config really exists KConfig config( ThemeHandler::themeFilePath(d->theme), KConfig::SimpleConfig ); this->setColorPalette(config); } else { //initialize the color palette with default colors this->setColorPalette(KConfig()); } return true; } //############################################################################## //######################### Theme management ################################## //############################################################################## void CartesianPlot::loadTheme(const QString& theme) { KConfig config(ThemeHandler::themeFilePath(theme), KConfig::SimpleConfig); loadThemeConfig(config); } void CartesianPlot::loadThemeConfig(const KConfig& config) { QString str = config.name(); // theme path is saved with UNIX dir separator str = str.right(str.length() - str.lastIndexOf(QLatin1Char('/')) - 1); DEBUG(" set theme to " << STDSTRING(str)); this->setTheme(str); //load the color palettes for the curves this->setColorPalette(config); //load the theme for all the children - for (auto* child : children(AbstractAspect::IncludeHidden)) + for (auto* child : children(ChildIndexFlag::IncludeHidden)) child->loadThemeConfig(config); Q_D(CartesianPlot); d->update(this->rect()); } void CartesianPlot::saveTheme(KConfig &config) { - const QVector& axisElements = children(AbstractAspect::IncludeHidden); - const QVector& plotAreaElements = children(AbstractAspect::IncludeHidden); - const QVector& textLabelElements = children(AbstractAspect::IncludeHidden); + const QVector& axisElements = children(ChildIndexFlag::IncludeHidden); + const QVector& plotAreaElements = children(ChildIndexFlag::IncludeHidden); + const QVector& textLabelElements = children(ChildIndexFlag::IncludeHidden); axisElements.at(0)->saveThemeConfig(config); plotAreaElements.at(0)->saveThemeConfig(config); textLabelElements.at(0)->saveThemeConfig(config); - for (auto *child : children(AbstractAspect::IncludeHidden)) + for (auto *child : children(ChildIndexFlag::IncludeHidden)) child->saveThemeConfig(config); } //Generating colors from 5-color theme palette void CartesianPlot::setColorPalette(const KConfig& config) { if (config.hasGroup(QLatin1String("Theme"))) { KConfigGroup group = config.group(QLatin1String("Theme")); //read the five colors defining the palette m_themeColorPalette.clear(); m_themeColorPalette.append(group.readEntry("ThemePaletteColor1", QColor())); m_themeColorPalette.append(group.readEntry("ThemePaletteColor2", QColor())); m_themeColorPalette.append(group.readEntry("ThemePaletteColor3", QColor())); m_themeColorPalette.append(group.readEntry("ThemePaletteColor4", QColor())); m_themeColorPalette.append(group.readEntry("ThemePaletteColor5", QColor())); } else { //no theme is available, provide 5 "default colors" m_themeColorPalette.clear(); m_themeColorPalette.append(QColor(25, 25, 25)); m_themeColorPalette.append(QColor(0, 0, 127)); m_themeColorPalette.append(QColor(127 ,0, 0)); m_themeColorPalette.append(QColor(0, 127, 0)); m_themeColorPalette.append(QColor(85, 0, 127)); } //generate 30 additional shades if the color palette contains more than one color if (m_themeColorPalette.at(0) != m_themeColorPalette.at(1)) { QColor c; //3 factors to create shades from theme's palette std::array fac = {0.25f, 0.45f, 0.65f}; //Generate 15 lighter shades for (int i = 0; i < 5; i++) { for (int j = 1; j < 4; j++) { c.setRed( m_themeColorPalette.at(i).red()*(1-fac[j-1]) ); c.setGreen( m_themeColorPalette.at(i).green()*(1-fac[j-1]) ); c.setBlue( m_themeColorPalette.at(i).blue()*(1-fac[j-1]) ); m_themeColorPalette.append(c); } } //Generate 15 darker shades for (int i = 0; i < 5; i++) { for (int j = 4; j < 7; j++) { c.setRed( m_themeColorPalette.at(i).red()+((255-m_themeColorPalette.at(i).red())*fac[j-4]) ); c.setGreen( m_themeColorPalette.at(i).green()+((255-m_themeColorPalette.at(i).green())*fac[j-4]) ); c.setBlue( m_themeColorPalette.at(i).blue()+((255-m_themeColorPalette.at(i).blue())*fac[j-4]) ); m_themeColorPalette.append(c); } } } } const QList& CartesianPlot::themeColorPalette() const { return m_themeColorPalette; } diff --git a/src/commonfrontend/ProjectExplorer.cpp b/src/commonfrontend/ProjectExplorer.cpp index 6df6d4653..68635979d 100644 --- a/src/commonfrontend/ProjectExplorer.cpp +++ b/src/commonfrontend/ProjectExplorer.cpp @@ -1,943 +1,943 @@ /*************************************************************************** File : ProjectExplorer.cpp Project : LabPlot Description : A tree view for displaying and editing an AspectTreeModel. -------------------------------------------------------------------- Copyright : (C) 2007-2008 by Tilman Benkert (thzs@gmx.net) Copyright : (C) 2010-2018 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 "ProjectExplorer.h" #include "backend/core/AspectTreeModel.h" #include "backend/core/AbstractPart.h" #include "backend/core/Project.h" #include "backend/lib/XmlStreamReader.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "commonfrontend/core/PartMdiView.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /*! \class ProjectExplorer \brief A tree view for displaying and editing an AspectTreeModel. In addition to the functionality of QTreeView, ProjectExplorer allows the usage of the context menus provided by AspectTreeModel and propagates the item selection in the view to the model. Furthermore, features for searching and filtering in the model are provided. \ingroup commonfrontend */ ProjectExplorer::ProjectExplorer(QWidget* parent) : m_treeView(new QTreeView(parent)), m_frameFilter(new QFrame(this)) { auto* layout = new QVBoxLayout(this); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); auto* layoutFilter = new QHBoxLayout(m_frameFilter); layoutFilter->setSpacing(0); layoutFilter->setContentsMargins(0, 0, 0, 0); m_leFilter = new QLineEdit(m_frameFilter); m_leFilter->setClearButtonEnabled(true); m_leFilter->setPlaceholderText(i18n("Search/Filter")); layoutFilter->addWidget(m_leFilter); bFilterOptions = new QToolButton(m_frameFilter); bFilterOptions->setIcon(QIcon::fromTheme("configure")); bFilterOptions->setCheckable(true); layoutFilter->addWidget(bFilterOptions); layout->addWidget(m_frameFilter); m_treeView->setAnimated(true); m_treeView->setAlternatingRowColors(true); m_treeView->setSelectionBehavior(QAbstractItemView::SelectRows); m_treeView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_treeView->setUniformRowHeights(true); m_treeView->viewport()->installEventFilter(this); m_treeView->header()->setStretchLastSection(true); m_treeView->header()->installEventFilter(this); m_treeView->setDragEnabled(true); m_treeView->setAcceptDrops(true); m_treeView->setDropIndicatorShown(true); m_treeView->setDragDropMode(QAbstractItemView::InternalMove); layout->addWidget(m_treeView); this->createActions(); connect(m_leFilter, &QLineEdit::textChanged, this, &ProjectExplorer::filterTextChanged); connect(bFilterOptions, &QPushButton::toggled, this, &ProjectExplorer::toggleFilterOptionsMenu); } void ProjectExplorer::createActions() { caseSensitiveAction = new QAction(i18n("Case Sensitive"), this); caseSensitiveAction->setCheckable(true); caseSensitiveAction->setChecked(false); connect(caseSensitiveAction, &QAction::triggered, this, &ProjectExplorer::toggleFilterCaseSensitivity); matchCompleteWordAction = new QAction(i18n("Match Complete Word"), this); matchCompleteWordAction->setCheckable(true); matchCompleteWordAction->setChecked(false); connect(matchCompleteWordAction, &QAction::triggered, this, &ProjectExplorer::toggleFilterMatchCompleteWord); expandTreeAction = new QAction(QIcon::fromTheme(QLatin1String("expand-all")), i18n("Expand All"), this); connect(expandTreeAction, &QAction::triggered, m_treeView, &QTreeView::expandAll); expandSelectedTreeAction = new QAction(QIcon::fromTheme(QLatin1String("expand-all")), i18n("Expand Selected"), this); connect(expandSelectedTreeAction, &QAction::triggered, this, &ProjectExplorer::expandSelected); collapseTreeAction = new QAction(QIcon::fromTheme(QLatin1String("collapse-all")), i18n("Collapse All"), this); connect(collapseTreeAction, &QAction::triggered, m_treeView, &QTreeView::collapseAll); collapseSelectedTreeAction = new QAction(QIcon::fromTheme(QLatin1String("collapse-all")), i18n("Collapse Selected"), this); connect(collapseSelectedTreeAction, &QAction::triggered, this, &ProjectExplorer::collapseSelected); deleteSelectedTreeAction = new QAction(QIcon::fromTheme("edit-delete"), i18n("Delete Selected"), this); connect(deleteSelectedTreeAction, &QAction::triggered, this, &ProjectExplorer::deleteSelected); toggleFilterAction = new QAction(QIcon::fromTheme(QLatin1String("view-filter")), i18n("Hide Search/Filter Options"), this); connect(toggleFilterAction, &QAction::triggered, this, &ProjectExplorer::toggleFilterWidgets); showAllColumnsAction = new QAction(i18n("Show All"),this); showAllColumnsAction->setCheckable(true); showAllColumnsAction->setChecked(true); showAllColumnsAction->setEnabled(false); connect(showAllColumnsAction, &QAction::triggered, this, &ProjectExplorer::showAllColumns); } /*! shows the context menu in the tree. In addition to the context menu of the currently selected aspect, treeview specific options are added. */ void ProjectExplorer::contextMenuEvent(QContextMenuEvent *event) { if (!m_treeView->model()) return; const QModelIndex& index = m_treeView->indexAt(m_treeView->viewport()->mapFrom(this, event->pos())); if (!index.isValid()) m_treeView->clearSelection(); const QModelIndexList& items = m_treeView->selectionModel()->selectedIndexes(); QMenu* menu = nullptr; if (items.size()/4 == 1) { auto* aspect = static_cast(index.internalPointer()); menu = aspect->createContextMenu(); } else { menu = new QMenu(); QMenu* projectMenu = m_project->createContextMenu(); projectMenu->setTitle(m_project->name()); menu->addMenu(projectMenu); menu->addSeparator(); if (items.size()/4 > 1) { menu->addAction(expandSelectedTreeAction); menu->addAction(collapseSelectedTreeAction); menu->addSeparator(); menu->addAction(deleteSelectedTreeAction); menu->addSeparator(); } else { menu->addAction(expandTreeAction); menu->addAction(collapseTreeAction); menu->addSeparator(); menu->addAction(toggleFilterAction); //Menu for showing/hiding the columns in the tree view QMenu* columnsMenu = menu->addMenu(i18n("Show/Hide columns")); columnsMenu->addAction(showAllColumnsAction); columnsMenu->addSeparator(); for (auto* action : list_showColumnActions) columnsMenu->addAction(action); //TODO //Menu for showing/hiding the top-level aspects (Worksheet, Spreadhsheet, etc) in the tree view // QMenu* objectsMenu = menu->addMenu(i18n("Show/Hide objects")); } } if (menu) menu->exec(event->globalPos()); delete menu; } void ProjectExplorer::setCurrentAspect(const AbstractAspect* aspect) { const AspectTreeModel* tree_model = qobject_cast(m_treeView->model()); if (tree_model) m_treeView->setCurrentIndex(tree_model->modelIndexOfAspect(aspect)); } /*! Sets the \c model for the tree view to present. */ void ProjectExplorer::setModel(AspectTreeModel* treeModel) { m_treeView->setModel(treeModel); connect(treeModel, &AspectTreeModel::renameRequested, m_treeView, static_cast(&QAbstractItemView::edit)); connect(treeModel, &AspectTreeModel::indexSelected, this, &ProjectExplorer::selectIndex); connect(treeModel, &AspectTreeModel::indexDeselected, this, &ProjectExplorer::deselectIndex); connect(treeModel, &AspectTreeModel::hiddenAspectSelected, this, &ProjectExplorer::hiddenAspectSelected); connect(m_treeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ProjectExplorer::currentChanged); connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ProjectExplorer::selectionChanged); //create action for showing/hiding the columns in the tree. //this is done here since the number of columns is not available in createActions() yet. if (list_showColumnActions.size() == 0) { for (int i = 0; i < m_treeView->model()->columnCount(); i++) { QAction* showColumnAction = new QAction(treeModel->headerData(i, Qt::Horizontal).toString(), this); showColumnAction->setCheckable(true); showColumnAction->setChecked(true); list_showColumnActions.append(showColumnAction); connect(showColumnAction, &QAction::triggered, this, [=] { ProjectExplorer::toggleColumn(i); }); } } else { for (int i = 0; i < list_showColumnActions.size(); ++i) { if (!list_showColumnActions.at(i)->isChecked()) m_treeView->hideColumn(i); } } } void ProjectExplorer::setProject(Project* project) { connect(project, &Project::aspectAdded, this, &ProjectExplorer::aspectAdded); connect(project, &Project::requestSaveState, this, &ProjectExplorer::save); connect(project, &Project::requestLoadState, this, &ProjectExplorer::load); connect(project, &Project::requestNavigateTo, this, &ProjectExplorer::navigateTo); connect(project, &Project::loaded, this, &ProjectExplorer::projectLoaded); m_project = project; //for newly created projects, resize the header to fit the size of the header section names. //for projects loaded from a file, this function will be called laterto fit the sizes //of the content once the project is loaded resizeHeader(); } QModelIndex ProjectExplorer::currentIndex() const { return m_treeView->currentIndex(); } void ProjectExplorer::search() { m_leFilter->setFocus(); } /*! handles the contextmenu-event of the horizontal header in the tree view. Provides a menu for selective showing and hiding of columns. */ bool ProjectExplorer::eventFilter(QObject* obj, QEvent* event) { if (obj == m_treeView->header() && event->type() == QEvent::ContextMenu) { //Menu for showing/hiding the columns in the tree view QMenu* columnsMenu = new QMenu(m_treeView->header()); columnsMenu->addSection(i18n("Columns")); columnsMenu->addAction(showAllColumnsAction); columnsMenu->addSeparator(); for (auto* action : list_showColumnActions) columnsMenu->addAction(action); auto* e = static_cast(event); columnsMenu->exec(e->globalPos()); delete columnsMenu; return true; } else if (obj == m_treeView->viewport()) { auto* e = static_cast(event); if (event->type() == QEvent::MouseButtonPress) { if (e->button() == Qt::LeftButton) { QModelIndex index = m_treeView->indexAt(e->pos()); if (!index.isValid()) return false; auto* aspect = static_cast(index.internalPointer()); if (aspect->isDraggable()) { m_dragStartPos = e->globalPos(); m_dragStarted = false; } } } else if (event->type() == QEvent::MouseMove) { if ( !m_dragStarted && m_treeView->selectionModel()->selectedIndexes().size() > 0 && (e->globalPos() - m_dragStartPos).manhattanLength() >= QApplication::startDragDistance()) { m_dragStarted = true; auto* drag = new QDrag(this); auto* mimeData = new QMimeData; //determine the selected objects and serialize the pointers to QMimeData QVector vec; QModelIndexList items = m_treeView->selectionModel()->selectedIndexes(); //there are four model indices in each row -> divide by 4 to obtain the number of selected rows (=aspects) for (int i = 0; i < items.size()/4; ++i) { const QModelIndex& index = items.at(i*4); auto* aspect = static_cast(index.internalPointer()); vec << (quintptr)aspect; } QByteArray data; QDataStream stream(&data, QIODevice::WriteOnly); stream << vec; mimeData->setData("labplot-dnd", data); drag->setMimeData(mimeData); drag->exec(); } } else if (event->type() == QEvent::DragEnter) { //ignore events not related to internal drags of columns etc., e.g. dropping of external files onto LabPlot auto* dragEnterEvent = static_cast(event); const QMimeData* mimeData = dragEnterEvent->mimeData(); if (!mimeData) { event->ignore(); return false; } if (mimeData->formats().at(0) != QLatin1String("labplot-dnd")) { event->ignore(); return false; } event->setAccepted(true); } else if (event->type() == QEvent::DragMove) { auto* dragMoveEvent = static_cast(event); const QMimeData* mimeData = dragMoveEvent->mimeData(); //determine the first aspect being dragged QByteArray data = mimeData->data(QLatin1String("labplot-dnd")); QVector vec; QDataStream stream(&data, QIODevice::ReadOnly); stream >> vec; AbstractAspect* sourceAspect{nullptr}; if (!vec.isEmpty()) sourceAspect = reinterpret_cast(vec.at(0)); if (!sourceAspect) return false; //determine the aspect under the cursor QModelIndex index = m_treeView->indexAt(dragMoveEvent->pos()); if (!index.isValid()) return false; //accept only the events when the aspect being dragged is dropable onto the aspect under the cursor //and the aspect under the cursor is not already the parent of the dragged aspect auto* destinationAspect = static_cast(index.internalPointer()); bool accept = sourceAspect->dropableOn().indexOf(destinationAspect->type()) != -1 && sourceAspect->parentAspect() != destinationAspect; event->setAccepted(accept); } else if (event->type() == QEvent::Drop) { auto* dropEvent = static_cast(event); QModelIndex index = m_treeView->indexAt(dropEvent->pos()); if (!index.isValid()) return false; auto* aspect = static_cast(index.internalPointer()); aspect->processDropEvent(dropEvent); } } return QObject::eventFilter(obj, event); } //############################################################################## //################################# SLOTS #################################### //############################################################################## /*! * called after the project was loaded. * resize the header of the view to adjust to the content * and re-select the currently selected object to show * its final properties in the dock widget after in Project::load() all * pointers were restored, relative paths replaced by absolute, etc. */ void ProjectExplorer::projectLoaded() { resizeHeader(); selectionChanged(m_treeView->selectionModel()->selection(), QItemSelection()); } /*! expand the aspect \c aspect (the tree index corresponding to it) in the tree view and makes it visible and selected. Called when a new aspect is added to the project. */ void ProjectExplorer::aspectAdded(const AbstractAspect* aspect) { if (m_project->isLoading() ||m_project->aspectAddedSignalSuppressed()) return; //don't do anything if hidden aspects were added if (aspect->hidden()) return; //don't do anything for newly added data spreadsheets of data picker curves if (aspect->inherits(AspectType::Spreadsheet) && aspect->parentAspect()->inherits(AspectType::DatapickerCurve)) return; const AspectTreeModel* tree_model = qobject_cast(m_treeView->model()); const QModelIndex& index = tree_model->modelIndexOfAspect(aspect); //expand and make the aspect visible m_treeView->setExpanded(index, true); // newly added columns are only expanded but not selected, return here if (aspect->inherits(AspectType::Column)) { m_treeView->setExpanded(tree_model->modelIndexOfAspect(aspect->parentAspect()), true); return; } m_treeView->scrollTo(index); m_treeView->setCurrentIndex(index); m_treeView->header()->resizeSections(QHeaderView::ResizeToContents); m_treeView->header()->resizeSection(0, m_treeView->header()->sectionSize(0)*1.2); } void ProjectExplorer::navigateTo(const QString& path) { const AspectTreeModel* tree_model = qobject_cast(m_treeView->model()); if (tree_model) { const QModelIndex& index = tree_model->modelIndexOfAspect(path); m_treeView->scrollTo(index); m_treeView->setCurrentIndex(index); auto* aspect = static_cast(index.internalPointer()); aspect->setSelected(true); } } void ProjectExplorer::currentChanged(const QModelIndex & current, const QModelIndex & previous) { if (m_project->isLoading()) return; Q_UNUSED(previous); emit currentAspectChanged(static_cast(current.internalPointer())); } void ProjectExplorer::toggleColumn(int index) { //determine the total number of checked column actions int checked = 0; for (const auto* action : list_showColumnActions) { if (action->isChecked()) checked++; } if (list_showColumnActions.at(index)->isChecked()) { m_treeView->showColumn(index); m_treeView->header()->resizeSection(0,0 ); m_treeView->header()->resizeSections(QHeaderView::ResizeToContents); for (auto* action : list_showColumnActions) action->setEnabled(true); //deactivate the "show all column"-action, if all actions are checked if ( checked == list_showColumnActions.size() ) { showAllColumnsAction->setEnabled(false); showAllColumnsAction->setChecked(true); } } else { m_treeView->hideColumn(index); showAllColumnsAction->setEnabled(true); showAllColumnsAction->setChecked(false); //if there is only one checked column-action, deactivated it. //It should't be possible to hide all columns if ( checked == 1 ) { int i = 0; while ( !list_showColumnActions.at(i)->isChecked() ) i++; list_showColumnActions.at(i)->setEnabled(false); } } } void ProjectExplorer::showAllColumns() { for (int i = 0; i < m_treeView->model()->columnCount(); i++) { m_treeView->showColumn(i); m_treeView->header()->resizeSection(0,0 ); m_treeView->header()->resizeSections(QHeaderView::ResizeToContents); } showAllColumnsAction->setEnabled(false); for (auto* action : list_showColumnActions) { action->setEnabled(true); action->setChecked(true); } } /*! shows/hides the frame with the search/filter widgets */ void ProjectExplorer::toggleFilterWidgets() { if (m_frameFilter->isVisible()) { m_frameFilter->hide(); toggleFilterAction->setText(i18n("Show Search/Filter Options")); } else { m_frameFilter->show(); toggleFilterAction->setText(i18n("Hide Search/Filter Options")); } } /*! toggles the menu for the filter/search options */ void ProjectExplorer::toggleFilterOptionsMenu(bool checked) { if (checked) { QMenu menu; menu.addAction(caseSensitiveAction); menu.addAction(matchCompleteWordAction); connect(&menu, &QMenu::aboutToHide, bFilterOptions, &QPushButton::toggle); menu.exec(bFilterOptions->mapToGlobal(QPoint(0,bFilterOptions->height()))); } } void ProjectExplorer::resizeHeader() { m_treeView->header()->resizeSections(QHeaderView::ResizeToContents); m_treeView->header()->resizeSection(0, m_treeView->header()->sectionSize(0)*1.2); //make the column "Name" somewhat bigger } /*! called when the filter/search text was changend. */ void ProjectExplorer::filterTextChanged(const QString& text) { QModelIndex root = m_treeView->model()->index(0,0); filter(root, text); } bool ProjectExplorer::filter(const QModelIndex& index, const QString& text) { Qt::CaseSensitivity sensitivity = caseSensitiveAction->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive; bool matchCompleteWord = matchCompleteWordAction->isChecked(); bool childVisible = false; const int rows = index.model()->rowCount(index); for (int i = 0; i < rows; i++) { QModelIndex child = index.model()->index(i, 0, index); auto* aspect = static_cast(child.internalPointer()); bool visible; if (text.isEmpty()) visible = true; else if (matchCompleteWord) visible = aspect->name().startsWith(text, sensitivity); else visible = aspect->name().contains(text, sensitivity); if (visible) { //current item is visible -> make all its children visible without applying the filter for (int j = 0; j < child.model()->rowCount(child); ++j) { m_treeView->setRowHidden(j, child, false); if (text.isEmpty()) filter(child, text); } childVisible = true; } else { //check children items. if one of the children is visible, make the parent (current) item visible too. visible = filter(child, text); if (visible) childVisible = true; } m_treeView->setRowHidden(i, index, !visible); } return childVisible; } void ProjectExplorer::toggleFilterCaseSensitivity() { filterTextChanged(m_leFilter->text()); } void ProjectExplorer::toggleFilterMatchCompleteWord() { filterTextChanged(m_leFilter->text()); } void ProjectExplorer::selectIndex(const QModelIndex& index) { if (m_project->isLoading()) return; if ( !m_treeView->selectionModel()->isSelected(index) ) { m_treeView->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); m_treeView->setExpanded(index, true); m_treeView->scrollTo(index); } } void ProjectExplorer::deselectIndex(const QModelIndex & index) { if (m_project->isLoading()) return; if ( m_treeView->selectionModel()->isSelected(index) ) m_treeView->selectionModel()->select(index, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); } void ProjectExplorer::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { QModelIndex index; QModelIndexList items; AbstractAspect* aspect = nullptr; //there are four model indices in each row //-> divide by 4 to obtain the number of selected rows (=aspects) items = selected.indexes(); for (int i = 0; i < items.size()/4; ++i) { index = items.at(i*4); aspect = static_cast(index.internalPointer()); aspect->setSelected(true); } items = deselected.indexes(); for (int i = 0; i < items.size()/4; ++i) { index = items.at(i*4); aspect = static_cast(index.internalPointer()); aspect->setSelected(false); } items = m_treeView->selectionModel()->selectedRows(); QList selectedAspects; for (const QModelIndex& index : items) { aspect = static_cast(index.internalPointer()); selectedAspects<selectionModel()->selectedRows(); QList selectedAspects; for (const QModelIndex& index : items) { selectedAspects << static_cast(index.internalPointer()); } emit selectedAspectsChanged(selectedAspects); } void ProjectExplorer::expandSelected() { const QModelIndexList items = m_treeView->selectionModel()->selectedIndexes(); for (const auto& index : items) m_treeView->setExpanded(index, true); } void ProjectExplorer::collapseSelected() { const QModelIndexList items = m_treeView->selectionModel()->selectedIndexes(); for (const auto& index : items) m_treeView->setExpanded(index, false); } void ProjectExplorer::deleteSelected() { const QModelIndexList items = m_treeView->selectionModel()->selectedIndexes(); if (!items.size()) return; int rc = KMessageBox::warningYesNo( this, i18np("Do you really want to delete the selected object?", "Do you really want to delete the selected %1 objects?", items.size()/4), i18np("Delete selected object", "Delete selected objects", items.size()/4)); if (rc == KMessageBox::No) return; m_project->beginMacro(i18np("Project Explorer: delete %1 selected object", "Project Explorer: delete %1 selected objects", items.size()/4)); //determine all selected aspect QVector aspects; for (int i = 0; i < items.size()/4; ++i) { const QModelIndex& index = items.at(i*4); auto* aspect = static_cast(index.internalPointer()); aspects << aspect; } //determine aspects to be deleted: //it's enough to delete parent items in the selection only, //skip all selected aspects where one of the parents is also in the selection //for example selected columns of a selected spreadsheet, etc. QVector aspectsToDelete; for (auto* aspect : aspects) { auto* parent = aspect->parentAspect(); int parentSelected = false; while (parent) { if (aspects.indexOf(parent) != -1) { parentSelected = true; break; } parent = parent->parentAspect(); } if (!parentSelected) aspectsToDelete << aspect; //parent is not in the selection } for (auto* aspect : aspectsToDelete) aspect->remove(); m_project->endMacro(); } //############################################################################## //################## Serialization/Deserialization ########################### //############################################################################## struct ViewState { Qt::WindowStates state; QRect geometry; }; /** * \brief Save the current state of the tree view * (expanded items and the currently selected item) as XML */ void ProjectExplorer::save(QXmlStreamWriter* writer) const { auto* model = qobject_cast(m_treeView->model()); QList selected; QList expanded; QList withView; QVector viewStates; int currentRow = -1; //row corresponding to the current index in the tree view, -1 for the root element (=project) QModelIndexList selectedRows = m_treeView->selectionModel()->selectedRows(); //check whether the project node itself is expanded if (m_treeView->isExpanded(m_treeView->model()->index(0,0))) expanded.push_back(-1); int row = 0; - for (const auto* aspect : m_project->children(AspectType::AbstractAspect, AbstractAspect::Recursive)) { + for (const auto* aspect : m_project->children(AspectType::AbstractAspect, AbstractAspect::ChildIndexFlag::Recursive)) { const QModelIndex& index = model->modelIndexOfAspect(aspect); const auto* part = dynamic_cast(aspect); if (part && part->hasMdiSubWindow()) { withView.push_back(row); ViewState s = {part->view()->windowState(), part->view()->geometry()}; viewStates.push_back(s); } if (model->rowCount(index)>0 && m_treeView->isExpanded(index)) expanded.push_back(row); if (selectedRows.indexOf(index) != -1) selected.push_back(row); if (index == m_treeView->currentIndex()) currentRow = row; row++; } writer->writeStartElement("state"); writer->writeStartElement("expanded"); for (const auto e : expanded) writer->writeTextElement("row", QString::number(e)); writer->writeEndElement(); writer->writeStartElement("selected"); for (const auto s : selected) writer->writeTextElement("row", QString::number(s)); writer->writeEndElement(); writer->writeStartElement("view"); for (int i = 0; i < withView.size(); ++i) { writer->writeStartElement("row"); const ViewState& s = viewStates.at(i); writer->writeAttribute( "state", QString::number(s.state) ); writer->writeAttribute( "x", QString::number(s.geometry.x()) ); writer->writeAttribute( "y", QString::number(s.geometry.y()) ); writer->writeAttribute( "width", QString::number(s.geometry.width()) ); writer->writeAttribute( "height", QString::number(s.geometry.height()) ); writer->writeCharacters(QString::number(withView.at(i))); writer->writeEndElement(); } writer->writeEndElement(); writer->writeStartElement("current"); writer->writeTextElement("row", QString::number(currentRow)); writer->writeEndElement(); writer->writeEndElement(); } /** * \brief Load from XML */ bool ProjectExplorer::load(XmlStreamReader* reader) { const AspectTreeModel* model = qobject_cast(m_treeView->model()); - const auto aspects = m_project->children(AspectType::AbstractAspect, AbstractAspect::Recursive); + const auto aspects = m_project->children(AspectType::AbstractAspect, AbstractAspect::ChildIndexFlag::Recursive); bool expandedItem = false; bool selectedItem = false; bool viewItem = false; (void)viewItem; // because of a strange g++-warning about unused viewItem bool currentItem = false; QModelIndex currentIndex; QString str; int row; QVector selected; QList expanded; QXmlStreamAttributes attribs; KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used"); while (!reader->atEnd()) { reader->readNext(); if (reader->isEndElement() && reader->name() == "state") break; if (!reader->isStartElement()) continue; if (reader->name() == "expanded") { expandedItem = true; selectedItem = false; viewItem = false; currentItem = false; } else if (reader->name() == "selected") { expandedItem = false; selectedItem = true; viewItem = false; currentItem = false; } else if (reader->name() == "view") { expandedItem = false; selectedItem = false; viewItem = true; currentItem = false; } else if (reader->name() == "current") { expandedItem = false; selectedItem = false; viewItem = false; currentItem = true; } else if (reader->name() == "row") { //we need to read the attributes first and before readElementText() otherwise they are empty attribs = reader->attributes(); row = reader->readElementText().toInt(); QModelIndex index; if (row == -1) index = model->modelIndexOfAspect(m_project); //-1 corresponds to the project-item (s.a. ProjectExplorer::save()) else if (row >= aspects.size() || row < 0 /* checking for <0 to protect against wrong values in the XML */) continue; else index = model->modelIndexOfAspect(aspects.at(row)); if (expandedItem) expanded.push_back(index); else if (selectedItem) selected.push_back(index); else if (currentItem) currentIndex = index; else if (viewItem) { if (row < 0) //should never happen, but we need to handle the corrupted file continue; auto* part = dynamic_cast(aspects.at(row)); if (!part) continue; //TODO: add error/warning message here? emit currentAspectChanged(part); str = attribs.value("state").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("state").toString()); else { part->view()->setWindowState(Qt::WindowStates(str.toInt())); part->mdiSubWindow()->setWindowState(Qt::WindowStates(str.toInt())); } if (str != "0") continue; //no geometry settings required for maximized/minimized windows QRect geometry; str = attribs.value("x").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("x").toString()); else geometry.setX(str.toInt()); str = attribs.value("y").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("y").toString()); else geometry.setY(str.toInt()); str = attribs.value("width").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("width").toString()); else geometry.setWidth(str.toInt()); str = attribs.value("height").toString(); if (str.isEmpty()) reader->raiseWarning(attributeWarning.subs("height").toString()); else geometry.setHeight(str.toInt()); part->mdiSubWindow()->setGeometry(geometry); } } } for (const auto& index : expanded) { m_treeView->setExpanded(index, true); collapseParents(index, expanded);//collapse all parent indices if they are not expanded } for (const auto& index : selected) m_treeView->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); m_treeView->setCurrentIndex(currentIndex); m_treeView->scrollTo(currentIndex); auto* aspect = static_cast(currentIndex.internalPointer()); emit currentAspectChanged(aspect); //when setting the current index above it gets expanded, collapse all parent indices if they are were not expanded when saved collapseParents(currentIndex, expanded); return true; } void ProjectExplorer::collapseParents(const QModelIndex& index, const QList& expanded) { //root index doesn't have any parents - this case is not caught by the second if-statement below if (index.column() == 0 && index.row() == 0) return; const QModelIndex parent = index.parent(); if (parent == QModelIndex()) return; if (expanded.indexOf(parent) == -1) m_treeView->collapse(parent); } diff --git a/src/commonfrontend/datapicker/DatapickerImageView.cpp b/src/commonfrontend/datapicker/DatapickerImageView.cpp index ee152944e..cecafe410 100644 --- a/src/commonfrontend/datapicker/DatapickerImageView.cpp +++ b/src/commonfrontend/datapicker/DatapickerImageView.cpp @@ -1,859 +1,859 @@ /*************************************************************************** File : DatapickerImageView.cpp Project : LabPlot Description : DatapickerImage view for datapicker -------------------------------------------------------------------- Copyright : (C) 2015 by Ankit Wagadre (wagadre.ankit@gmail.com) Copyright : (C) 2015-2016 by 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 "commonfrontend/datapicker/DatapickerImageView.h" #include "backend/worksheet/Worksheet.h" #include "backend/datapicker/DatapickerPoint.h" #include "backend/datapicker/Datapicker.h" #include "backend/datapicker/Transform.h" #include "backend/datapicker/DatapickerCurve.h" #include "backend/datapicker/DatapickerImage.h" #include #include #include #include #include #include #include #include #include #include #include #include /** * \class DatapickerImageView * \brief Datapicker/DatapickerImage view */ /*! Constructur of the class. Creates a view for the DatapickerImage \c image and initializes the internal model. */ DatapickerImageView::DatapickerImageView(DatapickerImage* image) : QGraphicsView(), m_image(image), m_datapicker(dynamic_cast(m_image->parentAspect())), m_transform(new Transform()) { setScene(m_image->scene()); setRenderHint(QPainter::Antialiasing); setRubberBandSelectionMode(Qt::ContainsItemBoundingRect); setTransformationAnchor(QGraphicsView::AnchorUnderMouse); setResizeAnchor(QGraphicsView::AnchorViewCenter); setMinimumSize(16, 16); setFocusPolicy(Qt::StrongFocus); viewport()->setAttribute( Qt::WA_OpaquePaintEvent ); viewport()->setAttribute( Qt::WA_NoSystemBackground ); setCacheMode(QGraphicsView::CacheBackground); initActions(); initMenus(); m_image->setSegmentsHoverEvent(true); setInteractive(true); changeZoom(zoomOriginAction); currentZoomAction = zoomInViewAction; if (m_image->plotPointsType() == DatapickerImage::AxisPoints) setAxisPointsAction->setChecked(true); else if (m_image->plotPointsType() == DatapickerImage::CurvePoints) setCurvePointsAction->setChecked(true); else selectSegmentAction->setChecked(true); handleImageActions(); changeRotationAngle(); //signal/slot connections //for general actions connect(m_image, &DatapickerImage::requestProjectContextMenu, this, &DatapickerImageView::createContextMenu); connect(m_image, &DatapickerImage::requestUpdate, this, &DatapickerImageView::updateBackground); connect(m_image, &DatapickerImage::requestUpdateActions, this, &DatapickerImageView::handleImageActions); connect(m_datapicker, &Datapicker::requestUpdateActions, this, &DatapickerImageView::handleImageActions); connect(m_image, &DatapickerImage::rotationAngleChanged, this, &DatapickerImageView::changeRotationAngle); //resize the view to make the complete scene visible. //no need to resize the view when the project is being opened, //all views will be resized to the stored values at the end if (!m_image->isLoading()) { float w = Worksheet::convertFromSceneUnits(sceneRect().width(), Worksheet::Inch); float h = Worksheet::convertFromSceneUnits(sceneRect().height(), Worksheet::Inch); w *= QApplication::desktop()->physicalDpiX(); h *= QApplication::desktop()->physicalDpiY(); resize(w*1.1, h*1.1); } //rescale to the original size static const float hscale = QApplication::desktop()->physicalDpiX()/(Worksheet::convertToSceneUnits(1,Worksheet::Inch)); static const float vscale = QApplication::desktop()->physicalDpiY()/(Worksheet::convertToSceneUnits(1,Worksheet::Inch)); setTransform(QTransform::fromScale(hscale, vscale)); } DatapickerImageView::~DatapickerImageView() { delete m_transform; } void DatapickerImageView::initActions() { auto* zoomActionGroup = new QActionGroup(this); auto* mouseModeActionGroup = new QActionGroup(this); navigationActionGroup = new QActionGroup(this); magnificationActionGroup = new QActionGroup(this); //Zoom actions zoomInViewAction = new QAction(QIcon::fromTheme("zoom-in"), i18n("Zoom In"), zoomActionGroup); zoomInViewAction->setShortcut(Qt::CTRL+Qt::Key_Plus); zoomOutViewAction = new QAction(QIcon::fromTheme("zoom-out"), i18n("Zoom Out"), zoomActionGroup); zoomOutViewAction->setShortcut(Qt::CTRL+Qt::Key_Minus); zoomOriginAction = new QAction(QIcon::fromTheme("zoom-original"), i18n("Original Size"), zoomActionGroup); zoomOriginAction->setShortcut(Qt::CTRL+Qt::Key_1); zoomFitPageHeightAction = new QAction(QIcon::fromTheme("zoom-fit-height"), i18n("Fit to Height"), zoomActionGroup); zoomFitPageWidthAction = new QAction(QIcon::fromTheme("zoom-fit-width"), i18n("Fit to Width"), zoomActionGroup); // Mouse mode actions navigationModeAction = new QAction(QIcon::fromTheme("input-mouse"), i18n("Navigate"), mouseModeActionGroup); navigationModeAction->setCheckable(true); navigationModeAction->setData(NavigationMode); zoomSelectionModeAction = new QAction(QIcon::fromTheme("page-zoom"), i18n("Select and Zoom"), mouseModeActionGroup); zoomSelectionModeAction->setCheckable(true); zoomSelectionModeAction->setData(ZoomSelectionMode); setAxisPointsAction = new QAction(QIcon::fromTheme("labplot-plot-axis-points"), i18n("Set Axis Points"), mouseModeActionGroup); setAxisPointsAction->setCheckable(true); setAxisPointsAction->setData(ReferencePointsEntryMode); setCurvePointsAction = new QAction(QIcon::fromTheme("labplot-xy-curve-points"), i18n("Set Curve Points"), mouseModeActionGroup); setCurvePointsAction->setCheckable(true); setCurvePointsAction->setData(CurvePointsEntryMode); selectSegmentAction = new QAction(QIcon::fromTheme("labplot-xy-curve-segments"), i18n("Select Curve Segments"), mouseModeActionGroup); selectSegmentAction->setCheckable(true); selectSegmentAction->setData(CurveSegmentsEntryMode); addCurveAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("New Curve"), this); shiftLeftAction = new QAction(QIcon::fromTheme("labplot-shift-left-x"), i18n("Shift Left"), navigationActionGroup); shiftLeftAction->setShortcut(Qt::Key_Right); shiftRightAction = new QAction(QIcon::fromTheme("labplot-shift-right-x"), i18n("Shift Right"), navigationActionGroup); shiftRightAction->setShortcut(Qt::Key_Left); shiftUpAction = new QAction(QIcon::fromTheme("labplot-shift-down-y"), i18n("Shift Up"), navigationActionGroup); shiftUpAction->setShortcut(Qt::Key_Up); shiftDownAction = new QAction(QIcon::fromTheme("labplot-shift-up-y"), i18n("Shift Down"), navigationActionGroup); shiftDownAction->setShortcut(Qt::Key_Down); noMagnificationAction = new QAction(QIcon::fromTheme("labplot-1x-zoom"), i18n("No Magnification"), magnificationActionGroup); noMagnificationAction->setCheckable(true); noMagnificationAction->setChecked(true); twoTimesMagnificationAction = new QAction(QIcon::fromTheme("labplot-2x-zoom"), i18n("2x Magnification"), magnificationActionGroup); twoTimesMagnificationAction->setCheckable(true); threeTimesMagnificationAction = new QAction(QIcon::fromTheme("labplot-3x-zoom"), i18n("3x Magnification"), magnificationActionGroup); threeTimesMagnificationAction->setCheckable(true); fourTimesMagnificationAction = new QAction(QIcon::fromTheme("labplot-4x-zoom"), i18n("4x Magnification"), magnificationActionGroup); fourTimesMagnificationAction->setCheckable(true); fiveTimesMagnificationAction = new QAction(QIcon::fromTheme("labplot-5x-zoom"), i18n("5x Magnification"), magnificationActionGroup); fiveTimesMagnificationAction->setCheckable(true); //set some default values currentZoomAction = zoomInViewAction; currentMagnificationAction = noMagnificationAction; switch(m_image->plotPointsType()) { case DatapickerImage::AxisPoints: currentPlotPointsTypeAction = setAxisPointsAction; setAxisPointsAction->setChecked(true); mouseModeChanged(setAxisPointsAction); break; case DatapickerImage::CurvePoints: currentPlotPointsTypeAction = setCurvePointsAction; setCurvePointsAction->setChecked(true); mouseModeChanged(setCurvePointsAction); break; case DatapickerImage::SegmentPoints: currentPlotPointsTypeAction = selectSegmentAction; selectSegmentAction->setChecked(true); mouseModeChanged(selectSegmentAction); } //signal-slot connections connect(mouseModeActionGroup, &QActionGroup::triggered, this, &DatapickerImageView::mouseModeChanged); connect(zoomActionGroup, &QActionGroup::triggered, this, &DatapickerImageView::changeZoom); connect(addCurveAction, &QAction::triggered, this, &DatapickerImageView::addCurve); connect(navigationActionGroup, &QActionGroup::triggered, this, &DatapickerImageView::changeSelectedItemsPosition); connect(magnificationActionGroup, &QActionGroup::triggered, this, &DatapickerImageView::magnificationChanged); } void DatapickerImageView::initMenus() { m_viewMouseModeMenu = new QMenu(i18n("Mouse Mode"), this); m_viewMouseModeMenu->setIcon(QIcon::fromTheme("input-mouse")); m_viewMouseModeMenu->addAction(setAxisPointsAction); m_viewMouseModeMenu->addAction(setCurvePointsAction); m_viewMouseModeMenu->addAction(selectSegmentAction); m_viewMouseModeMenu->addSeparator(); m_viewMouseModeMenu->addAction(navigationModeAction); m_viewMouseModeMenu->addAction(zoomSelectionModeAction); m_zoomMenu = new QMenu(i18n("Zoom View"), this); m_zoomMenu->setIcon(QIcon::fromTheme("zoom-draw")); m_zoomMenu->addAction(zoomInViewAction); m_zoomMenu->addAction(zoomOutViewAction); m_zoomMenu->addAction(zoomOriginAction); m_zoomMenu->addAction(zoomFitPageHeightAction); m_zoomMenu->addAction(zoomFitPageWidthAction); m_navigationMenu = new QMenu(i18n("Move Last Point"), this); m_navigationMenu->addAction(shiftLeftAction); m_navigationMenu->addAction(shiftRightAction); m_navigationMenu->addAction(shiftUpAction); m_navigationMenu->addAction(shiftDownAction); m_magnificationMenu = new QMenu(i18n("Magnification"), this); m_magnificationMenu->setIcon(QIcon::fromTheme("zoom-in")); m_magnificationMenu->addAction(noMagnificationAction); m_magnificationMenu->addAction(twoTimesMagnificationAction); m_magnificationMenu->addAction(threeTimesMagnificationAction); m_magnificationMenu->addAction(fourTimesMagnificationAction); m_magnificationMenu->addAction(fiveTimesMagnificationAction); } /*! * Populates the menu \c menu with the image and image-view relevant actions. * The menu is used * - as the context menu in DatapickerImageView * - as the "datapicker menu" in the main menu-bar (called form MainWin) * - as a part of the image context menu in project explorer */ void DatapickerImageView::createContextMenu(QMenu* menu) const { Q_ASSERT(menu); QAction* firstAction = nullptr; // if we're populating the context menu for the project explorer, then //there're already actions available there. Skip the first title-action //and insert the action at the beginning of the menu. if (menu->actions().size()>1) firstAction = menu->actions().at(1); menu->insertAction(firstAction, addCurveAction); menu->insertSeparator(firstAction); menu->insertMenu(firstAction, m_navigationMenu); menu->insertSeparator(firstAction); menu->insertMenu(firstAction, m_viewMouseModeMenu); menu->insertMenu(firstAction, m_zoomMenu); menu->insertMenu(firstAction, m_magnificationMenu); menu->insertSeparator(firstAction); } void DatapickerImageView::fillToolBar(QToolBar* toolBar) { toolBar->addSeparator(); toolBar->addAction(setAxisPointsAction); toolBar->addAction(setCurvePointsAction); toolBar->addAction(selectSegmentAction); toolBar->addAction(navigationModeAction); toolBar->addAction(zoomSelectionModeAction); toolBar->addSeparator(); toolBar->addAction(addCurveAction); toolBar->addSeparator(); toolBar->addAction(shiftRightAction); toolBar->addAction(shiftLeftAction); toolBar->addAction(shiftUpAction); toolBar->addAction(shiftDownAction); tbZoom = new QToolButton(toolBar); tbZoom->setPopupMode(QToolButton::MenuButtonPopup); tbZoom->setMenu(m_zoomMenu); tbZoom->setDefaultAction(currentZoomAction); toolBar->addSeparator(); toolBar->addWidget(tbZoom); tbMagnification = new QToolButton(toolBar); tbMagnification->setPopupMode(QToolButton::MenuButtonPopup); tbMagnification->setMenu(m_magnificationMenu); tbMagnification->setDefaultAction(currentMagnificationAction); toolBar->addWidget(tbMagnification); } void DatapickerImageView::setScene(QGraphicsScene* scene) { QGraphicsView::setScene(scene); setTransform(QTransform()); } void DatapickerImageView::drawForeground(QPainter* painter, const QRectF& rect) { if (m_mouseMode == ZoomSelectionMode && m_selectionBandIsShown) { painter->save(); const QRectF& selRect = mapToScene(QRect(m_selectionStart, m_selectionEnd).normalized()).boundingRect(); painter->setPen(QPen(Qt::black, 5/transform().m11())); painter->drawRect(selRect); painter->setBrush(Qt::blue); painter->setOpacity(0.2); painter->drawRect(selRect); painter->restore(); } QGraphicsView::drawForeground(painter, rect); } void DatapickerImageView::drawBackground(QPainter* painter, const QRectF& rect) { painter->save(); QRectF scene_rect = sceneRect(); if (!scene_rect.contains(rect)) painter->fillRect(rect, Qt::lightGray); // canvas if (m_image->isLoaded) { if (m_image->plotImageType() == DatapickerImage::OriginalImage) { const QImage& todraw = m_image->originalPlotImage.scaled(scene_rect.width(), scene_rect.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); painter->drawImage(scene_rect.topLeft(), todraw); } else if (m_image->plotImageType() == DatapickerImage::ProcessedImage) { const QImage& todraw = m_image->processedPlotImage.scaled(scene_rect.width(), scene_rect.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); painter->drawImage(scene_rect.topLeft(), todraw); } else { painter->fillRect(scene_rect, Qt::white); } } else { painter->setBrush(QBrush(Qt::gray)); painter->drawRect(scene_rect); } invalidateScene(rect, QGraphicsScene::BackgroundLayer); painter->restore(); } //############################################################################## //#################################### Events ############################### //############################################################################## void DatapickerImageView::wheelEvent(QWheelEvent* event) { //https://wiki.qt.io/Smooth_Zoom_In_QGraphicsView if (m_mouseMode == ZoomSelectionMode || (QApplication::keyboardModifiers() & Qt::ControlModifier)) { int numDegrees = event->delta() / 8; int numSteps = numDegrees / 15; // see QWheelEvent documentation zoom(numSteps); } else QGraphicsView::wheelEvent(event); } void DatapickerImageView::zoom(int numSteps) { m_numScheduledScalings += numSteps; if (m_numScheduledScalings * numSteps < 0) // if user moved the wheel in another direction, we reset previously scheduled scalings m_numScheduledScalings = numSteps; auto* anim = new QTimeLine(350, this); anim->setUpdateInterval(20); connect(anim, &QTimeLine::valueChanged, this, &DatapickerImageView::scalingTime); connect(anim, &QTimeLine::finished, this, &DatapickerImageView::animFinished); anim->start(); } void DatapickerImageView::scalingTime() { qreal factor = 1.0 + qreal(m_numScheduledScalings) / 300.0; scale(factor, factor); } void DatapickerImageView::animFinished() { if (m_numScheduledScalings > 0) m_numScheduledScalings--; else m_numScheduledScalings++; sender()->~QObject(); } void DatapickerImageView::mousePressEvent(QMouseEvent* event) { //prevent the deselection of items when context menu event //was triggered (right button click) if (event->button() == Qt::RightButton) { event->accept(); return; } if (event->button() == Qt::LeftButton && m_mouseMode == ZoomSelectionMode) { m_selectionStart = event->pos(); m_selectionBandIsShown = true; return; } QPointF eventPos = mapToScene(event->pos()); bool entryMode = (m_mouseMode == ReferencePointsEntryMode || m_mouseMode == CurvePointsEntryMode || m_mouseMode == CurveSegmentsEntryMode); //check whether there is a point item under the cursor bool pointsUnderCursor = false; auto items = this->items(event->pos()); for (auto* item : items) { if (item != m_image->m_magnificationWindow) { pointsUnderCursor = true; break; } } if (entryMode && !pointsUnderCursor && m_image->isLoaded && sceneRect().contains(eventPos)) { if ( m_image->plotPointsType() == DatapickerImage::AxisPoints ) { - int childCount = m_image->childCount(AbstractAspect::IncludeHidden); + int childCount = m_image->childCount(AbstractAspect::ChildIndexFlag::IncludeHidden); if (childCount < 3) m_datapicker->addNewPoint(eventPos, m_image); } else if ( m_image->plotPointsType() == DatapickerImage::CurvePoints && m_datapicker->activeCurve() ) { m_datapicker->addNewPoint(eventPos, m_datapicker->activeCurve()); } if (m_image->m_magnificationWindow && m_image->m_magnificationWindow->isVisible()) updateMagnificationWindow(); } // make sure the datapicker (or its currently active curve) is selected in the project explorer if the view was clicked. // We need this for the case when we change from the project-node in the project explorer to the datapicker node by clicking the view. if (m_datapicker->activeCurve() && m_image->plotPointsType() != DatapickerImage::AxisPoints) { m_datapicker->setSelectedInView(false); m_datapicker->activeCurve()->setSelectedInView(true); } else { if (m_datapicker->activeCurve()) m_datapicker->activeCurve()->setSelectedInView(false); m_datapicker->setSelectedInView(true); } QGraphicsView::mousePressEvent(event); } void DatapickerImageView::mouseReleaseEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton && m_mouseMode == ZoomSelectionMode) { m_selectionBandIsShown = false; viewport()->repaint(QRect(m_selectionStart, m_selectionEnd).normalized()); //don't zoom if very small region was selected, avoid occasional/unwanted zooming m_selectionEnd = event->pos(); if ( abs(m_selectionEnd.x()-m_selectionStart.x())>20 && abs(m_selectionEnd.y()-m_selectionStart.y())>20 ) fitInView(mapToScene(QRect(m_selectionStart, m_selectionEnd).normalized()).boundingRect(), Qt::KeepAspectRatio); } QGraphicsView::mouseReleaseEvent(event); } void DatapickerImageView::mouseMoveEvent(QMouseEvent* event) { //show the selection band if (m_selectionBandIsShown) { QRect rect = QRect(m_selectionStart, m_selectionEnd).normalized(); m_selectionEnd = event->pos(); rect = rect.united(QRect(m_selectionStart, m_selectionEnd).normalized()); int penWidth = 5/transform().m11(); rect.setX(rect.x()-penWidth); rect.setY(rect.y()-penWidth); rect.setHeight(rect.height()+2*penWidth); rect.setWidth(rect.width()+2*penWidth); viewport()->repaint(rect); return; } QPointF pos = mapToScene(event->pos()); //show the current coordinates under the mouse cursor in the status bar if (m_image->plotPointsType() == DatapickerImage::CurvePoints) { QVector3D logicalPos = m_transform->mapSceneToLogical(pos, m_image->axisPoints()); if (m_image->axisPoints().type == DatapickerImage::Ternary) { emit statusInfo( "a =" + QString::number(logicalPos.x()) + ", b =" + QString::number(logicalPos.y()) + ", c =" + QString::number(logicalPos.z())); } else { QString xLabel('x'); QString yLabel('y'); if (m_image->axisPoints().type == DatapickerImage::PolarInDegree) { xLabel = 'r'; yLabel = "y(deg)"; } else if (m_image->axisPoints().type == DatapickerImage::PolarInRadians) { xLabel = 'r'; yLabel = "y(rad)"; } if (m_datapicker->activeCurve()) { QString statusText = i18n("%1, active curve \"%2\": %3=%4, %5=%6", m_datapicker->name(), m_datapicker->activeCurve()->name(), xLabel, QString::number(logicalPos.x()), yLabel, QString::number(logicalPos.y())); emit statusInfo(statusText); } } } //show the magnification window if ( magnificationFactor && m_image->isLoaded && sceneRect().contains(pos) ) { if (!m_image->m_magnificationWindow) { m_image->m_magnificationWindow = new QGraphicsPixmapItem; scene()->addItem(m_image->m_magnificationWindow); m_image->m_magnificationWindow->setZValue(std::numeric_limits::max()); } updateMagnificationWindow(); } else if (m_image->m_magnificationWindow) { m_image->m_magnificationWindow->setVisible(false); } QGraphicsView::mouseMoveEvent(event); } void DatapickerImageView::updateMagnificationWindow() { m_image->m_magnificationWindow->setVisible(false); QPointF pos = mapToScene(mapFromGlobal(QCursor::pos())); //copy the part of the view to be shown magnified const int size = Worksheet::convertToSceneUnits(2.0, Worksheet::Centimeter)/transform().m11(); const QRectF copyRect(pos.x() - size/(2*magnificationFactor), pos.y() - size/(2*magnificationFactor), size/magnificationFactor, size/magnificationFactor); QPixmap px = grab(mapFromScene(copyRect).boundingRect()); px = px.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); //draw the bounding rect QPainter painter(&px); const QPen pen = QPen(Qt::lightGray, 2/transform().m11()); painter.setPen(pen); QRect rect = px.rect(); rect.setWidth(rect.width()-pen.widthF()/2); rect.setHeight(rect.height()-pen.widthF()/2); painter.drawRect(rect); //set the pixmap m_image->m_magnificationWindow->setPixmap(px); m_image->m_magnificationWindow->setPos(pos.x()- px.width()/2, pos.y()- px.height()/2); m_image->m_magnificationWindow->setVisible(true); } void DatapickerImageView::contextMenuEvent(QContextMenuEvent* e) { Q_UNUSED(e); //no need to propagate the event to the scene and graphics items QMenu *menu = new QMenu(this); this->createContextMenu(menu); menu->exec(QCursor::pos()); } //############################################################################## //#################################### SLOTs ############################### //############################################################################## void DatapickerImageView::mouseModeChanged(QAction* action) { m_mouseMode = (DatapickerImageView::MouseMode)action->data().toInt(); if (action == navigationModeAction) { setInteractive(false); setDragMode(QGraphicsView::ScrollHandDrag); m_image->setSegmentsHoverEvent(false); } else if (action == zoomSelectionModeAction){ setInteractive(false); setDragMode(QGraphicsView::NoDrag); m_image->setSegmentsHoverEvent(false); setCursor(Qt::ArrowCursor); } else { setInteractive(true); setDragMode(QGraphicsView::NoDrag); m_image->setSegmentsHoverEvent(true); setCursor(Qt::CrossCursor); if (currentPlotPointsTypeAction != action) { if (action == setAxisPointsAction) { - int count = m_image->childCount(AbstractAspect::IncludeHidden); + int count = m_image->childCount(AbstractAspect::ChildIndexFlag::IncludeHidden); if (count) { auto button = QMessageBox::question(this, i18n("Remove existing reference points?"), i18n("All available reference points will be removed. Do you want to continue?")); if (button != QMessageBox::Yes) { currentPlotPointsTypeAction->setChecked(true); return; } } m_image->setPlotPointsType(DatapickerImage::AxisPoints); } else if (action == setCurvePointsAction) m_image->setPlotPointsType(DatapickerImage::CurvePoints); else if (action == selectSegmentAction) m_image->setPlotPointsType(DatapickerImage::SegmentPoints); currentPlotPointsTypeAction = action; } } } void DatapickerImageView::changeZoom(QAction* action) { if (action == zoomInViewAction) zoom(1); else if (action == zoomOutViewAction) zoom(-1); else if (action == zoomOriginAction) { static const float hscale = QApplication::desktop()->physicalDpiX()/(25.4*Worksheet::convertToSceneUnits(1,Worksheet::Millimeter)); static const float vscale = QApplication::desktop()->physicalDpiY()/(25.4*Worksheet::convertToSceneUnits(1,Worksheet::Millimeter)); setTransform(QTransform::fromScale(hscale, vscale)); m_rotationAngle = 0; } else if (action == zoomFitPageWidthAction) { float scaleFactor = viewport()->width()/scene()->sceneRect().width(); setTransform(QTransform::fromScale(scaleFactor, scaleFactor)); m_rotationAngle = 0; } else if (action == zoomFitPageHeightAction) { float scaleFactor = viewport()->height()/scene()->sceneRect().height(); setTransform(QTransform::fromScale(scaleFactor, scaleFactor)); m_rotationAngle = 0; } currentZoomAction = action; if (tbZoom) tbZoom->setDefaultAction(action); //change and set angle if tranform reset changeRotationAngle(); } void DatapickerImageView::changeSelectedItemsPosition(QAction* action) { if (scene()->selectedItems().isEmpty()) return; QPointF shift(0, 0); if (action == shiftLeftAction) shift.setX(1); else if (action == shiftRightAction) shift.setX(-1); else if (action == shiftUpAction) shift.setY(-1); else if (action == shiftDownAction) shift.setY(1); m_image->beginMacro(i18n("%1: change position of selected DatapickerPoints.", m_image->name())); - const QVector axisPoints = m_image->children(AbstractAspect::IncludeHidden); + const QVector axisPoints = m_image->children(AbstractAspect::ChildIndexFlag::IncludeHidden); for (auto* point : axisPoints) { if (!point->graphicsItem()->isSelected()) continue; QPointF newPos = point->position(); newPos = newPos + shift; point->setPosition(newPos); - int pointIndex = m_image->indexOfChild(point, AbstractAspect::IncludeHidden); + int pointIndex = m_image->indexOfChild(point, AbstractAspect::ChildIndexFlag::IncludeHidden); if (pointIndex == -1) continue; DatapickerImage::ReferencePoints points = m_image->axisPoints(); points.scenePos[pointIndex].setX(point->position().x()); points.scenePos[pointIndex].setY(point->position().y()); m_image->setUndoAware(false); m_image->setAxisPoints(points); m_image->setUndoAware(true); } for (auto* curve : m_image->parentAspect()->children()) { - for (auto* point : curve->children(AbstractAspect::IncludeHidden)) { + for (auto* point : curve->children(AbstractAspect::ChildIndexFlag::IncludeHidden)) { if (!point->graphicsItem()->isSelected()) continue; QPointF newPos = point->position(); newPos = newPos + shift; point->setPosition(newPos); } } m_image->endMacro(); if (m_image->m_magnificationWindow && m_image->m_magnificationWindow->isVisible()) updateMagnificationWindow(); } void DatapickerImageView::magnificationChanged(QAction* action) { if (action == noMagnificationAction) magnificationFactor = 0; else if (action == twoTimesMagnificationAction) magnificationFactor = 2; else if (action == threeTimesMagnificationAction) magnificationFactor = 3; else if (action == fourTimesMagnificationAction) magnificationFactor = 4; else if (action == fiveTimesMagnificationAction) magnificationFactor = 5; } void DatapickerImageView::addCurve() { m_datapicker->beginMacro(i18n("%1: add new curve.", m_datapicker->name())); DatapickerCurve* curve = new DatapickerCurve(i18n("Curve")); curve->addDatasheet(m_image->axisPoints().type); m_datapicker->addChild(curve); m_datapicker->endMacro(); setCurvePointsAction->setChecked(true); mouseModeChanged(setCurvePointsAction); } void DatapickerImageView::changeRotationAngle() { this->rotate(m_rotationAngle); this->rotate(-m_image->rotationAngle()); m_rotationAngle = m_image->rotationAngle(); updateBackground(); } void DatapickerImageView::handleImageActions() { if (m_image->isLoaded) { magnificationActionGroup->setEnabled(true); setAxisPointsAction->setEnabled(true); - int pointsCount = m_image->childCount(AbstractAspect::IncludeHidden); + int pointsCount = m_image->childCount(AbstractAspect::ChildIndexFlag::IncludeHidden); if (pointsCount > 0) navigationActionGroup->setEnabled(true); else navigationActionGroup->setEnabled(false); if (pointsCount > 2) { addCurveAction->setEnabled(true); if (m_datapicker->activeCurve()) { setCurvePointsAction->setEnabled(true); selectSegmentAction->setEnabled(true); } else { setCurvePointsAction->setEnabled(false); selectSegmentAction->setEnabled(false); } } else { addCurveAction->setEnabled(false); setCurvePointsAction->setEnabled(false); selectSegmentAction->setEnabled(false); } } else { navigationActionGroup->setEnabled(false); magnificationActionGroup->setEnabled(false); setAxisPointsAction->setEnabled(false); addCurveAction->setEnabled(false); setCurvePointsAction->setEnabled(false); selectSegmentAction->setEnabled(false); } } void DatapickerImageView::exportToFile(const QString& path, const WorksheetView::ExportFormat format, const int resolution) { QRectF sourceRect; sourceRect = scene()->sceneRect(); //print if (format == WorksheetView::Pdf) { QPrinter printer(QPrinter::HighResolution); printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(path); int w = Worksheet::convertFromSceneUnits(sourceRect.width(), Worksheet::Millimeter); int h = Worksheet::convertFromSceneUnits(sourceRect.height(), Worksheet::Millimeter); printer.setPaperSize( QSizeF(w, h), QPrinter::Millimeter); printer.setPageMargins(0,0,0,0, QPrinter::Millimeter); printer.setPrintRange(QPrinter::PageRange); printer.setCreator( QLatin1String("LabPlot ") + LVERSION ); QPainter painter(&printer); painter.setRenderHint(QPainter::Antialiasing); QRectF targetRect(0, 0, painter.device()->width(),painter.device()->height()); painter.begin(&printer); exportPaint(&painter, targetRect, sourceRect); painter.end(); } else if (format == WorksheetView::Svg) { QSvgGenerator generator; generator.setFileName(path); int w = Worksheet::convertFromSceneUnits(sourceRect.width(), Worksheet::Millimeter); int h = Worksheet::convertFromSceneUnits(sourceRect.height(), Worksheet::Millimeter); w = w*QApplication::desktop()->physicalDpiX()/25.4; h = h*QApplication::desktop()->physicalDpiY()/25.4; generator.setSize(QSize(w, h)); QRectF targetRect(0, 0, w, h); generator.setViewBox(targetRect); QPainter painter; painter.begin(&generator); exportPaint(&painter, targetRect, sourceRect); painter.end(); } else { //PNG //TODO add all formats supported by Qt in QImage int w = Worksheet::convertFromSceneUnits(sourceRect.width(), Worksheet::Millimeter); int h = Worksheet::convertFromSceneUnits(sourceRect.height(), Worksheet::Millimeter); w = w*resolution/25.4; h = h*resolution/25.4; QImage image(QSize(w, h), QImage::Format_ARGB32_Premultiplied); image.fill(Qt::transparent); QRectF targetRect(0, 0, w, h); QPainter painter; painter.begin(&image); painter.setRenderHint(QPainter::Antialiasing); exportPaint(&painter, targetRect, sourceRect); painter.end(); image.save(path, "png"); } } void DatapickerImageView::exportPaint(QPainter* painter, const QRectF& targetRect, const QRectF& sourceRect) { painter->save(); painter->scale(targetRect.width()/sourceRect.width(), targetRect.height()/sourceRect.height()); drawBackground(painter, sourceRect); painter->restore(); m_image->setPrinting(true); scene()->render(painter, QRectF(), sourceRect); m_image->setPrinting(false); } void DatapickerImageView::print(QPrinter* printer) { const QRectF scene_rect = sceneRect(); int w = Worksheet::convertFromSceneUnits(scene_rect.width(), Worksheet::Millimeter); int h = Worksheet::convertFromSceneUnits(scene_rect.height(), Worksheet::Millimeter); printer->setPaperSize( QSizeF(w, h), QPrinter::Millimeter); printer->setPageMargins(0,0,0,0, QPrinter::Millimeter); printer->setPrintRange(QPrinter::PageRange); printer->setCreator( QString("LabPlot ") + LVERSION ); QPainter painter(printer); QRectF targetRect(0, 0, painter.device()->width(),painter.device()->height()); painter.setRenderHint(QPainter::Antialiasing); painter.begin(printer); painter.save(); painter.scale(targetRect.width()/scene_rect.width(), targetRect.height()/scene_rect.height()); // canvas if (m_image->isLoaded) { if (m_image->plotImageType() == DatapickerImage::OriginalImage) { QImage todraw = m_image->originalPlotImage.scaled(scene_rect.width(), scene_rect.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); painter.drawImage(scene_rect.topLeft(), todraw); } else if (m_image->plotImageType() == DatapickerImage::ProcessedImage) { QImage todraw = m_image->processedPlotImage.scaled(scene_rect.width(), scene_rect.height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); painter.drawImage(scene_rect.topLeft(), todraw); } else { painter.fillRect(scene_rect, Qt::white); } } else { painter.setBrush(QBrush(Qt::gray)); painter.drawRect(scene_rect); } painter.restore(); m_image->setPrinting(true); scene()->render(&painter, QRectF(), scene_rect); m_image->setPrinting(false); painter.end(); } void DatapickerImageView::updateBackground() { invalidateScene(sceneRect(), QGraphicsScene::BackgroundLayer); } diff --git a/src/commonfrontend/datapicker/DatapickerView.cpp b/src/commonfrontend/datapicker/DatapickerView.cpp index 8ca49a670..aaaf1cce4 100644 --- a/src/commonfrontend/datapicker/DatapickerView.cpp +++ b/src/commonfrontend/datapicker/DatapickerView.cpp @@ -1,219 +1,219 @@ /*************************************************************************** File : DatapickerView.cpp Project : LabPlot Description : View class for Datapicker -------------------------------------------------------------------- Copyright : (C) 2015 by Ankit Wagadre (wagadre.ankit@gmail.com) Copyright : (C) 2015-2020 by 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 "DatapickerView.h" #include "backend/datapicker/Datapicker.h" #include "backend/lib/macros.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/datapicker/DatapickerImage.h" #include "commonfrontend/workbook/WorkbookView.h" #include "backend/datapicker/DatapickerCurve.h" #include "commonfrontend/datapicker/DatapickerImageView.h" #include #include #include #include #include /*! \class DatapickerView \brief View class for Datapicker \ingroup commonfrontend */ DatapickerView::DatapickerView(Datapicker* datapicker) : QWidget(), m_tabWidget(new QTabWidget(this)), m_datapicker(datapicker) { m_tabWidget->setTabPosition(QTabWidget::South); m_tabWidget->setTabShape(QTabWidget::Rounded); // m_tabWidget->setMovable(true); m_tabWidget->setContextMenuPolicy(Qt::CustomContextMenu); m_tabWidget->setMinimumSize(600, 600); auto* layout = new QHBoxLayout(this); - layout->setContentsMargins(0,0,0,0); + layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_tabWidget); //add tab for each children view m_initializing = true; - for (const auto* aspect : m_datapicker->children(AbstractAspect::IncludeHidden)) { + for (const auto* aspect : m_datapicker->children(AbstractAspect::ChildIndexFlag::IncludeHidden)) { handleAspectAdded(aspect); for (const auto* child : aspect->children()) { handleAspectAdded(child); } } m_initializing = false; //SIGNALs/SLOTs connect(m_datapicker, &Datapicker::aspectDescriptionChanged, this, &DatapickerView::handleDescriptionChanged); connect(m_datapicker, &Datapicker::aspectAdded, this, &DatapickerView::handleAspectAdded); connect(m_datapicker, &Datapicker::aspectAboutToBeRemoved, this, &DatapickerView::handleAspectAboutToBeRemoved); connect(m_datapicker, &Datapicker::datapickerItemSelected, this, &DatapickerView::itemSelected); connect(m_tabWidget, &QTabWidget::currentChanged, this, &DatapickerView::tabChanged); connect(m_tabWidget, &QTabWidget::customContextMenuRequested, this, &DatapickerView::showTabContextMenu); connect(m_tabWidget->tabBar(), &QTabBar::tabMoved, this, &DatapickerView::tabMoved); } DatapickerView::~DatapickerView() { //delete all children views here, its own view will be deleted in ~AbstractPart() - for (const auto* aspect : m_datapicker->children(AbstractAspect::IncludeHidden)) { + for (const auto* aspect : m_datapicker->children(AbstractAspect::ChildIndexFlag::IncludeHidden)) { for (const auto* child : aspect->children()) { const auto* part = dynamic_cast(child); if (part) part->deleteView(); } const auto* part = dynamic_cast(aspect); if (part) part->deleteView(); } } void DatapickerView::fillToolBar(QToolBar* toolBar) { auto* view = static_cast(m_datapicker->image()->view()); view->fillToolBar(toolBar); } void DatapickerView::createContextMenu(QMenu* menu) const { Q_ASSERT(menu); m_datapicker->image()->createContextMenu(menu); } int DatapickerView::currentIndex() const { return m_tabWidget->currentIndex(); } //############################################################################## //######################### Private slots #################################### //############################################################################## /*! called when the current tab was changed. Propagates the selection of \c Spreadsheet or of a \c DatapickerImage object to \c Datapicker. */ void DatapickerView::tabChanged(int index) { if (m_initializing) return; if (index == -1) return; m_datapicker->setChildSelectedInView(lastSelectedIndex, false); m_datapicker->setChildSelectedInView(index, true); lastSelectedIndex = index; } void DatapickerView::tabMoved(int from, int to) { Q_UNUSED(from); Q_UNUSED(to); //TODO: // AbstractAspect* aspect = m_datapicker->child(to); // if (aspect) { // m_tabMoving = true; // AbstractAspect* sibling = m_datapicker->child(from); // qDebug()<<"insert: " << to << " " << aspect->name() << ", " << from << " " << sibling->name(); // aspect->remove(); // m_datapicker->insertChildBefore(aspect, sibling); // qDebug()<<"inserted"; // m_tabMoving = false; // } } void DatapickerView::itemSelected(int index) { m_initializing = true; m_tabWidget->setCurrentIndex(index); m_initializing = false; } void DatapickerView::showTabContextMenu(QPoint point) { QMenu* menu = nullptr; - auto* aspect = m_datapicker->child(m_tabWidget->currentIndex(), AbstractAspect::IncludeHidden); + auto* aspect = m_datapicker->child(m_tabWidget->currentIndex(), AbstractAspect::ChildIndexFlag::IncludeHidden); auto* spreadsheet = dynamic_cast(aspect); if (spreadsheet) { menu = spreadsheet->createContextMenu(); } else { auto* image = dynamic_cast(aspect); if (image) menu = image->createContextMenu(); } if (menu) menu->exec(m_tabWidget->mapToGlobal(point)); } void DatapickerView::handleDescriptionChanged(const AbstractAspect* aspect) { if (aspect == m_datapicker) return; //determine the child that was changed and adjust the name of the corresponding tab widget int index = -1; QString name; if (aspect->parentAspect() == m_datapicker) { //datapicker curve was renamed - index = m_datapicker->indexOfChild(aspect, AbstractAspect::IncludeHidden); + index = m_datapicker->indexOfChild(aspect, AbstractAspect::ChildIndexFlag::IncludeHidden); if (index != -1) name = aspect->name() + ": " + aspect->children().constFirst()->name(); } else { //data spreadsheet was renamed or one of its columns, which is not relevant here - index = m_datapicker->indexOfChild(aspect->parentAspect(), AbstractAspect::IncludeHidden); + index = m_datapicker->indexOfChild(aspect->parentAspect(), AbstractAspect::ChildIndexFlag::IncludeHidden); if (index != -1) name = aspect->parentAspect()->name() + ": " + aspect->name(); } if (index != -1) m_tabWidget->setTabText(index, name); } void DatapickerView::handleAspectAdded(const AbstractAspect* aspect) { int index; QString name; const AbstractPart* part = dynamic_cast(aspect); if (part) { index = 0; name = aspect->name(); } else if (dynamic_cast(aspect)) { - index = m_datapicker->indexOfChild(aspect, AbstractAspect::IncludeHidden); + index = m_datapicker->indexOfChild(aspect, AbstractAspect::ChildIndexFlag::IncludeHidden); const Spreadsheet* spreadsheet = static_cast(aspect->child(0)); part = spreadsheet; name = aspect->name() + ": " + spreadsheet->name(); } else return; m_tabWidget->insertTab(index, part->view(), name); m_tabWidget->setTabIcon(m_tabWidget->count(), aspect->icon()); } void DatapickerView::handleAspectAboutToBeRemoved(const AbstractAspect* aspect) { const auto* curve = dynamic_cast(aspect); if (curve) { - int index = m_datapicker->indexOfChild(aspect, AbstractAspect::IncludeHidden); + int index = m_datapicker->indexOfChild(aspect, AbstractAspect::ChildIndexFlag::IncludeHidden); m_tabWidget->removeTab(index); } } diff --git a/src/kdefrontend/MainWin.cpp b/src/kdefrontend/MainWin.cpp index 106db5c0a..a589613bd 100644 --- a/src/kdefrontend/MainWin.cpp +++ b/src/kdefrontend/MainWin.cpp @@ -1,2442 +1,2442 @@ /*************************************************************************** File : MainWin.cc Project : LabPlot Description : Main window of the application -------------------------------------------------------------------- Copyright : (C) 2008-2018 Stefan Gerlach (stefan.gerlach@uni.kn) Copyright : (C) 2009-2020 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 "MainWin.h" #include "backend/core/Project.h" #include "backend/core/Folder.h" #include "backend/core/AspectTreeModel.h" #include "backend/core/Workbook.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/matrix/Matrix.h" #include "backend/worksheet/Worksheet.h" #include "backend/datasources/LiveDataSource.h" #include "backend/datasources/DatasetHandler.h" #ifdef HAVE_LIBORIGIN #include "backend/datasources/projects/OriginProjectParser.h" #endif #ifdef HAVE_CANTOR_LIBS #include "backend/cantorWorksheet/CantorWorksheet.h" #endif #include "backend/datapicker/Datapicker.h" #include "backend/note/Note.h" #include "backend/lib/macros.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #ifdef HAVE_MQTT #include "backend/datasources/MQTTClient.h" #endif #include "commonfrontend/core/PartMdiView.h" #include "commonfrontend/ProjectExplorer.h" #include "commonfrontend/matrix/MatrixView.h" #include "commonfrontend/spreadsheet/SpreadsheetView.h" #include "commonfrontend/worksheet/WorksheetView.h" #ifdef HAVE_CANTOR_LIBS #include "commonfrontend/cantorWorksheet/CantorWorksheetView.h" #endif #include "commonfrontend/datapicker/DatapickerView.h" #include "commonfrontend/datapicker/DatapickerImageView.h" #include "commonfrontend/note/NoteView.h" #include "commonfrontend/widgets/MemoryWidget.h" #include "kdefrontend/datasources/ImportFileDialog.h" #include "kdefrontend/datasources/ImportDatasetDialog.h" #include "kdefrontend/datasources/ImportDatasetWidget.h" #include "kdefrontend/datasources/ImportProjectDialog.h" #include "kdefrontend/datasources/ImportSQLDatabaseDialog.h" #include #include "kdefrontend/dockwidgets/ProjectDock.h" #include "kdefrontend/HistoryDialog.h" #include "kdefrontend/SettingsDialog.h" #include "kdefrontend/GuiObserver.h" #include "kdefrontend/widgets/LabelWidget.h" #include "kdefrontend/widgets/FITSHeaderEditDialog.h" #include "DatasetModel.h" // #include "welcomescreen/WelcomeScreenHelper.h" #ifdef HAVE_KUSERFEEDBACK #include #include #include #include #include #include #endif #ifdef Q_OS_MAC #include "3rdparty/kdmactouchbar/src/kdmactouchbar.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include // #include // #include // #include // #include // #include // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_CANTOR_LIBS #include #include #include #include //required to parse Cantor and Jupyter files #include #include #include #include #endif /*! \class MainWin \brief Main application window. \ingroup kdefrontend */ MainWin::MainWin(QWidget *parent, const QString& filename) : KXmlGuiWindow(parent), m_schemeManager(new KColorSchemeManager(this)) { initGUI(filename); setAcceptDrops(true); #ifdef HAVE_KUSERFEEDBACK m_userFeedbackProvider.setProductIdentifier(QStringLiteral("org.kde.labplot")); m_userFeedbackProvider.setFeedbackServer(QUrl(QStringLiteral("https://telemetry.kde.org/"))); m_userFeedbackProvider.setSubmissionInterval(7); m_userFeedbackProvider.setApplicationStartsUntilEncouragement(5); m_userFeedbackProvider.setEncouragementDelay(30); // software version info m_userFeedbackProvider.addDataSource(new KUserFeedback::ApplicationVersionSource); m_userFeedbackProvider.addDataSource(new KUserFeedback::QtVersionSource); // info about the machine m_userFeedbackProvider.addDataSource(new KUserFeedback::PlatformInfoSource); m_userFeedbackProvider.addDataSource(new KUserFeedback::ScreenInfoSource); // usage info m_userFeedbackProvider.addDataSource(new KUserFeedback::StartCountSource); m_userFeedbackProvider.addDataSource(new KUserFeedback::UsageTimeSource); #endif //restore the geometry KConfigGroup group = KSharedConfig::openConfig()->group("MainWin"); restoreGeometry(group.readEntry("geometry", QByteArray())); } MainWin::~MainWin() { //save the recent opened files m_recentProjectsAction->saveEntries( KSharedConfig::openConfig()->group("Recent Files") ); KConfigGroup group = KSharedConfig::openConfig()->group("MainWin"); group.writeEntry("geometry", saveGeometry()); KSharedConfig::openConfig()->sync(); //if welcome screen is shown, save its settings prior to deleting it // if(dynamic_cast(centralWidget())) // QMetaObject::invokeMethod(m_welcomeWidget->rootObject(), "saveWidgetDimensions"); if (m_project) { // if(dynamic_cast(centralWidget()) == nullptr) // m_mdiArea->closeAllSubWindows(); disconnect(m_project, nullptr, this, nullptr); delete m_project; } if (m_aspectTreeModel) delete m_aspectTreeModel; if (m_guiObserver) delete m_guiObserver; // if(m_welcomeScreenHelper) // delete m_welcomeScreenHelper; } void MainWin::showPresenter() { const Worksheet* w = dynamic_cast(m_currentAspect); if (w) { auto* view = static_cast(w->view()); view->presenterMode(); } else { //currently active object is not a worksheet but we're asked to start in the presenter mode //determine the first available worksheet and show it in the presenter mode auto worksheets = m_project->children(); if (worksheets.size() > 0) { auto* view = static_cast(worksheets.constFirst()->view()); view->presenterMode(); } else { QMessageBox::information(this, i18n("Presenter Mode"), i18n("No worksheets are available in the project. The presenter mode will not be started.")); } } } AspectTreeModel* MainWin::model() const { return m_aspectTreeModel; } Project* MainWin::project() const { return m_project; } void MainWin::initGUI(const QString& fileName) { statusBar()->showMessage(i18nc("%1 is the LabPlot version", "Welcome to LabPlot %1", QLatin1String(LVERSION))); initActions(); #ifdef Q_OS_MAC setupGUI(Default, QLatin1String("/Applications/labplot2.app/Contents/Resources/labplot2ui.rc")); m_touchBar = new KDMacTouchBar(this); //m_touchBar->setTouchButtonStyle(KDMacTouchBar::IconOnly); #else setupGUI(Default, KXMLGUIClient::xmlFile()); // should be "labplot2ui.rc" #endif DEBUG("Component name: " << STDSTRING(KXMLGUIClient::componentName())); DEBUG("XML file: " << STDSTRING(KXMLGUIClient::xmlFile()) << " (should be \"labplot2ui.rc\")"); //all toolbars created via the KXMLGUI framework are locked on default: // * on the very first program start, unlock all toolbars // * on later program starts, set stored lock status //Furthermore, we want to show icons only after the first program start. KConfigGroup groupMain = KSharedConfig::openConfig()->group("MainWindow"); if (groupMain.exists()) { //KXMLGUI framework automatically stores "Disabled" for the key "ToolBarsMovable" //in case the toolbars are locked -> load this value const QString& str = groupMain.readEntry(QLatin1String("ToolBarsMovable"), ""); bool locked = (str == QLatin1String("Disabled")); KToolBar::setToolBarsLocked(locked); } else { //first start KToolBar::setToolBarsLocked(false); //show icons only for (auto* container : factory()->containers(QLatin1String("ToolBar"))) { auto* toolbar = dynamic_cast(container); if (toolbar) toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); } } initMenus(); auto* mainToolBar = qobject_cast(factory()->container("main_toolbar", this)); if (!mainToolBar) { QMessageBox::critical(this, i18n("GUI configuration file not found"), i18n("%1 file was not found. Please check your installation.", KXMLGUIClient::xmlFile())); //TODO: the application is not really usable if the rc file was not found. We should quit the application. The following line crashes //the application because of the splash screen. We need to find another solution. // QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); //call close as soon as we enter the eventloop return; } auto* tbImport = new QToolButton(mainToolBar); tbImport->setPopupMode(QToolButton::MenuButtonPopup); tbImport->setMenu(m_importMenu); tbImport->setDefaultAction(m_importFileAction); mainToolBar->addWidget(tbImport); qobject_cast(factory()->container("import", this))->setIcon(QIcon::fromTheme("document-import")); setWindowIcon(QIcon::fromTheme("LabPlot2", QGuiApplication::windowIcon())); setAttribute( Qt::WA_DeleteOnClose ); //make the status bar of a fixed size in order to avoid height changes when placing a ProgressBar there. QFont font; font.setFamily(font.defaultFamily()); QFontMetrics fm(font); statusBar()->setFixedHeight(fm.height() + 5); //load recently used projects m_recentProjectsAction->loadEntries( KSharedConfig::openConfig()->group("Recent Files") ); //auto-save KConfigGroup group = KSharedConfig::openConfig()->group("Settings_General"); m_autoSaveActive = group.readEntry("AutoSave", false); int interval = group.readEntry("AutoSaveInterval", 1); interval = interval*60*1000; m_autoSaveTimer.setInterval(interval); connect(&m_autoSaveTimer, &QTimer::timeout, this, &MainWin::autoSaveProject); if (!fileName.isEmpty()) { createMdiArea(); setCentralWidget(m_mdiArea); #ifdef HAVE_LIBORIGIN if (Project::isLabPlotProject(fileName) || OriginProjectParser::isOriginProject(fileName)) { #else if (Project::isLabPlotProject(fileName)) { #endif QTimer::singleShot(0, this, [=] () { openProject(fileName); }); } else { newProject(); QTimer::singleShot(0, this, [=] () { importFileDialog(fileName); }); } } else { //There is no file to open. Depending on the settings do nothing, //create a new project or open the last used project. LoadOnStart load = (LoadOnStart)group.readEntry("LoadOnStart", (int)Nothing); if (load != WelcomeScreen) { createMdiArea(); setCentralWidget(m_mdiArea); if (load == NewProject) //create new project newProject(); else if (load == NewProjectWorksheet) { //create new project with a worksheet newProject(); newWorksheet(); } else if (load == LastProject) { //open last used project if (!m_recentProjectsAction->urls().isEmpty()) { QDEBUG("TO OPEN m_recentProjectsAction->urls() =" << m_recentProjectsAction->urls().constFirst()); openRecentProject( m_recentProjectsAction->urls().constFirst() ); } } updateGUIOnProjectChanges(); } else{ //welcome screen // m_showWelcomeScreen = true; // m_welcomeWidget = createWelcomeScreen(); // setCentralWidget(m_welcomeWidget); } } //show memory info const bool showMemoryInfo = group.readEntry(QLatin1String("ShowMemoryInfo"), true); if (showMemoryInfo) { m_memoryInfoWidget = new MemoryWidget(statusBar()); statusBar()->addPermanentWidget(m_memoryInfoWidget); } } /** * @brief Creates a new welcome screen to be set as central widget. */ /* QQuickWidget* MainWin::createWelcomeScreen() { QSize maxSize = qApp->primaryScreen()->availableSize(); resize(maxSize); setMinimumSize(700, 400); showMaximized(); KToolBar* toolbar = toolBar(); if(toolbar) toolbar->setVisible(false); QList recentList; for (QUrl& url : m_recentProjectsAction->urls()) recentList.append(QVariant(url)); //Set context property QQuickWidget* quickWidget = new QQuickWidget(this); QQmlContext* ctxt = quickWidget->rootContext(); QVariant variant(recentList); ctxt->setContextProperty("recentProjects", variant); //Create helper object if(m_welcomeScreenHelper) delete m_welcomeScreenHelper; m_welcomeScreenHelper = new WelcomeScreenHelper(); connect(m_welcomeScreenHelper, &WelcomeScreenHelper::openExampleProject, this, QOverload::of(&MainWin::openProject)); ctxt->setContextProperty("datasetModel", m_welcomeScreenHelper->getDatasetModel()); ctxt->setContextProperty("helper", m_welcomeScreenHelper); quickWidget->setSource(QUrl(QLatin1String("qrc:///main.qml"))); quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); QObject *item = quickWidget->rootObject(); //connect qml's signals QObject::connect(item, SIGNAL(recentProjectClicked(QUrl)), this, SLOT(openRecentProject(QUrl))); QObject::connect(item, SIGNAL(datasetClicked(QString,QString,QString)), m_welcomeScreenHelper, SLOT(datasetClicked(QString,QString,QString))); QObject::connect(item, SIGNAL(openDataset()), this, SLOT(openDatasetExample())); QObject::connect(item, SIGNAL(openExampleProject(QString)), m_welcomeScreenHelper, SLOT(exampleProjectClicked(QString))); emit m_welcomeScreenHelper->showFirstDataset(); return quickWidget; } */ /** * @brief Initiates resetting the layout of the welcome screen */ /* void MainWin::resetWelcomeScreen() { if(dynamic_cast(centralWidget())) QMetaObject::invokeMethod(m_welcomeWidget->rootObject(), "restoreOriginalLayout"); } */ /** * @brief Creates a new MDI area, to replace the Welcome Screen as central widget */ void MainWin::createMdiArea() { KToolBar* toolbar = toolBar(); if(toolbar) toolbar->setVisible(true); //Save welcome screen's dimensions. // if(m_showWelcomeScreen) // QMetaObject::invokeMethod(m_welcomeWidget->rootObject(), "saveWidgetDimensions"); m_mdiArea = new QMdiArea; setCentralWidget(m_mdiArea); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, &MainWin::handleCurrentSubWindowChanged); //set the view mode of the mdi area KConfigGroup group = KSharedConfig::openConfig()->group( "Settings_General" ); int viewMode = group.readEntry("ViewMode", 0); if (viewMode == 1) { m_mdiArea->setViewMode(QMdiArea::TabbedView); int tabPosition = group.readEntry("TabPosition", 0); m_mdiArea->setTabPosition(QTabWidget::TabPosition(tabPosition)); m_mdiArea->setTabsClosable(true); m_mdiArea->setTabsMovable(true); m_tileWindowsAction->setVisible(false); m_cascadeWindowsAction->setVisible(false); } connect(m_closeWindowAction, &QAction::triggered, m_mdiArea, &QMdiArea::closeActiveSubWindow); connect(m_closeAllWindowsAction, &QAction::triggered, m_mdiArea, &QMdiArea::closeAllSubWindows); connect(m_tileWindowsAction, &QAction::triggered, m_mdiArea, &QMdiArea::tileSubWindows); connect(m_cascadeWindowsAction, &QAction::triggered, m_mdiArea, &QMdiArea::cascadeSubWindows); connect(m_nextWindowAction, &QAction::triggered, m_mdiArea, &QMdiArea::activateNextSubWindow); connect(m_prevWindowAction, &QAction::triggered, m_mdiArea, &QMdiArea::activatePreviousSubWindow); } void MainWin::initActions() { // ******************** File-menu ******************************* //add some standard actions m_newProjectAction = KStandardAction::openNew(this, SLOT(newProject()),actionCollection()); m_openProjectAction = KStandardAction::open(this, SLOT(openProject()),actionCollection()); m_recentProjectsAction = KStandardAction::openRecent(this, SLOT(openRecentProject(QUrl)),actionCollection()); m_closeAction = KStandardAction::close(this, SLOT(closeProject()),actionCollection()); actionCollection()->setDefaultShortcut(m_closeAction, QKeySequence()); //remove the shortcut, QKeySequence::Close will be used for closing sub-windows m_saveAction = KStandardAction::save(this, SLOT(saveProject()),actionCollection()); m_saveAsAction = KStandardAction::saveAs(this, SLOT(saveProjectAs()),actionCollection()); m_printAction = KStandardAction::print(this, SLOT(print()),actionCollection()); m_printPreviewAction = KStandardAction::printPreview(this, SLOT(printPreview()),actionCollection()); //TODO: on Mac OS when going full-screen we get a crash because of an stack-overflow #ifndef Q_OS_MAC KStandardAction::fullScreen(this, SLOT(toggleFullScreen()), this, actionCollection()); #endif //New Folder/Workbook/Spreadsheet/Matrix/Worksheet/Datasources m_newWorkbookAction = new QAction(QIcon::fromTheme("labplot-workbook-new"),i18n("Workbook"),this); actionCollection()->addAction("new_workbook", m_newWorkbookAction); m_newWorkbookAction->setWhatsThis(i18n("Creates a new workbook for collection spreadsheets, matrices and plots")); connect(m_newWorkbookAction, &QAction::triggered, this, &MainWin::newWorkbook); m_newDatapickerAction = new QAction(QIcon::fromTheme("color-picker-black"), i18n("Datapicker"), this); m_newDatapickerAction->setWhatsThis(i18n("Creates a data picker for getting data from a picture")); actionCollection()->addAction("new_datapicker", m_newDatapickerAction); connect(m_newDatapickerAction, &QAction::triggered, this, &MainWin::newDatapicker); m_newSpreadsheetAction = new QAction(QIcon::fromTheme("labplot-spreadsheet-new"),i18n("Spreadsheet"),this); // m_newSpreadsheetAction->setShortcut(Qt::CTRL+Qt::Key_Equal); m_newSpreadsheetAction->setWhatsThis(i18n("Creates a new spreadsheet for data editing")); actionCollection()->addAction("new_spreadsheet", m_newSpreadsheetAction); connect(m_newSpreadsheetAction, &QAction::triggered, this, &MainWin::newSpreadsheet); m_newMatrixAction = new QAction(QIcon::fromTheme("labplot-matrix-new"),i18n("Matrix"),this); // m_newMatrixAction->setShortcut(Qt::CTRL+Qt::Key_Equal); m_newMatrixAction->setWhatsThis(i18n("Creates a new matrix for data editing")); actionCollection()->addAction("new_matrix", m_newMatrixAction); connect(m_newMatrixAction, &QAction::triggered, this, &MainWin::newMatrix); m_newWorksheetAction = new QAction(QIcon::fromTheme("labplot-worksheet-new"),i18n("Worksheet"),this); // m_newWorksheetAction->setShortcut(Qt::ALT+Qt::Key_X); m_newWorksheetAction->setWhatsThis(i18n("Creates a new worksheet for data plotting")); actionCollection()->addAction("new_worksheet", m_newWorksheetAction); connect(m_newWorksheetAction, &QAction::triggered, this, &MainWin::newWorksheet); m_newNotesAction = new QAction(QIcon::fromTheme("document-new"),i18n("Note"),this); m_newNotesAction->setWhatsThis(i18n("Creates a new note for arbitrary text")); actionCollection()->addAction("new_notes", m_newNotesAction); connect(m_newNotesAction, &QAction::triggered, this, &MainWin::newNotes); // m_newScriptAction = new QAction(QIcon::fromTheme("insert-text"),i18n("Note/Script"),this); // actionCollection()->addAction("new_script", m_newScriptAction); // connect(m_newScriptAction, &QAction::triggered,SLOT(newScript())); m_newFolderAction = new QAction(QIcon::fromTheme("folder-new"),i18n("Folder"),this); m_newFolderAction->setWhatsThis(i18n("Creates a new folder to collect sheets and other elements")); actionCollection()->addAction("new_folder", m_newFolderAction); connect(m_newFolderAction, &QAction::triggered, this, &MainWin::newFolder); //"New file datasources" m_newLiveDataSourceAction = new QAction(QIcon::fromTheme("application-octet-stream"),i18n("Live Data Source"),this); m_newLiveDataSourceAction->setWhatsThis(i18n("Creates a live data source to read data from a real time device")); actionCollection()->addAction("new_live_datasource", m_newLiveDataSourceAction); connect(m_newLiveDataSourceAction, &QAction::triggered, this, &MainWin::newLiveDataSourceActionTriggered); //Import/Export m_importFileAction = new QAction(QIcon::fromTheme("document-import"), i18n("Import from File"), this); actionCollection()->setDefaultShortcut(m_importFileAction, Qt::CTRL+Qt::SHIFT+Qt::Key_I); m_importFileAction->setWhatsThis(i18n("Import data from a regular file")); actionCollection()->addAction("import_file", m_importFileAction); connect(m_importFileAction, &QAction::triggered, this, [=]() {importFileDialog();}); m_importSqlAction = new QAction(QIcon::fromTheme("network-server-database"), i18n("From SQL Database"), this); m_importSqlAction->setWhatsThis(i18n("Import data from a SQL database")); actionCollection()->addAction("import_sql", m_importSqlAction); connect(m_importSqlAction, &QAction::triggered, this, &MainWin::importSqlDialog); m_importDatasetAction = new QAction(QIcon::fromTheme(QLatin1String("database-index")), i18n("From Dataset Collection"), this); m_importDatasetAction->setWhatsThis(i18n("Imports data from an online dataset")); actionCollection()->addAction("import_dataset_datasource", m_importDatasetAction); connect(m_importDatasetAction, &QAction::triggered, this, &MainWin::importDatasetDialog); m_importLabPlotAction = new QAction(QIcon::fromTheme("project-open"), i18n("LabPlot Project"), this); m_importLabPlotAction->setWhatsThis(i18n("Import a project from a LabPlot project file (.lml)")); actionCollection()->addAction("import_labplot", m_importLabPlotAction); connect(m_importLabPlotAction, &QAction::triggered, this, &MainWin::importProjectDialog); #ifdef HAVE_LIBORIGIN m_importOpjAction = new QAction(QIcon::fromTheme("project-open"), i18n("Origin Project (OPJ)"), this); m_importOpjAction->setWhatsThis(i18n("Import a project from an OriginLab Origin project file (.opj)")); actionCollection()->addAction("import_opj", m_importOpjAction); connect(m_importOpjAction, &QAction::triggered, this, &MainWin::importProjectDialog); #endif m_exportAction = new QAction(QIcon::fromTheme("document-export"), i18n("Export"), this); m_exportAction->setWhatsThis(i18n("Export selected element")); actionCollection()->setDefaultShortcut(m_exportAction, Qt::CTRL+Qt::SHIFT+Qt::Key_E); actionCollection()->addAction("export", m_exportAction); connect(m_exportAction, &QAction::triggered, this, &MainWin::exportDialog); m_editFitsFileAction = new QAction(QIcon::fromTheme("editor"), i18n("FITS Metadata Editor"), this); m_editFitsFileAction->setWhatsThis(i18n("Open editor to edit FITS meta data")); actionCollection()->addAction("edit_fits", m_editFitsFileAction); connect(m_editFitsFileAction, &QAction::triggered, this, &MainWin::editFitsFileDialog); // Edit //Undo/Redo-stuff m_undoAction = KStandardAction::undo(this, SLOT(undo()), actionCollection()); m_redoAction = KStandardAction::redo(this, SLOT(redo()), actionCollection()); m_historyAction = new QAction(QIcon::fromTheme("view-history"), i18n("Undo/Redo History"),this); actionCollection()->addAction("history", m_historyAction); connect(m_historyAction, &QAction::triggered, this, &MainWin::historyDialog); #ifdef Q_OS_MAC m_undoIconOnlyAction = new QAction(m_undoAction->icon(), QString()); connect(m_undoIconOnlyAction, &QAction::triggered, this, &MainWin::undo); m_redoIconOnlyAction = new QAction(m_redoAction->icon(), QString()); connect(m_redoIconOnlyAction, &QAction::triggered, this, &MainWin::redo); #endif // TODO: more menus // Appearance // Analysis: see WorksheetView.cpp // Drawing // Script //Windows m_closeWindowAction = new QAction(i18n("&Close"), this); actionCollection()->setDefaultShortcut(m_closeWindowAction, QKeySequence::Close); m_closeWindowAction->setStatusTip(i18n("Close the active window")); actionCollection()->addAction("close window", m_closeWindowAction); m_closeAllWindowsAction = new QAction(i18n("Close &All"), this); m_closeAllWindowsAction->setStatusTip(i18n("Close all the windows")); actionCollection()->addAction("close all windows", m_closeAllWindowsAction); m_tileWindowsAction = new QAction(i18n("&Tile"), this); m_tileWindowsAction->setStatusTip(i18n("Tile the windows")); actionCollection()->addAction("tile windows", m_tileWindowsAction); m_cascadeWindowsAction = new QAction(i18n("&Cascade"), this); m_cascadeWindowsAction->setStatusTip(i18n("Cascade the windows")); actionCollection()->addAction("cascade windows", m_cascadeWindowsAction); m_nextWindowAction = new QAction(QIcon::fromTheme("go-next-view"), i18n("Ne&xt"), this); actionCollection()->setDefaultShortcut(m_nextWindowAction, QKeySequence::NextChild); m_nextWindowAction->setStatusTip(i18n("Move the focus to the next window")); actionCollection()->addAction("next window", m_nextWindowAction); m_prevWindowAction = new QAction(QIcon::fromTheme("go-previous-view"), i18n("Pre&vious"), this); actionCollection()->setDefaultShortcut(m_prevWindowAction, QKeySequence::PreviousChild); m_prevWindowAction->setStatusTip(i18n("Move the focus to the previous window")); actionCollection()->addAction("previous window", m_prevWindowAction); //"Standard actions" KStandardAction::preferences(this, SLOT(settingsDialog()), actionCollection()); KStandardAction::quit(this, SLOT(close()), actionCollection()); //Actions for window visibility auto* windowVisibilityActions = new QActionGroup(this); windowVisibilityActions->setExclusive(true); m_visibilityFolderAction = new QAction(QIcon::fromTheme("folder"), i18n("Current &Folder Only"), windowVisibilityActions); m_visibilityFolderAction->setCheckable(true); - m_visibilityFolderAction->setData(Project::folderOnly); + m_visibilityFolderAction->setData(static_cast(Project::MdiWindowVisibility::folderOnly)); m_visibilitySubfolderAction = new QAction(QIcon::fromTheme("folder-documents"), i18n("Current Folder and &Subfolders"), windowVisibilityActions); m_visibilitySubfolderAction->setCheckable(true); - m_visibilitySubfolderAction->setData(Project::folderAndSubfolders); + m_visibilitySubfolderAction->setData(static_cast(Project::MdiWindowVisibility::folderAndSubfolders)); m_visibilityAllAction = new QAction(i18n("&All"), windowVisibilityActions); m_visibilityAllAction->setCheckable(true); - m_visibilityAllAction->setData(Project::allMdiWindows); + m_visibilityAllAction->setData(static_cast(Project::MdiWindowVisibility::allMdiWindows)); connect(windowVisibilityActions, &QActionGroup::triggered, this, &MainWin::setMdiWindowVisibility); //Actions for hiding/showing the dock widgets auto* docksActions = new QActionGroup(this); docksActions->setExclusive(false); m_toggleProjectExplorerDockAction = new QAction(QIcon::fromTheme("view-list-tree"), i18n("Project Explorer"), docksActions); m_toggleProjectExplorerDockAction->setCheckable(true); m_toggleProjectExplorerDockAction->setChecked(true); actionCollection()->addAction("toggle_project_explorer_dock", m_toggleProjectExplorerDockAction); m_togglePropertiesDockAction = new QAction(QIcon::fromTheme("view-list-details"), i18n("Properties Explorer"), docksActions); m_togglePropertiesDockAction->setCheckable(true); m_togglePropertiesDockAction->setChecked(true); actionCollection()->addAction("toggle_properties_explorer_dock", m_togglePropertiesDockAction); connect(docksActions, &QActionGroup::triggered, this, &MainWin::toggleDockWidget); //global search QAction* searchAction = new QAction(actionCollection()); searchAction->setShortcut(QKeySequence::Find); connect(searchAction, &QAction::triggered, this, [=]() { if (m_project) { if (!m_projectExplorerDock->isVisible()) { m_toggleProjectExplorerDockAction->setChecked(true); toggleDockWidget(m_toggleProjectExplorerDockAction); } m_projectExplorer->search(); } }); this->addAction(searchAction); } void MainWin::initMenus() { //menu in the main toolbar for adding new aspects auto* menu = dynamic_cast(factory()->container("new", this)); menu->setIcon(QIcon::fromTheme("window-new")); //menu in the project explorer and in the toolbar for adding new aspects m_newMenu = new QMenu(i18n("Add New"), this); m_newMenu->setIcon(QIcon::fromTheme("window-new")); m_newMenu->addAction(m_newFolderAction); m_newMenu->addAction(m_newWorkbookAction); m_newMenu->addAction(m_newSpreadsheetAction); m_newMenu->addAction(m_newMatrixAction); m_newMenu->addAction(m_newWorksheetAction); m_newMenu->addAction(m_newNotesAction); m_newMenu->addAction(m_newDatapickerAction); m_newMenu->addSeparator(); m_newMenu->addAction(m_newLiveDataSourceAction); //import menu m_importMenu = new QMenu(this); m_importMenu->setIcon(QIcon::fromTheme("document-import")); m_importMenu ->addAction(m_importFileAction); m_importMenu ->addAction(m_importSqlAction); m_importMenu->addAction(m_importDatasetAction); m_importMenu->addSeparator(); m_importMenu->addAction(m_importLabPlotAction); #ifdef HAVE_LIBORIGIN m_importMenu ->addAction(m_importOpjAction); #endif #ifdef HAVE_CANTOR_LIBS m_newMenu->addSeparator(); m_newCantorWorksheetMenu = new QMenu(i18n("CAS Worksheet"), this); m_newCantorWorksheetMenu->setIcon(QIcon::fromTheme("archive-insert")); //"Adding Cantor backends to menu and context menu" QStringList m_availableBackend = Cantor::Backend::listAvailableBackends(); if (m_availableBackend.count() > 0) { unplugActionList(QLatin1String("backends_list")); QList newBackendActions; for (auto* backend : Cantor::Backend::availableBackends()) { if (!backend->isEnabled()) continue; QAction* action = new QAction(QIcon::fromTheme(backend->icon()), backend->name(), this); action->setData(backend->name()); newBackendActions << action; m_newCantorWorksheetMenu->addAction(action); } connect(m_newCantorWorksheetMenu, &QMenu::triggered, this, &MainWin::newCantorWorksheet); plugActionList(QLatin1String("backends_list"), newBackendActions); } m_newMenu->addMenu(m_newCantorWorksheetMenu); #else delete this->guiFactory()->container("cas_worksheet", this); delete this->guiFactory()->container("new_cas_worksheet", this); delete this->guiFactory()->container("cas_worksheet_toolbar", this); #endif //menu subwindow visibility policy m_visibilityMenu = new QMenu(i18n("Window Visibility Policy"), this); m_visibilityMenu->setIcon(QIcon::fromTheme("window-duplicate")); m_visibilityMenu ->addAction(m_visibilityFolderAction); m_visibilityMenu ->addAction(m_visibilitySubfolderAction); m_visibilityMenu ->addAction(m_visibilityAllAction); //menu for editing files m_editMenu = new QMenu(i18n("Edit"), this); m_editMenu->addAction(m_editFitsFileAction); //set the action for the current color scheme checked KConfigGroup group = KSharedConfig::openConfig()->group(QLatin1String("Settings_General")); #if KCONFIGWIDGETS_VERSION > QT_VERSION_CHECK(5,67,0) // Since version 5.67 KColorSchemeManager has a system default option QString schemeName = group.readEntry("ColorScheme"); #else KConfigGroup generalGlobalsGroup = KSharedConfig::openConfig(QLatin1String("kdeglobals"))->group("General"); QString defaultSchemeName = generalGlobalsGroup.readEntry("ColorScheme", QStringLiteral("Breeze")); QString schemeName = group.readEntry("ColorScheme", defaultSchemeName); #endif KActionMenu* schemesMenu = m_schemeManager->createSchemeSelectionMenu(i18n("Color Scheme"), schemeName, this); schemesMenu->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-color"))); QMenu* settingsMenu = dynamic_cast(factory()->container("settings", this)); if (settingsMenu) settingsMenu->insertMenu(settingsMenu->actions().constFirst(), schemesMenu->menu()); connect(schemesMenu->menu(), &QMenu::triggered, this, &MainWin::colorSchemeChanged); #ifdef HAVE_CANTOR_LIBS QAction* action = new QAction(QIcon::fromTheme(QLatin1String("cantor")), i18n("Configure CAS"), this); connect(action, &QAction::triggered, this, &MainWin::cantorSettingsDialog); if (settingsMenu) settingsMenu->addAction(action); #endif } void MainWin::colorSchemeChanged(QAction* action) { QString schemeName = KLocalizedString::removeAcceleratorMarker(action->text()); //background of the mdi area is not updated on theme changes, do it here. QModelIndex index = m_schemeManager->indexForScheme(schemeName); const QPalette& palette = KColorScheme::createApplicationPalette( KSharedConfig::openConfig(index.data(Qt::UserRole).toString()) ); const QBrush& brush = palette.brush(QPalette::Dark); m_mdiArea->setBackground(brush); //save the selected color scheme KConfigGroup group = KSharedConfig::openConfig()->group(QLatin1String("Settings_General")); group.writeEntry("ColorScheme", schemeName); group.sync(); } /*! Asks to save the project if it was modified. \return \c true if the project still needs to be saved ("cancel" clicked), \c false otherwise. */ bool MainWin::warnModified() { if (m_project->hasChanged()) { int want_save = KMessageBox::warningYesNoCancel( this, i18n("The current project %1 has been modified. Do you want to save it?", m_project->name()), i18n("Save Project")); switch (want_save) { case KMessageBox::Yes: return !saveProject(); case KMessageBox::No: break; case KMessageBox::Cancel: return true; } } return false; } /*! * updates the state of actions, menus and toolbars (enabled or disabled) * on project changes (project closes and opens) */ void MainWin::updateGUIOnProjectChanges() { if (m_closing) return; KXMLGUIFactory* factory = this->guiFactory(); if (factory->container("worksheet", this) == nullptr) { //no worksheet menu found, most probably labplot2ui.rc //was not properly installed -> return here in order not to crash return; } //disable all menus if there is no project bool b = (m_project == nullptr); m_saveAction->setEnabled(!b); m_saveAsAction->setEnabled(!b); m_printAction->setEnabled(!b); m_printPreviewAction->setEnabled(!b); m_importFileAction->setEnabled(!b); m_importSqlAction->setEnabled(!b); #ifdef HAVE_LIBORIGIN m_importOpjAction->setEnabled(!b); #endif m_exportAction->setEnabled(!b); m_newWorkbookAction->setEnabled(!b); m_newSpreadsheetAction->setEnabled(!b); m_newMatrixAction->setEnabled(!b); m_newWorksheetAction->setEnabled(!b); m_newDatapickerAction->setEnabled(!b); m_closeAction->setEnabled(!b); m_toggleProjectExplorerDockAction->setEnabled(!b); m_togglePropertiesDockAction->setEnabled(!b); if (!m_mdiArea || !m_mdiArea->currentSubWindow()) { factory->container("spreadsheet", this)->setEnabled(false); factory->container("matrix", this)->setEnabled(false); factory->container("worksheet", this)->setEnabled(false); factory->container("analysis", this)->setEnabled(false); factory->container("datapicker", this)->setEnabled(false); factory->container("spreadsheet_toolbar", this)->hide(); factory->container("worksheet_toolbar", this)->hide(); factory->container("cartesian_plot_toolbar", this)->hide(); // factory->container("histogram_toolbar",this)->hide(); // factory->container("barchart_toolbar",this)->hide(); factory->container("datapicker_toolbar", this)->hide(); #ifdef HAVE_CANTOR_LIBS factory->container("cas_worksheet", this)->setEnabled(false); factory->container("cas_worksheet_toolbar", this)->hide(); #endif } factory->container("new", this)->setEnabled(!b); factory->container("edit", this)->setEnabled(!b); factory->container("import", this)->setEnabled(!b); if (b) setCaption("LabPlot2"); else setCaption(m_project->name()); #ifdef Q_OS_MAC m_touchBar->clear(); if (b){ m_touchBar->addAction(m_newProjectAction); m_touchBar->addAction(m_openProjectAction); } else { m_touchBar->addAction(m_importFileAction); m_touchBar->addAction(m_newWorksheetAction); m_touchBar->addAction(m_newSpreadsheetAction); m_touchBar->addAction(m_newMatrixAction); } #endif // undo/redo actions are disabled in both cases - when the project is closed or opened m_undoAction->setEnabled(false); m_redoAction->setEnabled(false); } /* * updates the state of actions, menus and toolbars (enabled or disabled) * depending on the currently active window (worksheet or spreadsheet). */ void MainWin::updateGUI() { if (m_project == nullptr || m_project->isLoading()) return; if (m_closing || m_projectClosing) return; KXMLGUIFactory* factory = this->guiFactory(); if (factory->container("worksheet", this) == nullptr) { //no worksheet menu found, most probably labplot2ui.rc //was not properly installed -> return here in order not to crash return; } //reset the touchbar #ifdef Q_OS_MAC m_touchBar->clear(); m_touchBar->addAction(m_undoIconOnlyAction); m_touchBar->addAction(m_redoIconOnlyAction); m_touchBar->addSeparator(); #endif if (!m_mdiArea || !m_mdiArea->currentSubWindow()) { factory->container("spreadsheet", this)->setEnabled(false); factory->container("matrix", this)->setEnabled(false); factory->container("worksheet", this)->setEnabled(false); factory->container("analysis", this)->setEnabled(false); factory->container("datapicker", this)->setEnabled(false); factory->container("spreadsheet_toolbar", this)->hide(); factory->container("worksheet_toolbar", this)->hide(); // factory->container("histogram_toolbar",this)->hide(); // factory->container("barchart_toolbar",this)->hide(); factory->container("cartesian_plot_toolbar", this)->hide(); factory->container("datapicker_toolbar", this)->hide(); #ifdef HAVE_CANTOR_LIBS factory->container("cas_worksheet", this)->setEnabled(false); factory->container("cas_worksheet_toolbar", this)->hide(); #endif return; } #ifdef Q_OS_MAC if (dynamic_cast(m_currentAspect)) { m_touchBar->addAction(m_newWorksheetAction); m_touchBar->addAction(m_newSpreadsheetAction); m_touchBar->addAction(m_newMatrixAction); } #endif //Handle the Worksheet-object const Worksheet* w = dynamic_cast(m_currentAspect); if (!w) w = dynamic_cast(m_currentAspect->parent(AspectType::Worksheet)); if (w) { //populate worksheet menu auto* view = qobject_cast(w->view()); auto* menu = qobject_cast(factory->container("worksheet", this)); menu->clear(); view->createContextMenu(menu); menu->setEnabled(true); //populate analysis menu menu = qobject_cast(factory->container("analysis", this)); menu->clear(); view->createAnalysisMenu(menu); menu->setEnabled(true); //populate worksheet-toolbar auto* toolbar = qobject_cast(factory->container("worksheet_toolbar", this)); toolbar->clear(); view->fillToolBar(toolbar); toolbar->setVisible(true); toolbar->setEnabled(true); //populate the toolbar for cartesian plots toolbar = qobject_cast(factory->container("cartesian_plot_toolbar", this)); toolbar->clear(); view->fillCartesianPlotToolBar(toolbar); toolbar->setVisible(true); toolbar->setEnabled(true); //populate the touchbar on Mac #ifdef Q_OS_MAC view->fillTouchBar(m_touchBar); #endif //hide the spreadsheet toolbar factory->container("spreadsheet_toolbar", this)->setVisible(false); } else { factory->container("worksheet", this)->setEnabled(false); factory->container("worksheet_toolbar", this)->setVisible(false); factory->container("analysis", this)->setEnabled(false); // factory->container("drawing", this)->setEnabled(false); factory->container("worksheet_toolbar", this)->setEnabled(false); factory->container("cartesian_plot_toolbar", this)->setEnabled(false); } //Handle the Spreadsheet-object const auto* spreadsheet = this->activeSpreadsheet(); if (!spreadsheet) spreadsheet = dynamic_cast(m_currentAspect->parent(AspectType::Spreadsheet)); if (spreadsheet) { //populate spreadsheet-menu auto* view = qobject_cast(spreadsheet->view()); auto* menu = qobject_cast(factory->container("spreadsheet", this)); menu->clear(); view->createContextMenu(menu); menu->setEnabled(true); //populate spreadsheet-toolbar auto* toolbar = qobject_cast(factory->container("spreadsheet_toolbar", this)); toolbar->clear(); view->fillToolBar(toolbar); toolbar->setVisible(true); toolbar->setEnabled(true); //populate the touchbar on Mac #ifdef Q_OS_MAC m_touchBar->addAction(m_importFileAction); view->fillTouchBar(m_touchBar); #endif } else { factory->container("spreadsheet", this)->setEnabled(false); factory->container("spreadsheet_toolbar", this)->setVisible(false); } //Handle the Matrix-object const Matrix* matrix = dynamic_cast(m_currentAspect); if (!matrix) matrix = dynamic_cast(m_currentAspect->parent(AspectType::Matrix)); if (matrix) { //populate matrix-menu auto* view = qobject_cast(matrix->view()); auto* menu = qobject_cast(factory->container("matrix", this)); menu->clear(); view->createContextMenu(menu); menu->setEnabled(true); //populate the touchbar on Mac #ifdef Q_OS_MAC m_touchBar->addAction(m_importFileAction); //view->fillTouchBar(m_touchBar); #endif } else factory->container("matrix", this)->setEnabled(false); #ifdef HAVE_CANTOR_LIBS const CantorWorksheet* cantorworksheet = dynamic_cast(m_currentAspect); if (!cantorworksheet) cantorworksheet = dynamic_cast(m_currentAspect->parent(AspectType::CantorWorksheet)); if (cantorworksheet) { auto* view = qobject_cast(cantorworksheet->view()); auto* menu = qobject_cast(factory->container("cas_worksheet", this)); menu->clear(); view->createContextMenu(menu); menu->setEnabled(true); auto* toolbar = qobject_cast(factory->container("cas_worksheet_toolbar", this)); toolbar->setVisible(true); toolbar->clear(); view->fillToolBar(toolbar); } else { //no Cantor worksheet selected -> deactivate Cantor worksheet related menu and toolbar factory->container("cas_worksheet", this)->setEnabled(false); factory->container("cas_worksheet_toolbar", this)->setVisible(false); } #endif const Datapicker* datapicker = dynamic_cast(m_currentAspect); if (!datapicker) datapicker = dynamic_cast(m_currentAspect->parent(AspectType::Datapicker)); if (!datapicker) { if (m_currentAspect->type() == AspectType::DatapickerCurve) datapicker = dynamic_cast(m_currentAspect->parentAspect()); } if (datapicker) { //populate datapicker-menu auto* view = qobject_cast(datapicker->view()); auto* menu = qobject_cast(factory->container("datapicker", this)); menu->clear(); view->createContextMenu(menu); menu->setEnabled(true); //populate spreadsheet-toolbar auto* toolbar = qobject_cast(factory->container("datapicker_toolbar", this)); toolbar->clear(); view->fillToolBar(toolbar); toolbar->setVisible(true); } else { factory->container("datapicker", this)->setEnabled(false); factory->container("datapicker_toolbar", this)->setVisible(false); } } /*! creates a new empty project. Returns \c true, if a new project was created. */ bool MainWin::newProject() { //close the current project, if available if (!closeProject()) return false; // if(dynamic_cast(centralWidget())) { // createMdiArea(); // setCentralWidget(m_mdiArea); // } QApplication::processEvents(QEventLoop::AllEvents, 100); if (m_project) delete m_project; if (m_aspectTreeModel) delete m_aspectTreeModel; m_project = new Project(); m_currentAspect = m_project; m_currentFolder = m_project; KConfigGroup group = KSharedConfig::openConfig()->group( "Settings_General" ); Project::MdiWindowVisibility vis = Project::MdiWindowVisibility(group.readEntry("MdiWindowVisibility", 0)); m_project->setMdiWindowVisibility( vis ); - if (vis == Project::folderOnly) + if (vis == Project::MdiWindowVisibility::folderOnly) m_visibilityFolderAction->setChecked(true); - else if (vis == Project::folderAndSubfolders) + else if (vis == Project::MdiWindowVisibility::folderAndSubfolders) m_visibilitySubfolderAction->setChecked(true); else m_visibilityAllAction->setChecked(true); m_aspectTreeModel = new AspectTreeModel(m_project, this); connect(m_aspectTreeModel, &AspectTreeModel::statusInfo, [=](const QString& text){ statusBar()->showMessage(text); }); //newProject is called for the first time, there is no project explorer yet //-> initialize the project explorer, the GUI-observer and the dock widgets. if (m_projectExplorer == nullptr) { m_projectExplorerDock = new QDockWidget(this); m_projectExplorerDock->setObjectName("projectexplorer"); m_projectExplorerDock->setWindowTitle(i18nc("@title:window", "Project Explorer")); addDockWidget(Qt::LeftDockWidgetArea, m_projectExplorerDock); m_projectExplorer = new ProjectExplorer(m_projectExplorerDock); m_projectExplorerDock->setWidget(m_projectExplorer); connect(m_projectExplorer, &ProjectExplorer::currentAspectChanged, this, &MainWin::handleCurrentAspectChanged); connect(m_projectExplorerDock, &QDockWidget::visibilityChanged, this, &MainWin::projectExplorerDockVisibilityChanged); //Properties dock m_propertiesDock = new QDockWidget(this); m_propertiesDock->setObjectName("aspect_properties_dock"); m_propertiesDock->setWindowTitle(i18nc("@title:window", "Properties")); addDockWidget(Qt::RightDockWidgetArea, m_propertiesDock); auto* sa = new QScrollArea(m_propertiesDock); stackedWidget = new QStackedWidget(sa); sa->setWidget(stackedWidget); sa->setWidgetResizable(true); m_propertiesDock->setWidget(sa); connect(m_propertiesDock, &QDockWidget::visibilityChanged, this, &MainWin::propertiesDockVisibilityChanged); //GUI-observer; m_guiObserver = new GuiObserver(this); } m_projectExplorer->setModel(m_aspectTreeModel); m_projectExplorer->setProject(m_project); m_projectExplorer->setCurrentAspect(m_project); m_projectExplorerDock->show(); m_propertiesDock->show(); updateGUIOnProjectChanges(); connect(m_project, &Project::aspectAdded, this, &MainWin::handleAspectAdded); connect(m_project, &Project::aspectRemoved, this, &MainWin::handleAspectRemoved); connect(m_project, &Project::aspectAboutToBeRemoved, this, &MainWin::handleAspectAboutToBeRemoved); connect(m_project, SIGNAL(statusInfo(QString)), statusBar(), SLOT(showMessage(QString))); connect(m_project, &Project::changed, this, &MainWin::projectChanged); connect(m_project, &Project::requestProjectContextMenu, this, &MainWin::createContextMenu); connect(m_project, &Project::requestFolderContextMenu, this, &MainWin::createFolderContextMenu); connect(m_project, &Project::mdiWindowVisibilityChanged, this, &MainWin::updateMdiWindowVisibility); connect(m_project, &Project::closeRequested, this, &MainWin::closeProject); m_undoViewEmptyLabel = i18n("%1: created", m_project->name()); setCaption(m_project->name()); return true; } void MainWin::openProject() { KConfigGroup conf(KSharedConfig::openConfig(), "MainWin"); const QString& dir = conf.readEntry("LastOpenDir", ""); QString extensions = i18n("LabPlot Projects (%1)", Project::supportedExtensions()); #ifdef HAVE_LIBORIGIN extensions += i18n(";;Origin Projects (%1)", OriginProjectParser::supportedExtensions()); #endif #ifdef HAVE_CANTOR_LIBS extensions += i18n(";;Cantor Projects (.cws)"); extensions += i18n(";;Jupyter Notebooks (.ipynb)"); #endif const QString& path = QFileDialog::getOpenFileName(this,i18n("Open Project"), dir, extensions); if (path.isEmpty())// "Cancel" was clicked return; this->openProject(path); //save new "last open directory" int pos = path.lastIndexOf(QDir::separator()); if (pos != -1) { const QString& newDir = path.left(pos); if (newDir != dir) conf.writeEntry("LastOpenDir", newDir); } } void MainWin::openProject(const QString& filename) { if (filename == m_currentFileName) { KMessageBox::information(this, i18n("The project file %1 is already opened.", filename), i18n("Open Project")); return; } // if(dynamic_cast(centralWidget())) { // createMdiArea(); // setCentralWidget(m_mdiArea); // } if (!newProject()) return; WAIT_CURSOR; QElapsedTimer timer; timer.start(); bool rc = false; if (Project::isLabPlotProject(filename)) { m_project->setFileName(filename); rc = m_project->load(filename); } #ifdef HAVE_LIBORIGIN else if (OriginProjectParser::isOriginProject(filename)) { OriginProjectParser parser; parser.setProjectFileName(filename); parser.importTo(m_project, QStringList()); //TODO: add return code rc = true; } #endif #ifdef HAVE_CANTOR_LIBS else if (QFileInfo(filename).completeSuffix() == QLatin1String("cws")) { QFile file(filename); KZip archive(&file); rc = archive.open(QIODevice::ReadOnly); if (rc) { const auto* contentEntry = archive.directory()->entry(QLatin1String("content.xml")); if (contentEntry && contentEntry->isFile()) { const auto* contentFile = static_cast(contentEntry); QByteArray data = contentFile->data(); archive.close(); //determine the name of the backend QDomDocument doc; doc.setContent(data); QString backendName = doc.documentElement().attribute(QLatin1String("backend")); if (!backendName.isEmpty()) { //create new Cantor worksheet and load the data auto* worksheet = new CantorWorksheet(backendName); worksheet->setName(QFileInfo(filename).fileName()); worksheet->setComment(filename); rc = file.open(QIODevice::ReadOnly); if (rc) { QByteArray content = file.readAll(); rc = worksheet->init(&content); if (rc) m_project->addChild(worksheet); else { delete worksheet; RESET_CURSOR; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to process the content of the file '%1'.", filename)); } }else { RESET_CURSOR; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to open the file '%1'.", filename)); } } else { RESET_CURSOR; rc = false; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to process the content of the file '%1'.", filename)); } } else { RESET_CURSOR; rc = false; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to process the content of the file '%1'.", filename)); } } else { RESET_CURSOR; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to open the file '%1'.", filename)); } } else if (QFileInfo(filename).completeSuffix() == QLatin1String("ipynb")) { QFile file(filename); rc = file.open(QIODevice::ReadOnly); if (rc) { QByteArray content = file.readAll(); QJsonParseError error; // TODO: use QJsonDocument& doc = QJsonDocument::fromJson(content, &error); if minimum Qt version is at least 5.10 const QJsonDocument& jsonDoc = QJsonDocument::fromJson(content, &error); const QJsonObject& doc = jsonDoc.object(); if (error.error == QJsonParseError::NoError) { //determine the backend name QString backendName; // TODO: use doc["metadata"]["kernelspec"], etc. if minimum Qt version is at least 5.10 if ((doc["metadata"] != QJsonValue::Undefined && doc["metadata"].isObject()) && (doc["metadata"].toObject()["kernelspec"] != QJsonValue::Undefined && doc["metadata"].toObject()["kernelspec"].isObject()) ) { QString kernel; if (doc["metadata"].toObject()["kernelspec"].toObject()["name"] != QJsonValue::Undefined) kernel = doc["metadata"].toObject()["kernelspec"].toObject()["name"].toString(); if (!kernel.isEmpty()) { if (kernel.startsWith(QLatin1String("julia"))) backendName = QLatin1String("julia"); else if (kernel == QLatin1String("sagemath")) backendName = QLatin1String("sage"); else if (kernel == QLatin1String("ir")) backendName = QLatin1String("r"); else backendName = kernel; } else backendName = doc["metadata"].toObject()["kernelspec"].toObject()["language"].toString(); if (!backendName.isEmpty()) { //create new Cantor worksheet and load the data auto* worksheet = new CantorWorksheet(backendName); worksheet->setName(QFileInfo(filename).fileName()); worksheet->setComment(filename); rc = worksheet->init(&content); if (rc) m_project->addChild(worksheet); else { delete worksheet; RESET_CURSOR; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to process the content of the file '%1'.", filename)); } } else { RESET_CURSOR; rc = false; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to process the content of the file '%1'.", filename)); } } else { RESET_CURSOR; rc = false; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to process the content of the file '%1'.", filename)); } } } else { RESET_CURSOR; rc = false; QMessageBox::critical(this, i18n("Failed to open project"), i18n("Failed to open the file '%1'.", filename)); } } #endif m_project->setChanged(false); if (!rc) { closeProject(); RESET_CURSOR; return; } m_currentFileName = filename; m_project->undoStack()->clear(); m_undoViewEmptyLabel = i18n("%1: opened", m_project->name()); m_recentProjectsAction->addUrl( QUrl(filename) ); setCaption(m_project->name()); updateGUIOnProjectChanges(); updateGUI(); //there are most probably worksheets or spreadsheets in the open project -> update the GUI m_saveAction->setEnabled(false); statusBar()->showMessage( i18n("Project successfully opened (in %1 seconds).", (float)timer.elapsed()/1000) ); if (m_autoSaveActive) m_autoSaveTimer.start(); RESET_CURSOR; } void MainWin::openRecentProject(const QUrl& url) { // if(dynamic_cast(centralWidget())) { // createMdiArea(); // setCentralWidget(m_mdiArea); // } if (url.isLocalFile()) // fix for Windows this->openProject(url.toLocalFile()); else this->openProject(url.path()); } /*! Closes the current project, if available. Return \c true, if the project was closed. */ bool MainWin::closeProject() { if (m_project == nullptr) return true; //nothing to close if (warnModified()) return false; if(!m_closing) { // if(dynamic_cast(centralWidget()) && m_showWelcomeScreen) { // m_welcomeWidget = createWelcomeScreen(); // setCentralWidget(m_welcomeWidget); // } } m_projectClosing = true; statusBar()->clearMessage(); delete m_aspectTreeModel; m_aspectTreeModel = nullptr; delete m_project; m_project = nullptr; m_currentFileName.clear(); m_projectClosing = false; //update the UI if we're just closing a project //and not closing(quitting) the application if (!m_closing) { m_projectExplorerDock->hide(); m_propertiesDock->hide(); m_currentAspect = nullptr; m_currentFolder = nullptr; updateGUIOnProjectChanges(); if (m_autoSaveActive) m_autoSaveTimer.stop(); } removeDockWidget(cursorDock); delete cursorDock; cursorDock = nullptr; cursorWidget = nullptr; // is deleted, because it's the cild of cursorDock return true; } bool MainWin::saveProject() { const QString& fileName = m_project->fileName(); if (fileName.isEmpty()) return saveProjectAs(); else return save(fileName); } bool MainWin::saveProjectAs() { KConfigGroup conf(KSharedConfig::openConfig(), "MainWin"); const QString& dir = conf.readEntry("LastOpenDir", ""); QString path = QFileDialog::getSaveFileName(this, i18n("Save Project As"), dir, i18n("LabPlot Projects (*.lml *.lml.gz *.lml.bz2 *.lml.xz *.LML *.LML.GZ *.LML.BZ2 *.LML.XZ)")); if (path.isEmpty())// "Cancel" was clicked return false; if (path.contains(QLatin1String(".lml"), Qt::CaseInsensitive) == false) path.append(QLatin1String(".lml")); //save new "last open directory" int pos = path.lastIndexOf(QDir::separator()); if (pos != -1) { const QString& newDir = path.left(pos); if (newDir != dir) conf.writeEntry("LastOpenDir", newDir); } return save(path); } /*! * auxiliary function that does the actual saving of the project */ bool MainWin::save(const QString& fileName) { QTemporaryFile tempFile(QDir::tempPath() + QLatin1Char('/') + QLatin1String("labplot_save_XXXXXX")); if (!tempFile.open()) { KMessageBox::error(this, i18n("Couldn't open the temporary file for writing.")); return false; } WAIT_CURSOR; const QString& tempFileName = tempFile.fileName(); DEBUG("Using temporary file " << STDSTRING(tempFileName)) tempFile.close(); // use file ending to find out how to compress file QIODevice* file; // if ending is .lml, do gzip compression anyway if (fileName.endsWith(QLatin1String(".lml"))) file = new KCompressionDevice(tempFileName, KCompressionDevice::GZip); else file = new KFilterDev(tempFileName); if (file == nullptr) file = new QFile(tempFileName); bool ok; if (file->open(QIODevice::WriteOnly)) { m_project->setFileName(fileName); QPixmap thumbnail = centralWidget()->grab(); QXmlStreamWriter writer(file); m_project->setFileName(fileName); m_project->save(thumbnail, &writer); m_project->undoStack()->clear(); m_project->setChanged(false); file->close(); // target file must not exist if (QFile::exists(fileName)) QFile::remove(fileName); // do not rename temp file. Qt still holds a handle (which fails renaming on Windows) and deletes it bool rc = QFile::copy(tempFileName, fileName); if (rc) { setCaption(m_project->name()); statusBar()->showMessage(i18n("Project saved")); m_saveAction->setEnabled(false); m_recentProjectsAction->addUrl( QUrl(fileName) ); ok = true; //if the project dock is visible, refresh the shown content //(version and modification time might have been changed) if (stackedWidget->currentWidget() == projectDock) projectDock->setProject(m_project); //we have a file name now // -> auto save can be activated now if not happened yet if (m_autoSaveActive && !m_autoSaveTimer.isActive()) m_autoSaveTimer.start(); } else { RESET_CURSOR; KMessageBox::error(this, i18n("Couldn't save the file '%1'.", fileName)); ok = false; } } else { RESET_CURSOR; KMessageBox::error(this, i18n("Couldn't open the file '%1' for writing.", fileName)); ok = false; } delete file; RESET_CURSOR; return ok; } /*! * automatically saves the project in the specified time interval. */ void MainWin::autoSaveProject() { //don't auto save when there are no changes or the file name //was not provided yet (the project was never explicitly saved yet). if ( !m_project->hasChanged() || m_project->fileName().isEmpty()) return; this->saveProject(); } /*! prints the current sheet (worksheet, spreadsheet or matrix) */ void MainWin::print() { QMdiSubWindow* win = m_mdiArea->currentSubWindow(); if (!win) return; AbstractPart* part = static_cast(win)->part(); statusBar()->showMessage(i18n("Preparing printing of %1", part->name())); if (part->printView()) statusBar()->showMessage(i18n("%1 printed", part->name())); else statusBar()->clearMessage(); } void MainWin::printPreview() { QMdiSubWindow* win = m_mdiArea->currentSubWindow(); if (!win) return; AbstractPart* part = static_cast(win)->part(); statusBar()->showMessage(i18n("Preparing printing of %1", part->name())); if (part->printPreview()) statusBar()->showMessage(i18n("%1 printed", part->name())); else statusBar()->clearMessage(); } /**************************************************************************************/ /*! adds a new Folder to the project. */ void MainWin::newFolder() { Folder* folder = new Folder(i18n("Folder")); this->addAspectToProject(folder); } /*! adds a new Workbook to the project. */ void MainWin::newWorkbook() { Workbook* workbook = new Workbook(i18n("Workbook")); this->addAspectToProject(workbook); } /*! adds a new Datapicker to the project. */ void MainWin::newDatapicker() { Datapicker* datapicker = new Datapicker(i18n("Datapicker")); this->addAspectToProject(datapicker); } /*! adds a new Spreadsheet to the project. */ void MainWin::newSpreadsheet() { Spreadsheet* spreadsheet = new Spreadsheet(i18n("Spreadsheet")); //if the current active window is a workbook and no folder/project is selected in the project explorer, //add the new spreadsheet to the workbook Workbook* workbook = dynamic_cast(m_currentAspect); if (workbook) { QModelIndex index = m_projectExplorer->currentIndex(); const auto* aspect = static_cast(index.internalPointer()); if (!aspect->inherits(AspectType::Folder)) { workbook->addChild(spreadsheet); return; } } this->addAspectToProject(spreadsheet); } /*! adds a new Matrix to the project. */ void MainWin::newMatrix() { Matrix* matrix = new Matrix(i18n("Matrix")); //if the current active window is a workbook and no folder/project is selected in the project explorer, //add the new matrix to the workbook Workbook* workbook = dynamic_cast(m_currentAspect); if (workbook) { QModelIndex index = m_projectExplorer->currentIndex(); const auto* aspect = static_cast(index.internalPointer()); if (!aspect->inherits(AspectType::Folder)) { workbook->addChild(matrix); return; } } this->addAspectToProject(matrix); } /*! adds a new Worksheet to the project. */ void MainWin::newWorksheet() { Worksheet* worksheet = new Worksheet(i18n("Worksheet")); this->addAspectToProject(worksheet); } /*! adds a new Note to the project. */ void MainWin::newNotes() { Note* notes = new Note(i18n("Note")); this->addAspectToProject(notes); } /*! returns a pointer to a \c Spreadsheet object, if the currently active Mdi-Subwindow or if the currently selected tab in a \c WorkbookView is a \c SpreadsheetView Otherwise returns \c 0. */ Spreadsheet* MainWin::activeSpreadsheet() const { // if(dynamic_cast(centralWidget())) // return nullptr; if (!m_currentAspect) return nullptr; Spreadsheet* spreadsheet = nullptr; if (m_currentAspect->type() == AspectType::Spreadsheet) spreadsheet = dynamic_cast(m_currentAspect); else { //check whether one of spreadsheet columns is selected and determine the spreadsheet auto* parent = m_currentAspect->parentAspect(); if (parent && parent->type() == AspectType::Spreadsheet) spreadsheet = dynamic_cast(parent); } return spreadsheet; } #ifdef HAVE_CANTOR_LIBS /* adds a new Cantor Spreadsheet to the project. */ void MainWin::newCantorWorksheet(QAction* action) { CantorWorksheet* cantorworksheet = new CantorWorksheet(action->data().toString()); this->addAspectToProject(cantorworksheet); } /********************************************************************************/ #endif /*! called if there were changes in the project. Adds "changed" to the window caption and activates the save-Action. */ void MainWin::projectChanged() { setCaption(i18n("%1 [Changed]", m_project->name())); m_saveAction->setEnabled(true); m_undoAction->setEnabled(true); return; } void MainWin::handleCurrentSubWindowChanged(QMdiSubWindow* win) { if (!win) { updateGUI(); return; } auto* view = static_cast(win); if (view == m_currentSubWindow) { //do nothing, if the current sub-window gets selected again. //This event happens, when labplot loses the focus (modal window is opened or the user switches to another application) //and gets it back (modal window is closed or the user switches back to labplot). return; } else m_currentSubWindow = view; updateGUI(); if (!m_suppressCurrentSubWindowChangedEvent) m_projectExplorer->setCurrentAspect(view->part()); } void MainWin::handleAspectAdded(const AbstractAspect* aspect) { const auto* part = dynamic_cast(aspect); if (part) { // connect(part, &AbstractPart::importFromFileRequested, this, &MainWin::importFileDialog); connect(part, &AbstractPart::importFromFileRequested, this, [=]() {importFileDialog();}); connect(part, &AbstractPart::importFromSQLDatabaseRequested, this, &MainWin::importSqlDialog); //TODO: export, print and print preview should be handled in the views and not in MainWin. connect(part, &AbstractPart::exportRequested, this, &MainWin::exportDialog); connect(part, &AbstractPart::printRequested, this, &MainWin::print); connect(part, &AbstractPart::printPreviewRequested, this, &MainWin::printPreview); connect(part, &AbstractPart::showRequested, this, &MainWin::handleShowSubWindowRequested); const auto* worksheet = dynamic_cast(aspect); if (worksheet) connect(worksheet, &Worksheet::cartesianPlotMouseModeChanged, this, &MainWin::cartesianPlotMouseModeChanged); } } void MainWin::handleAspectRemoved(const AbstractAspect* parent,const AbstractAspect* before,const AbstractAspect* aspect) { Q_UNUSED(before); Q_UNUSED(aspect); //no need to react on removal of // - AbstractSimpleFilter // - columns in the data spreadsheet of a datapicker curve, // this can only happen when changing the error type and is done on the level of DatapickerImage if (!dynamic_cast(aspect) && !(parent->parentAspect() && parent->parentAspect()->type() == AspectType::DatapickerCurve) ) m_projectExplorer->setCurrentAspect(parent); } void MainWin::handleAspectAboutToBeRemoved(const AbstractAspect *aspect) { const auto* part = dynamic_cast(aspect); if (!part) return; const auto* workbook = dynamic_cast(aspect->parentAspect()); auto* datapicker = dynamic_cast(aspect->parentAspect()); if (!datapicker) datapicker = dynamic_cast(aspect->parentAspect()->parentAspect()); if (!workbook && !datapicker) { PartMdiView* win = part->mdiSubWindow(); if (win) m_mdiArea->removeSubWindow(win); } } /*! called when the current aspect in the tree of the project explorer was changed. Selects the new aspect. */ void MainWin::handleCurrentAspectChanged(AbstractAspect *aspect) { if (!aspect) aspect = m_project; // should never happen, just in case m_suppressCurrentSubWindowChangedEvent = true; if (aspect->folder() != m_currentFolder) { m_currentFolder = aspect->folder(); updateMdiWindowVisibility(); } m_currentAspect = aspect; //activate the corresponding MDI sub window for the current aspect activateSubWindowForAspect(aspect); m_suppressCurrentSubWindowChangedEvent = false; updateGUI(); } void MainWin::activateSubWindowForAspect(const AbstractAspect* aspect) const { const auto* part = dynamic_cast(aspect); if (part) { PartMdiView* win{nullptr}; //for aspects being children of a Workbook, we show workbook's window, otherwise the window of the selected part const auto* workbook = dynamic_cast(aspect->parentAspect()); auto* datapicker = dynamic_cast(aspect->parentAspect()); if (!datapicker) datapicker = dynamic_cast(aspect->parentAspect()->parentAspect()); if (workbook) win = workbook->mdiSubWindow(); else if (datapicker) win = datapicker->mdiSubWindow(); else win = part->mdiSubWindow(); if (m_mdiArea && m_mdiArea->subWindowList().indexOf(win) == -1) { if (dynamic_cast(part)) m_mdiArea->addSubWindow(win, Qt::Tool); else m_mdiArea->addSubWindow(win); win->show(); //Qt provides its own "system menu" for every sub-window. The shortcut for the close-action //in this menu collides with our global m_closeAction. //remove the shortcuts in the system menu to avoid this collision. QMenu* menu = win->systemMenu(); if (menu) { for (QAction* action : menu->actions()) action->setShortcut(QKeySequence()); } } if (m_mdiArea) m_mdiArea->setActiveSubWindow(win); } else { //activate the mdiView of the parent, if a child was selected const AbstractAspect* parent = aspect->parentAspect(); if (parent) { activateSubWindowForAspect(parent); //if the parent's parent is a Workbook (a column of a spreadsheet in workbook was selected), //we need to select the corresponding tab in WorkbookView too if (parent->parentAspect()) { auto* workbook = dynamic_cast(parent->parentAspect()); auto* datapicker = dynamic_cast(parent->parentAspect()); if (!datapicker) datapicker = dynamic_cast(parent->parentAspect()->parentAspect()); if (workbook) workbook->childSelected(parent); else if (datapicker) datapicker->childSelected(parent); } } } return; } void MainWin::setMdiWindowVisibility(QAction* action) { m_project->setMdiWindowVisibility((Project::MdiWindowVisibility)(action->data().toInt())); } /*! shows the sub window of a worksheet, matrix or a spreadsheet. Used if the window was closed before and the user asks to show the window again via the context menu in the project explorer. */ void MainWin::handleShowSubWindowRequested() { activateSubWindowForAspect(m_currentAspect); } /*! this is called on a right click on the root folder in the project explorer */ void MainWin::createContextMenu(QMenu* menu) const { QAction* firstAction = nullptr; // if we're populating the context menu for the project explorer, then //there're already actions available there. Skip the first title-action //and insert the action at the beginning of the menu. if (menu->actions().size()>1) firstAction = menu->actions().at(1); menu->insertMenu(firstAction, m_newMenu); //The tabbed view collides with the visibility policy for the subwindows. //Hide the menus for the visibility policy if the tabbed view is used. if (m_mdiArea->viewMode() != QMdiArea::TabbedView) { menu->insertSeparator(firstAction); menu->insertMenu(firstAction, m_visibilityMenu); menu->insertSeparator(firstAction); } } /*! this is called on a right click on a non-root folder in the project explorer */ void MainWin::createFolderContextMenu(const Folder* folder, QMenu* menu) const { Q_UNUSED(folder); //Folder provides it's own context menu. Add a separator before adding additional actions. menu->addSeparator(); this->createContextMenu(menu); } void MainWin::undo() { WAIT_CURSOR; m_project->undoStack()->undo(); if (m_project->undoStack()->index() == 0) { setCaption(m_project->name()); m_saveAction->setEnabled(false); m_undoAction->setEnabled(false); m_project->setChanged(false); } m_redoAction->setEnabled(true); RESET_CURSOR; } void MainWin::redo() { WAIT_CURSOR; m_project->undoStack()->redo(); projectChanged(); if (m_project->undoStack()->index() == m_project->undoStack()->count()) m_redoAction->setEnabled(false); RESET_CURSOR; } /*! Shows/hides mdi sub-windows depending on the current visibility policy. */ void MainWin::updateMdiWindowVisibility() const { QList windows = m_mdiArea->subWindowList(); PartMdiView* part_view; switch (m_project->mdiWindowVisibility()) { - case Project::allMdiWindows: + case Project::MdiWindowVisibility::allMdiWindows: for (auto* window : windows) window->show(); break; - case Project::folderOnly: + case Project::MdiWindowVisibility::folderOnly: for (auto* window : windows) { part_view = qobject_cast(window); Q_ASSERT(part_view); if (part_view->part()->folder() == m_currentFolder) part_view->show(); else part_view->hide(); } break; - case Project::folderAndSubfolders: + case Project::MdiWindowVisibility::folderAndSubfolders: for (auto* window : windows) { part_view = qobject_cast(window); if (part_view->part()->isDescendantOf(m_currentFolder)) part_view->show(); else part_view->hide(); } break; } } void MainWin::toggleDockWidget(QAction* action) { if (action->objectName() == "toggle_project_explorer_dock") { if (m_projectExplorerDock->isVisible()) m_projectExplorerDock->hide(); // toggleHideWidget(m_projectExplorerDock, true); else m_projectExplorerDock->show(); // toggleShowWidget(m_projectExplorerDock, true); } else if (action->objectName() == "toggle_properties_explorer_dock") { if (m_propertiesDock->isVisible()) m_propertiesDock->hide(); // toggleHideWidget(m_propertiesDock, false); else m_propertiesDock->show(); // toggleShowWidget(m_propertiesDock, false); } } /* void MainWin::toggleHideWidget(QWidget* widget, bool hideToLeft) { auto* timeline = new QTimeLine(800, this); timeline->setEasingCurve(QEasingCurve::InOutQuad); connect(timeline, &QTimeLine::valueChanged, [=] { const qreal value = timeline->currentValue(); const int widgetWidth = widget->width(); const int widgetPosY = widget->pos().y(); int moveX = 0; if (hideToLeft) { moveX = static_cast(value * widgetWidth) - widgetWidth; } else { const int frameRight = this->frameGeometry().right(); moveX = frameRight - static_cast(value * widgetWidth); } widget->move(moveX, widgetPosY); }); timeline->setDirection(QTimeLine::Backward); timeline->start(); connect(timeline, &QTimeLine::finished, [widget] {widget->hide();}); connect(timeline, &QTimeLine::finished, timeline, &QTimeLine::deleteLater); } void MainWin::toggleShowWidget(QWidget* widget, bool showToRight) { auto* timeline = new QTimeLine(800, this); timeline->setEasingCurve(QEasingCurve::InOutQuad); connect(timeline, &QTimeLine::valueChanged, [=]() { if (widget->isHidden()) { widget->show(); } const qreal value = timeline->currentValue(); const int widgetWidth = widget->width(); const int widgetPosY = widget->pos().y(); int moveX = 0; if (showToRight) { moveX = static_cast(value * widgetWidth) - widgetWidth; } else { const int frameRight = this->frameGeometry().right(); moveX = frameRight - static_cast(value * widgetWidth); } widget->move(moveX, widgetPosY); }); timeline->setDirection(QTimeLine::Forward); timeline->start(); connect(timeline, &QTimeLine::finished, timeline, &QTimeLine::deleteLater); } */ void MainWin::projectExplorerDockVisibilityChanged(bool visible) { m_toggleProjectExplorerDockAction->setChecked(visible); } void MainWin::propertiesDockVisibilityChanged(bool visible) { m_togglePropertiesDockAction->setChecked(visible); } void MainWin::cursorDockVisibilityChanged(bool visible) { //if the cursor dock was closed, switch to the "Select and Edit" mouse mode if (!visible) { // auto* worksheet = activeWorksheet(); //TODO: } } void MainWin::cartesianPlotMouseModeChanged(CartesianPlot::MouseMode mode) { if (mode != CartesianPlot::Cursor) { if (cursorDock) cursorDock->hide(); } else { if (!cursorDock) { cursorDock = new QDockWidget(i18n("Cursor"), this); cursorWidget = new CursorDock(cursorDock); cursorDock->setWidget(cursorWidget); connect(cursorDock, &QDockWidget::visibilityChanged, this, &MainWin::cursorDockVisibilityChanged); // cursorDock->setFloating(true); // does not work. Don't understand why // if (m_propertiesDock) // tabifyDockWidget(cursorDock, m_propertiesDock); // else addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, cursorDock); } auto* worksheet = static_cast(QObject::sender()); cursorWidget->setWorksheet(worksheet); cursorDock->show(); } } void MainWin::toggleFullScreen() { if (this->windowState() == Qt::WindowFullScreen) this->setWindowState(m_lastWindowState); else { m_lastWindowState = this->windowState(); this->showFullScreen(); } } void MainWin::closeEvent(QCloseEvent* event) { m_closing = true; if (!this->closeProject()) { m_closing = false; event->ignore(); } } void MainWin::dragEnterEvent(QDragEnterEvent* event) { event->accept(); } void MainWin::dropEvent(QDropEvent* event) { if (event->mimeData() && !event->mimeData()->urls().isEmpty()) { QUrl url = event->mimeData()->urls().at(0); const QString& f = url.toLocalFile(); bool open = Project::isLabPlotProject(f); #ifdef HAVE_LIBORIGIN if (!open) open = OriginProjectParser::isOriginProject(f); #endif #ifdef HAVE_CANTOR_LIBS if (!open) { QFileInfo fi(f); open = (fi.completeSuffix() == QLatin1String("cws")) || (fi.completeSuffix() == QLatin1String("ipynb")); } #endif if (open) openProject(f); else { if (!m_project) newProject(); importFileDialog(f); } event->accept(); } else event->ignore(); } void MainWin::handleSettingsChanges() { const KConfigGroup group = KSharedConfig::openConfig()->group( "Settings_General" ); // if(dynamic_cast(centralWidget()) == nullptr) { // QMdiArea::ViewMode viewMode = QMdiArea::ViewMode(group.readEntry("ViewMode", 0)); // if (m_mdiArea->viewMode() != viewMode) { // m_mdiArea->setViewMode(viewMode); // if (viewMode == QMdiArea::SubWindowView) // this->updateMdiWindowVisibility(); // } // // if (m_mdiArea->viewMode() == QMdiArea::TabbedView) { // m_tileWindowsAction->setVisible(false); // m_cascadeWindowsAction->setVisible(false); // QTabWidget::TabPosition tabPosition = QTabWidget::TabPosition(group.readEntry("TabPosition", 0)); // if (m_mdiArea->tabPosition() != tabPosition) // m_mdiArea->setTabPosition(tabPosition); // } else { // m_tileWindowsAction->setVisible(true); // m_cascadeWindowsAction->setVisible(true); // } // } //autosave bool autoSave = group.readEntry("AutoSave", 0); if (m_autoSaveActive != autoSave) { m_autoSaveActive = autoSave; if (autoSave) m_autoSaveTimer.start(); else m_autoSaveTimer.stop(); } int interval = group.readEntry("AutoSaveInterval", 1); interval *= 60*1000; if (interval != m_autoSaveTimer.interval()) m_autoSaveTimer.setInterval(interval); //show memory info bool showMemoryInfo = group.readEntry(QLatin1String("ShowMemoryInfo"), true); if (m_showMemoryInfo != showMemoryInfo) { m_showMemoryInfo = showMemoryInfo; if (showMemoryInfo) { m_memoryInfoWidget = new MemoryWidget(statusBar()); statusBar()->addPermanentWidget(m_memoryInfoWidget); } else { if (m_memoryInfoWidget) { statusBar()->removeWidget(m_memoryInfoWidget); delete m_memoryInfoWidget; m_memoryInfoWidget = nullptr; } } } //update the units if (stackedWidget) { for (int i = 0; i < stackedWidget->count(); ++i) { auto* widget = stackedWidget->widget(i); BaseDock* dock = dynamic_cast(widget); if (dock) dock->updateUnits(); else { auto* labelWidget = dynamic_cast(widget); if (labelWidget) labelWidget->updateUnits(); } } } bool showWelcomeScreen = group.readEntry(QLatin1String("ShowWelcomeScreen"), true); if(m_showWelcomeScreen != showWelcomeScreen) m_showWelcomeScreen = showWelcomeScreen; } void MainWin::openDatasetExample() { newProject(); // addAspectToProject(m_welcomeScreenHelper->releaseConfiguredSpreadsheet()); } /***************************************************************************************/ /************************************** dialogs ***************************************/ /***************************************************************************************/ /*! shows the dialog with the Undo-history. */ void MainWin::historyDialog() { if (!m_project->undoStack()) return; auto* dialog = new HistoryDialog(this, m_project->undoStack(), m_undoViewEmptyLabel); int index = m_project->undoStack()->index(); if (dialog->exec() != QDialog::Accepted) { if (m_project->undoStack()->count() != 0) m_project->undoStack()->setIndex(index); } //disable undo/redo-actions if the history was cleared //(in both cases, when accepted or rejected in the dialog) if (m_project->undoStack()->count() == 0) { m_undoAction->setEnabled(false); m_redoAction->setEnabled(false); } } /*! Opens the dialog to import data to the selected workbook, spreadsheet or matrix */ void MainWin::importFileDialog(const QString& fileName) { DEBUG("MainWin::importFileDialog()"); auto* dlg = new ImportFileDialog(this, false, fileName); // select existing container if (m_currentAspect->type() == AspectType::Spreadsheet || m_currentAspect->type() == AspectType::Matrix || m_currentAspect->type() == AspectType::Workbook) dlg->setCurrentIndex(m_projectExplorer->currentIndex()); else if (m_currentAspect->type() == AspectType::Column && m_currentAspect->parentAspect()->type() == AspectType::Spreadsheet) dlg->setCurrentIndex(m_aspectTreeModel->modelIndexOfAspect(m_currentAspect->parentAspect())); if (dlg->exec() == QDialog::Accepted) { dlg->importTo(statusBar()); m_project->setChanged(true); } delete dlg; DEBUG("MainWin::importFileDialog() DONE"); } void MainWin::importSqlDialog() { DEBUG("MainWin::importSqlDialog()"); auto* dlg = new ImportSQLDatabaseDialog(this); // select existing container if (m_currentAspect->type() == AspectType::Spreadsheet || m_currentAspect->type() == AspectType::Matrix || m_currentAspect->type() == AspectType::Workbook) dlg->setCurrentIndex(m_projectExplorer->currentIndex()); else if (m_currentAspect->type() == AspectType::Column && m_currentAspect->parentAspect()->type() == AspectType::Spreadsheet) dlg->setCurrentIndex(m_aspectTreeModel->modelIndexOfAspect(m_currentAspect->parentAspect())); if (dlg->exec() == QDialog::Accepted) { dlg->importTo(statusBar()); m_project->setChanged(true); } delete dlg; DEBUG("MainWin::importSqlDialog() DONE"); } void MainWin::importProjectDialog() { DEBUG("MainWin::importProjectDialog()"); ImportProjectDialog::ProjectType type; if (QObject::sender() == m_importOpjAction) type = ImportProjectDialog::ProjectOrigin; else type = ImportProjectDialog::ProjectLabPlot; auto* dlg = new ImportProjectDialog(this, type); // set current folder dlg->setCurrentFolder(m_currentFolder); if (dlg->exec() == QDialog::Accepted) { dlg->importTo(statusBar()); m_project->setChanged(true); } delete dlg; DEBUG("MainWin::importProjectDialog() DONE"); } /*! * \brief opens a dialog to import datasets */ void MainWin::importDatasetDialog() { ImportDatasetDialog* dlg = new ImportDatasetDialog(this); if (dlg->exec() == QDialog::Accepted) { Spreadsheet* spreadsheet = new Spreadsheet(i18n("Dataset%1", 1)); DatasetHandler* dataset = new DatasetHandler(spreadsheet); dlg->importToDataset(dataset, statusBar()); QTimer timer; timer.setSingleShot(true); QEventLoop loop; connect(dataset, &DatasetHandler::downloadCompleted, &loop, &QEventLoop::quit); connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); timer.start(1500); loop.exec(); if(timer.isActive()){ timer.stop(); addAspectToProject(spreadsheet); delete dataset; } else delete dataset; } delete dlg; } /*! opens the dialog for the export of the currently active worksheet, spreadsheet or matrix. */ void MainWin::exportDialog() { QMdiSubWindow* win = m_mdiArea->currentSubWindow(); if (!win) return; AbstractPart* part = static_cast(win)->part(); if (part->exportView()) statusBar()->showMessage(i18n("%1 exported", part->name())); } void MainWin::editFitsFileDialog() { auto* editDialog = new FITSHeaderEditDialog(this); if (editDialog->exec() == QDialog::Accepted) { if (editDialog->saved()) statusBar()->showMessage(i18n("FITS files saved")); } } /*! adds a new file data source to the current project. */ void MainWin::newLiveDataSourceActionTriggered() { ImportFileDialog* dlg = new ImportFileDialog(this, true); if (dlg->exec() == QDialog::Accepted) { if (static_cast(dlg->sourceType()) == LiveDataSource::MQTT) { #ifdef HAVE_MQTT MQTTClient* mqttClient = new MQTTClient(i18n("MQTT Client%1", 1)); dlg->importToMQTT(mqttClient); mqttClient->setName(mqttClient->clientHostName()); QVector existingClients = m_project->children(AbstractAspect::Recursive); //doesn't make sense to have more MQTTClients connected to the same broker bool found = false; for (const auto* client : existingClients) { if (client->clientHostName() == mqttClient->clientHostName() && client->clientPort() == mqttClient->clientPort()) { found = true; break; } } if (!found) addAspectToProject(mqttClient); else { delete mqttClient; QMessageBox::warning(this, "Warning", "There already is a MQTTClient with this host!"); } #endif } else { LiveDataSource* dataSource = new LiveDataSource(i18n("Live data source%1", 1), false); dlg->importToLiveDataSource(dataSource, statusBar()); addAspectToProject(dataSource); } } delete dlg; } void MainWin::addAspectToProject(AbstractAspect* aspect) { const QModelIndex& index = m_projectExplorer->currentIndex(); if (index.isValid()) { auto* parent = static_cast(index.internalPointer()); #ifdef HAVE_MQTT //doesn't make sense to add a new MQTTClient to an existing MQTTClient or to any of its successors QString className = parent->metaObject()->className(); MQTTClient* clientAncestor = parent->ancestor(); if (className == "MQTTClient") parent = parent->parentAspect(); else if (clientAncestor != nullptr) parent = clientAncestor->parentAspect(); #endif parent->folder()->addChild(aspect); } else m_project->addChild(aspect); } void MainWin::settingsDialog() { auto* dlg = new SettingsDialog(this); connect (dlg, &SettingsDialog::settingsChanged, this, &MainWin::handleSettingsChanges); // connect (dlg, &SettingsDialog::resetWelcomeScreen, this, &MainWin::resetWelcomeScreen); dlg->exec(); } #ifdef HAVE_CANTOR_LIBS void MainWin::cantorSettingsDialog() { static KCoreConfigSkeleton* emptyConfig = new KCoreConfigSkeleton(); KConfigDialog* cantorDialog = new KConfigDialog(this, QLatin1String("Cantor Settings"), emptyConfig); for (auto* backend : Cantor::Backend::availableBackends()) if (backend->config()) //It has something to configure, so add it to the dialog cantorDialog->addPage(backend->settingsWidget(cantorDialog), backend->config(), backend->name(), backend->icon()); cantorDialog->show(); } #endif diff --git a/src/kdefrontend/datasources/ImportProjectDialog.cpp b/src/kdefrontend/datasources/ImportProjectDialog.cpp index 89fe4bcea..cb7e0c9f3 100644 --- a/src/kdefrontend/datasources/ImportProjectDialog.cpp +++ b/src/kdefrontend/datasources/ImportProjectDialog.cpp @@ -1,439 +1,439 @@ /*************************************************************************** File : ImportProjectDialog.cpp Project : LabPlot Description : import project dialog -------------------------------------------------------------------- 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 "ImportProjectDialog.h" #include "backend/core/AspectTreeModel.h" #include "backend/core/Project.h" #include "backend/datasources/projects/LabPlotProjectParser.h" #ifdef HAVE_LIBORIGIN #include "backend/datasources/projects/OriginProjectParser.h" #endif #include "kdefrontend/MainWin.h" #include "commonfrontend/widgets/TreeViewComboBox.h" #include #include #include #include #include #include #include #include #include #include #include #include /*! \class ImportProjectDialog \brief Dialog for importing project files. \ingroup kdefrontend */ ImportProjectDialog::ImportProjectDialog(MainWin* parent, ProjectType type) : QDialog(parent), m_mainWin(parent), m_projectType(type), m_aspectTreeModel(new AspectTreeModel(parent->project())) { auto* vLayout = new QVBoxLayout(this); //main widget QWidget* mainWidget = new QWidget(this); ui.setupUi(mainWidget); ui.chbUnusedObjects->hide(); vLayout->addWidget(mainWidget); ui.tvPreview->setAnimated(true); ui.tvPreview->setAlternatingRowColors(true); ui.tvPreview->setSelectionBehavior(QAbstractItemView::SelectRows); ui.tvPreview->setSelectionMode(QAbstractItemView::ExtendedSelection); ui.tvPreview->setUniformRowHeights(true); ui.bOpen->setIcon( QIcon::fromTheme("document-open") ); m_cbAddTo = new TreeViewComboBox(ui.gbImportTo); m_cbAddTo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); ui.gbImportTo->layout()->addWidget(m_cbAddTo); QList list{AspectType::Folder}; m_cbAddTo->setTopLevelClasses(list); m_aspectTreeModel->setSelectableAspects(list); m_cbAddTo->setModel(m_aspectTreeModel); m_bNewFolder = new QPushButton(ui.gbImportTo); m_bNewFolder->setIcon(QIcon::fromTheme("list-add")); m_bNewFolder->setToolTip(i18n("Add new folder")); ui.gbImportTo->layout()->addWidget(m_bNewFolder); //dialog buttons m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); vLayout->addWidget(m_buttonBox); //ok-button is only enabled if some project objects were selected (s.a. ImportProjectDialog::selectionChanged()) m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); //Signals/Slots connect(ui.leFileName, &QLineEdit::textChanged, this, &ImportProjectDialog::fileNameChanged); connect(ui.bOpen, &QPushButton::clicked, this, &ImportProjectDialog::selectFile); connect(m_bNewFolder, &QPushButton::clicked, this, &ImportProjectDialog::newFolder); connect(ui.chbUnusedObjects, &QCheckBox::stateChanged, this, &ImportProjectDialog::refreshPreview); connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QString title; switch (m_projectType) { case (ProjectLabPlot): m_projectParser = new LabPlotProjectParser(); title = i18nc("@title:window", "Import LabPlot Project"); break; case (ProjectOrigin): #ifdef HAVE_LIBORIGIN m_projectParser = new OriginProjectParser(); title = i18nc("@title:window", "Import Origin Project"); #endif break; } //dialog title and icon setWindowTitle(title); setWindowIcon(QIcon::fromTheme("document-import")); //"What's this?" texts QString info = i18n("Specify the file where the project content has to be imported from."); ui.leFileName->setWhatsThis(info); info = i18n("Select one or several objects to be imported into the current project.\n" "Note, all children of the selected objects as well as all the dependent objects will be automatically selected.\n" "To import the whole project, select the top-level project node." ); ui.tvPreview->setWhatsThis(info); info = i18n("Specify the target folder in the current project where the selected objects have to be imported into."); m_cbAddTo->setWhatsThis(info); //restore saved settings if available create(); // ensure there's a window created KConfigGroup conf(KSharedConfig::openConfig(), "ImportProjectDialog"); if (conf.exists()) { KWindowConfig::restoreWindowSize(windowHandle(), conf); resize(windowHandle()->size()); // workaround for QTBUG-40584 } else resize(QSize(300, 0).expandedTo(minimumSize())); QString lastImportedFile; switch (m_projectType) { case (ProjectLabPlot): lastImportedFile = QLatin1String("LastImportedLabPlotProject"); break; case (ProjectOrigin): lastImportedFile = QLatin1String("LastImportedOriginProject"); break; } QApplication::processEvents(QEventLoop::AllEvents, 100); ui.leFileName->setText(conf.readEntry(lastImportedFile, "")); } ImportProjectDialog::~ImportProjectDialog() { //save current settings KConfigGroup conf(KSharedConfig::openConfig(), "ImportProjectDialog"); KWindowConfig::saveWindowSize(windowHandle(), conf); QString lastImportedFile; switch (m_projectType) { case (ProjectLabPlot): lastImportedFile = QLatin1String("LastImportedLabPlotProject"); break; case (ProjectOrigin): lastImportedFile = QLatin1String("LastImportedOriginProject"); break; } conf.writeEntry(lastImportedFile, ui.leFileName->text()); } void ImportProjectDialog::setCurrentFolder(const Folder* folder) { m_cbAddTo->setCurrentModelIndex(m_aspectTreeModel->modelIndexOfAspect(folder)); } void ImportProjectDialog::importTo(QStatusBar* statusBar) const { DEBUG("ImportProjectDialog::importTo()"); //determine the selected objects, convert the model indexes to string pathes const QModelIndexList& indexes = ui.tvPreview->selectionModel()->selectedIndexes(); QStringList selectedPathes; for (int i = 0; i < indexes.size()/4; ++i) { QModelIndex index = indexes.at(i*4); const auto* aspect = static_cast(index.internalPointer()); //path of the current aspect and the pathes of all aspects it depends on selectedPathes << aspect->path(); QDEBUG(" aspect path: " << aspect->path()); for (const auto* depAspect : aspect->dependsOn()) selectedPathes << depAspect->path(); } selectedPathes.removeDuplicates(); Folder* targetFolder = static_cast(m_cbAddTo->currentModelIndex().internalPointer()); //check whether the selected pathes already exist in the target folder and warn the user const QString& targetFolderPath = targetFolder->path(); const Project* targetProject = targetFolder->project(); QStringList targetAllPathes; - for (const auto* aspect : targetProject->children(AbstractAspect::Recursive)) { + for (const auto* aspect : targetProject->children(AbstractAspect::ChildIndexFlag::Recursive)) { if (!dynamic_cast(aspect)) targetAllPathes << aspect->path(); } QStringList existingPathes; for (const auto& path : selectedPathes) { const QString& newPath = targetFolderPath + path.right(path.length() - path.indexOf('/')); if (targetAllPathes.indexOf(newPath) != -1) existingPathes << path; } QDEBUG("project objects to be imported: " << selectedPathes); QDEBUG("all already available project objects: " << targetAllPathes); QDEBUG("already available project objects to be overwritten: " << existingPathes); if (!existingPathes.isEmpty()) { QString msg = i18np("The object listed below already exists in target folder and will be overwritten:", "The objects listed below already exist in target folder and will be overwritten:", existingPathes.size()); msg += '\n'; for (const auto& path : existingPathes) msg += '\n' + path.right(path.length() - path.indexOf('/') - 1); //strip away the name of the root folder "Project" msg += "\n\n" + i18n("Do you want to proceed?"); const int rc = KMessageBox::warningYesNo(nullptr, msg, i18n("Override existing objects?")); if (rc == KMessageBox::No) return; } //show a progress bar in the status bar auto* progressBar = new QProgressBar(); progressBar->setMinimum(0); progressBar->setMaximum(100); statusBar->clearMessage(); statusBar->addWidget(progressBar, 1); QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); QApplication::processEvents(QEventLoop::AllEvents, 100); //import the selected project objects into the specified folder QElapsedTimer timer; timer.start(); connect(m_projectParser, &ProjectParser::completed, progressBar, &QProgressBar::setValue); #ifdef HAVE_LIBORIGIN if (m_projectType == ProjectOrigin && ui.chbUnusedObjects->isVisible() && ui.chbUnusedObjects->isChecked()) reinterpret_cast(m_projectParser)->setImportUnusedObjects(true); #endif m_projectParser->importTo(targetFolder, selectedPathes); statusBar->showMessage( i18n("Project data imported in %1 seconds.", (float)timer.elapsed()/1000) ); QApplication::restoreOverrideCursor(); statusBar->removeWidget(progressBar); } /*! * show the content of the project in the tree view */ void ImportProjectDialog::refreshPreview() { QString project = ui.leFileName->text(); m_projectParser->setProjectFileName(project); #ifdef HAVE_LIBORIGIN if (m_projectType == ProjectOrigin) { auto* originParser = reinterpret_cast(m_projectParser); if (originParser->hasUnusedObjects()) ui.chbUnusedObjects->show(); else ui.chbUnusedObjects->hide(); originParser->setImportUnusedObjects(ui.chbUnusedObjects->isVisible() && ui.chbUnusedObjects->isChecked()); } #endif ui.tvPreview->setModel(m_projectParser->model()); connect(ui.tvPreview->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ImportProjectDialog::selectionChanged); //show top-level containers only if (ui.tvPreview->model()) { QModelIndex root = ui.tvPreview->model()->index(0,0); showTopLevelOnly(root); } //select the first top-level node and //expand the tree to show all available top-level objects and adjust the header sizes ui.tvPreview->setCurrentIndex(ui.tvPreview->model()->index(0,0)); ui.tvPreview->expandAll(); ui.tvPreview->header()->resizeSections(QHeaderView::ResizeToContents); } /*! Hides the non-toplevel items of the model used in the tree view. */ void ImportProjectDialog::showTopLevelOnly(const QModelIndex& index) { int rows = index.model()->rowCount(index); for (int i = 0; i < rows; ++i) { QModelIndex child = index.model()->index(i, 0, index); showTopLevelOnly(child); const auto* aspect = static_cast(child.internalPointer()); ui.tvPreview->setRowHidden(i, index, !isTopLevel(aspect)); } } /*! checks whether \c aspect is one of the allowed top level types */ bool ImportProjectDialog::isTopLevel(const AbstractAspect* aspect) const { foreach (AspectType type, m_projectParser->topLevelClasses()) { if (aspect->inherits(type)) return true; } return false; } //############################################################################## //################################# SLOTS #################################### //############################################################################## void ImportProjectDialog::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { Q_UNUSED(deselected); //determine the dependent objects and select/deselect them too const QModelIndexList& indexes = selected.indexes(); if (indexes.isEmpty()) return; //for the just selected aspect, determine all the objects it depends on and select them, too //TODO: we need a better "selection", maybe with tri-state check boxes in the tree view const auto* aspect = static_cast(indexes.at(0).internalPointer()); const QVector aspects = aspect->dependsOn(); const auto* model = reinterpret_cast(ui.tvPreview->model()); for (const auto* aspect : aspects) { QModelIndex index = model->modelIndexOfAspect(aspect, 0); ui.tvPreview->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); } //Ok-button is only enabled if some project objects were selected bool enable = (selected.indexes().size() != 0); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enable); if (enable) m_buttonBox->button(QDialogButtonBox::Ok)->setToolTip(i18n("Close the dialog and import the selected objects.")); else m_buttonBox->button(QDialogButtonBox::Ok)->setToolTip(i18n("Select object(s) to be imported.")); } /*! opens a file dialog and lets the user select the project file. */ void ImportProjectDialog::selectFile() { KConfigGroup conf(KSharedConfig::openConfig(), "ImportProjectDialog"); QString title; QString lastDir; QString supportedFormats; QString lastDirConfEntryName; switch (m_projectType) { case (ProjectLabPlot): title = i18nc("@title:window", "Open LabPlot Project"); lastDirConfEntryName = QLatin1String("LastImportLabPlotProjectDir"); supportedFormats = i18n("LabPlot Projects (%1)", Project::supportedExtensions()); break; case (ProjectOrigin): #ifdef HAVE_LIBORIGIN title = i18nc("@title:window", "Open Origin Project"); lastDirConfEntryName = QLatin1String("LastImportOriginProjecttDir"); supportedFormats = i18n("Origin Projects (%1)", OriginProjectParser::supportedExtensions()); #endif break; } lastDir = conf.readEntry(lastDirConfEntryName, ""); QString path = QFileDialog::getOpenFileName(this, title, lastDir, supportedFormats); if (path.isEmpty()) return; //cancel was clicked in the file-dialog int pos = path.lastIndexOf(QDir::separator()); if (pos != -1) { QString newDir = path.left(pos); if (newDir != lastDir) conf.writeEntry(lastDirConfEntryName, newDir); } ui.leFileName->setText(path); refreshPreview(); } void ImportProjectDialog::fileNameChanged(const QString& name) { QString fileName = name; #ifndef HAVE_WINDOWS // make relative path if ( !fileName.isEmpty() && fileName.at(0) != QDir::separator()) fileName = QDir::homePath() + QDir::separator() + fileName; #endif bool fileExists = QFile::exists(fileName); if (fileExists) ui.leFileName->setStyleSheet(QString()); else ui.leFileName->setStyleSheet("QLineEdit{background:red;}"); if (!fileExists) { //file doesn't exist -> delete the content preview that is still potentially //available from the previously selected file ui.tvPreview->setModel(nullptr); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); return; } refreshPreview(); } void ImportProjectDialog::newFolder() { QString path = ui.leFileName->text(); QString name = path.right( path.length()-path.lastIndexOf(QDir::separator())-1 ); bool ok; QInputDialog* dlg = new QInputDialog(this); name = dlg->getText(this, i18n("Add new folder"), i18n("Folder name:"), QLineEdit::Normal, name, &ok); if (ok) { auto* folder = new Folder(name); m_mainWin->addAspectToProject(folder); m_cbAddTo->setCurrentModelIndex(m_mainWin->model()->modelIndexOfAspect(folder)); } delete dlg; } diff --git a/src/kdefrontend/spreadsheet/FunctionValuesDialog.cpp b/src/kdefrontend/spreadsheet/FunctionValuesDialog.cpp index 161b0cb05..2ed6d60b5 100644 --- a/src/kdefrontend/spreadsheet/FunctionValuesDialog.cpp +++ b/src/kdefrontend/spreadsheet/FunctionValuesDialog.cpp @@ -1,431 +1,431 @@ /*************************************************************************** File : FunctionValuesDialog.cpp Project : LabPlot Description : Dialog for generating values from a mathematical function -------------------------------------------------------------------- Copyright : (C) 2014-2018 by 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 "FunctionValuesDialog.h" #include "backend/core/AspectTreeModel.h" #include "backend/core/column/Column.h" #include "backend/core/Project.h" #include "backend/lib/macros.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/gsl/ExpressionParser.h" #include "commonfrontend/widgets/TreeViewComboBox.h" #include "kdefrontend/widgets/ConstantsWidget.h" #include "kdefrontend/widgets/FunctionsWidget.h" #include #include #include #include #include #include #include #include #include /*! \class FunctionValuesDialog \brief Dialog for generating values from a mathematical function. \ingroup kdefrontend */ FunctionValuesDialog::FunctionValuesDialog(Spreadsheet* s, QWidget* parent) : QDialog(parent), m_spreadsheet(s) { Q_ASSERT(s != nullptr); setWindowTitle(i18nc("@title:window", "Function Values")); ui.setupUi(this); setAttribute(Qt::WA_DeleteOnClose); ui.tbConstants->setIcon( QIcon::fromTheme("labplot-format-text-symbol") ); ui.tbConstants->setIcon( QIcon::fromTheme("format-text-symbol") ); ui.tbFunctions->setIcon( QIcon::fromTheme("preferences-desktop-font") ); ui.teEquation->setMaximumHeight(QLineEdit().sizeHint().height()*2); ui.teEquation->setFocus(); m_topLevelClasses = {AspectType::Folder, AspectType::Workbook, AspectType::Spreadsheet, AspectType::Column }; m_selectableClasses = {AspectType::Column}; // needed for buggy compiler #if __cplusplus < 201103L m_aspectTreeModel = std::auto_ptr(new AspectTreeModel(m_spreadsheet->project())); #else m_aspectTreeModel = std::unique_ptr(new AspectTreeModel(m_spreadsheet->project())); #endif m_aspectTreeModel->setSelectableAspects(m_selectableClasses); m_aspectTreeModel->enableNumericColumnsOnly(true); m_aspectTreeModel->enableNonEmptyNumericColumnsOnly(true); ui.bAddVariable->setIcon(QIcon::fromTheme("list-add")); ui.bAddVariable->setToolTip(i18n("Add new variable")); ui.chkAutoUpdate->setToolTip(i18n("Automatically update the calculated values on changes in the variable columns")); QDialogButtonBox* btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); ui.verticalLayout->addWidget(btnBox); m_okButton = btnBox->button(QDialogButtonBox::Ok); connect(btnBox, &QDialogButtonBox::accepted, this, &FunctionValuesDialog::accept); connect(btnBox, &QDialogButtonBox::rejected, this, &FunctionValuesDialog::reject); m_okButton->setText(i18n("&Generate")); m_okButton->setToolTip(i18n("Generate function values")); connect(ui.bAddVariable, &QPushButton::pressed, this, &FunctionValuesDialog::addVariable); connect(ui.teEquation, &ExpressionTextEdit::expressionChanged, this, &FunctionValuesDialog::checkValues); connect(ui.tbConstants, &QToolButton::clicked, this, &FunctionValuesDialog::showConstants); connect(ui.tbFunctions, &QToolButton::clicked, this, &FunctionValuesDialog::showFunctions); connect(m_okButton, &QPushButton::clicked, this, &FunctionValuesDialog::generate); //restore saved settings if available create(); // ensure there's a window created KConfigGroup conf(KSharedConfig::openConfig(), "FunctionValuesDialog"); if (conf.exists()) { KWindowConfig::restoreWindowSize(windowHandle(), conf); resize(windowHandle()->size()); // workaround for QTBUG-40584 } else resize(QSize(300, 0).expandedTo(minimumSize())); } FunctionValuesDialog::~FunctionValuesDialog() { KConfigGroup conf(KSharedConfig::openConfig(), "FunctionValuesDialog"); KWindowConfig::saveWindowSize(windowHandle(), conf); } void FunctionValuesDialog::setColumns(const QVector& columns) { m_columns = columns; //formula expression ui.teEquation->setPlainText(m_columns.first()->formula()); //variables const QStringList& variableNames = m_columns.first()->formulaVariableNames(); if (!variableNames.size()) { //no formula was used for this column -> add the first variable "x" addVariable(); m_variableNames[0]->setText("x"); } else { //formula and variables are available const QVector& variableColumns = m_columns.first()->formulaVariableColumns(); const QStringList columnPaths = m_columns.first()->formulaVariableColumnPaths(); //add all available variables and select the corresponding columns - const QVector cols = m_spreadsheet->project()->children(AspectType::Column, AbstractAspect::Recursive); + const QVector cols = m_spreadsheet->project()->children(AspectType::Column, AbstractAspect::ChildIndexFlag::Recursive); for (int i = 0; i < variableNames.size(); ++i) { addVariable(); m_variableNames[i]->setText(variableNames.at(i)); bool found = false; for (const auto* col : cols) { if (col != variableColumns.at(i)) continue; const auto* column = dynamic_cast(col); if (column) m_variableDataColumns[i]->setCurrentModelIndex(m_aspectTreeModel->modelIndexOfAspect(column)); else m_variableDataColumns[i]->setCurrentModelIndex(QModelIndex()); m_variableDataColumns[i]->useCurrentIndexText(true); m_variableDataColumns[i]->setInvalid(false); found = true; break; } //for the current variable name no column is existing anymore (was deleted) //->highlight the combobox red if (!found) { m_variableDataColumns[i]->setCurrentModelIndex(QModelIndex()); m_variableDataColumns[i]->useCurrentIndexText(false); m_variableDataColumns[i]->setInvalid(true, i18n("The column \"%1\"\nis not available anymore. It will be automatically used once it is created again.", columnPaths[i])); m_variableDataColumns[i]->setText(columnPaths[i].split('/').last()); } } } //auto update ui.chkAutoUpdate->setChecked(m_columns.first()->formulaAutoUpdate()); checkValues(); } bool FunctionValuesDialog::validVariableName(QLineEdit* le) { if (ExpressionParser::getInstance()->constants().indexOf(le->text()) != -1) { le->setStyleSheet("QLineEdit{background: red;}"); le->setToolTip(i18n("Provided variable name is already reserved for a name of a constant. Please use another name.")); return false; } if (ExpressionParser::getInstance()->functions().indexOf(le->text()) != -1) { le->setStyleSheet("QLineEdit{background: red;}"); le->setToolTip(i18n("Provided variable name is already reserved for a name of a function. Please use another name.")); return false; } le->setStyleSheet(QString()); le->setToolTip(""); return true; } /*! check the user input and enables/disables the Ok-button depending on the correctness of the input */ void FunctionValuesDialog::checkValues() { //check whether the formula syntax is correct if (!ui.teEquation->isValid()) { m_okButton->setEnabled(false); return; } //check whether for the variables where a name was provided also a column was selected. for (int i = 0; i < m_variableDataColumns.size(); ++i) { if (m_variableNames.at(i)->text().simplified().isEmpty()) continue; TreeViewComboBox* cb = m_variableDataColumns.at(i); AbstractAspect* aspect = static_cast(cb->currentModelIndex().internalPointer()); if (!aspect) { m_okButton->setEnabled(false); return; } if (!validVariableName(m_variableNames[i])) { m_okButton->setEnabled(false); return; } /* Column* column = dynamic_cast(aspect); DEBUG("row count = " << (static_cast* >(column->data()))->size()); if (!column || column->rowCount() < 1) { m_okButton->setEnabled(false); //Warning: x column is empty return; } */ } m_okButton->setEnabled(true); } void FunctionValuesDialog::showConstants() { QMenu menu; ConstantsWidget constants(&menu); connect(&constants, &ConstantsWidget::constantSelected, this, &FunctionValuesDialog::insertConstant); connect(&constants, &ConstantsWidget::constantSelected, &menu, &QMenu::close); connect(&constants, &ConstantsWidget::canceled, &menu, &QMenu::close); auto* widgetAction = new QWidgetAction(this); widgetAction->setDefaultWidget(&constants); menu.addAction(widgetAction); QPoint pos(-menu.sizeHint().width()+ui.tbConstants->width(),-menu.sizeHint().height()); menu.exec(ui.tbConstants->mapToGlobal(pos)); } void FunctionValuesDialog::showFunctions() { QMenu menu; FunctionsWidget functions(&menu); connect(&functions, &FunctionsWidget::functionSelected, this, &FunctionValuesDialog::insertFunction); connect(&functions, &FunctionsWidget::functionSelected, &menu, &QMenu::close); connect(&functions, &FunctionsWidget::canceled, &menu, &QMenu::close); auto* widgetAction = new QWidgetAction(this); widgetAction->setDefaultWidget(&functions); menu.addAction(widgetAction); QPoint pos(-menu.sizeHint().width()+ui.tbFunctions->width(),-menu.sizeHint().height()); menu.exec(ui.tbFunctions->mapToGlobal(pos)); } void FunctionValuesDialog::insertFunction(const QString& str) { //TODO: not all functions have only one argument ui.teEquation->insertPlainText(str + "(x)"); } void FunctionValuesDialog::insertConstant(const QString& str) { ui.teEquation->insertPlainText(str); } void FunctionValuesDialog::addVariable() { auto* layout = ui.gridLayoutVariables; int row = m_variableNames.size(); //text field for the variable name auto* le = new QLineEdit(); le->setMaximumWidth(30); connect(le, &QLineEdit::textChanged, this, &FunctionValuesDialog::variableNameChanged); layout->addWidget(le, row, 0, 1, 1); m_variableNames << le; //label for the "="-sign auto* l = new QLabel("="); layout->addWidget(l, row, 1, 1, 1); m_variableLabels << l; //combo box for the data column auto* cb = new TreeViewComboBox(); cb->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred)); connect(cb, &TreeViewComboBox::currentModelIndexChanged, this, &FunctionValuesDialog::variableColumnChanged); layout->addWidget(cb, row, 2, 1, 1); m_variableDataColumns << cb; cb->setTopLevelClasses(m_topLevelClasses); cb->setModel(m_aspectTreeModel.get()); //don't allow to select columns to be calculated as variable columns (avoid circular dependencies) QList aspects; for (auto* col : m_columns) aspects << col; cb->setHiddenAspects(aspects); //for the variable column select the first non-selected column in the spreadsheet for (auto* col : m_spreadsheet->children()) { if (m_columns.indexOf(col) == -1) { cb->setCurrentModelIndex(m_aspectTreeModel->modelIndexOfAspect(col)); break; } } //move the add-button to the next row layout->removeWidget(ui.bAddVariable); layout->addWidget(ui.bAddVariable, row+1,3, 1, 1); //add delete-button for the just added variable if (row != 0) { auto* b = new QToolButton(); b->setIcon(QIcon::fromTheme("list-remove")); b->setToolTip(i18n("Delete variable")); layout->addWidget(b, row, 3, 1, 1); m_variableDeleteButtons<setText(i18n("Variables:")); //TODO: adjust the tab-ordering after new widgets were added } void FunctionValuesDialog::deleteVariable() { QObject* ob = QObject::sender(); int index = m_variableDeleteButtons.indexOf(qobject_cast(ob)) ; delete m_variableNames.takeAt(index+1); delete m_variableLabels.takeAt(index+1); delete m_variableDataColumns.takeAt(index+1); delete m_variableDeleteButtons.takeAt(index); variableNameChanged(); checkValues(); //adjust the layout resize( QSize(width(),0).expandedTo(minimumSize()) ); m_variableNames.size() > 1 ? ui.lVariable->setText(i18n("Variables:")) : ui.lVariable->setText(i18n("Variable:")); //TODO: adjust the tab-ordering after some widgets were deleted } void FunctionValuesDialog::variableNameChanged() { QStringList vars; QString text; for (auto* varName : m_variableNames) { QString name = varName->text().simplified(); if (!name.isEmpty()) { vars << name; if (text.isEmpty()) { text += name; } else { text += ", " + name; } } } if (!text.isEmpty()) text = "f(" + text + ") = "; else text = "f = "; ui.lFunction->setText(text); ui.teEquation->setVariables(vars); checkValues(); } /*! * called if a new column was selected in the comboboxes for the variable columns. */ void FunctionValuesDialog::variableColumnChanged(const QModelIndex& index) { //combobox was potentially red-highlighted because of a missing column //remove the highlighting if we have a valid selection now auto* aspect = static_cast(index.internalPointer()); if (aspect) { auto* cb = dynamic_cast(QObject::sender()); if (cb) cb->setStyleSheet(""); } checkValues(); } void FunctionValuesDialog::generate() { Q_ASSERT(m_spreadsheet); WAIT_CURSOR; m_spreadsheet->beginMacro(i18np("%1: fill column with function values", "%1: fill columns with function values", m_spreadsheet->name(), m_columns.size())); //determine variable names and the data vectors of the specified columns QStringList variableNames; QVector variableColumns; for (int i = 0; i < m_variableNames.size(); ++i) { variableNames << m_variableNames.at(i)->text().simplified(); AbstractAspect* aspect = static_cast(m_variableDataColumns.at(i)->currentModelIndex().internalPointer()); Q_ASSERT(aspect); auto* column = dynamic_cast(aspect); Q_ASSERT(column); variableColumns << column; } //set the new values and store the expression, variable names and the used data columns const QString& expression = ui.teEquation->toPlainText(); bool autoUpdate = (ui.chkAutoUpdate->checkState() == Qt::Checked); for (auto* col : m_columns) { if (col->columnMode() != AbstractColumn::Numeric) col->setColumnMode(AbstractColumn::Numeric); col->setFormula(expression, variableNames, variableColumns, autoUpdate); col->updateFormula(); } m_spreadsheet->endMacro(); RESET_CURSOR; } diff --git a/src/kdefrontend/spreadsheet/PlotDataDialog.cpp b/src/kdefrontend/spreadsheet/PlotDataDialog.cpp index 366360261..5b66fe3a7 100644 --- a/src/kdefrontend/spreadsheet/PlotDataDialog.cpp +++ b/src/kdefrontend/spreadsheet/PlotDataDialog.cpp @@ -1,732 +1,732 @@ /*************************************************************************** File : PlotDataDialog.cpp Project : LabPlot Description : Dialog for generating plots for the spreadsheet data -------------------------------------------------------------------- Copyright : (C) 2017-2019 by 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 "PlotDataDialog.h" #include "backend/core/AspectTreeModel.h" #include "backend/core/Project.h" #include "backend/core/column/Column.h" #include "backend/datapicker/DatapickerCurve.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/worksheet/plots/cartesian/Axis.h" #include "backend/worksheet/plots/cartesian/XYAnalysisCurve.h" #include "backend/worksheet/plots/cartesian/XYCurve.h" #include "backend/worksheet/plots/cartesian/XYDataReductionCurve.h" #include "backend/worksheet/plots/cartesian/XYDifferentiationCurve.h" #include "backend/worksheet/plots/cartesian/XYIntegrationCurve.h" #include "backend/worksheet/plots/cartesian/XYInterpolationCurve.h" #include "backend/worksheet/plots/cartesian/XYSmoothCurve.h" #include "backend/worksheet/plots/cartesian/XYFitCurve.h" #include "backend/worksheet/plots/cartesian/XYFourierFilterCurve.h" #ifdef HAVE_MQTT #include "backend/datasources/MQTTTopic.h" #endif #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/worksheet/Worksheet.h" #include "backend/worksheet/TextLabel.h" #include "commonfrontend/spreadsheet/SpreadsheetView.h" #include "commonfrontend/widgets/TreeViewComboBox.h" #include #include #include #include #include #include #include "ui_plotdatawidget.h" /*! \class PlotDataDialog \brief Dialog for generating plots for the spreadsheet data. \ingroup kdefrontend */ PlotDataDialog::PlotDataDialog(Spreadsheet* s, PlotType type, QWidget* parent) : QDialog(parent), ui(new Ui::PlotDataWidget()), m_spreadsheet(s), m_plotsModel(new AspectTreeModel(m_spreadsheet->project())), m_worksheetsModel(new AspectTreeModel(m_spreadsheet->project())), m_plotType(type) { setAttribute(Qt::WA_DeleteOnClose); setWindowTitle(i18nc("@title:window", "Plot Spreadsheet Data")); setWindowIcon(QIcon::fromTheme("office-chart-line")); QWidget* mainWidget = new QWidget(this); ui->setupUi(mainWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); m_okButton = buttonBox->button(QDialogButtonBox::Ok); m_okButton->setDefault(true); m_okButton->setToolTip(i18n("Plot the selected data")); m_okButton->setText(i18n("&Plot")); auto* layout = new QVBoxLayout(this); layout->addWidget(mainWidget); layout->addWidget(buttonBox); setLayout(layout); //create combox boxes for the existing plots and worksheets auto* gridLayout = dynamic_cast(ui->gbPlotPlacement->layout()); cbExistingPlots = new TreeViewComboBox(ui->gbPlotPlacement); cbExistingPlots->setMinimumWidth(250);//TODO: use proper sizeHint in TreeViewComboBox gridLayout->addWidget(cbExistingPlots, 0, 1, 1, 1); cbExistingWorksheets = new TreeViewComboBox(ui->gbPlotPlacement); cbExistingWorksheets->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred)); gridLayout->addWidget(cbExistingWorksheets, 1, 1, 1, 1); QList list{AspectType::Folder, AspectType::Worksheet, AspectType::CartesianPlot}; cbExistingPlots->setTopLevelClasses(list); list = {AspectType::CartesianPlot}; m_plotsModel->setSelectableAspects(list); cbExistingPlots->setModel(m_plotsModel); //select the first available plot, if available - auto plots = m_spreadsheet->project()->children(AbstractAspect::Recursive); + auto plots = m_spreadsheet->project()->children(AbstractAspect::ChildIndexFlag::Recursive); if (!plots.isEmpty()) { const auto* plot = plots.first(); cbExistingPlots->setCurrentModelIndex(m_plotsModel->modelIndexOfAspect(plot)); } list = {AspectType::Folder, AspectType::Worksheet}; cbExistingWorksheets->setTopLevelClasses(list); list = {AspectType::Worksheet}; m_worksheetsModel->setSelectableAspects(list); cbExistingWorksheets->setModel(m_worksheetsModel); //select the first available worksheet, if available - auto worksheets = m_spreadsheet->project()->children(AbstractAspect::Recursive); + auto worksheets = m_spreadsheet->project()->children(AbstractAspect::ChildIndexFlag::Recursive); if (!worksheets.isEmpty()) { const auto* worksheet = worksheets.first(); cbExistingWorksheets->setCurrentModelIndex(m_worksheetsModel->modelIndexOfAspect(worksheet)); } //in the grid layout of the scroll area we have on default one row for the x-column, //one row for the separating line and one line for the y-column. //set the height of this default content as the minimal size of the scroll area. gridLayout = dynamic_cast(ui->scrollAreaColumns->widget()->layout()); int height = 2*ui->cbXColumn->height() + ui->line->height() + 2*gridLayout->verticalSpacing() + gridLayout->contentsMargins().top() + gridLayout->contentsMargins().bottom(); ui->scrollAreaColumns->setMinimumSize(0, height); //hide the check box for creation of original data, only shown if analysis curves are to be created ui->spacer->changeSize(0, 0); ui->chkCreateDataCurve->hide(); //SIGNALs/SLOTs connect(buttonBox, &QDialogButtonBox::accepted, this, [=]() { hide(); plot(); }); connect(buttonBox, &QDialogButtonBox::rejected, this, &PlotDataDialog::reject); connect(buttonBox, &QDialogButtonBox::accepted, this, &PlotDataDialog::accept); connect(ui->rbCurvePlacement1, &QRadioButton::toggled, this, &PlotDataDialog::curvePlacementChanged); connect(ui->rbCurvePlacement2, &QRadioButton::toggled, this, &PlotDataDialog::curvePlacementChanged); connect(ui->rbPlotPlacement1, &QRadioButton::toggled, this, &PlotDataDialog::plotPlacementChanged); connect(ui->rbPlotPlacement2, &QRadioButton::toggled, this, &PlotDataDialog::plotPlacementChanged); connect(ui->rbPlotPlacement3, &QRadioButton::toggled, this, &PlotDataDialog::plotPlacementChanged); connect(cbExistingPlots, &TreeViewComboBox::currentModelIndexChanged, this, &PlotDataDialog::checkOkButton); connect(cbExistingWorksheets, &TreeViewComboBox::currentModelIndexChanged, this, &PlotDataDialog::checkOkButton); //restore saved settings if available create(); // ensure there's a window created KConfigGroup conf(KSharedConfig::openConfig(), "PlotDataDialog"); if (conf.exists()) { int index = conf.readEntry("CurvePlacement", 0); if (index == 2) ui->rbCurvePlacement2->setChecked(true); index = conf.readEntry("PlotPlacement", 0); if (index == 2) ui->rbPlotPlacement2->setChecked(true); if (index == 3) ui->rbPlotPlacement3->setChecked(true); KWindowConfig::restoreWindowSize(windowHandle(), conf); resize(windowHandle()->size()); // workaround for QTBUG-40584 } else resize(QSize(0, 0).expandedTo(minimumSize())); processColumns(); plotPlacementChanged(); } PlotDataDialog::~PlotDataDialog() { //save current settings KConfigGroup conf(KSharedConfig::openConfig(), "PlotDataDialog"); int index = 0; if (ui->rbCurvePlacement1->isChecked()) index = 1; if (ui->rbCurvePlacement2->isChecked()) index = 2; conf.writeEntry("CurvePlacement", index); if (ui->rbPlotPlacement1->isChecked()) index = 1; if (ui->rbPlotPlacement2->isChecked()) index = 2; if (ui->rbPlotPlacement3->isChecked()) index = 3; conf.writeEntry("PlotPlacement", index); KWindowConfig::saveWindowSize(windowHandle(), conf); delete m_plotsModel; delete m_worksheetsModel; } void PlotDataDialog::setAnalysisAction(AnalysisAction action) { m_analysisAction = action; m_analysisMode = true; ui->spacer->changeSize(0, 40); ui->chkCreateDataCurve->show(); } void PlotDataDialog::processColumns() { //columns to plot auto* view = reinterpret_cast(m_spreadsheet->view()); QVector selectedColumns = view->selectedColumns(true); //use all spreadsheet columns if no columns are selected if (selectedColumns.isEmpty()) selectedColumns = m_spreadsheet->children(); //skip error and non-plottable columns for (Column* col : selectedColumns) { if ((col->plotDesignation() == AbstractColumn::X || col->plotDesignation() == AbstractColumn::Y || col->plotDesignation() == AbstractColumn::NoDesignation) && col->isPlottable()) m_columns << col; } //disable everything if the spreadsheet doesn't have any columns to plot if (m_columns.isEmpty()) { ui->gbCurvePlacement->setEnabled(false); ui->gbPlotPlacement->setEnabled(false); return; } //determine the column names //and the name of the first column having "X" as the plot designation (relevant for xy-curves only) QStringList columnNames; QString xColumnName; for (const Column* column : m_columns) { columnNames << column->name(); if (m_plotType == PlotXYCurve && xColumnName.isEmpty() && column->plotDesignation() == AbstractColumn::X) xColumnName = column->name(); } if (m_plotType == PlotXYCurve && xColumnName.isEmpty()) { //no X-column was selected -> look for the first non-selected X-column left to the first selected column const int index = m_spreadsheet->indexOfChild(selectedColumns.first()) - 1; if (index >= 0) { for (int i = index; i >= 0; --i) { Column* column = m_spreadsheet->column(i); if (column->plotDesignation() == AbstractColumn::X && column->isPlottable()) { xColumnName = column->name(); m_columns.prepend(column); columnNames.prepend(xColumnName); break; } } } } switch (m_plotType) { case PlotXYCurve: processColumnsForXYCurve(columnNames, xColumnName); break; case PlotHistogram: processColumnsForHistogram(columnNames); break; } //resize the scroll area to show five ComboBoxes at maximum without showing the scroll bars int size = m_columnComboBoxes.size() >= 5 ? 5 : m_columnComboBoxes.size(); int height = size * ui->cbXColumn->height(); auto* layout = dynamic_cast(ui->scrollAreaColumns->widget()->layout()); if (layout) { height += (size + 1)*layout->verticalSpacing(); if (m_plotType == PlotXYCurve) height += layout->verticalSpacing(); //one more spacing for the separating line } ui->scrollAreaColumns->setMinimumSize(ui->scrollAreaColumns->width(), height); } void PlotDataDialog::processColumnsForXYCurve(const QStringList& columnNames, const QString& xColumnName) { m_columnComboBoxes << ui->cbXColumn; m_columnComboBoxes << ui->cbYColumn; //ui-widget only has one combobox for the y-data -> add additional comboboxes dynamically if required if (m_columns.size() > 2) { auto* gridLayout = dynamic_cast(ui->scrollAreaColumns->widget()->layout()); for (int i = 2; i < m_columns.size(); ++i) { QLabel* label = new QLabel(i18n("Y-data")); auto* comboBox = new QComboBox(); gridLayout->addWidget(label, i+1, 0, 1, 1); gridLayout->addWidget(comboBox, i+1, 2, 1, 1); m_columnComboBoxes << comboBox; } } else { //two columns provided, only one curve is possible -> hide the curve placement options ui->rbCurvePlacement1->setChecked(true); ui->gbCurvePlacement->hide(); ui->gbPlotPlacement->setTitle(i18n("Add Curve to")); } //show all selected/available column names in the data comboboxes for (QComboBox* const comboBox : m_columnComboBoxes) comboBox->addItems(columnNames); if (!xColumnName.isEmpty()) { //show in the X-data combobox the first column having X as the plot designation ui->cbXColumn->setCurrentIndex(ui->cbXColumn->findText(xColumnName)); //for the remaining columns, show the names in the comboboxes for the Y-data //TODO: handle columns with error-designations int yColumnIndex = 1; //the index of the first Y-data comboBox in m_columnComboBoxes for (const QString& name : columnNames) { if (name != xColumnName) { QComboBox* comboBox = m_columnComboBoxes[yColumnIndex]; comboBox->setCurrentIndex(comboBox->findText(name)); yColumnIndex++; } } } else { //no column with "x plot designation" is selected, simply show all columns in the order they were selected. //first selected column will serve as the x-column. int yColumnIndex = 0; for (const QString& name : columnNames) { QComboBox* comboBox = m_columnComboBoxes[yColumnIndex]; comboBox->setCurrentIndex(comboBox->findText(name)); yColumnIndex++; } } } void PlotDataDialog::processColumnsForHistogram(const QStringList& columnNames) { ui->gbData->setTitle(i18n("Histogram Data")); ui->line->hide(); ui->spacer->changeSize(0, 0); ui->chkCreateDataCurve->hide(); //use the already available cbXColumn combo box ui->lXColumn->setText(i18n("Data")); m_columnComboBoxes << ui->cbXColumn; ui->cbXColumn->addItems(columnNames); ui->cbXColumn->setCurrentIndex(0); if (m_columns.size() == 1) { //one column provided, only one histogram is possible //-> hide the curve placement options and the scroll areas for further columns ui->lYColumn->hide(); ui->cbYColumn->hide(); ui->rbCurvePlacement1->setChecked(true); ui->gbCurvePlacement->hide(); ui->gbPlotPlacement->setTitle(i18n("Add Histogram to")); } else { ui->gbCurvePlacement->setTitle(i18n("Histogram Placement")); ui->rbCurvePlacement1->setText(i18n("All histograms in one plot")); ui->rbCurvePlacement2->setText(i18n("One plot per histogram")); ui->gbPlotPlacement->setTitle(i18n("Add Histograms to")); //use the already available cbYColumn combo box ui->lYColumn->setText(i18n("Data")); m_columnComboBoxes << ui->cbYColumn; ui->cbYColumn->addItems(columnNames); ui->cbYColumn->setCurrentIndex(1); //add a ComboBox for every further column to be plotted auto* gridLayout = dynamic_cast(ui->scrollAreaColumns->widget()->layout()); for (int i = 2; i < m_columns.size(); ++i) { auto* label = new QLabel(i18n("Data")); auto* comboBox = new QComboBox(); gridLayout->addWidget(label, i+1, 0, 1, 1); gridLayout->addWidget(comboBox, i+1, 2, 1, 1); comboBox->addItems(columnNames); comboBox->setCurrentIndex(i); m_columnComboBoxes << comboBox; } } } void PlotDataDialog::plot() { DEBUG("PlotDataDialog::plot()"); WAIT_CURSOR; m_spreadsheet->project()->setSuppressAspectAddedSignal(true); m_lastAddedCurve = nullptr; if (ui->rbPlotPlacement1->isChecked()) { //add curves to an existing plot auto* aspect = static_cast(cbExistingPlots->currentModelIndex().internalPointer()); auto* plot = static_cast(aspect); plot->beginMacro( i18n("Plot data from %1", m_spreadsheet->name()) ); addCurvesToPlot(plot); plot->endMacro(); } else if (ui->rbPlotPlacement2->isChecked()) { //add curves to a new plot in an existing worksheet auto* aspect = static_cast(cbExistingWorksheets->currentModelIndex().internalPointer()); auto* worksheet = static_cast(aspect); worksheet->beginMacro( i18n("Plot data from %1", m_spreadsheet->name()) ); if (ui->rbCurvePlacement1->isChecked()) { //all curves in one plot CartesianPlot* plot = new CartesianPlot( i18n("Plot data from %1", m_spreadsheet->name()) ); plot->initDefault(CartesianPlot::FourAxes); //set the axis titles before we add the plot to the worksheet //set the x-axis names const QString& xColumnName = ui->cbXColumn->currentText(); for (auto* axis : plot->children()) { if (axis->orientation() == Axis::AxisHorizontal) { axis->title()->setText(xColumnName); break; } } //if we only have one single y-column to plot, we can set the title of the y-axes if (m_columnComboBoxes.size() == 2) { const QString& yColumnName = m_columnComboBoxes[1]->currentText(); for (auto* axis : plot->children()) { if (axis->orientation() == Axis::AxisVertical) { axis->title()->setText(yColumnName); break; } } } worksheet->addChild(plot); addCurvesToPlot(plot); } else { //one plot per curve addCurvesToPlots(worksheet); } worksheet->endMacro(); } else { //add curves to a new plot(s) in a new worksheet AbstractAspect* parent = m_spreadsheet->parentAspect(); if (parent->type() == AspectType::DatapickerCurve) parent = parent->parentAspect()->parentAspect(); else if (parent->type() == AspectType::Workbook) parent = parent->parentAspect(); #ifdef HAVE_MQTT else if (dynamic_cast(m_spreadsheet)) parent = m_spreadsheet->project(); #endif parent->beginMacro( i18n("Plot data from %1", m_spreadsheet->name()) ); Worksheet* worksheet = new Worksheet(i18n("Plot data from %1", m_spreadsheet->name())); parent->addChild(worksheet); if (ui->rbCurvePlacement1->isChecked()) { //all curves in one plot CartesianPlot* plot = new CartesianPlot( i18n("Plot data from %1", m_spreadsheet->name()) ); plot->initDefault(CartesianPlot::FourAxes); //set the axis titles before we add the plot to the worksheet //set the x-axis names const QString& xColumnName = ui->cbXColumn->currentText(); for (auto* axis : plot->children()) { if (axis->orientation() == Axis::AxisHorizontal) { axis->title()->setText(xColumnName); break; } } //if we only have one single y-column to plot, we can set the title of the y-axes if (m_columnComboBoxes.size() == 2) { const QString& yColumnName = m_columnComboBoxes[1]->currentText(); for (auto* axis : plot->children()) { if (axis->orientation() == Axis::AxisVertical) { axis->title()->setText(yColumnName); break; } } } worksheet->addChild(plot); addCurvesToPlot(plot); } else { //one plot per curve addCurvesToPlots(worksheet); } parent->endMacro(); } //select the parent plot of the last added curve in the project explorer m_spreadsheet->project()->setSuppressAspectAddedSignal(false); if (m_lastAddedCurve) emit m_spreadsheet->project()->requestNavigateTo(m_lastAddedCurve->parentAspect()->path()); RESET_CURSOR; } Column* PlotDataDialog::columnFromName(const QString& name) const { for (auto* column : m_columns) { if (column->name() == name) return column; } return nullptr; } /*! * * for the selected columns in this dialog, creates a curve in the already existing plot \c plot. */ void PlotDataDialog::addCurvesToPlot(CartesianPlot* plot) { QApplication::processEvents(QEventLoop::AllEvents, 100); switch (m_plotType) { case PlotXYCurve: { Column* xColumn = columnFromName(ui->cbXColumn->currentText()); for (auto* comboBox : m_columnComboBoxes) { const QString& name = comboBox->currentText(); Column* yColumn = columnFromName(name); //if only one column was selected, allow to use this column for x and for y. //otherwise, don't assign xColumn to y if (m_columns.size() > 1 && yColumn == xColumn) continue; addCurve(name, xColumn, yColumn, plot); } break; } case PlotHistogram: { for (auto* comboBox : m_columnComboBoxes) { const QString& name = comboBox->currentText(); Column* column = columnFromName(name); addHistogram(name, column, plot); } break; } } plot->dataChanged(); } /*! * for the selected columns in this dialog, creates a plot and a curve in the already existing worksheet \c worksheet. */ void PlotDataDialog::addCurvesToPlots(Worksheet* worksheet) { QApplication::processEvents(QEventLoop::AllEvents, 100); worksheet->setSuppressLayoutUpdate(true); switch (m_plotType) { case PlotXYCurve: { const QString& xColumnName = ui->cbXColumn->currentText(); Column* xColumn = columnFromName(xColumnName); for (auto* comboBox : m_columnComboBoxes) { const QString& name = comboBox->currentText(); Column* yColumn = columnFromName(name); if (yColumn == xColumn) continue; CartesianPlot* plot = new CartesianPlot(i18n("Plot %1", name)); plot->initDefault(CartesianPlot::FourAxes); //set the axis names in the new plot bool xSet = false; bool ySet = false; for (auto* axis : plot->children()) { if (axis->orientation() == Axis::AxisHorizontal && !xSet) { axis->title()->setText(xColumnName); xSet = true; } else if (axis->orientation() == Axis::AxisVertical && !ySet) { axis->title()->setText(name); ySet = true; } } worksheet->addChild(plot); addCurve(name, xColumn, yColumn, plot); plot->scaleAuto(); } break; } case PlotHistogram: { for (auto* comboBox : m_columnComboBoxes) { const QString& name = comboBox->currentText(); Column* column = columnFromName(name); CartesianPlot* plot = new CartesianPlot(i18n("Plot %1", name)); plot->initDefault(CartesianPlot::FourAxes); //set the axis names in the new plot bool xSet = false; for (auto* axis : plot->children()) { if (axis->orientation() == Axis::AxisHorizontal && !xSet) { axis->title()->setText(name); xSet = true; } } worksheet->addChild(plot); addHistogram(name, column, plot); plot->scaleAuto(); } } } worksheet->setSuppressLayoutUpdate(false); worksheet->updateLayout(); } /*! * helper function that does the actual creation of the curve and adding it as child to the \c plot. */ void PlotDataDialog::addCurve(const QString& name, Column* xColumn, Column* yColumn, CartesianPlot* plot) { DEBUG("PlotDataDialog::addCurve()"); if (!m_analysisMode) { auto* curve = new XYCurve(name); curve->suppressRetransform(true); curve->setXColumn(xColumn); curve->setYColumn(yColumn); curve->suppressRetransform(false); plot->addChild(curve); m_lastAddedCurve = curve; } else { bool createDataCurve = ui->chkCreateDataCurve->isChecked(); XYCurve* curve = nullptr; if (createDataCurve) { curve = new XYCurve(name); curve->suppressRetransform(true); curve->setXColumn(xColumn); curve->setYColumn(yColumn); curve->suppressRetransform(false); plot->addChild(curve); m_lastAddedCurve = curve; } XYAnalysisCurve* analysisCurve = nullptr; switch (m_analysisAction) { case DataReduction: analysisCurve = new XYDataReductionCurve(i18n("Reduction of '%1'", name)); break; case Differentiation: analysisCurve = new XYDifferentiationCurve(i18n("Derivative of '%1'", name)); break; case Integration: analysisCurve = new XYIntegrationCurve(i18n("Integral of '%1'", name)); break; case Interpolation: analysisCurve = new XYInterpolationCurve(i18n("Interpolation of '%1'", name)); break; case Smoothing: analysisCurve = new XYSmoothCurve(i18n("Smoothing of '%1'", name)); break; case FitLinear: case FitPower: case FitExp1: case FitExp2: case FitInvExp: case FitGauss: case FitCauchyLorentz: case FitTan: case FitTanh: case FitErrFunc: case FitCustom: analysisCurve = new XYFitCurve(i18n("Fit to '%1'", name)); static_cast(analysisCurve)->initFitData(m_analysisAction); static_cast(analysisCurve)->initStartValues(curve); break; case FourierFilter: analysisCurve = new XYFourierFilterCurve(i18n("Fourier Filter of '%1'", name)); break; } if (analysisCurve != nullptr) { analysisCurve->suppressRetransform(true); analysisCurve->setXDataColumn(xColumn); analysisCurve->setYDataColumn(yColumn); if (m_analysisAction != FitCustom) //no custom fit-model set yet, no need to recalculate analysisCurve->recalculate(); analysisCurve->suppressRetransform(false); plot->addChild(analysisCurve); m_lastAddedCurve = analysisCurve; } } } void PlotDataDialog::addHistogram(const QString& name, Column* column, CartesianPlot* plot) { auto* hist = new Histogram(name); plot->addChild(hist); // hist->suppressRetransform(true); hist->setDataColumn(column); // hist->suppressRetransform(false); m_lastAddedCurve = hist; } //################################################################ //########################## Slots ############################### //################################################################ void PlotDataDialog::curvePlacementChanged() { if (ui->rbCurvePlacement1->isChecked()) { ui->rbPlotPlacement1->setEnabled(true); ui->rbPlotPlacement2->setText(i18n("new plot in an existing worksheet")); ui->rbPlotPlacement3->setText(i18n("new plot in a new worksheet")); } else { ui->rbPlotPlacement1->setEnabled(false); if (ui->rbPlotPlacement1->isChecked()) ui->rbPlotPlacement2->setChecked(true); ui->rbPlotPlacement2->setText(i18n("new plots in an existing worksheet")); ui->rbPlotPlacement3->setText(i18n("new plots in a new worksheet")); } } void PlotDataDialog::plotPlacementChanged() { if (ui->rbPlotPlacement1->isChecked()) { cbExistingPlots->setEnabled(true); cbExistingWorksheets->setEnabled(false); } else if (ui->rbPlotPlacement2->isChecked()) { cbExistingPlots->setEnabled(false); cbExistingWorksheets->setEnabled(true); } else { cbExistingPlots->setEnabled(false); cbExistingWorksheets->setEnabled(false); } checkOkButton(); } void PlotDataDialog::checkOkButton() { bool enable = false; QString msg; if ( (m_plotType == PlotXYCurve && (ui->cbXColumn->currentIndex() == -1 || ui->cbYColumn->currentIndex() == -1)) || (m_plotType == PlotHistogram && ui->cbXColumn->currentIndex() == -1) ) msg = i18n("No data selected to plot."); else if (ui->rbPlotPlacement1->isChecked()) { AbstractAspect* aspect = static_cast(cbExistingPlots->currentModelIndex().internalPointer()); enable = (aspect != nullptr); if (!enable) msg = i18n("An already existing plot has to be selected."); } else if (ui->rbPlotPlacement2->isChecked()) { AbstractAspect* aspect = static_cast(cbExistingWorksheets->currentModelIndex().internalPointer()); enable = (aspect != nullptr); if (!enable) msg = i18n("An already existing worksheet has to be selected."); } else enable = true; m_okButton->setEnabled(enable); if (enable) m_okButton->setToolTip(i18n("Close the dialog and plot the data.")); else m_okButton->setToolTip(msg); } diff --git a/src/kdefrontend/widgets/DatapickerCurveWidget.cpp b/src/kdefrontend/widgets/DatapickerCurveWidget.cpp index df7bffbe8..cef98aba6 100644 --- a/src/kdefrontend/widgets/DatapickerCurveWidget.cpp +++ b/src/kdefrontend/widgets/DatapickerCurveWidget.cpp @@ -1,519 +1,519 @@ /*************************************************************************** File : ImageWidget.cpp Project : LabPlot Description : widget for datapicker properties -------------------------------------------------------------------- Copyright : (C) 2015 by Ankit Wagadre (wagadre.ankit@gmail.com) Copyright : (C) 2015-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 "DatapickerCurveWidget.h" #include "backend/datapicker/DatapickerPoint.h" #include "backend/worksheet/Worksheet.h" #include "kdefrontend/GuiTools.h" #include #include #include DatapickerCurveWidget::DatapickerCurveWidget(QWidget* parent) : BaseDock(parent) { ui.setupUi(this); m_leName = ui.leName; m_leComment = ui.leComment; ui.cbXErrorType->addItem(i18n("No Error")); ui.cbXErrorType->addItem(i18n("Symmetric")); ui.cbXErrorType->addItem(i18n("Asymmetric")); ui.cbYErrorType->addItem(i18n("No Error")); ui.cbYErrorType->addItem(i18n("Symmetric")); ui.cbYErrorType->addItem(i18n("Asymmetric")); QString info = i18n("Specify whether the data points have errors and of which type.\n" "Note, changing this type is not possible once at least one point was read."); ui.lXErrorType->setToolTip(info); ui.cbXErrorType->setToolTip(info); ui.lYErrorType->setToolTip(info); ui.cbYErrorType->setToolTip(info); connect(ui.leName, &QLineEdit::textChanged, this, &DatapickerCurveWidget::nameChanged); connect(ui.leComment, &QLineEdit::textChanged, this, &DatapickerCurveWidget::commentChanged); connect(ui.cbXErrorType, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerCurveWidget::xErrorTypeChanged); connect(ui.cbYErrorType, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerCurveWidget::yErrorTypeChanged); //symbol connect(ui.cbStyle, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerCurveWidget::styleChanged); connect(ui.sbSize, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerCurveWidget::sizeChanged); connect(ui.sbRotation, static_cast(&QSpinBox::valueChanged), this, &DatapickerCurveWidget::rotationChanged); connect(ui.sbOpacity, static_cast(&QSpinBox::valueChanged), this, &DatapickerCurveWidget::opacityChanged); //Filling connect(ui.cbFillingStyle, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerCurveWidget::fillingStyleChanged); connect(ui.kcbFillingColor, &KColorButton::changed, this, &DatapickerCurveWidget::fillingColorChanged); //border connect(ui.cbBorderStyle, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerCurveWidget::borderStyleChanged); connect(ui.kcbBorderColor, &KColorButton::changed, this, &DatapickerCurveWidget::borderColorChanged); connect(ui.sbBorderWidth, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerCurveWidget::borderWidthChanged); connect(ui.chbVisible, &QCheckBox::clicked, this, &DatapickerCurveWidget::visibilityChanged); //error bar connect(ui.cbErrorBarFillingStyle, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerCurveWidget::errorBarFillingStyleChanged); connect(ui.kcbErrorBarFillingColor, &KColorButton::changed, this, &DatapickerCurveWidget::errorBarFillingColorChanged); connect(ui.sbErrorBarSize, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerCurveWidget::errorBarSizeChanged); init(); hideErrorBarWidgets(true); } DatapickerCurveWidget::~DatapickerCurveWidget() = default; void DatapickerCurveWidget::init() { m_initializing = true; GuiTools::updatePenStyles(ui.cbBorderStyle, Qt::black); QPainter pa; int iconSize = 20; QPixmap pm(iconSize, iconSize); QPen pen(Qt::SolidPattern, 0); ui.cbStyle->setIconSize(QSize(iconSize, iconSize)); QTransform trafo; trafo.scale(15, 15); //TODO: constant? for (int i = 1; i < 19; ++i) { auto style = (Symbol::Style)i; pm.fill(Qt::transparent); pa.begin(&pm); pa.setPen(pen); pa.setRenderHint(QPainter::Antialiasing); pa.translate(iconSize/2,iconSize/2); pa.drawPath(trafo.map(Symbol::pathFromStyle(style))); pa.end(); ui.cbStyle->addItem(QIcon(pm), Symbol::nameFromStyle(style)); } GuiTools::updateBrushStyles(ui.cbFillingStyle, Qt::black); GuiTools::updateBrushStyles(ui.cbErrorBarFillingStyle, Qt::black); m_initializing = false; } void DatapickerCurveWidget::setCurves(QList list) { if (list.isEmpty()) return; m_curveList = list; m_curve = list.first(); m_aspect = list.first(); if (list.size() == 1) { ui.lName->setEnabled(true); ui.leName->setEnabled(true); ui.lComment->setEnabled(true); ui.leComment->setEnabled(true); ui.leName->setText(m_curve->name()); ui.leComment->setText(m_curve->comment()); } else { ui.lName->setEnabled(false); ui.leName->setEnabled(false); ui.lComment->setEnabled(false); ui.leComment->setEnabled(false); ui.leName->setText(QString()); ui.leComment->setText(QString()); } load(); updateSymbolWidgets(); connect(m_curve, &AbstractAspect::aspectDescriptionChanged,this, &DatapickerCurveWidget::curveDescriptionChanged); connect(m_curve, &AbstractAspect::aspectRemoved,this, &DatapickerCurveWidget::updateSymbolWidgets); connect(m_curve, &AbstractAspect::aspectAdded, this, &DatapickerCurveWidget::updateSymbolWidgets); connect(m_curve, &DatapickerCurve::curveErrorTypesChanged, this, &DatapickerCurveWidget::curveErrorsChanged); connect(m_curve, &DatapickerCurve::pointStyleChanged, this, &DatapickerCurveWidget::symbolStyleChanged); connect(m_curve, &DatapickerCurve::pointSizeChanged, this, &DatapickerCurveWidget::symbolSizeChanged); connect(m_curve, &DatapickerCurve::pointRotationAngleChanged, this, &DatapickerCurveWidget::symbolRotationAngleChanged); connect(m_curve, &DatapickerCurve::pointOpacityChanged, this, &DatapickerCurveWidget::symbolOpacityChanged); connect(m_curve, &DatapickerCurve::pointBrushChanged, this, &DatapickerCurveWidget::symbolBrushChanged); connect(m_curve, &DatapickerCurve::pointPenChanged, this, &DatapickerCurveWidget::symbolPenChanged); connect(m_curve, &DatapickerCurve::pointVisibilityChanged, this, &DatapickerCurveWidget::symbolVisibleChanged); connect(m_curve, &DatapickerCurve::pointErrorBarBrushChanged, this, &DatapickerCurveWidget::symbolErrorBarBrushChanged); connect(m_curve, &DatapickerCurve::pointErrorBarSizeChanged, this, &DatapickerCurveWidget::symbolErrorBarSizeChanged); } void DatapickerCurveWidget::hideErrorBarWidgets(bool on) { ui.lErrorBar->setVisible(!on); ui.cbErrorBarFillingStyle->setVisible(!on); ui.kcbErrorBarFillingColor->setVisible(!on); ui.lErrorBarFillingColor->setVisible(!on); ui.lErrorBarFillingStyle->setVisible(!on); ui.sbErrorBarSize->setVisible(!on); ui.lErrorBarSize->setVisible(!on); } //************************************************************* //**** SLOTs for changes triggered in DatapickerCurveWidget *** //************************************************************* //"General"-tab void DatapickerCurveWidget::xErrorTypeChanged(int index) { if ( DatapickerCurve::ErrorType(index) != DatapickerCurve::NoError || m_curve->curveErrorTypes().y != DatapickerCurve::NoError ) hideErrorBarWidgets(false); else hideErrorBarWidgets(true); if (m_initializing || m_suppressTypeChange) return; DatapickerCurve::Errors errors = m_curve->curveErrorTypes(); errors.x = DatapickerCurve::ErrorType(index); for (auto* curve : m_curveList) curve->setCurveErrorTypes(errors); } void DatapickerCurveWidget::yErrorTypeChanged(int index) { if ( DatapickerCurve::ErrorType(index) != DatapickerCurve::NoError || m_curve->curveErrorTypes().x != DatapickerCurve::NoError ) hideErrorBarWidgets(false); else hideErrorBarWidgets(true); if (m_initializing || m_suppressTypeChange) return; DatapickerCurve::Errors errors = m_curve->curveErrorTypes(); errors.y = DatapickerCurve::ErrorType(index); for (auto* curve : m_curveList) curve->setCurveErrorTypes(errors); } void DatapickerCurveWidget::styleChanged(int index) { auto style = Symbol::Style(index + 1); //enable/disable the filling options in the GUI depending on the currently selected points. if (style != Symbol::Line && style != Symbol::Cross) { ui.cbFillingStyle->setEnabled(true); bool noBrush = (Qt::BrushStyle(ui.cbFillingStyle->currentIndex()) == Qt::NoBrush); ui.kcbFillingColor->setEnabled(!noBrush); } else { ui.kcbFillingColor->setEnabled(false); ui.cbFillingStyle->setEnabled(false); } bool noLine = (Qt::PenStyle(ui.cbBorderStyle->currentIndex()) == Qt::NoPen); ui.kcbBorderColor->setEnabled(!noLine); ui.sbBorderWidth->setEnabled(!noLine); if (m_initializing) return; for (auto* curve : m_curveList) curve->setPointStyle(style); } void DatapickerCurveWidget::sizeChanged(double value) { if (m_initializing) return; for (auto* curve : m_curveList) curve->setPointSize( Worksheet::convertToSceneUnits(value, Worksheet::Point) ); } void DatapickerCurveWidget::rotationChanged(int value) { if (m_initializing) return; for (auto* curve : m_curveList) curve->setPointRotationAngle(value); } void DatapickerCurveWidget::opacityChanged(int value) { if (m_initializing) return; qreal opacity = (float)value/100.; for (auto* curve : m_curveList) curve->setPointOpacity(opacity); } void DatapickerCurveWidget::errorBarSizeChanged(double value) { if (m_initializing) return; for (auto* curve : m_curveList) curve->setPointErrorBarSize( Worksheet::convertToSceneUnits(value, Worksheet::Point) ); } void DatapickerCurveWidget::fillingStyleChanged(int index) { auto brushStyle = Qt::BrushStyle(index); ui.kcbFillingColor->setEnabled(!(brushStyle == Qt::NoBrush)); if (m_initializing) return; QBrush brush; for (auto* curve : m_curveList) { brush = curve->pointBrush(); brush.setStyle(brushStyle); curve->setPointBrush(brush); } } void DatapickerCurveWidget::errorBarFillingStyleChanged(int index) { auto brushStyle = Qt::BrushStyle(index); ui.kcbErrorBarFillingColor->setEnabled(!(brushStyle == Qt::NoBrush)); if (m_initializing) return; QBrush brush; for (auto* curve : m_curveList) { brush = curve->pointBrush(); brush.setStyle(brushStyle); curve->setPointErrorBarBrush(brush); } } void DatapickerCurveWidget::fillingColorChanged(const QColor& color) { if (m_initializing) return; QBrush brush; for (auto* curve : m_curveList) { brush = curve->pointBrush(); brush.setColor(color); curve->setPointBrush(brush); } m_initializing = true; GuiTools::updateBrushStyles(ui.cbFillingStyle, color ); m_initializing = false; } void DatapickerCurveWidget::errorBarFillingColorChanged(const QColor& color) { if (m_initializing) return; QBrush brush; for (auto* curve : m_curveList) { brush = curve->pointErrorBarBrush(); brush.setColor(color); curve->setPointErrorBarBrush(brush); } m_initializing = true; GuiTools::updateBrushStyles(ui.cbErrorBarFillingStyle, color ); m_initializing = false; } void DatapickerCurveWidget::borderStyleChanged(int index) { auto penStyle = Qt::PenStyle(index); if ( penStyle == Qt::NoPen ) { ui.kcbBorderColor->setEnabled(false); ui.sbBorderWidth->setEnabled(false); } else { ui.kcbBorderColor->setEnabled(true); ui.sbBorderWidth->setEnabled(true); } if (m_initializing) return; QPen pen; for (auto* curve : m_curveList) { pen = curve->pointPen(); pen.setStyle(penStyle); curve->setPointPen(pen); } } void DatapickerCurveWidget::borderColorChanged(const QColor& color) { if (m_initializing) return; QPen pen; for (auto* curve : m_curveList) { pen = curve->pointPen(); pen.setColor(color); curve->setPointPen(pen); } m_initializing = true; GuiTools::updatePenStyles(ui.cbBorderStyle, color); m_initializing = false; } void DatapickerCurveWidget::borderWidthChanged(double value) { if (m_initializing) return; QPen pen; for (auto* curve : m_curveList) { pen = curve->pointPen(); pen.setWidthF( Worksheet::convertToSceneUnits(value, Worksheet::Point) ); curve->setPointPen(pen); } } void DatapickerCurveWidget::visibilityChanged(bool state) { if (m_initializing) return; for (auto* curve : m_curveList) curve->setPointVisibility(state); } void DatapickerCurveWidget::updateSymbolWidgets() { - auto list = m_curve->children(AbstractAspect::IncludeHidden); + auto list = m_curve->children(AbstractAspect::ChildIndexFlag::IncludeHidden); if (list.isEmpty()) { ui.cbXErrorType->setEnabled(true); ui.cbYErrorType->setEnabled(true); m_suppressTypeChange = false; } else { ui.cbXErrorType->setEnabled(false); ui.cbYErrorType->setEnabled(false); m_suppressTypeChange = true; } } //************************************************************* //******** SLOTs for changes triggered in DatapickerCurve ***** //************************************************************* void DatapickerCurveWidget::curveDescriptionChanged(const AbstractAspect* aspect) { if (m_curve != aspect) return; m_initializing = true; if (aspect->name() != ui.leName->text()) ui.leName->setText(aspect->name()); else if (aspect->comment() != ui.leComment->text()) ui.leComment->setText(aspect->comment()); m_initializing = false; } void DatapickerCurveWidget::curveErrorsChanged(DatapickerCurve::Errors errors) { m_initializing = true; ui.cbXErrorType->setCurrentIndex((int) errors.x); ui.cbYErrorType->setCurrentIndex((int) errors.y); m_initializing = false; } void DatapickerCurveWidget::symbolStyleChanged(Symbol::Style style) { m_initializing = true; ui.cbStyle->setCurrentIndex((int)style - 1); m_initializing = false; } void DatapickerCurveWidget::symbolSizeChanged(qreal size) { m_initializing = true; ui.sbSize->setValue( Worksheet::convertFromSceneUnits(size, Worksheet::Point) ); m_initializing = false; } void DatapickerCurveWidget::symbolErrorBarSizeChanged(qreal size) { m_initializing = true; ui.sbErrorBarSize->setValue( Worksheet::convertFromSceneUnits(size, Worksheet::Point) ); m_initializing = false; } void DatapickerCurveWidget::symbolRotationAngleChanged(qreal angle) { m_initializing = true; ui.sbRotation->setValue(round(angle)); m_initializing = false; } void DatapickerCurveWidget::symbolOpacityChanged(qreal opacity) { m_initializing = true; ui.sbOpacity->setValue( round(opacity*100.0) ); m_initializing = false; } void DatapickerCurveWidget::symbolBrushChanged(const QBrush& brush) { m_initializing = true; ui.cbFillingStyle->setCurrentIndex((int) brush.style()); ui.kcbFillingColor->setColor(brush.color()); GuiTools::updateBrushStyles(ui.cbFillingStyle, brush.color()); m_initializing = false; } void DatapickerCurveWidget::symbolErrorBarBrushChanged(const QBrush& brush) { m_initializing = true; ui.cbErrorBarFillingStyle->setCurrentIndex((int) brush.style()); ui.kcbErrorBarFillingColor->setColor(brush.color()); GuiTools::updateBrushStyles(ui.cbErrorBarFillingStyle, brush.color()); m_initializing = false; } void DatapickerCurveWidget::symbolPenChanged(const QPen& pen) { m_initializing = true; ui.cbBorderStyle->setCurrentIndex( (int) pen.style()); ui.kcbBorderColor->setColor( pen.color()); GuiTools::updatePenStyles(ui.cbBorderStyle, pen.color()); ui.sbBorderWidth->setValue( Worksheet::convertFromSceneUnits(pen.widthF(), Worksheet::Point)); m_initializing = false; } void DatapickerCurveWidget::symbolVisibleChanged(bool on) { m_initializing = true; ui.chbVisible->setChecked(on); m_initializing = false; } //********************************************************** //******************** SETTINGS **************************** //********************************************************** void DatapickerCurveWidget::load() { if (!m_curve) return; m_initializing = true; ui.cbXErrorType->setCurrentIndex((int) m_curve->curveErrorTypes().x); ui.cbYErrorType->setCurrentIndex((int) m_curve->curveErrorTypes().y); ui.cbStyle->setCurrentIndex( (int)m_curve->pointStyle() - 1 ); ui.sbSize->setValue( Worksheet::convertFromSceneUnits(m_curve->pointSize(), Worksheet::Point) ); ui.sbRotation->setValue( m_curve->pointRotationAngle() ); ui.sbOpacity->setValue( round(m_curve->pointOpacity()*100.0) ); ui.cbFillingStyle->setCurrentIndex( (int) m_curve->pointBrush().style() ); ui.kcbFillingColor->setColor( m_curve->pointBrush().color() ); ui.cbBorderStyle->setCurrentIndex( (int) m_curve->pointPen().style() ); ui.kcbBorderColor->setColor( m_curve->pointPen().color() ); ui.sbBorderWidth->setValue( Worksheet::convertFromSceneUnits(m_curve->pointPen().widthF(), Worksheet::Point) ); ui.chbVisible->setChecked( m_curve->pointVisibility() ); ui.cbErrorBarFillingStyle->setCurrentIndex( (int) m_curve->pointErrorBarBrush().style() ); ui.kcbErrorBarFillingColor->setColor( m_curve->pointErrorBarBrush().color() ); ui.sbErrorBarSize->setValue( Worksheet::convertFromSceneUnits(m_curve->pointErrorBarSize(), Worksheet::Point) ); m_initializing = false; } diff --git a/src/kdefrontend/widgets/DatapickerImageWidget.cpp b/src/kdefrontend/widgets/DatapickerImageWidget.cpp index fbeb27b92..ecb30b6ab 100644 --- a/src/kdefrontend/widgets/DatapickerImageWidget.cpp +++ b/src/kdefrontend/widgets/DatapickerImageWidget.cpp @@ -1,902 +1,902 @@ /*************************************************************************** File : DatapickerImageWidget.cpp Project : LabPlot Description : widget for datapicker properties -------------------------------------------------------------------- Copyright : (C) 2015-2016 by Ankit Wagadre (wagadre.ankit@gmail.com) Copyright : (C) 2015-2019 by 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 "DatapickerImageWidget.h" #include "backend/datapicker/DatapickerPoint.h" #include "commonfrontend/widgets/qxtspanslider.h" #include "kdefrontend/GuiTools.h" #include "backend/worksheet/Worksheet.h" #include "backend/worksheet/plots/cartesian/Symbol.h" #include "backend/datapicker/ImageEditor.h" #include #include #include #include #include #include #include #include #include #include #include HistogramView::HistogramView(QWidget* parent, int range) : QGraphicsView(parent), m_scene(new QGraphicsScene()), m_range(range) { setTransform(QTransform()); QRectF pageRect( 0, 0, 1000, 100 ); m_scene->setSceneRect(pageRect); setScene(m_scene); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_lowerSlider = new QGraphicsRectItem(pageRect, nullptr); m_lowerSlider->setPen(QPen(Qt::black, 0.5)); m_lowerSlider->setBrush(Qt::blue); m_lowerSlider->setOpacity(0.2); m_scene->addItem(m_lowerSlider); m_upperSlider = new QGraphicsRectItem(pageRect, nullptr); m_upperSlider->setPen(QPen(Qt::black, 0.5)); m_upperSlider->setBrush(Qt::blue); m_upperSlider->setOpacity(0.2); m_scene->addItem(m_upperSlider); } void HistogramView::setScalePixmap(const QString& file) { // scene rect is 1000*100 where upper 1000*80 is for histogram graph // and lower 1000*20 is for histogram scale QGraphicsPixmapItem* pixmap = new QGraphicsPixmapItem(QPixmap(file).scaled( 1000, 20, Qt::IgnoreAspectRatio), nullptr); pixmap->setZValue(-1); pixmap->setPos(0, 90); m_scene->addItem(pixmap); } void HistogramView::setSpan(int l, int h) { l = l*1000/m_range; h = h*1000/m_range; m_lowerSlider->setPos(QPointF(l - 1000, 0)); m_upperSlider->setPos(QPointF(h, 0)); invalidateScene(sceneRect(), QGraphicsScene::BackgroundLayer); } void HistogramView::resizeEvent(QResizeEvent *event) { fitInView(m_scene->sceneRect(), Qt::IgnoreAspectRatio); QGraphicsView::resizeEvent(event); } void HistogramView::drawBackground(QPainter* painter, const QRectF& rect) { if (!bins) return; painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); int max = 1; for (int i = 0; i <= m_range; i++) if (bins [i] > max) max = bins [i]; // convert y-scale count to log scale so small counts are still visible // scene rect is 1000*100 where upper 1000*80 is for histogram graph // and lower 1000*20 is for histogram scale QPainterPath path(QPointF(0, (log(bins[0])*100/log(max)))); for (int i = 1; i <= m_range; i++) { int x = i*1000/m_range; int y = 80; if ( bins[i] > 1 ) y = 80 - (log(bins[i])*80/log(max)); path.lineTo(QPointF(x, y)); } painter->drawPath(path); invalidateScene(rect, QGraphicsScene::BackgroundLayer); painter->restore(); } DatapickerImageWidget::DatapickerImageWidget(QWidget* parent) : BaseDock(parent), m_image(nullptr) { ui.setupUi(this); m_leName = ui.leName; m_leComment = ui.leComment; ui.leFileName->setClearButtonEnabled(true); ui.bOpen->setIcon( QIcon::fromTheme("document-open") ); ui.leFileName->setCompleter(new QCompleter(new QDirModel, this)); auto* editTabLayout = static_cast(ui.tEdit->layout()); editTabLayout->setContentsMargins(2,2,2,2); editTabLayout->setHorizontalSpacing(2); editTabLayout->setVerticalSpacing(4); ssHue = new QxtSpanSlider(Qt::Horizontal, ui.tEdit); ssHue->setToolTip(i18n("Select the range for the hue.\nEverything outside of this range will be set to white.")); ssHue->setRange(0, 360); editTabLayout->addWidget(ssHue, 3, 2); ssSaturation = new QxtSpanSlider(Qt::Horizontal, ui.tEdit); ssSaturation->setToolTip(i18n("Select the range for the saturation.\nEverything outside of this range will be set to white.")); ssSaturation->setRange(0,100); editTabLayout->addWidget(ssSaturation, 5, 2); ssValue = new QxtSpanSlider(Qt::Horizontal, ui.tEdit); ssValue->setToolTip(i18n("Select the range for the value, the degree of lightness of the color.\nEverything outside of this range will be set to white.")); ssValue->setRange(0,100); editTabLayout->addWidget(ssValue, 7, 2); ssIntensity = new QxtSpanSlider(Qt::Horizontal, ui.tEdit); ssIntensity->setToolTip(i18n("Select the range for the intensity.\nEverything outside of this range will be set to white.")); ssIntensity->setRange(0, 100); editTabLayout->addWidget(ssIntensity, 9, 2); ssForeground = new QxtSpanSlider(Qt::Horizontal, ui.tEdit); ssForeground->setToolTip(i18n("Select the range for the colors that are not part of the background color.\nEverything outside of this range will be set to white.")); ssForeground->setRange(0, 100); editTabLayout->addWidget(ssForeground, 11, 2); ui.cbGraphType->addItem(i18n("Cartesian (x, y)")); ui.cbGraphType->addItem(i18n("Polar (x, y°)")); ui.cbGraphType->addItem(i18n("Polar (x, y(rad))")); ui.cbGraphType->addItem(i18n("Logarithmic (ln(x), y)")); ui.cbGraphType->addItem(i18n("Logarithmic (x, ln(y))")); ui.cbGraphType->addItem(i18n("Ternary (x, y, z)")); ui.lTernaryScale->setHidden(true); ui.sbTernaryScale->setHidden(true); ui.lPositionZ1->setHidden(true); ui.lPositionZ2->setHidden(true); ui.lPositionZ3->setHidden(true); ui.sbPositionZ1->setHidden(true); ui.sbPositionZ2->setHidden(true); ui.sbPositionZ3->setHidden(true); ui.cbPlotImageType->addItem(i18n("No Image")); ui.cbPlotImageType->addItem(i18n("Original Image")); ui.cbPlotImageType->addItem(i18n("Processed Image")); QString valueFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, "pics/colorchooser/colorchooser_value.xpm"); QString hueFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, "pics/colorchooser/colorchooser_hue.xpm"); QString saturationFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, "pics/colorchooser/colorchooser_saturation.xpm"); gvHue = new HistogramView(ui.tEdit, ImageEditor::colorAttributeMax(DatapickerImage::Hue)); gvHue->setToolTip(i18n("Select the range for the hue.\nEverything outside of this range will be set to white.")); editTabLayout->addWidget(gvHue, 2, 2); gvHue->setScalePixmap(hueFile); gvSaturation = new HistogramView(ui.tEdit, ImageEditor::colorAttributeMax(DatapickerImage::Saturation)); gvSaturation->setToolTip(i18n("Select the range for the saturation.\nEverything outside of this range will be set to white.")); editTabLayout->addWidget(gvSaturation, 4, 2); gvSaturation->setScalePixmap(saturationFile); gvValue = new HistogramView(ui.tEdit, ImageEditor::colorAttributeMax(DatapickerImage::Value)); gvValue->setToolTip(i18n("Select the range for the value, the degree of lightness of the color.\nEverything outside of this range will be set to white.")); editTabLayout->addWidget(gvValue, 6,2); gvValue->setScalePixmap(valueFile); gvIntensity = new HistogramView(ui.tEdit, ImageEditor::colorAttributeMax(DatapickerImage::Intensity)); gvIntensity->setToolTip(i18n("Select the range for the intensity.\nEverything outside of this range will be set to white.")); editTabLayout->addWidget(gvIntensity, 8, 2); gvIntensity->setScalePixmap(valueFile); gvForeground = new HistogramView(ui.tEdit, ImageEditor::colorAttributeMax(DatapickerImage::Foreground)); gvForeground->setToolTip(i18n("Select the range for the colors that are not part of the background color.\nEverything outside of this range will be set to white.")); editTabLayout->addWidget(gvForeground, 10, 2); gvForeground->setScalePixmap(valueFile); //SLOTS //general connect(ui.leName, &QLineEdit::textChanged, this, &DatapickerImageWidget::nameChanged); connect(ui.leComment, &QLineEdit::textChanged, this, &DatapickerImageWidget::commentChanged); connect(ui.bOpen, &QPushButton::clicked, this, &DatapickerImageWidget::selectFile); connect(ui.leFileName, &QLineEdit::returnPressed, this, &DatapickerImageWidget::fileNameChanged); connect(ui.leFileName, &QLineEdit::textChanged, this, &DatapickerImageWidget::fileNameChanged); // edit image connect(ui.cbPlotImageType, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerImageWidget::plotImageTypeChanged); connect(ui.sbRotation, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::rotationChanged); connect(ssIntensity, &QxtSpanSlider::spanChanged, this, &DatapickerImageWidget::intensitySpanChanged); connect(ssIntensity, &QxtSpanSlider::spanChanged, gvIntensity, &HistogramView::setSpan); connect(ssForeground, &QxtSpanSlider::spanChanged, this, &DatapickerImageWidget::foregroundSpanChanged); connect(ssForeground, &QxtSpanSlider::spanChanged, gvForeground, &HistogramView::setSpan ); connect(ssHue, &QxtSpanSlider::spanChanged, this, &DatapickerImageWidget::hueSpanChanged); connect(ssHue, &QxtSpanSlider::spanChanged, gvHue, &HistogramView::setSpan ); connect(ssSaturation, &QxtSpanSlider::spanChanged, this, &DatapickerImageWidget::saturationSpanChanged); connect(ssSaturation, &QxtSpanSlider::spanChanged, gvSaturation, &HistogramView::setSpan ); connect(ssValue, &QxtSpanSlider::spanChanged, this, &DatapickerImageWidget::valueSpanChanged); connect(ssValue, &QxtSpanSlider::spanChanged, gvValue, &HistogramView::setSpan ); connect(ui.sbMinSegmentLength, static_cast(&QSpinBox::valueChanged), this, &DatapickerImageWidget::minSegmentLengthChanged); connect(ui.sbPointSeparation, static_cast(&QSpinBox::valueChanged), this, &DatapickerImageWidget::pointSeparationChanged); //axis point connect(ui.cbGraphType, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerImageWidget::graphTypeChanged); connect(ui.sbTernaryScale, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::ternaryScaleChanged); connect(ui.sbPositionX1, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); connect(ui.sbPositionY1, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); connect(ui.sbPositionX2, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); connect(ui.sbPositionY2, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); connect(ui.sbPositionX3, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); connect(ui.sbPositionY3, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); connect(ui.sbPositionZ1, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); connect(ui.sbPositionZ2, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); connect(ui.sbPositionZ3, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::logicalPositionChanged); //SYMBOL connect(ui.cbSymbolStyle, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerImageWidget::pointsStyleChanged); connect(ui.sbSymbolSize, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::pointsSizeChanged); connect(ui.sbSymbolRotation,static_cast(&QSpinBox::valueChanged), this, &DatapickerImageWidget::pointsRotationChanged); connect(ui.sbSymbolOpacity, static_cast(&QSpinBox::valueChanged), this, &DatapickerImageWidget::pointsOpacityChanged); //Filling connect(ui.cbSymbolFillingStyle, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerImageWidget::pointsFillingStyleChanged); connect(ui.kcbSymbolFillingColor, &KColorButton::changed, this, &DatapickerImageWidget::pointsFillingColorChanged); //border connect(ui.cbSymbolBorderStyle, static_cast(&QComboBox::currentIndexChanged), this, &DatapickerImageWidget::pointsBorderStyleChanged); connect(ui.kcbSymbolBorderColor, &KColorButton::changed, this, &DatapickerImageWidget::pointsBorderColorChanged); connect(ui.sbSymbolBorderWidth, static_cast(&QDoubleSpinBox::valueChanged), this, &DatapickerImageWidget::pointsBorderWidthChanged); connect(ui.chbSymbolVisible, &QCheckBox::clicked, this, &DatapickerImageWidget::pointsVisibilityChanged); init(); } void DatapickerImageWidget::init() { m_initializing = true; GuiTools::updatePenStyles(ui.cbSymbolBorderStyle, Qt::black); QPainter pa; int iconSize = 20; QPixmap pm(iconSize, iconSize); QPen pen(Qt::SolidPattern, 0); ui.cbSymbolStyle->setIconSize(QSize(iconSize, iconSize)); QTransform trafo; trafo.scale(15, 15); //TODO: constant? for (int i = 1; i < 19; ++i) { auto style = (Symbol::Style)i; pm.fill(Qt::transparent); pa.begin(&pm); pa.setPen( pen ); pa.setRenderHint(QPainter::Antialiasing); pa.translate(iconSize/2,iconSize/2); pa.drawPath(trafo.map(Symbol::pathFromStyle(style))); pa.end(); ui.cbSymbolStyle->addItem(QIcon(pm), Symbol::nameFromStyle(style)); } GuiTools::updateBrushStyles(ui.cbSymbolFillingStyle, Qt::black); m_initializing = false; } void DatapickerImageWidget::setImages(QList list) { m_initializing = true; m_imagesList = list; m_image = list.first(); m_aspect = list.first()->parentAspect(); if (list.size() == 1) { ui.lName->setEnabled(true); ui.leName->setEnabled(true); ui.lComment->setEnabled(true); ui.leComment->setEnabled(true); ui.leName->setText(m_image->parentAspect()->name()); ui.leComment->setText(m_image->parentAspect()->comment()); } else { ui.lName->setEnabled(false); ui.leName->setEnabled(false); ui.lComment->setEnabled(false); ui.leComment->setEnabled(false); ui.leName->setText(QString()); ui.leComment->setText(QString()); } this->load(); initConnections(); handleWidgetActions(); updateSymbolWidgets(); m_initializing = false; } void DatapickerImageWidget::initConnections() { connect(m_image->parentAspect(), &AbstractAspect::aspectDescriptionChanged, this, &DatapickerImageWidget::imageDescriptionChanged); connect(m_image, &DatapickerImage::fileNameChanged, this, &DatapickerImageWidget::imageFileNameChanged); connect(m_image, &DatapickerImage::rotationAngleChanged, this, &DatapickerImageWidget::imageRotationAngleChanged); connect(m_image, &AbstractAspect::aspectRemoved, this, &DatapickerImageWidget::updateSymbolWidgets); connect(m_image, &AbstractAspect::aspectAdded, this, &DatapickerImageWidget::updateSymbolWidgets); connect(m_image, &DatapickerImage::axisPointsChanged, this, &DatapickerImageWidget::imageAxisPointsChanged); connect(m_image, &DatapickerImage::settingsChanged, this, &DatapickerImageWidget::imageEditorSettingsChanged); connect(m_image, &DatapickerImage::minSegmentLengthChanged, this, &DatapickerImageWidget::imageMinSegmentLengthChanged); connect(m_image, &DatapickerImage::pointStyleChanged, this, &DatapickerImageWidget::symbolStyleChanged); connect(m_image, &DatapickerImage::pointSizeChanged, this, &DatapickerImageWidget::symbolSizeChanged); connect(m_image, &DatapickerImage::pointRotationAngleChanged, this, &DatapickerImageWidget::symbolRotationAngleChanged); connect(m_image, &DatapickerImage::pointOpacityChanged, this, &DatapickerImageWidget::symbolOpacityChanged); connect(m_image, &DatapickerImage::pointBrushChanged, this, &DatapickerImageWidget::symbolBrushChanged); connect(m_image, &DatapickerImage::pointPenChanged, this, &DatapickerImageWidget::symbolPenChanged); connect(m_image, &DatapickerImage::pointVisibilityChanged, this, &DatapickerImageWidget::symbolVisibleChanged); } void DatapickerImageWidget::handleWidgetActions() { QString fileName = ui.leFileName->text().trimmed(); bool b = !fileName.isEmpty(); ui.tEdit->setEnabled(b); ui.cbGraphType->setEnabled(b); ui.sbRotation->setEnabled(b); ui.sbPositionX1->setEnabled(b); ui.sbPositionX2->setEnabled(b); ui.sbPositionX3->setEnabled(b); ui.sbPositionY1->setEnabled(b); ui.sbPositionY2->setEnabled(b); ui.sbPositionY3->setEnabled(b); ui.sbMinSegmentLength->setEnabled(b); ui.sbPointSeparation->setEnabled(b); if (b) { //upload histogram to view gvIntensity->bins = m_image->intensityBins; gvForeground->bins = m_image->foregroundBins; gvHue->bins = m_image->hueBins; gvSaturation->bins = m_image->saturationBins; gvValue->bins = m_image->valueBins; } } //********************************************************** //****** SLOTs for changes triggered in DatapickerImageWidget ******** //********************************************************** //"General"-tab void DatapickerImageWidget::selectFile() { KConfigGroup conf(KSharedConfig::openConfig(), "DatapickerImageWidget"); QString dir = conf.readEntry("LastImageDir", ""); QString formats; for (const QByteArray& format : QImageReader::supportedImageFormats()) { QString f = "*." + QString(format.constData()); if (f == QLatin1String("*.svg")) continue; formats.isEmpty() ? formats += f : formats += " " + f; } QString path = QFileDialog::getOpenFileName(this, i18n("Select the image file"), dir, i18n("Images (%1)", formats)); if (path.isEmpty()) return; //cancel was clicked in the file-dialog int pos = path.lastIndexOf(QDir::separator()); if (pos != -1) { QString newDir = path.left(pos); if (newDir != dir) conf.writeEntry("LastImageDir", newDir); } ui.leFileName->setText( path ); handleWidgetActions(); for (auto* image : m_imagesList) image->setFileName(path); } void DatapickerImageWidget::fileNameChanged() { if (m_initializing) return; handleWidgetActions(); QString fileName = ui.leFileName->text(); if (!fileName.isEmpty() && !QFile::exists(fileName)) ui.leFileName->setStyleSheet("QLineEdit{background:red;}"); else ui.leFileName->setStyleSheet(QString()); for (auto* image : m_imagesList) image->setFileName(fileName); } void DatapickerImageWidget::graphTypeChanged() { if (m_initializing) return; DatapickerImage::ReferencePoints points = m_image->axisPoints(); points.type = DatapickerImage::GraphType(ui.cbGraphType->currentIndex()); if (points.type != DatapickerImage::Ternary) { ui.lTernaryScale->setHidden(true); ui.sbTernaryScale->setHidden(true); ui.lPositionZ1->setHidden(true); ui.lPositionZ2->setHidden(true); ui.lPositionZ3->setHidden(true); ui.sbPositionZ1->setHidden(true); ui.sbPositionZ2->setHidden(true); ui.sbPositionZ3->setHidden(true); } else { ui.lTernaryScale->setHidden(false); ui.sbTernaryScale->setHidden(false); ui.lPositionZ1->setHidden(false); ui.lPositionZ2->setHidden(false); ui.lPositionZ3->setHidden(false); ui.sbPositionZ1->setHidden(false); ui.sbPositionZ2->setHidden(false); ui.sbPositionZ3->setHidden(false); } for (auto* image : m_imagesList) image->setAxisPoints(points); } void DatapickerImageWidget::ternaryScaleChanged(double value) { if (m_initializing) return; DatapickerImage::ReferencePoints points = m_image->axisPoints(); points.ternaryScale = value; for (auto* image : m_imagesList) image->setAxisPoints(points); } void DatapickerImageWidget::logicalPositionChanged() { if (m_initializing) return; DatapickerImage::ReferencePoints points = m_image->axisPoints(); points.logicalPos[0].setX(ui.sbPositionX1->value()); points.logicalPos[0].setY(ui.sbPositionY1->value()); points.logicalPos[1].setX(ui.sbPositionX2->value()); points.logicalPos[1].setY(ui.sbPositionY2->value()); points.logicalPos[2].setX(ui.sbPositionX3->value()); points.logicalPos[2].setY(ui.sbPositionY3->value()); points.logicalPos[0].setZ(ui.sbPositionZ1->value()); points.logicalPos[1].setZ(ui.sbPositionZ2->value()); points.logicalPos[2].setZ(ui.sbPositionZ3->value()); const Lock lock(m_initializing); for (auto* image : m_imagesList) image->setAxisPoints(points); } void DatapickerImageWidget::pointsStyleChanged(int index) { auto style = Symbol::Style(index + 1); //enable/disable the filling options in the GUI depending on the currently selected points. if (style != Symbol::Line && style != Symbol::Cross) { ui.cbSymbolFillingStyle->setEnabled(true); bool noBrush = (Qt::BrushStyle(ui.cbSymbolFillingStyle->currentIndex()) == Qt::NoBrush); ui.kcbSymbolFillingColor->setEnabled(!noBrush); } else { ui.kcbSymbolFillingColor->setEnabled(false); ui.cbSymbolFillingStyle->setEnabled(false); } bool noLine = (Qt::PenStyle(ui.cbSymbolBorderStyle->currentIndex()) == Qt::NoPen); ui.kcbSymbolBorderColor->setEnabled(!noLine); ui.sbSymbolBorderWidth->setEnabled(!noLine); if (m_initializing) return; for (auto* image : m_imagesList) image->setPointStyle(style); } void DatapickerImageWidget::pointsSizeChanged(double value) { if (m_initializing) return; for (auto* image : m_imagesList) image->setPointSize( Worksheet::convertToSceneUnits(value, Worksheet::Point) ); } void DatapickerImageWidget::pointsRotationChanged(int value) { if (m_initializing) return; for (auto* image : m_imagesList) image->setPointRotationAngle(value); } void DatapickerImageWidget::pointsOpacityChanged(int value) { if (m_initializing) return; qreal opacity = (float)value/100.; for (auto* image : m_imagesList) image->setPointOpacity(opacity); } void DatapickerImageWidget::pointsFillingStyleChanged(int index) { auto brushStyle = Qt::BrushStyle(index); ui.kcbSymbolFillingColor->setEnabled(!(brushStyle == Qt::NoBrush)); if (m_initializing) return; QBrush brush; for (auto* image : m_imagesList) { brush = image->pointBrush(); brush.setStyle(brushStyle); image->setPointBrush(brush); } } void DatapickerImageWidget::pointsFillingColorChanged(const QColor& color) { if (m_initializing) return; QBrush brush; for (auto* image : m_imagesList) { brush = image->pointBrush(); brush.setColor(color); image->setPointBrush(brush); } m_initializing = true; GuiTools::updateBrushStyles(ui.cbSymbolFillingStyle, color ); m_initializing = false; } void DatapickerImageWidget::pointsBorderStyleChanged(int index) { auto penStyle = Qt::PenStyle(index); if ( penStyle == Qt::NoPen ) { ui.kcbSymbolBorderColor->setEnabled(false); ui.sbSymbolBorderWidth->setEnabled(false); } else { ui.kcbSymbolBorderColor->setEnabled(true); ui.sbSymbolBorderWidth->setEnabled(true); } if (m_initializing) return; QPen pen; for (auto* image : m_imagesList) { pen = image->pointPen(); pen.setStyle(penStyle); image->setPointPen(pen); } } void DatapickerImageWidget::pointsBorderColorChanged(const QColor& color) { if (m_initializing) return; QPen pen; for (auto* image : m_imagesList) { pen = image->pointPen(); pen.setColor(color); image->setPointPen(pen); } m_initializing = true; GuiTools::updatePenStyles(ui.cbSymbolBorderStyle, color); m_initializing = false; } void DatapickerImageWidget::pointsBorderWidthChanged(double value) { if (m_initializing) return; QPen pen; for (auto* image : m_imagesList) { pen = image->pointPen(); pen.setWidthF( Worksheet::convertToSceneUnits(value, Worksheet::Point) ); image->setPointPen(pen); } } void DatapickerImageWidget::pointsVisibilityChanged(bool state) { if (m_initializing) return; for (auto* image : m_imagesList) image->setPointVisibility(state); } void DatapickerImageWidget::intensitySpanChanged(int lowerLimit, int upperLimit) { if (m_initializing) return; DatapickerImage::EditorSettings settings = m_image->settings(); settings.intensityThresholdHigh = upperLimit; settings.intensityThresholdLow = lowerLimit; for (auto* image : m_imagesList) image->setSettings(settings); } void DatapickerImageWidget::foregroundSpanChanged(int lowerLimit, int upperLimit) { if (m_initializing) return; DatapickerImage::EditorSettings settings = m_image->settings(); settings.foregroundThresholdHigh = upperLimit; settings.foregroundThresholdLow = lowerLimit; for (auto* image : m_imagesList) image->setSettings(settings); } void DatapickerImageWidget::hueSpanChanged(int lowerLimit, int upperLimit) { if (m_initializing) return; DatapickerImage::EditorSettings settings = m_image->settings(); settings.hueThresholdHigh = upperLimit; settings.hueThresholdLow = lowerLimit; for (auto* image : m_imagesList) image->setSettings(settings); } void DatapickerImageWidget::saturationSpanChanged(int lowerLimit, int upperLimit) { if (m_initializing) return; DatapickerImage::EditorSettings settings = m_image->settings(); settings.saturationThresholdHigh = upperLimit; settings.saturationThresholdLow = lowerLimit; for (auto* image : m_imagesList) image->setSettings(settings); } void DatapickerImageWidget::valueSpanChanged(int lowerLimit, int upperLimit) { if (m_initializing) return; DatapickerImage::EditorSettings settings = m_image->settings(); settings.valueThresholdHigh = upperLimit; settings.valueThresholdLow = lowerLimit; for (auto* image : m_imagesList) image->setSettings(settings); } void DatapickerImageWidget::plotImageTypeChanged(int index) { if (m_initializing) return; for (auto* image : m_imagesList) image->setPlotImageType(DatapickerImage::PlotImageType(index)); } void DatapickerImageWidget::rotationChanged(double value) { if (m_initializing) return; for (auto* image : m_imagesList) image->setRotationAngle(value); } void DatapickerImageWidget::minSegmentLengthChanged(int value) { if (m_initializing) return; for (auto* image : m_imagesList) image->setminSegmentLength(value); } void DatapickerImageWidget::pointSeparationChanged(int value) { if (m_initializing) return; for (auto* image : m_imagesList) image->setPointSeparation(value); } //******************************************************************* //******** SLOTs for changes triggered in DatapickerImage *********** //******************************************************************* /*! * called when the name or comment of image's parent (datapicker) was changed. */ void DatapickerImageWidget::imageDescriptionChanged(const AbstractAspect* aspect) { if (m_image->parentAspect() != aspect) return; m_initializing = true; if (aspect->name() != ui.leName->text()) { ui.leName->setText(aspect->name()); } else if (aspect->comment() != ui.leComment->text()) { ui.leComment->setText(aspect->comment()); } m_initializing = false; } void DatapickerImageWidget::imageFileNameChanged(const QString& name) { m_initializing = true; ui.leFileName->setText(name); m_initializing = false; } void DatapickerImageWidget::imageRotationAngleChanged(float angle) { m_initializing = true; ui.sbRotation->setValue(angle); m_initializing = false; } void DatapickerImageWidget::imageAxisPointsChanged(const DatapickerImage::ReferencePoints& axisPoints) { if (m_initializing)return; const Lock lock(m_initializing); m_initializing = true; ui.cbGraphType->setCurrentIndex((int) axisPoints.type); ui.sbTernaryScale->setValue(axisPoints.ternaryScale); ui.sbPositionX1->setValue(axisPoints.logicalPos[0].x()); ui.sbPositionY1->setValue(axisPoints.logicalPos[0].y()); ui.sbPositionX2->setValue(axisPoints.logicalPos[1].x()); ui.sbPositionY2->setValue(axisPoints.logicalPos[1].y()); ui.sbPositionX3->setValue(axisPoints.logicalPos[2].x()); ui.sbPositionY3->setValue(axisPoints.logicalPos[2].y()); ui.sbPositionZ1->setValue(axisPoints.logicalPos[0].z()); ui.sbPositionZ2->setValue(axisPoints.logicalPos[1].z()); ui.sbPositionZ3->setValue(axisPoints.logicalPos[2].z()); m_initializing = false; } void DatapickerImageWidget::imageEditorSettingsChanged(const DatapickerImage::EditorSettings& settings) { m_initializing = true; ssIntensity->setSpan(settings.intensityThresholdLow, settings.intensityThresholdHigh); ssForeground->setSpan(settings.foregroundThresholdLow, settings.foregroundThresholdHigh); ssHue->setSpan(settings.hueThresholdLow, settings.hueThresholdHigh); ssSaturation->setSpan(settings.saturationThresholdLow, settings.saturationThresholdHigh); ssValue->setSpan(settings.valueThresholdLow, settings.valueThresholdHigh); gvIntensity->setSpan(settings.intensityThresholdLow, settings.intensityThresholdHigh); gvForeground->setSpan(settings.foregroundThresholdLow, settings.foregroundThresholdHigh); gvHue->setSpan(settings.hueThresholdLow, settings.hueThresholdHigh); gvSaturation->setSpan(settings.saturationThresholdLow, settings.saturationThresholdHigh); gvValue->setSpan(settings.valueThresholdLow, settings.valueThresholdHigh); m_initializing = false; } void DatapickerImageWidget::imageMinSegmentLengthChanged(const int value) { m_initializing = true; ui.sbMinSegmentLength->setValue(value); m_initializing = false; } void DatapickerImageWidget::updateSymbolWidgets() { - int pointCount = m_image->childCount(AbstractAspect::IncludeHidden); + int pointCount = m_image->childCount(AbstractAspect::ChildIndexFlag::IncludeHidden); if (pointCount) ui.tSymbol->setEnabled(true); else ui.tSymbol->setEnabled(false); } void DatapickerImageWidget::symbolStyleChanged(Symbol::Style style) { m_initializing = true; ui.cbSymbolStyle->setCurrentIndex((int)style - 1); m_initializing = false; } void DatapickerImageWidget::symbolSizeChanged(qreal size) { m_initializing = true; ui.sbSymbolSize->setValue( Worksheet::convertFromSceneUnits(size, Worksheet::Point) ); m_initializing = false; } void DatapickerImageWidget::symbolRotationAngleChanged(qreal angle) { m_initializing = true; ui.sbSymbolRotation->setValue(round(angle)); m_initializing = false; } void DatapickerImageWidget::symbolOpacityChanged(qreal opacity) { m_initializing = true; ui.sbSymbolOpacity->setValue( round(opacity*100.0) ); m_initializing = false; } void DatapickerImageWidget::symbolBrushChanged(const QBrush& brush) { m_initializing = true; ui.cbSymbolFillingStyle->setCurrentIndex((int) brush.style()); ui.kcbSymbolFillingColor->setColor(brush.color()); GuiTools::updateBrushStyles(ui.cbSymbolFillingStyle, brush.color()); m_initializing = false; } void DatapickerImageWidget::symbolPenChanged(const QPen& pen) { m_initializing = true; ui.cbSymbolBorderStyle->setCurrentIndex( (int) pen.style()); ui.kcbSymbolBorderColor->setColor( pen.color()); GuiTools::updatePenStyles(ui.cbSymbolBorderStyle, pen.color()); ui.sbSymbolBorderWidth->setValue( Worksheet::convertFromSceneUnits(pen.widthF(), Worksheet::Point)); m_initializing = false; } void DatapickerImageWidget::symbolVisibleChanged(bool on) { m_initializing = true; ui.chbSymbolVisible->setChecked(on); m_initializing = false; } //********************************************************** //******************** SETTINGS **************************** //********************************************************** void DatapickerImageWidget::load() { if (!m_image) return; m_initializing = true; ui.leFileName->setText( m_image->fileName() ); //highlight the text field for the background image red if an image is used and cannot be found if (!m_image->fileName().isEmpty() && !QFile::exists(m_image->fileName())) ui.leFileName->setStyleSheet("QLineEdit{background:red;}"); else ui.leFileName->setStyleSheet(QString()); ui.cbGraphType->setCurrentIndex((int) m_image->axisPoints().type); ui.sbTernaryScale->setValue(m_image->axisPoints().ternaryScale); ui.sbPositionX1->setValue(m_image->axisPoints().logicalPos[0].x()); ui.sbPositionY1->setValue(m_image->axisPoints().logicalPos[0].y()); ui.sbPositionX2->setValue(m_image->axisPoints().logicalPos[1].x()); ui.sbPositionY2->setValue(m_image->axisPoints().logicalPos[1].y()); ui.sbPositionX3->setValue(m_image->axisPoints().logicalPos[2].x()); ui.sbPositionY3->setValue(m_image->axisPoints().logicalPos[2].y()); ui.sbPositionZ1->setValue(m_image->axisPoints().logicalPos[0].z()); ui.sbPositionZ2->setValue(m_image->axisPoints().logicalPos[1].z()); ui.sbPositionZ3->setValue(m_image->axisPoints().logicalPos[2].z()); ui.cbPlotImageType->setCurrentIndex((int) m_image->plotImageType()); ssIntensity->setSpan(m_image->settings().intensityThresholdLow, m_image->settings().intensityThresholdHigh); ssForeground->setSpan(m_image->settings().foregroundThresholdLow, m_image->settings().foregroundThresholdHigh); ssHue->setSpan(m_image->settings().hueThresholdLow, m_image->settings().hueThresholdHigh); ssSaturation->setSpan(m_image->settings().saturationThresholdLow, m_image->settings().saturationThresholdHigh); ssValue->setSpan(m_image->settings().valueThresholdLow, m_image->settings().valueThresholdHigh); gvIntensity->setSpan(m_image->settings().intensityThresholdLow, m_image->settings().intensityThresholdHigh); gvForeground->setSpan(m_image->settings().foregroundThresholdLow, m_image->settings().foregroundThresholdHigh); gvHue->setSpan(m_image->settings().hueThresholdLow, m_image->settings().hueThresholdHigh); gvSaturation->setSpan(m_image->settings().saturationThresholdLow, m_image->settings().saturationThresholdHigh); gvValue->setSpan(m_image->settings().valueThresholdLow, m_image->settings().valueThresholdHigh); ui.sbPointSeparation->setValue(m_image->pointSeparation()); ui.sbMinSegmentLength->setValue(m_image->minSegmentLength()); ui.cbSymbolStyle->setCurrentIndex( (int)m_image->pointStyle() - 1 ); ui.sbSymbolSize->setValue( Worksheet::convertFromSceneUnits(m_image->pointSize(), Worksheet::Point) ); ui.sbSymbolRotation->setValue( m_image->pointRotationAngle() ); ui.sbSymbolOpacity->setValue( round(m_image->pointOpacity()*100.0) ); ui.cbSymbolFillingStyle->setCurrentIndex( (int) m_image->pointBrush().style() ); ui.kcbSymbolFillingColor->setColor( m_image->pointBrush().color() ); ui.cbSymbolBorderStyle->setCurrentIndex( (int) m_image->pointPen().style() ); ui.kcbSymbolBorderColor->setColor( m_image->pointPen().color() ); ui.sbSymbolBorderWidth->setValue( Worksheet::convertFromSceneUnits(m_image->pointPen().widthF(), Worksheet::Point) ); ui.chbSymbolVisible->setChecked( m_image->pointVisibility() ); m_initializing = false; }