diff --git a/src/backend/core/AbstractAspect.cpp b/src/backend/core/AbstractAspect.cpp index 30b17bbd7..a24c2eac6 100644 --- a/src/backend/core/AbstractAspect.cpp +++ b/src/backend/core/AbstractAspect.cpp @@ -1,835 +1,859 @@ /*************************************************************************** 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/spreadsheet/Spreadsheet.h" #include "backend/datapicker/DatapickerCurve.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) : d(new AbstractAspectPrivate(this, name)) { } AbstractAspect::~AbstractAspect() { delete d; } QString AbstractAspect::name() const { return d->m_name; } void AbstractAspect::setName(const QString &value) { if (value.isEmpty()) { setName(QLatin1String("1")); return; } if (value == d->m_name) return; QString new_name; if (d->m_parent) { new_name = d->m_parent->uniqueNameFor(value); 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)); } 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; exec(new PropertyChangeCommand(i18n("%1: change hidden status", d->m_name), &d->m_hidden, value), "aspectHiddenAboutToChange", "aspectHiddenChanged", Q_ARG(const AbstractAspect*,this)); } 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(); - menu->addAction(QIcon::fromTheme(QLatin1String("edit-rename")), i18n("Rename"), this, SIGNAL(renameRequested())); - //don't allow to delete data spreadsheets in the datapicker curves - if ( !(dynamic_cast(this) && dynamic_cast(this->parentAspect())) ) + //don't allow to rename: + //1. Mqtt subscriptions + //2. Mqtt topics + //3. Columns in Mqtt topics +#ifdef HAVE_MQTT + if( !dynamic_cast(this) + && !dynamic_cast(this) + && !(dynamic_cast(this) && dynamic_cast(this->parentAspect())) ) +#endif + menu->addAction(QIcon::fromTheme(QLatin1String("edit-rename")), i18n("Rename"), this, SIGNAL(renameRequested())); + + //don't allow to delete: + //1. data spreadsheets in the datapicker curves + //2. Mqtt subscriptions + //3. Mqtt topics + //4. Columns in Mqtt topics + if ( !(dynamic_cast(this) && dynamic_cast(this->parentAspect())) +#ifdef HAVE_MQTT + && !dynamic_cast(this) + && !dynamic_cast(this) + && !(dynamic_cast(this) && dynamic_cast(this->parentAspect())) +#endif + ) menu->addAction(QIcon::fromTheme(QLatin1String("edit-delete")), i18n("Delete"), this, SLOT(remove())); return menu; } /** * \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("Folder")) return static_cast(this); AbstractAspect* parent_aspect = parentAspect(); while (parent_aspect && !parent_aspect->inherits("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() : QLatin1String(""); } /** * \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); 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); //TODO check/test this! emit aspectAboutToBeRemoved(this); 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); emit aspectAdded(this); endMacro(); } QVector AbstractAspect::children(const char* className, ChildIndexFlags flags) { QVector result; for (auto* child : children()) { if (flags & IncludeHidden || !child->hidden()) { if ( child->inherits(className) || !(flags & Compress)) { result << child; if (flags & Recursive) { result << child->children(className, 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; } //////////////////////////////////////////////////////////////////////////////////////////////////// //! \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-critial 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: e.g. Folder or XYFitCurve with //the child column for calculated residuals if (aspect->parentAspect() != nullptr && !aspect->parentAspect()->inherits("Folder") && !aspect->parentAspect()->inherits("XYFitCurve") && !aspect->parentAspect()->inherits("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: e.g. Folder or XYFitCurve with //the child column for calculated residuals if (aspect->parentAspect() != nullptr && !aspect->parentAspect()->inherits("Folder") && !aspect->parentAspect()->inherits("XYFitCurve") && !aspect->parentAspect()->inherits("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/AbstractPart.cpp b/src/backend/core/AbstractPart.cpp index c17f3c508..32bcf5004 100644 --- a/src/backend/core/AbstractPart.cpp +++ b/src/backend/core/AbstractPart.cpp @@ -1,146 +1,153 @@ /*************************************************************************** File : AbstractPart.cpp Project : LabPlot Description : Base class of Aspects with MDI windows as views. -------------------------------------------------------------------- Copyright : (C) 2008 Knut Franke (knut.franke@gmx.de) Copyright : (C) 2012 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/AbstractPart.h" #include "commonfrontend/core/PartMdiView.h" #include "backend/core/Workbook.h" #include "backend/datapicker/Datapicker.h" #include "backend/matrix/Matrix.h" #include "backend/spreadsheet/Spreadsheet.h" #include "backend/datapicker/DatapickerCurve.h" +#ifdef HAVE_MQTT +#include "backend/datasources/MQTTTopic.h" +#endif #include #include #include /** * \class AbstractPart * \brief Base class of Aspects with MDI windows as views (AspectParts). */ AbstractPart::AbstractPart(const QString& name) : AbstractAspect(name) { } AbstractPart::~AbstractPart() { if (m_mdiWindow) delete m_mdiWindow; } /** * \fn QWidget *AbstractPart::view() const * \brief Construct a primary view on me. * * The caller receives ownership of the view. * * This method may be called multiple times during the life time of a Part, or it might not get * called at all. Parts must not depend on the existence of a view for their operation. */ /** * \brief Wrap the view() into a PartMdiView. * * A new view is only created the first time this method is called; * after that, a pointer to the pre-existing view is returned. */ PartMdiView* AbstractPart::mdiSubWindow() const { if (!m_mdiWindow) m_mdiWindow = new PartMdiView(const_cast(this)); return m_mdiWindow; } bool AbstractPart::hasMdiSubWindow() const { return m_mdiWindow; } /*! * this function is called when PartMdiView, the mdi-subwindow-wrapper of the actual view, * is closed (=deleted) in MainWindow. Makes sure that the view also gets deleted. */ void AbstractPart::deleteView() const { //if the parent is a Workbook or Datapicker, the actual view was already deleted when QTabWidget was deleted. //here just set the pointer to 0. if (dynamic_cast(parentAspect()) || dynamic_cast(parentAspect()) || dynamic_cast(parentAspect()->parentAspect())) { m_partView = nullptr; return; } if (m_partView) { delete m_partView; m_partView = nullptr; m_mdiWindow = nullptr; } } /** * \brief Return AbstractAspect::createContextMenu() plus operations on the primary view. */ QMenu* AbstractPart::createContextMenu() { QMenu* menu = AbstractAspect::createContextMenu(); Q_ASSERT(menu); menu->addSeparator(); if (m_mdiWindow) { - if (dynamic_cast(this) || dynamic_cast(this)) { + if ( (dynamic_cast(this) || dynamic_cast(this)) +#ifdef HAVE_MQTT + && !dynamic_cast(this) +#endif + ) { QMenu* subMenu = new QMenu(i18n("Import Data"), menu); subMenu->addAction(QIcon::fromTheme("document-import"), i18n("From File"), this, SIGNAL(importFromFileRequested())); subMenu->addAction(QIcon::fromTheme("document-import"), i18n("From SQL Database"), this, SIGNAL(importFromSQLDatabaseRequested())); menu->addMenu(subMenu); menu->addSeparator(); } menu->addAction(QIcon::fromTheme("document-export-database"), i18n("Export"), this, SIGNAL(exportRequested())); menu->addAction(QIcon::fromTheme("document-print"), i18n("Print"), this, SIGNAL(printRequested())); menu->addAction(QIcon::fromTheme("document-print-preview"), i18n("Print Preview"), this, SIGNAL(printPreviewRequested())); menu->addSeparator(); const QStyle *widget_style = m_mdiWindow->style(); if (m_mdiWindow->windowState() & (Qt::WindowMinimized | Qt::WindowMaximized)) { QAction* action = menu->addAction(i18n("&Restore"), m_mdiWindow, SLOT(showNormal())); action->setIcon(widget_style->standardIcon(QStyle::SP_TitleBarNormalButton)); } if (!(m_mdiWindow->windowState() & Qt::WindowMinimized)) { QAction* action = menu->addAction(i18n("Mi&nimize"), m_mdiWindow, SLOT(showMinimized())); action->setIcon(widget_style->standardIcon(QStyle::SP_TitleBarMinButton)); } if (!(m_mdiWindow->windowState() & Qt::WindowMaximized)) { QAction* action = menu->addAction(i18n("Ma&ximize"), m_mdiWindow, SLOT(showMaximized())); action->setIcon(widget_style->standardIcon(QStyle::SP_TitleBarMaxButton)); } } else { //data spreadsheets in the datapicker curves cannot be hidden/minimized, don't show this menu entry if ( !(dynamic_cast(this) && dynamic_cast(this->parentAspect())) ) menu->addAction(i18n("Show"), this, SIGNAL(showRequested())); } return menu; } diff --git a/src/backend/core/Folder.cpp b/src/backend/core/Folder.cpp index 2979c63a2..5216008cf 100644 --- a/src/backend/core/Folder.cpp +++ b/src/backend/core/Folder.cpp @@ -1,269 +1,274 @@ /*************************************************************************** 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 /** * \class Folder * \brief Folder in a project */ Folder::Folder(const QString &name) : AbstractAspect(name) {} 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()) + if (project() +#ifdef HAVE_MQTT + && !dynamic_cast(this) +#endif + ) return project()->createFolderContextMenu(this); return nullptr; } /** * \brief Save as XML */ void Folder::save(QXmlStreamWriter* writer) const { writer->writeStartElement(QLatin1String("folder")); writeBasicAttributes(writer); writeCommentElement(writer); for (auto* child : children(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(""); 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(""); if (!workbook->load(reader, preview)) { delete workbook; return false; } addChildFast(workbook); } else if (element_name == QLatin1String("spreadsheet")) { Spreadsheet* spreadsheet = new Spreadsheet("", true); if (!spreadsheet->load(reader, preview)) { delete spreadsheet; return false; } addChildFast(spreadsheet); } else if (element_name == QLatin1String("matrix")) { Matrix* matrix = new Matrix("", true); if (!matrix->load(reader, preview)) { delete matrix; return false; } addChildFast(matrix); } else if (element_name == QLatin1String("worksheet")) { Worksheet* worksheet = new Worksheet(""); 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")) { qDebug()<<"Load MQTTClient"; MQTTClient* client = new MQTTClient(""); if (!client->load(reader, preview)) { delete client; return false; } addChildFast(client); #endif } else if (element_name == QLatin1String("LiveDataSource")) { LiveDataSource* liveDataSource = new LiveDataSource("", true); if (!liveDataSource->load(reader, preview)) { delete liveDataSource; return false; } addChildFast(liveDataSource); } else if (element_name == QLatin1String("datapicker")) { Datapicker* datapicker = new Datapicker("", true); if (!datapicker->load(reader, preview)) { delete datapicker; return false; } addChildFast(datapicker); } else if (element_name == QLatin1String("note")) { Note* note = new Note(""); 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/commonfrontend/ProjectExplorer.cpp b/src/commonfrontend/ProjectExplorer.cpp index 5f50578fe..cf835f335 100644 --- a/src/commonfrontend/ProjectExplorer.cpp +++ b/src/commonfrontend/ProjectExplorer.cpp @@ -1,851 +1,854 @@ /*************************************************************************** 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); QLabel* lFilter = new QLabel(i18n("Search/Filter:")); layoutFilter->addWidget(lFilter); m_leFilter = new QLineEdit(m_frameFilter); m_leFilter->setClearButtonEnabled(true); m_leFilter->setPlaceholderText(i18n("Search/Filter text")); layoutFilter->addWidget(m_leFilter); bFilterOptions = new QPushButton(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("view-list-tree")), i18n("Expand All"), this); connect(expandTreeAction, &QAction::triggered, m_treeView, &QTreeView::expandAll); expandSelectedTreeAction = new QAction(QIcon::fromTheme(QLatin1String("view-list-tree")), i18n("Expand Selected"), this); connect(expandSelectedTreeAction, &QAction::triggered, this, &ProjectExplorer::expandSelected); collapseTreeAction = new QAction(i18n("Collapse All"), this); connect(collapseTreeAction, &QAction::triggered, m_treeView, &QTreeView::collapseAll); collapseSelectedTreeAction = new QAction(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(); 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")); } } - menu->exec(event->globalPos()); + + 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) { showColumnsSignalMapper = new QSignalMapper(this); 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, showColumnsSignalMapper, static_cast(&QSignalMapper::map)); showColumnsSignalMapper->setMapping(showColumnAction, i); } connect(showColumnsSignalMapper, static_cast(&QSignalMapper::mapped), this, &ProjectExplorer::toggleColumn); } 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::resizeHeader); 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(); } /*! 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) { 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) { // only accept drop events if we have a plot under the cursor where we can drop columns onto auto* dragMoveEvent = static_cast(event); QModelIndex index = m_treeView->indexAt(dragMoveEvent->pos()); if (!index.isValid()) return false; auto* aspect = static_cast(index.internalPointer()); const bool isPlot = (dynamic_cast(aspect) != nullptr); event->setAccepted(isPlot); } 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()); auto* plot = dynamic_cast(aspect); if (plot != nullptr) plot->processDropEvent(dropEvent); } } return QObject::eventFilter(obj, event); } //############################################################################## //################################# SLOTS #################################### //############################################################################## /*! 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()) 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("Spreadsheet") && aspect->parentAspect()->inherits("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("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) m_treeView->setCurrentIndex(tree_model->modelIndexOfAspect(path)); } void ProjectExplorer::currentChanged(const QModelIndex & current, const QModelIndex & previous) { 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.child(i, 0); 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()->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)); for (int i = 0; i < items.size()/4; ++i) { const QModelIndex& index = items.at(i*4); auto* aspect = static_cast(index.internalPointer()); 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; QVector aspects = const_cast(m_project)->children("AbstractAspect", AbstractAspect::Recursive); for (const auto* aspect : aspects) { 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 QVector aspects = const_cast(m_project)->children("AbstractAspect", AbstractAspect::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") { 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()) 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) { 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); //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); }