diff --git a/src/backend/core/AbstractAspect.cpp b/src/backend/core/AbstractAspect.cpp index da3775ffa..cad7734e9 100644 --- a/src/backend/core/AbstractAspect.cpp +++ b/src/backend/core/AbstractAspect.cpp @@ -1,893 +1,893 @@ /*************************************************************************** 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); 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)) { result << child; if (flags & Recursive) { result << child->children(type, flags); } } } } return result; } -const QVector AbstractAspect::children() const { +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: e.g. Folder or XYFitCurve with //the child column for calculated residuals if (aspect->parentAspect() && !aspect->parentAspect()->inherits(AspectType::Folder) && !aspect->parentAspect()->inherits(AspectType::XYFitCurve) && !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: e.g. Folder or XYFitCurve with //the child column for calculated residuals if (aspect->parentAspect() && !aspect->parentAspect()->inherits(AspectType::Folder) && !aspect->parentAspect()->inherits(AspectType::XYFitCurve) && !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 c8cf0ebc5..5e1695740 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 { 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()) { T* i = dynamic_cast(child); if (i) result << i; if (child && flags & Recursive) result << child->template children(flags); } } return result; } 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++) 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())) 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())) 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; + 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/Project.cpp b/src/backend/core/Project.cpp index 9920dcec8..206acabb0 100644 --- a/src/backend/core/Project.cpp +++ b/src/backend/core/Project.cpp @@ -1,679 +1,679 @@ /*************************************************************************** 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}; 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)) 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. - QVector curves = children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector& curves = children(AbstractAspect::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); } - QVector columns = children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector& columns = children(AbstractAspect::ChildIndexFlag::Recursive); for (auto* tempColumn : columns) { - const QStringList formulaVariableColumnsPath = tempColumn->formulaVariableColumnPaths(); + const QStringList& formulaVariableColumnsPath = tempColumn->formulaVariableColumnPaths(); for (int i = 0; i < formulaVariableColumnsPath.count(); i++) { - if (formulaVariableColumnsPath[i] == columnPath) + 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) { - QVector _children = aspect->children(AspectType::Column, ChildIndexFlag::Recursive); + 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) { - QVector curves = children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector& curves = children(AbstractAspect::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); } - QVector columns = children(AbstractAspect::ChildIndexFlag::Recursive); + const QVector& columns = children(AbstractAspect::ChildIndexFlag::Recursive); for (auto* tempColumn : columns) { - const QStringList formulaVariableColumnPaths = tempColumn->formulaVariableColumnPaths(); + const QStringList& formulaVariableColumnPaths = tempColumn->formulaVariableColumnPaths(); for (int i = 0; i < formulaVariableColumnPaths.count(); i++) { - if (formulaVariableColumnPaths[i] == column->path()) + 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)) { 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 QVector sources = children(AbstractAspect::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. QVector columns = children(AbstractAspect::Recursive); //xy-curves // cannot be removed by the column observer, because it does not react // on curve changes QVector curves = children(AbstractAspect::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 QVector axes = children(AbstractAspect::Recursive); for (auto* axis : axes) { if (!axis) continue; RESTORE_COLUMN_POINTER(axis, majorTicksColumn, MajorTicksColumn); RESTORE_COLUMN_POINTER(axis, minorTicksColumn, MinorTicksColumn); } //histograms QVector hists = children(AbstractAspect::Recursive); for (auto* hist : hists) { if (!hist) continue; RESTORE_COLUMN_POINTER(hist, dataColumn, DataColumn); } //data picker curves QVector dataPickerCurves = children(AbstractAspect::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)) { 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 = dynamic_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/commonfrontend/spreadsheet/SpreadsheetView.cpp b/src/commonfrontend/spreadsheet/SpreadsheetView.cpp index a4d52f73d..28596e931 100644 --- a/src/commonfrontend/spreadsheet/SpreadsheetView.cpp +++ b/src/commonfrontend/spreadsheet/SpreadsheetView.cpp @@ -1,3083 +1,3084 @@ /*************************************************************************** File : SpreadsheetView.cpp Project : LabPlot Description : View class for Spreadsheet -------------------------------------------------------------------- Copyright : (C) 2011-2019 by Alexander Semke (alexander.semke@web.de) Copyright : (C) 2016 by Fabian Kristof (fkristofszabolcs@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 "SpreadsheetView.h" #include "backend/worksheet/plots/cartesian/CartesianPlot.h" #include "backend/spreadsheet/SpreadsheetModel.h" #include "backend/spreadsheet/Spreadsheet.h" #include "commonfrontend/spreadsheet/SpreadsheetItemDelegate.h" #include "commonfrontend/spreadsheet/SpreadsheetHeaderView.h" #include "backend/datasources/filters/FITSFilter.h" #include "backend/lib/macros.h" #include "backend/lib/trace.h" #include "backend/core/column/Column.h" #include "backend/core/column/ColumnPrivate.h" #include "backend/core/datatypes/SimpleCopyThroughFilter.h" #include "backend/core/datatypes/Double2StringFilter.h" #include "backend/core/datatypes/String2DoubleFilter.h" #include "backend/core/datatypes/DateTime2StringFilter.h" #include "backend/core/datatypes/String2DateTimeFilter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kdefrontend/spreadsheet/ExportSpreadsheetDialog.h" #include "kdefrontend/spreadsheet/PlotDataDialog.h" #include "kdefrontend/spreadsheet/AddSubtractValueDialog.h" #include "kdefrontend/spreadsheet/DropValuesDialog.h" #include "kdefrontend/spreadsheet/SortDialog.h" #include "kdefrontend/spreadsheet/RandomValuesDialog.h" #include "kdefrontend/spreadsheet/EquidistantValuesDialog.h" #include "kdefrontend/spreadsheet/FunctionValuesDialog.h" #include "kdefrontend/spreadsheet/StatisticsDialog.h" #include //for std::reverse #ifdef Q_OS_MAC #include "3rdparty/kdmactouchbar/src/kdmactouchbar.h" #endif /*! \class SpreadsheetView \brief View class for Spreadsheet \ingroup commonfrontend */ SpreadsheetView::SpreadsheetView(Spreadsheet* spreadsheet, bool readOnly) : QWidget(), m_tableView(new QTableView(this)), m_spreadsheet(spreadsheet), m_model(new SpreadsheetModel(spreadsheet)), m_readOnly(readOnly) { auto* layout = new QHBoxLayout(this); layout->setContentsMargins(0,0,0,0); layout->addWidget(m_tableView); if (m_readOnly) m_tableView->setEditTriggers(QTableView::NoEditTriggers); init(); //resize the view to show alls columns and the first 10 rows. //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_spreadsheet->isLoading()) { int w = m_tableView->verticalHeader()->width(); int h = m_horizontalHeader->height(); for (int i = 0; i < m_horizontalHeader->count(); ++i) w += m_horizontalHeader->sectionSize(i); if (m_tableView->verticalHeader()->count() <= 10) h += m_tableView->verticalHeader()->sectionSize(0)*m_tableView->verticalHeader()->count(); else h += m_tableView->verticalHeader()->sectionSize(0)*11; resize(w+50, h); } KConfigGroup group = KSharedConfig::openConfig()->group(QLatin1String("Spreadsheet")); showComments(group.readEntry(QLatin1String("ShowComments"), false)); } SpreadsheetView::~SpreadsheetView() { delete m_model; } void SpreadsheetView::init() { initActions(); initMenus(); m_tableView->setModel(m_model); m_tableView->setItemDelegate(new SpreadsheetItemDelegate(this)); m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection); //horizontal header m_horizontalHeader = new SpreadsheetHeaderView(this); m_horizontalHeader->setSectionsClickable(true); m_horizontalHeader->setHighlightSections(true); m_tableView->setHorizontalHeader(m_horizontalHeader); m_horizontalHeader->setSectionsMovable(true); m_horizontalHeader->installEventFilter(this); resizeHeader(); connect(m_horizontalHeader, &SpreadsheetHeaderView::sectionMoved, this, &SpreadsheetView::handleHorizontalSectionMoved); connect(m_horizontalHeader, &SpreadsheetHeaderView::sectionDoubleClicked, this, &SpreadsheetView::handleHorizontalHeaderDoubleClicked); connect(m_horizontalHeader, &SpreadsheetHeaderView::sectionResized, this, &SpreadsheetView::handleHorizontalSectionResized); connect(m_horizontalHeader, &SpreadsheetHeaderView::sectionClicked, this, &SpreadsheetView::columnClicked); // vertical header QHeaderView* v_header = m_tableView->verticalHeader(); v_header->setSectionResizeMode(QHeaderView::Fixed); v_header->setSectionsMovable(false); v_header->installEventFilter(this); setFocusPolicy(Qt::StrongFocus); setFocus(); installEventFilter(this); connectActions(); showComments(false); connect(m_model, &SpreadsheetModel::headerDataChanged, this, &SpreadsheetView::updateHeaderGeometry); connect(m_model, &SpreadsheetModel::headerDataChanged, this, &SpreadsheetView::handleHeaderDataChanged); connect(m_spreadsheet, &Spreadsheet::aspectAdded, this, &SpreadsheetView::handleAspectAdded); connect(m_spreadsheet, &Spreadsheet::aspectAboutToBeRemoved,this, &SpreadsheetView::handleAspectAboutToBeRemoved); connect(m_spreadsheet, &Spreadsheet::requestProjectContextMenu, this, &SpreadsheetView::createContextMenu); for (auto* column : m_spreadsheet->children()) connect(column, &Column::requestProjectContextMenu, this, &SpreadsheetView::createColumnContextMenu); //selection relevant connections QItemSelectionModel* sel_model = m_tableView->selectionModel(); connect(sel_model, &QItemSelectionModel::currentColumnChanged, this, &SpreadsheetView::currentColumnChanged); connect(sel_model, &QItemSelectionModel::selectionChanged, this, &SpreadsheetView::selectionChanged); connect(sel_model, &QItemSelectionModel::selectionChanged, this, &SpreadsheetView::selectionChanged); connect(m_spreadsheet, &Spreadsheet::columnSelected, this, &SpreadsheetView::selectColumn); connect(m_spreadsheet, &Spreadsheet::columnDeselected, this, &SpreadsheetView::deselectColumn); } /*! set the column sizes to the saved values or resize to content if no size was saved yet */ void SpreadsheetView::resizeHeader() { DEBUG("SpreadsheetView::resizeHeader()"); - for (int i = 0; i < m_spreadsheet->children().size(); ++i) { - Column* col = m_spreadsheet->child(i); + const auto columns = m_spreadsheet->children(); + for (int i = 0; i < columns.size(); ++i) { + const Column* col = columns.at(i); if (col->width() == 0) m_tableView->resizeColumnToContents(i); else m_tableView->setColumnWidth(i, col->width()); } } void SpreadsheetView::initActions() { // selection related actions action_cut_selection = new QAction(QIcon::fromTheme("edit-cut"), i18n("Cu&t"), this); action_copy_selection = new QAction(QIcon::fromTheme("edit-copy"), i18n("&Copy"), this); action_paste_into_selection = new QAction(QIcon::fromTheme("edit-paste"), i18n("Past&e"), this); action_mask_selection = new QAction(QIcon::fromTheme("edit-node"), i18n("&Mask Selection"), this); action_unmask_selection = new QAction(QIcon::fromTheme("format-remove-node"), i18n("&Unmask Selection"), this); action_clear_selection = new QAction(QIcon::fromTheme("edit-clear"), i18n("Clea&r Selection"), this); action_select_all = new QAction(QIcon::fromTheme("edit-select-all"), i18n("Select All"), this); // action_set_formula = new QAction(QIcon::fromTheme(QString()), i18n("Assign &Formula"), this); // action_recalculate = new QAction(QIcon::fromTheme(QString()), i18n("Recalculate"), this); action_fill_sel_row_numbers = new QAction(QIcon::fromTheme(QString()), i18n("Row Numbers"), this); action_fill_row_numbers = new QAction(QIcon::fromTheme(QString()), i18n("Row Numbers"), this); action_fill_random = new QAction(QIcon::fromTheme(QString()), i18n("Uniform Random Values"), this); action_fill_random_nonuniform = new QAction(QIcon::fromTheme(QString()), i18n("Random Values"), this); action_fill_equidistant = new QAction(QIcon::fromTheme(QString()), i18n("Equidistant Values"), this); action_fill_function = new QAction(QIcon::fromTheme(QString()), i18n("Function Values"), this); action_fill_const = new QAction(QIcon::fromTheme(QString()), i18n("Const Values"), this); //spreadsheet related actions action_toggle_comments = new QAction(QIcon::fromTheme("document-properties"), i18n("Show Comments"), this); action_clear_spreadsheet = new QAction(QIcon::fromTheme("edit-clear"), i18n("Clear Spreadsheet"), this); action_clear_masks = new QAction(QIcon::fromTheme("format-remove-node"), i18n("Clear Masks"), this); action_sort_spreadsheet = new QAction(QIcon::fromTheme("view-sort-ascending"), i18n("&Sort Spreadsheet"), this); action_go_to_cell = new QAction(QIcon::fromTheme("go-jump"), i18n("&Go to Cell"), this); action_statistics_all_columns = new QAction(QIcon::fromTheme("view-statistics"), i18n("Statisti&cs"), this ); // column related actions action_insert_column_left = new QAction(QIcon::fromTheme("edit-table-insert-column-left"), i18n("Insert Column Left"), this); action_insert_column_right = new QAction(QIcon::fromTheme("edit-table-insert-column-right"), i18n("Insert Column Right"), this); action_insert_columns_left = new QAction(QIcon::fromTheme("edit-table-insert-column-left"), i18n("Insert Multiple Columns Left"), this); action_insert_columns_right = new QAction(QIcon::fromTheme("edit-table-insert-column-right"), i18n("Insert Multiple Columns Right"), this); action_remove_columns = new QAction(QIcon::fromTheme("edit-table-delete-column"), i18n("Remove Selected Columns"), this); action_clear_columns = new QAction(QIcon::fromTheme("edit-clear"), i18n("Clear Selected Columns"), this); action_set_as_none = new QAction(i18n("None"), this); action_set_as_none->setData(AbstractColumn::NoDesignation); action_set_as_x = new QAction("X", this); action_set_as_x->setData(AbstractColumn::X); action_set_as_y = new QAction("Y", this); action_set_as_y->setData(AbstractColumn::Y); action_set_as_z = new QAction("Z", this); action_set_as_z->setData(AbstractColumn::Z); action_set_as_xerr = new QAction(i18n("X-error"), this); action_set_as_xerr->setData(AbstractColumn::XError); action_set_as_xerr_minus = new QAction(i18n("X-error minus"), this); action_set_as_xerr_minus->setData(AbstractColumn::XErrorMinus); action_set_as_xerr_plus = new QAction(i18n("X-error plus"), this); action_set_as_xerr_plus->setData(AbstractColumn::XErrorPlus); action_set_as_yerr = new QAction(i18n("Y-error"), this); action_set_as_yerr->setData(AbstractColumn::YError); action_set_as_yerr_minus = new QAction(i18n("Y-error minus"), this); action_set_as_yerr_minus->setData(AbstractColumn::YErrorMinus); action_set_as_yerr_plus = new QAction(i18n("Y-error plus"), this); action_set_as_yerr_plus->setData(AbstractColumn::YErrorPlus); //data manipulation action_add_value = new QAction(i18n("Add Value"), this); action_add_value->setData(AddSubtractValueDialog::Add); action_subtract_value = new QAction(i18n("Subtract Value"), this); action_subtract_value->setData(AddSubtractValueDialog::Subtract); action_multiply_value = new QAction(i18n("Multiply by Value"), this); action_multiply_value->setData(AddSubtractValueDialog::Multiply); action_divide_value = new QAction(i18n("Divide by Value"), this); action_divide_value->setData(AddSubtractValueDialog::Divide); action_drop_values = new QAction(QIcon::fromTheme(QString()), i18n("Drop Values"), this); action_mask_values = new QAction(QIcon::fromTheme(QString()), i18n("Mask Values"), this); action_reverse_columns = new QAction(QIcon::fromTheme(QString()), i18n("Reverse"), this); // action_join_columns = new QAction(QIcon::fromTheme(QString()), i18n("Join"), this); action_normalize_columns = new QAction(QIcon::fromTheme(QString()), i18n("&Normalize"), this); action_normalize_selection = new QAction(QIcon::fromTheme(QString()), i18n("&Normalize Selection"), this); //sort and statistics action_sort_columns = new QAction(QIcon::fromTheme(QString()), i18n("&Selected Columns"), this); action_sort_asc_column = new QAction(QIcon::fromTheme("view-sort-ascending"), i18n("&Ascending"), this); action_sort_desc_column = new QAction(QIcon::fromTheme("view-sort-descending"), i18n("&Descending"), this); action_statistics_columns = new QAction(QIcon::fromTheme("view-statistics"), i18n("Column Statisti&cs"), this); // row related actions action_insert_row_above = new QAction(QIcon::fromTheme("edit-table-insert-row-above") ,i18n("Insert Row Above"), this); action_insert_row_below = new QAction(QIcon::fromTheme("edit-table-insert-row-below"), i18n("Insert Row Below"), this); action_insert_rows_above = new QAction(QIcon::fromTheme("edit-table-insert-row-above") ,i18n("Insert Multiple Rows Above"), this); action_insert_rows_below = new QAction(QIcon::fromTheme("edit-table-insert-row-below"), i18n("Insert Multiple Rows Below"), this); action_remove_rows = new QAction(QIcon::fromTheme("edit-table-delete-row"), i18n("Remo&ve Selected Rows"), this); action_clear_rows = new QAction(QIcon::fromTheme("edit-clear"), i18n("Clea&r Selected Rows"), this); action_statistics_rows = new QAction(QIcon::fromTheme("view-statistics"), i18n("Row Statisti&cs"), this); //plot data action action_plot_data_xycurve = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("xy-Curve"), this); action_plot_data_xycurve->setData(PlotDataDialog::PlotXYCurve); action_plot_data_histogram = new QAction(QIcon::fromTheme("view-object-histogram-linear"), i18n("Histogram"), this); action_plot_data_histogram->setData(PlotDataDialog::PlotHistogram); //Analyze and plot menu actions addDataReductionAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Reduce Data"), this); // addDataReductionAction = new QAction(QIcon::fromTheme("labplot-xy-data-reduction-curve"), i18n("Reduce Data"), this); addDataReductionAction->setData(PlotDataDialog::DataReduction); addDifferentiationAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Differentiate"), this); // addDifferentiationAction = new QAction(QIcon::fromTheme("labplot-xy-differentiation-curve"), i18n("Differentiate"), this); addDifferentiationAction->setData(PlotDataDialog::Differentiation); addIntegrationAction = new QAction(QIcon::fromTheme("labplot-xy-curve"), i18n("Integrate"), this); // addIntegrationAction = new QAction(QIcon::fromTheme("labplot-xy-integration-curve"), i18n("Integrate"), this); addIntegrationAction->setData(PlotDataDialog::Integration); addInterpolationAction = new QAction(QIcon::fromTheme("labplot-xy-interpolation-curve"), i18n("Interpolate"), this); addInterpolationAction->setData(PlotDataDialog::Interpolation); addSmoothAction = new QAction(QIcon::fromTheme("labplot-xy-smoothing-curve"), i18n("Smooth"), this); addSmoothAction->setData(PlotDataDialog::Smoothing); QAction* fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Linear"), this); fitAction->setData(PlotDataDialog::FitLinear); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Power"), this); fitAction->setData(PlotDataDialog::FitPower); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Exponential (degree 1)"), this); fitAction->setData(PlotDataDialog::FitExp1); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Exponential (degree 2)"), this); fitAction->setData(PlotDataDialog::FitExp2); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Inverse Exponential"), this); fitAction->setData(PlotDataDialog::FitInvExp); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Gauss"), this); fitAction->setData(PlotDataDialog::FitGauss); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Cauchy-Lorentz"), this); fitAction->setData(PlotDataDialog::FitCauchyLorentz); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Arc Tangent"), this); fitAction->setData(PlotDataDialog::FitTan); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Hyperbolic Tangent"), this); fitAction->setData(PlotDataDialog::FitTanh); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Error Function"), this); fitAction->setData(PlotDataDialog::FitErrFunc); addFitAction.append(fitAction); fitAction = new QAction(QIcon::fromTheme("labplot-xy-fit-curve"), i18n("Custom"), this); fitAction->setData(PlotDataDialog::FitCustom); addFitAction.append(fitAction); addFourierFilterAction = new QAction(QIcon::fromTheme("labplot-xy-fourier-filter-curve"), i18n("Fourier Filter"), this); addFourierFilterAction->setData(PlotDataDialog::FourierFilter); } void SpreadsheetView::initMenus() { //Selection menu m_selectionMenu = new QMenu(i18n("Selection"), this); m_selectionMenu->setIcon(QIcon::fromTheme("selection")); QMenu* submenu = nullptr; if (!m_readOnly) { submenu = new QMenu(i18n("Fi&ll Selection With"), this); submenu->setIcon(QIcon::fromTheme("select-rectangle")); submenu->addAction(action_fill_sel_row_numbers); submenu->addAction(action_fill_const); m_selectionMenu->addMenu(submenu); m_selectionMenu->addSeparator(); m_selectionMenu->addAction(action_cut_selection); } m_selectionMenu->addAction(action_copy_selection); if (!m_readOnly) { m_selectionMenu->addAction(action_paste_into_selection); m_selectionMenu->addAction(action_clear_selection); m_selectionMenu->addSeparator(); m_selectionMenu->addAction(action_mask_selection); m_selectionMenu->addAction(action_unmask_selection); m_selectionMenu->addSeparator(); m_selectionMenu->addAction(action_normalize_selection); } //plot data menu m_plotDataMenu = new QMenu(i18n("Plot Data"), this); m_plotDataMenu->addAction(action_plot_data_xycurve); m_plotDataMenu->addAction(action_plot_data_histogram); // Column menu m_columnMenu = new QMenu(this); m_columnMenu->addMenu(m_plotDataMenu); // Data manipulation sub-menu QMenu* dataManipulationMenu = new QMenu(i18n("Data Manipulation"), this); dataManipulationMenu->setIcon(QIcon::fromTheme("zoom-draw")); dataManipulationMenu->addAction(addDataReductionAction); // Data fit sub-menu QMenu* dataFitMenu = new QMenu(i18n("Fit"), this); 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)); //analyze and plot data menu m_analyzePlotMenu = new QMenu(i18n("Analyze and Plot Data"), this); m_analyzePlotMenu->insertMenu(nullptr, dataManipulationMenu); m_analyzePlotMenu->addSeparator(); m_analyzePlotMenu->addAction(addDifferentiationAction); m_analyzePlotMenu->addAction(addIntegrationAction); m_analyzePlotMenu->addSeparator(); m_analyzePlotMenu->addAction(addInterpolationAction); m_analyzePlotMenu->addAction(addSmoothAction); m_analyzePlotMenu->addAction(addFourierFilterAction); m_analyzePlotMenu->addSeparator(); m_analyzePlotMenu->addMenu(dataFitMenu); m_columnMenu->addMenu(m_analyzePlotMenu); m_columnSetAsMenu = new QMenu(i18n("Set Column As"), this); m_columnMenu->addSeparator(); m_columnSetAsMenu->addAction(action_set_as_x); m_columnSetAsMenu->addAction(action_set_as_y); m_columnSetAsMenu->addAction(action_set_as_z); m_columnSetAsMenu->addSeparator(); m_columnSetAsMenu->addAction(action_set_as_xerr); m_columnSetAsMenu->addAction(action_set_as_xerr_minus); m_columnSetAsMenu->addAction(action_set_as_xerr_plus); m_columnSetAsMenu->addSeparator(); m_columnSetAsMenu->addAction(action_set_as_yerr); m_columnSetAsMenu->addAction(action_set_as_yerr_minus); m_columnSetAsMenu->addAction(action_set_as_yerr_plus); m_columnSetAsMenu->addSeparator(); m_columnSetAsMenu->addAction(action_set_as_none); m_columnMenu->addMenu(m_columnSetAsMenu); if (!m_readOnly) { m_columnGenerateDataMenu = new QMenu(i18n("Generate Data"), this); m_columnGenerateDataMenu->addAction(action_fill_row_numbers); m_columnGenerateDataMenu->addAction(action_fill_const); m_columnGenerateDataMenu->addAction(action_fill_equidistant); m_columnGenerateDataMenu->addAction(action_fill_random_nonuniform); m_columnGenerateDataMenu->addAction(action_fill_function); m_columnMenu->addSeparator(); m_columnMenu->addMenu(m_columnGenerateDataMenu); m_columnMenu->addSeparator(); m_columnManipulateDataMenu = new QMenu(i18n("Manipulate Data"), this); m_columnManipulateDataMenu->addAction(action_add_value); m_columnManipulateDataMenu->addAction(action_subtract_value); m_columnManipulateDataMenu->addAction(action_multiply_value); m_columnManipulateDataMenu->addAction(action_divide_value); m_columnManipulateDataMenu->addSeparator(); m_columnManipulateDataMenu->addAction(action_reverse_columns); m_columnManipulateDataMenu->addSeparator(); m_columnManipulateDataMenu->addAction(action_drop_values); m_columnManipulateDataMenu->addAction(action_mask_values); m_columnManipulateDataMenu->addSeparator(); // m_columnManipulateDataMenu->addAction(action_join_columns); m_columnManipulateDataMenu->addAction(action_normalize_columns); m_columnMenu->addMenu(m_columnManipulateDataMenu); m_columnMenu->addSeparator(); m_columnSortMenu = new QMenu(i18n("Sort"), this); m_columnSortMenu->setIcon(QIcon::fromTheme("view-sort-ascending")); m_columnSortMenu->addAction(action_sort_asc_column); m_columnSortMenu->addAction(action_sort_desc_column); m_columnSortMenu->addAction(action_sort_columns); m_columnMenu->addSeparator(); m_columnMenu->addMenu(m_columnSortMenu); m_columnMenu->addSeparator(); m_columnMenu->addAction(action_insert_column_left); m_columnMenu->addAction(action_insert_column_right); m_columnMenu->addSeparator(); m_columnMenu->addAction(action_insert_columns_left); m_columnMenu->addAction(action_insert_columns_right); m_columnMenu->addSeparator(); m_columnMenu->addAction(action_remove_columns); m_columnMenu->addAction(action_clear_columns); } m_columnMenu->addSeparator(); m_columnMenu->addAction(action_toggle_comments); m_columnMenu->addSeparator(); m_columnMenu->addAction(action_statistics_columns); //Spreadsheet menu m_spreadsheetMenu = new QMenu(this); m_spreadsheetMenu->addMenu(m_plotDataMenu); m_spreadsheetMenu->addMenu(m_analyzePlotMenu); m_spreadsheetMenu->addSeparator(); m_spreadsheetMenu->addMenu(m_selectionMenu); m_spreadsheetMenu->addSeparator(); m_spreadsheetMenu->addAction(action_select_all); if (!m_readOnly) { m_spreadsheetMenu->addAction(action_clear_spreadsheet); m_spreadsheetMenu->addAction(action_clear_masks); m_spreadsheetMenu->addAction(action_sort_spreadsheet); } m_spreadsheetMenu->addSeparator(); m_spreadsheetMenu->addAction(action_go_to_cell); m_spreadsheetMenu->addSeparator(); m_spreadsheetMenu->addAction(action_toggle_comments); m_spreadsheetMenu->addSeparator(); m_spreadsheetMenu->addAction(action_statistics_all_columns); //Row menu m_rowMenu = new QMenu(this); if (!m_readOnly) { submenu = new QMenu(i18n("Fi&ll Selection With"), this); submenu->addAction(action_fill_sel_row_numbers); submenu->addAction(action_fill_const); m_rowMenu->addMenu(submenu); m_rowMenu->addSeparator(); m_rowMenu->addAction(action_insert_row_above); m_rowMenu->addAction(action_insert_row_below); m_rowMenu->addSeparator(); m_rowMenu->addAction(action_insert_rows_above); m_rowMenu->addAction(action_insert_rows_below); m_rowMenu->addSeparator(); m_rowMenu->addAction(action_remove_rows); m_rowMenu->addAction(action_clear_rows); } m_rowMenu->addSeparator(); m_rowMenu->addAction(action_statistics_rows); action_statistics_rows->setVisible(false); } void SpreadsheetView::connectActions() { connect(action_cut_selection, &QAction::triggered, this, &SpreadsheetView::cutSelection); connect(action_copy_selection, &QAction::triggered, this, &SpreadsheetView::copySelection); connect(action_paste_into_selection, &QAction::triggered, this, &SpreadsheetView::pasteIntoSelection); connect(action_mask_selection, &QAction::triggered, this, &SpreadsheetView::maskSelection); connect(action_unmask_selection, &QAction::triggered, this, &SpreadsheetView::unmaskSelection); connect(action_clear_selection, &QAction::triggered, this, &SpreadsheetView::clearSelectedCells); // connect(action_recalculate, &QAction::triggered, this, &SpreadsheetView::recalculateSelectedCells); connect(action_fill_row_numbers, &QAction::triggered, this, &SpreadsheetView::fillWithRowNumbers); connect(action_fill_sel_row_numbers, &QAction::triggered, this, &SpreadsheetView::fillSelectedCellsWithRowNumbers); // connect(action_fill_random, &QAction::triggered, this, &SpreadsheetView::fillSelectedCellsWithRandomNumbers); connect(action_fill_random_nonuniform, &QAction::triggered, this, &SpreadsheetView::fillWithRandomValues); connect(action_fill_equidistant, &QAction::triggered, this, &SpreadsheetView::fillWithEquidistantValues); connect(action_fill_function, &QAction::triggered, this, &SpreadsheetView::fillWithFunctionValues); connect(action_fill_const, &QAction::triggered, this, &SpreadsheetView::fillSelectedCellsWithConstValues); connect(action_select_all, &QAction::triggered, m_tableView, &QTableView::selectAll); connect(action_clear_spreadsheet, &QAction::triggered, m_spreadsheet, &Spreadsheet::clear); connect(action_clear_masks, &QAction::triggered, m_spreadsheet, &Spreadsheet::clearMasks); connect(action_sort_spreadsheet, &QAction::triggered, this, &SpreadsheetView::sortSpreadsheet); connect(action_go_to_cell, &QAction::triggered, this, static_cast(&SpreadsheetView::goToCell)); connect(action_insert_column_left, &QAction::triggered, this, &SpreadsheetView::insertColumnLeft); connect(action_insert_column_right, &QAction::triggered, this, &SpreadsheetView::insertColumnRight); connect(action_insert_columns_left, &QAction::triggered, this, static_cast(&SpreadsheetView::insertColumnsLeft)); connect(action_insert_columns_right, &QAction::triggered, this, static_cast(&SpreadsheetView::insertColumnsRight)); connect(action_remove_columns, &QAction::triggered, this, &SpreadsheetView::removeSelectedColumns); connect(action_clear_columns, &QAction::triggered, this, &SpreadsheetView::clearSelectedColumns); connect(action_set_as_none, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_x, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_y, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_z, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_xerr, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_xerr_minus, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_xerr_plus, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_yerr, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_yerr_minus, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); connect(action_set_as_yerr_plus, &QAction::triggered, this, &SpreadsheetView::setSelectionAs); //data manipulation connect(action_add_value, &QAction::triggered, this, &SpreadsheetView::modifyValues); connect(action_subtract_value, &QAction::triggered, this, &SpreadsheetView::modifyValues); connect(action_multiply_value, &QAction::triggered, this, &SpreadsheetView::modifyValues); connect(action_divide_value, &QAction::triggered, this, &SpreadsheetView::modifyValues); connect(action_reverse_columns, &QAction::triggered, this, &SpreadsheetView::reverseColumns); connect(action_drop_values, &QAction::triggered, this, &SpreadsheetView::dropColumnValues); connect(action_mask_values, &QAction::triggered, this, &SpreadsheetView::maskColumnValues); // connect(action_join_columns, &QAction::triggered, this, &SpreadsheetView::joinColumns); connect(action_normalize_columns, &QAction::triggered, this, &SpreadsheetView::normalizeSelectedColumns); connect(action_normalize_selection, &QAction::triggered, this, &SpreadsheetView::normalizeSelection); //sort connect(action_sort_columns, &QAction::triggered, this, &SpreadsheetView::sortSelectedColumns); connect(action_sort_asc_column, &QAction::triggered, this, &SpreadsheetView::sortColumnAscending); connect(action_sort_desc_column, &QAction::triggered, this, &SpreadsheetView::sortColumnDescending); //statistics connect(action_statistics_columns, &QAction::triggered, this, &SpreadsheetView::showColumnStatistics); connect(action_statistics_all_columns, &QAction::triggered, this, &SpreadsheetView::showAllColumnsStatistics); connect(action_insert_row_above, &QAction::triggered, this, &SpreadsheetView::insertRowAbove); connect(action_insert_row_below, &QAction::triggered, this, &SpreadsheetView::insertRowBelow); connect(action_insert_rows_above, &QAction::triggered, this, static_cast(&SpreadsheetView::insertRowsAbove)); connect(action_insert_rows_below, &QAction::triggered, this, static_cast(&SpreadsheetView::insertRowsBelow)); connect(action_remove_rows, &QAction::triggered, this, &SpreadsheetView::removeSelectedRows); connect(action_clear_rows, &QAction::triggered, this, &SpreadsheetView::clearSelectedRows); connect(action_statistics_rows, &QAction::triggered, this, &SpreadsheetView::showRowStatistics); connect(action_toggle_comments, &QAction::triggered, this, &SpreadsheetView::toggleComments); connect(action_plot_data_xycurve, &QAction::triggered, this, &SpreadsheetView::plotData); connect(action_plot_data_histogram, &QAction::triggered, this, &SpreadsheetView::plotData); connect(addDataReductionAction, &QAction::triggered, this, &SpreadsheetView::plotData); connect(addDifferentiationAction, &QAction::triggered, this, &SpreadsheetView::plotData); connect(addIntegrationAction, &QAction::triggered, this, &SpreadsheetView::plotData); connect(addInterpolationAction, &QAction::triggered, this, &SpreadsheetView::plotData); connect(addSmoothAction, &QAction::triggered, this, &SpreadsheetView::plotData); for (const auto& action : addFitAction) connect(action, &QAction::triggered, this, &SpreadsheetView::plotData); connect(addFourierFilterAction, &QAction::triggered,this, &SpreadsheetView::plotData); } void SpreadsheetView::fillToolBar(QToolBar* toolBar) { if (!m_readOnly) { toolBar->addAction(action_insert_row_above); toolBar->addAction(action_insert_row_below); toolBar->addAction(action_remove_rows); } toolBar->addAction(action_statistics_rows); toolBar->addSeparator(); if (!m_readOnly) { toolBar->addAction(action_insert_column_left); toolBar->addAction(action_insert_column_right); toolBar->addAction(action_remove_columns); } toolBar->addAction(action_statistics_columns); if (!m_readOnly) { toolBar->addSeparator(); toolBar->addAction(action_sort_asc_column); toolBar->addAction(action_sort_desc_column); } } #ifdef Q_OS_MAC void SpreadsheetView::fillTouchBar(KDMacTouchBar* touchBar){ //touchBar->addAction(action_insert_column_right); } #endif /*! * Populates the menu \c menu with the spreadsheet and spreadsheet view relevant actions. * The menu is used * - as the context menu in SpreadsheetView * - as the "spreadsheet menu" in the main menu-bar (called form MainWin) * - as a part of the spreadsheet context menu in project explorer */ void SpreadsheetView::createContextMenu(QMenu* menu) { Q_ASSERT(menu); checkSpreadsheetMenu(); 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); if (m_spreadsheet->columnCount() > 0 && m_spreadsheet->rowCount() > 0) { menu->insertMenu(firstAction, m_plotDataMenu); menu->insertSeparator(firstAction); } menu->insertMenu(firstAction, m_selectionMenu); menu->insertSeparator(firstAction); menu->insertAction(firstAction, action_select_all); if (!m_readOnly) { menu->insertAction(firstAction, action_clear_spreadsheet); menu->insertAction(firstAction, action_clear_masks); menu->insertAction(firstAction, action_sort_spreadsheet); menu->insertSeparator(firstAction); } menu->insertAction(firstAction, action_go_to_cell); menu->insertSeparator(firstAction); menu->insertAction(firstAction, action_toggle_comments); menu->insertSeparator(firstAction); menu->insertAction(firstAction, action_statistics_all_columns); menu->insertSeparator(firstAction); } /*! * adds column specific actions in SpreadsheetView to the context menu shown in the project explorer. */ void SpreadsheetView::createColumnContextMenu(QMenu* menu) { const Column* column = dynamic_cast(QObject::sender()); if (!column) return; //should never happen, since the sender is always a Column if (column->isNumeric()) { QAction* firstAction = menu->actions().at(1); //TODO: add these menus and synchronize the behavior with the context menu creation //on the spreadsheet header in eventFilter(), // menu->insertMenu(firstAction, m_plotDataMenu); // menu->insertMenu(firstAction, m_analyzePlotMenu); // menu->insertSeparator(firstAction); menu->insertMenu(firstAction, m_columnSetAsMenu); const bool hasValues = column->hasValues(); if (!m_readOnly) { menu->insertSeparator(firstAction); menu->insertMenu(firstAction, m_columnGenerateDataMenu); menu->insertSeparator(firstAction); menu->insertMenu(firstAction, m_columnManipulateDataMenu); menu->insertSeparator(firstAction); menu->insertMenu(firstAction, m_columnSortMenu); action_sort_asc_column->setVisible(true); action_sort_desc_column->setVisible(true); action_sort_columns->setVisible(false); //in case no cells are available, deactivate the actions that only make sense in the presence of cells const bool hasCells = m_spreadsheet->rowCount() > 0; m_columnGenerateDataMenu->setEnabled(hasCells); //in case no valid numerical values are available, deactivate the actions that only make sense in the presence of values m_columnManipulateDataMenu->setEnabled(hasValues); m_columnSortMenu->setEnabled(hasValues); } menu->insertSeparator(firstAction); menu->insertAction(firstAction, action_statistics_columns); action_statistics_columns->setEnabled(hasValues); } } //SLOTS void SpreadsheetView::handleAspectAdded(const AbstractAspect* aspect) { const Column* col = dynamic_cast(aspect); if (!col || col->parentAspect() != m_spreadsheet) return; int index = m_spreadsheet->indexOfChild(col); if (col->width() == 0) m_tableView->resizeColumnToContents(index); else m_tableView->setColumnWidth(index, col->width()); connect(col, &Column::requestProjectContextMenu, this, &SpreadsheetView::createColumnContextMenu); } void SpreadsheetView::handleAspectAboutToBeRemoved(const AbstractAspect* aspect) { const Column* col = dynamic_cast(aspect); if (!col || col->parentAspect() != m_spreadsheet) return; disconnect(col, nullptr, this, nullptr); } void SpreadsheetView::handleHorizontalSectionResized(int logicalIndex, int oldSize, int newSize) { Q_UNUSED(logicalIndex); Q_UNUSED(oldSize); //save the new size in the column Column* col = m_spreadsheet->child(logicalIndex); col->setWidth(newSize); } void SpreadsheetView::goToCell(int row, int col) { QModelIndex index = m_model->index(row, col); m_tableView->scrollTo(index); m_tableView->setCurrentIndex(index); } void SpreadsheetView::handleHorizontalSectionMoved(int index, int from, int to) { Q_UNUSED(index); static bool inside = false; if (inside) return; Q_ASSERT(index == from); inside = true; m_tableView->horizontalHeader()->moveSection(to, from); inside = false; m_spreadsheet->moveColumn(from, to); } //TODO Implement the "change of the column name"-mode upon a double click void SpreadsheetView::handleHorizontalHeaderDoubleClicked(int index) { Q_UNUSED(index); } /*! Returns whether comments are shown currently or not */ bool SpreadsheetView::areCommentsShown() const { return m_horizontalHeader->areCommentsShown(); } /*! toggles the column comment in the horizontal header */ void SpreadsheetView::toggleComments() { showComments(!areCommentsShown()); //TODO if (areCommentsShown()) action_toggle_comments->setText(i18n("Hide Comments")); else action_toggle_comments->setText(i18n("Show Comments")); } //! Shows (\c on=true) or hides (\c on=false) the column comments in the horizontal header void SpreadsheetView::showComments(bool on) { m_horizontalHeader->showComments(on); } void SpreadsheetView::currentColumnChanged(const QModelIndex & current, const QModelIndex & previous) { Q_UNUSED(previous); int col = current.column(); if (col < 0 || col >= m_spreadsheet->columnCount()) return; } //TODO void SpreadsheetView::handleHeaderDataChanged(Qt::Orientation orientation, int first, int last) { if (orientation != Qt::Horizontal) return; QItemSelectionModel * sel_model = m_tableView->selectionModel(); int col = sel_model->currentIndex().column(); if (col < first || col > last) return; } /*! Returns the number of selected columns. If \c full is \c true, this function only returns the number of fully selected columns. */ int SpreadsheetView::selectedColumnCount(bool full) const { int count = 0; const int cols = m_spreadsheet->columnCount(); for (int i = 0; i < cols; i++) if (isColumnSelected(i, full)) count++; return count; } /*! Returns the number of (at least partly) selected columns with the plot designation \param pd . */ int SpreadsheetView::selectedColumnCount(AbstractColumn::PlotDesignation pd) const{ int count = 0; const int cols = m_spreadsheet->columnCount(); for (int i = 0; i < cols; i++) if ( isColumnSelected(i, false) && (m_spreadsheet->column(i)->plotDesignation() == pd) ) count++; return count; } /*! Returns \c true if column \param col is selected, otherwise returns \c false. If \param full is \c true, this function only returns true if the whole column is selected. */ bool SpreadsheetView::isColumnSelected(int col, bool full) const { if (full) return m_tableView->selectionModel()->isColumnSelected(col, QModelIndex()); else return m_tableView->selectionModel()->columnIntersectsSelection(col, QModelIndex()); } /*! Returns all selected columns. If \param full is true, this function only returns a column if the whole column is selected. */ QVector SpreadsheetView::selectedColumns(bool full) const { QVector columns; const int cols = m_spreadsheet->columnCount(); for (int i = 0; i < cols; i++) if (isColumnSelected(i, full)) columns << m_spreadsheet->column(i); return columns; } /*! Returns \c true if row \param row is selected; otherwise returns \c false If \param full is \c true, this function only returns \c true if the whole row is selected. */ bool SpreadsheetView::isRowSelected(int row, bool full) const { if (full) return m_tableView->selectionModel()->isRowSelected(row, QModelIndex()); else return m_tableView->selectionModel()->rowIntersectsSelection(row, QModelIndex()); } /*! Return the index of the first selected column. If \param full is \c true, this function only looks for fully selected columns. */ int SpreadsheetView::firstSelectedColumn(bool full) const { const int cols = m_spreadsheet->columnCount(); for (int i = 0; i < cols; i++) { if (isColumnSelected(i, full)) return i; } return -1; } /*! Return the index of the last selected column. If \param full is \c true, this function only looks for fully selected columns. */ int SpreadsheetView::lastSelectedColumn(bool full) const { const int cols = m_spreadsheet->columnCount(); for (int i = cols - 1; i >= 0; i--) if (isColumnSelected(i, full)) return i; return -2; } /*! Return the index of the first selected row. If \param full is \c true, this function only looks for fully selected rows. */ int SpreadsheetView::firstSelectedRow(bool full) const{ QModelIndexList indexes; if (!full) indexes = m_tableView->selectionModel()->selectedIndexes(); else indexes = m_tableView->selectionModel()->selectedRows(); if (!indexes.empty()) return indexes.first().row(); else return -1; } /*! Return the index of the last selected row. If \param full is \c true, this function only looks for fully selected rows. */ int SpreadsheetView::lastSelectedRow(bool full) const { QModelIndexList indexes; if (!full) indexes = m_tableView->selectionModel()->selectedIndexes(); else indexes = m_tableView->selectionModel()->selectedRows(); if (!indexes.empty()) return indexes.last().row(); else return -2; } /*! Return whether a cell is selected */ bool SpreadsheetView::isCellSelected(int row, int col) const { if (row < 0 || col < 0 || row >= m_spreadsheet->rowCount() || col >= m_spreadsheet->columnCount()) return false; return m_tableView->selectionModel()->isSelected(m_model->index(row, col)); } /*! Get the complete set of selected rows. */ IntervalAttribute SpreadsheetView::selectedRows(bool full) const { IntervalAttribute result; const int rows = m_spreadsheet->rowCount(); for (int i = 0; i < rows; i++) if (isRowSelected(i, full)) result.setValue(i, true); return result; } /*! Select/Deselect a cell. */ void SpreadsheetView::setCellSelected(int row, int col, bool select) { m_tableView->selectionModel()->select(m_model->index(row, col), select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect); } /*! Select/Deselect a range of cells. */ void SpreadsheetView::setCellsSelected(int first_row, int first_col, int last_row, int last_col, bool select) { QModelIndex top_left = m_model->index(first_row, first_col); QModelIndex bottom_right = m_model->index(last_row, last_col); m_tableView->selectionModel()->select(QItemSelection(top_left, bottom_right), select ? QItemSelectionModel::SelectCurrent : QItemSelectionModel::Deselect); } /*! Determine the current cell (-1 if no cell is designated as the current). */ void SpreadsheetView::getCurrentCell(int* row, int* col) const { QModelIndex index = m_tableView->selectionModel()->currentIndex(); if (index.isValid()) { *row = index.row(); *col = index.column(); } else { *row = -1; *col = -1; } } bool SpreadsheetView::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::ContextMenu) { auto* cm_event = static_cast(event); const QPoint global_pos = cm_event->globalPos(); if (watched == m_tableView->verticalHeader()) { bool onlyNumeric = true; for (int i = 0; i < m_spreadsheet->columnCount(); ++i) { if (m_spreadsheet->column(i)->columnMode() != AbstractColumn::Numeric) { onlyNumeric = false; break; } } action_statistics_rows->setVisible(onlyNumeric); m_rowMenu->exec(global_pos); } else if (watched == m_horizontalHeader) { const int col = m_horizontalHeader->logicalIndexAt(cm_event->pos()); if (!isColumnSelected(col, true)) { QItemSelectionModel* sel_model = m_tableView->selectionModel(); sel_model->clearSelection(); sel_model->select(QItemSelection(m_model->index(0, col, QModelIndex()), m_model->index(m_model->rowCount()-1, col, QModelIndex())), QItemSelectionModel::Select); } if (selectedColumns().size() == 1) { action_sort_columns->setVisible(false); action_sort_asc_column->setVisible(true); action_sort_desc_column->setVisible(true); } else { action_sort_columns->setVisible(true); action_sort_asc_column->setVisible(false); action_sort_desc_column->setVisible(false); } //check whether we have non-numeric columns selected and deactivate actions for numeric columns bool numeric = true; bool plottable = true; bool datetime = false; for (const Column* col : selectedColumns()) { if ( !(col->columnMode() == AbstractColumn::Numeric || col->columnMode() == AbstractColumn::Integer) ) { datetime = (col->columnMode() == AbstractColumn::DateTime); if (!datetime) plottable = false; numeric = false; break; } } m_plotDataMenu->setEnabled(plottable); m_analyzePlotMenu->setEnabled(numeric); m_columnSetAsMenu->setEnabled(numeric); if (!m_readOnly) { m_columnGenerateDataMenu->setEnabled(numeric); m_columnManipulateDataMenu->setEnabled(numeric || datetime); m_columnSortMenu->setEnabled(numeric); } action_statistics_columns->setEnabled(numeric); if (numeric) { bool hasValues = false; for (const Column* col : selectedColumns()) { if (col->hasValues()) { hasValues = true; break; } } if (!m_readOnly) { //in case no cells are available, deactivate the actions that only make sense in the presence of cells const bool hasCells = m_spreadsheet->rowCount() > 0; m_columnGenerateDataMenu->setEnabled(hasCells); //in case no valid numerical values are available, deactivate the actions that only make sense in the presence of values m_columnManipulateDataMenu->setEnabled(hasValues); m_columnSortMenu->setEnabled(hasValues); } action_statistics_columns->setEnabled(hasValues); } m_columnMenu->exec(global_pos); } else if (watched == this) { checkSpreadsheetMenu(); m_spreadsheetMenu->exec(global_pos); } return true; } else if (event->type() == QEvent::KeyPress) { auto* key_event = static_cast(event); if (key_event->matches(QKeySequence::Copy)) copySelection(); else if (key_event->matches(QKeySequence::Paste)) pasteIntoSelection(); } return QWidget::eventFilter(watched, event); } /*! * disables cell data relevant actions in the spreadsheet menu if there're no cells available */ void SpreadsheetView::checkSpreadsheetMenu() { const bool cellsAvail = m_spreadsheet->columnCount()>0 && m_spreadsheet->rowCount()>0; m_plotDataMenu->setEnabled(cellsAvail); m_selectionMenu->setEnabled(cellsAvail); action_select_all->setEnabled(cellsAvail); action_clear_spreadsheet->setEnabled(cellsAvail); action_clear_masks->setEnabled(cellsAvail); action_sort_spreadsheet->setEnabled(cellsAvail); action_go_to_cell->setEnabled(cellsAvail); action_statistics_all_columns->setEnabled(cellsAvail); //deactivate mask/unmask actions if there are no unmasked/masked cells //in the current selection QModelIndexList indexes = m_tableView->selectionModel()->selectedIndexes(); bool hasMasked = false; bool hasUnmasked = false; for (auto index : indexes) { int row = index.row(); int col = index.column(); if (m_spreadsheet->column(col)->isMasked(row)) { hasMasked = true; break; } } for (auto index : indexes) { int row = index.row(); int col = index.column(); if (!m_spreadsheet->column(col)->isMasked(row)) { hasUnmasked = true; break; } } action_mask_selection->setEnabled(hasUnmasked); action_unmask_selection->setEnabled(hasMasked); } bool SpreadsheetView::formulaModeActive() const { return m_model->formulaModeActive(); } void SpreadsheetView::activateFormulaMode(bool on) { m_model->activateFormulaMode(on); } void SpreadsheetView::goToNextColumn() { if (m_spreadsheet->columnCount() == 0) return; QModelIndex idx = m_tableView->currentIndex(); int col = idx.column()+1; if (col >= m_spreadsheet->columnCount()) col = 0; m_tableView->setCurrentIndex(idx.sibling(idx.row(), col)); } void SpreadsheetView::goToPreviousColumn() { if (m_spreadsheet->columnCount() == 0) return; QModelIndex idx = m_tableView->currentIndex(); int col = idx.column()-1; if (col < 0) col = m_spreadsheet->columnCount()-1; m_tableView->setCurrentIndex(idx.sibling(idx.row(), col)); } void SpreadsheetView::cutSelection() { int first = firstSelectedRow(); if ( first < 0 ) return; WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: cut selected cells", m_spreadsheet->name())); copySelection(); clearSelectedCells(); m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::copySelection() { PERFTRACE("copy selected cells"); const int first_col = firstSelectedColumn(); if (first_col == -1) return; const int last_col = lastSelectedColumn(); if (last_col == -2) return; const int first_row = firstSelectedRow(); if (first_row == -1) return; const int last_row = lastSelectedRow(); if (last_row == -2) return; const int cols = last_col - first_col + 1; const int rows = last_row - first_row + 1; WAIT_CURSOR; QString output_str; QVector columns; QVector formats; for (int c = 0; c < cols; c++) { Column* col = m_spreadsheet->column(first_col + c); columns << col; const Double2StringFilter* out_fltr = static_cast(col->outputFilter()); formats << out_fltr->numericFormat(); } QLocale locale; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { const Column* col_ptr = columns.at(c); if (isCellSelected(first_row + r, first_col + c)) { // if (formulaModeActive()) // output_str += col_ptr->formula(first_row + r); // else if (col_ptr->columnMode() == AbstractColumn::Numeric) output_str += locale.toString(col_ptr->valueAt(first_row + r), formats.at(c), 16); // copy with max. precision else if (col_ptr->columnMode() == AbstractColumn::Integer) output_str += QString::number(col_ptr->valueAt(first_row + r)); else output_str += col_ptr->asStringColumn()->textAt(first_row + r); } if (c < cols-1) output_str += '\t'; } if (r < rows-1) output_str += '\n'; } QApplication::clipboard()->setText(output_str); RESET_CURSOR; } void SpreadsheetView::pasteIntoSelection() { if (m_spreadsheet->columnCount() < 1 || m_spreadsheet->rowCount() < 1) return; const QMimeData* mime_data = QApplication::clipboard()->mimeData(); if (!mime_data->hasFormat("text/plain")) return; PERFTRACE("paste selected cells"); WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: paste from clipboard", m_spreadsheet->name())); int first_col = firstSelectedColumn(); int last_col = lastSelectedColumn(); int first_row = firstSelectedRow(); int last_row = lastSelectedRow(); int input_row_count = 0; int input_col_count = 0; QString input_str = QString(mime_data->data("text/plain")).trimmed(); QVector cellTexts; QString separator; if (input_str.indexOf(QLatin1String("\r\n")) != -1) separator = QLatin1String("\r\n"); else separator = QLatin1Char('\n'); QStringList input_rows(input_str.split(separator)); input_row_count = input_rows.count(); input_col_count = 0; for (int i = 0; i < input_row_count; i++) { cellTexts.append(input_rows.at(i).split(QRegExp(QLatin1String("\\s+")))); if (cellTexts.at(i).count() > input_col_count) input_col_count = cellTexts.at(i).count(); } //independent of the current default locale, set the locale to C //if we have point as the decimal separator in the text to be copied QLocale::Language lang = QLocale::AnyLanguage; if (input_str.indexOf(QChar('.')) != -1) lang = QLocale::C; QLocale locale(lang); if ( (first_col == -1 || first_row == -1) || (last_row == first_row && last_col == first_col) ) { // if there is no selection or only one cell selected, the // selection will be expanded to the needed size from the current cell int current_row, current_col; getCurrentCell(¤t_row, ¤t_col); if (current_row == -1) current_row = 0; if (current_col == -1) current_col = 0; setCellSelected(current_row, current_col); first_col = current_col; first_row = current_row; last_row = first_row + input_row_count -1; last_col = first_col + input_col_count -1; const int columnCount = m_spreadsheet->columnCount(); //if the target columns that are already available don't have any values yet, //convert their mode to the mode of the data to be pasted for (int c = first_col; c <= last_col && c < columnCount; ++c) { Column* col = m_spreadsheet->column(c); if (col->hasValues() ) continue; //first non-empty value in the column to paste determines the column mode/type of the new column to be added const int curCol = c - first_col; QString nonEmptyValue; for (auto r : cellTexts) { if (curCol < r.count() && !r.at(curCol).isEmpty()) { nonEmptyValue = r.at(curCol); break; } } const AbstractColumn::ColumnMode mode = AbstractFileFilter::columnMode(nonEmptyValue, QLatin1String("yyyy-dd-MM hh:mm:ss:zzz"), lang); col->setColumnMode(mode); } //add columns if necessary if (last_col >= columnCount) { for (int c = 0; c < last_col - (columnCount - 1); ++c) { const int curCol = columnCount - first_col + c; //first non-empty value in the column to paste determines the column mode/type of the new column to be added QString nonEmptyValue; for (auto r : cellTexts) { if (curCol < r.count() && !r.at(curCol).isEmpty()) { nonEmptyValue = r.at(curCol); break; } } const AbstractColumn::ColumnMode mode = AbstractFileFilter::columnMode(nonEmptyValue, QLatin1String("yyyy-dd-MM hh:mm:ss:zzz"), lang); Column* new_col = new Column(QString::number(curCol), mode); new_col->setPlotDesignation(AbstractColumn::Y); new_col->insertRows(0, m_spreadsheet->rowCount()); m_spreadsheet->addChild(new_col); } } //add rows if necessary if (last_row >= m_spreadsheet->rowCount()) m_spreadsheet->appendRows(last_row + 1 - m_spreadsheet->rowCount()); // select the rectangle to be pasted in setCellsSelected(first_row, first_col, last_row, last_col); } const int rows = last_row - first_row + 1; const int cols = last_col - first_col + 1; for (int c = 0; c < cols && c < input_col_count; c++) { Column* col = m_spreadsheet->column(first_col + c); col->setSuppressDataChangedSignal(true); if (col->columnMode() == AbstractColumn::Numeric) { if (rows == m_spreadsheet->rowCount() && rows <= cellTexts.size()) { QVector new_data(rows); for (int r = 0; r < rows; ++r) new_data[r] = locale.toDouble(cellTexts.at(r).at(c)); col->replaceValues(0, new_data); } else { for (int r = 0; r < rows && r < input_row_count; r++) { if ( isCellSelected(first_row + r, first_col + c) && (c < cellTexts.at(r).count()) ) { if (!cellTexts.at(r).at(c).isEmpty()) col->setValueAt(first_row + r, locale.toDouble(cellTexts.at(r).at(c))); else col->setValueAt(first_row + r, std::numeric_limits::quiet_NaN()); } } } } else if (col->columnMode() == AbstractColumn::Integer) { if (rows == m_spreadsheet->rowCount() && rows <= cellTexts.size()) { QVector new_data(rows); for (int r = 0; r < rows; ++r) new_data[r] = locale.toInt(cellTexts.at(r).at(c)); col->replaceInteger(0, new_data); } else { for (int r = 0; r < rows && r < input_row_count; r++) { if ( isCellSelected(first_row + r, first_col + c) && (c < cellTexts.at(r).count()) ) { if (!cellTexts.at(r).at(c).isEmpty()) col->setIntegerAt(first_row + r, locale.toInt(cellTexts.at(r).at(c))); else col->setIntegerAt(first_row + r, 0); } } } } else { for (int r = 0; r < rows && r < input_row_count; r++) { if (isCellSelected(first_row + r, first_col + c) && (c < cellTexts.at(r).count()) ) { // if (formulaModeActive()) // col->setFormula(first_row + r, cellTexts.at(r).at(c)); // else col->asStringColumn()->setTextAt(first_row + r, cellTexts.at(r).at(c)); } } } col->setSuppressDataChangedSignal(false); col->setChanged(); } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::maskSelection() { int first = firstSelectedRow(); if (first < 0) return; int last = lastSelectedRow(); WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: mask selected cells", m_spreadsheet->name())); QVector plots; //determine the dependent plots for (auto* column : selectedColumns()) column->addUsedInPlots(plots); //suppress retransform in the dependent plots for (auto* plot : plots) plot->setSuppressDataChangedSignal(true); //mask the selected cells for (auto* column : selectedColumns()) { int col = m_spreadsheet->indexOfChild(column); for (int row = first; row <= last; row++) if (isCellSelected(row, col)) column->setMasked(row); } //retransform the dependent plots for (auto* plot : plots) { plot->setSuppressDataChangedSignal(false); plot->dataChanged(); } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::unmaskSelection() { int first = firstSelectedRow(); if (first < 0) return; int last = lastSelectedRow(); WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: unmask selected cells", m_spreadsheet->name())); QVector plots; //determine the dependent plots for (auto* column : selectedColumns()) column->addUsedInPlots(plots); //suppress retransform in the dependent plots for (auto* plot : plots) plot->setSuppressDataChangedSignal(true); //unmask the selected cells for (auto* column : selectedColumns()) { int col = m_spreadsheet->indexOfChild(column); for (int row = first; row <= last; row++) if (isCellSelected(row, col)) column->setMasked(row, false); } //retransform the dependent plots for (auto* plot : plots) { plot->setSuppressDataChangedSignal(false); plot->dataChanged(); } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::plotData() { const QAction* action = dynamic_cast(QObject::sender()); PlotDataDialog::PlotType type = PlotDataDialog::PlotXYCurve; if (action == action_plot_data_xycurve || action == action_plot_data_histogram) type = (PlotDataDialog::PlotType)action->data().toInt(); auto* dlg = new PlotDataDialog(m_spreadsheet, type); if (action != action_plot_data_xycurve && action != action_plot_data_histogram) { PlotDataDialog::AnalysisAction type = (PlotDataDialog::AnalysisAction)action->data().toInt(); dlg->setAnalysisAction(type); } dlg->exec(); } void SpreadsheetView::fillSelectedCellsWithRowNumbers() { if (selectedColumnCount() < 1) return; int first = firstSelectedRow(); if (first < 0) return; int last = lastSelectedRow(); WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: fill cells with row numbers", m_spreadsheet->name())); for (auto* col_ptr : selectedColumns()) { int col = m_spreadsheet->indexOfChild(col_ptr); col_ptr->setSuppressDataChangedSignal(true); switch (col_ptr->columnMode()) { case AbstractColumn::Numeric: { QVector results(last-first+1); for (int row = first; row <= last; row++) if (isCellSelected(row, col)) results[row-first] = row + 1; else results[row-first] = col_ptr->valueAt(row); col_ptr->replaceValues(first, results); break; } case AbstractColumn::Integer: { QVector results(last-first+1); for (int row = first; row <= last; row++) if (isCellSelected(row, col)) results[row-first] = row + 1; else results[row-first] = col_ptr->integerAt(row); col_ptr->replaceInteger(first, results); break; } case AbstractColumn::Text: { QVector results; for (int row = first; row <= last; row++) if (isCellSelected(row, col)) results << QString::number(row+1); else results << col_ptr->textAt(row); col_ptr->replaceTexts(first, results); break; } //TODO: handle other modes case AbstractColumn::DateTime: case AbstractColumn::Month: case AbstractColumn::Day: break; } col_ptr->setSuppressDataChangedSignal(false); col_ptr->setChanged(); } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::fillWithRowNumbers() { if (selectedColumnCount() < 1) return; WAIT_CURSOR; m_spreadsheet->beginMacro(i18np("%1: fill column with row numbers", "%1: fill columns with row numbers", m_spreadsheet->name(), selectedColumnCount())); const int rows = m_spreadsheet->rowCount(); QVector int_data(rows); for (int i = 0; i < rows; ++i) int_data[i] = i + 1; for (auto* col : selectedColumns()) { switch (col->columnMode()) { case AbstractColumn::Numeric: col->setColumnMode(AbstractColumn::Integer); col->replaceInteger(0, int_data); break; case AbstractColumn::Integer: col->replaceInteger(0, int_data); break; case AbstractColumn::Text: case AbstractColumn::DateTime: case AbstractColumn::Day: case AbstractColumn::Month: break; } } m_spreadsheet->endMacro(); RESET_CURSOR; } //TODO: this function is not used currently. void SpreadsheetView::fillSelectedCellsWithRandomNumbers() { if (selectedColumnCount() < 1) return; int first = firstSelectedRow(); int last = lastSelectedRow(); if (first < 0) return; WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: fill cells with random values", m_spreadsheet->name())); qsrand(QTime::currentTime().msec()); for (auto* col_ptr : selectedColumns()) { int col = m_spreadsheet->indexOfChild(col_ptr); col_ptr->setSuppressDataChangedSignal(true); switch (col_ptr->columnMode()) { case AbstractColumn::Numeric: { QVector results(last-first+1); for (int row = first; row <= last; row++) if (isCellSelected(row, col)) results[row-first] = double(qrand())/double(RAND_MAX); else results[row-first] = col_ptr->valueAt(row); col_ptr->replaceValues(first, results); break; } case AbstractColumn::Integer: { QVector results(last-first+1); for (int row = first; row <= last; row++) if (isCellSelected(row, col)) results[row-first] = qrand(); else results[row-first] = col_ptr->integerAt(row); col_ptr->replaceInteger(first, results); break; } case AbstractColumn::Text: { QVector results; for (int row = first; row <= last; row++) if (isCellSelected(row, col)) results << QString::number(double(qrand())/double(RAND_MAX)); else results << col_ptr->textAt(row); col_ptr->replaceTexts(first, results); break; } case AbstractColumn::DateTime: case AbstractColumn::Month: case AbstractColumn::Day: { QVector results; QDate earliestDate(1, 1, 1); QDate latestDate(2999, 12, 31); QTime midnight(0, 0, 0, 0); for (int row = first; row <= last; row++) if (isCellSelected(row, col)) results << QDateTime( earliestDate.addDays(((double)qrand())*((double)earliestDate.daysTo(latestDate))/((double)RAND_MAX)), midnight.addMSecs(((qint64)qrand())*1000*60*60*24/RAND_MAX)); else results << col_ptr->dateTimeAt(row); col_ptr->replaceDateTimes(first, results); break; } } col_ptr->setSuppressDataChangedSignal(false); col_ptr->setChanged(); } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::fillWithRandomValues() { if (selectedColumnCount() < 1) return; auto* dlg = new RandomValuesDialog(m_spreadsheet); dlg->setColumns(selectedColumns()); dlg->exec(); } void SpreadsheetView::fillWithEquidistantValues() { if (selectedColumnCount() < 1) return; auto* dlg = new EquidistantValuesDialog(m_spreadsheet); dlg->setColumns(selectedColumns()); dlg->exec(); } void SpreadsheetView::fillWithFunctionValues() { if (selectedColumnCount() < 1) return; auto* dlg = new FunctionValuesDialog(m_spreadsheet); dlg->setColumns(selectedColumns()); dlg->exec(); } void SpreadsheetView::fillSelectedCellsWithConstValues() { if (selectedColumnCount() < 1) return; int first = firstSelectedRow(); int last = lastSelectedRow(); if (first < 0) return; bool doubleOk = false; bool intOk = false; bool stringOk = false; double doubleValue = 0; int intValue = 0; QString stringValue; m_spreadsheet->beginMacro(i18n("%1: fill cells with const values", m_spreadsheet->name())); for (auto* col_ptr : selectedColumns()) { int col = m_spreadsheet->indexOfChild(col_ptr); col_ptr->setSuppressDataChangedSignal(true); switch (col_ptr->columnMode()) { case AbstractColumn::Numeric: if (!doubleOk) doubleValue = QInputDialog::getDouble(this, i18n("Fill the selection with constant value"), i18n("Value"), 0, -2147483647, 2147483647, 6, &doubleOk); if (doubleOk) { WAIT_CURSOR; QVector results(last-first+1); for (int row = first; row <= last; row++) { if (isCellSelected(row, col)) results[row-first] = doubleValue; else results[row-first] = col_ptr->valueAt(row); } col_ptr->replaceValues(first, results); RESET_CURSOR; } break; case AbstractColumn::Integer: if (!intOk) intValue = QInputDialog::getInt(this, i18n("Fill the selection with constant value"), i18n("Value"), 0, -2147483647, 2147483647, 1, &intOk); if (intOk) { WAIT_CURSOR; QVector results(last-first+1); for (int row = first; row <= last; row++) { if (isCellSelected(row, col)) results[row-first] = intValue; else results[row-first] = col_ptr->integerAt(row); } col_ptr->replaceInteger(first, results); RESET_CURSOR; } break; case AbstractColumn::Text: if (!stringOk) stringValue = QInputDialog::getText(this, i18n("Fill the selection with constant value"), i18n("Value"), QLineEdit::Normal, nullptr, &stringOk); if (stringOk && !stringValue.isEmpty()) { WAIT_CURSOR; QVector results; for (int row = first; row <= last; row++) { if (isCellSelected(row, col)) results << stringValue; else results << col_ptr->textAt(row); } col_ptr->replaceTexts(first, results); RESET_CURSOR; } break; //TODO: handle other modes case AbstractColumn::DateTime: case AbstractColumn::Month: case AbstractColumn::Day: break; } col_ptr->setSuppressDataChangedSignal(false); col_ptr->setChanged(); } m_spreadsheet->endMacro(); } /*! Open the sort dialog for all columns. */ void SpreadsheetView::sortSpreadsheet() { sortDialog(m_spreadsheet->children()); } /*! Insert an empty column left to the first selected column */ void SpreadsheetView::insertColumnLeft() { insertColumnsLeft(1); } /*! Insert multiple empty columns left to the firt selected column */ void SpreadsheetView::insertColumnsLeft() { bool ok = false; int count = QInputDialog::getInt(nullptr, i18n("Insert empty columns"), i18n("Enter the number of columns to insert"), 1/*value*/, 1/*min*/, 1000/*max*/, 1/*step*/, &ok); if (!ok) return; insertColumnsLeft(count); } /*! * private helper function doing the actual insertion of columns to the left */ void SpreadsheetView::insertColumnsLeft(int count) { WAIT_CURSOR; m_spreadsheet->beginMacro(i18np("%1: insert empty column", "%1: insert empty columns", m_spreadsheet->name(), count )); const int first = firstSelectedColumn(); if (first >= 0) { //determine the first selected column Column* firstCol = m_spreadsheet->child(first); for (int i = 0; i < count; ++i) { Column* newCol = new Column(QString::number(i + 1), AbstractColumn::Numeric); newCol->setPlotDesignation(AbstractColumn::Y); //resize the new column and insert it before the first selected column newCol->insertRows(0, m_spreadsheet->rowCount()); m_spreadsheet->insertChildBefore(newCol, firstCol); } } else { if (m_spreadsheet->columnCount()>0) { //columns available but no columns selected -> prepend the new column at the very beginning Column* firstCol = m_spreadsheet->child(0); for (int i = 0; i < count; ++i) { Column* newCol = new Column(QString::number(i + 1), AbstractColumn::Numeric); newCol->setPlotDesignation(AbstractColumn::Y); newCol->insertRows(0, m_spreadsheet->rowCount()); m_spreadsheet->insertChildBefore(newCol, firstCol); } } else { //no columns available anymore -> resize the spreadsheet and the new column to the default size KConfigGroup group = KSharedConfig::openConfig()->group(QLatin1String("Spreadsheet")); const int rows = group.readEntry(QLatin1String("RowCount"), 100); m_spreadsheet->setRowCount(rows); for (int i = 0; i < count; ++i) { Column* newCol = new Column(QString::number(i + 1), AbstractColumn::Numeric); (i == 0) ? newCol->setPlotDesignation(AbstractColumn::X) : newCol->setPlotDesignation(AbstractColumn::Y); newCol->insertRows(0, rows); //add/append a new column m_spreadsheet->addChild(newCol); } } } m_spreadsheet->endMacro(); RESET_CURSOR; } /*! Insert an empty column right to the last selected column */ void SpreadsheetView::insertColumnRight() { insertColumnsRight(1); } /*! Insert multiple empty columns right to the last selected column */ void SpreadsheetView::insertColumnsRight() { bool ok = false; int count = QInputDialog::getInt(nullptr, i18n("Insert empty columns"), i18n("Enter the number of columns to insert"), 1/*value*/, 1/*min*/, 1000/*max*/, 1/*step*/, &ok); if (!ok) return; insertColumnsRight(count); } /*! * private helper function doing the actual insertion of columns to the right */ void SpreadsheetView::insertColumnsRight(int count) { WAIT_CURSOR; m_spreadsheet->beginMacro(i18np("%1: insert empty column", "%1: insert empty columns", m_spreadsheet->name(), count )); const int last = lastSelectedColumn(); if (last >= 0) { if (last < m_spreadsheet->columnCount() - 1) { //determine the column next to the last selected column Column* nextCol = m_spreadsheet->child(last + 1); for (int i = 0; i < count; ++i) { Column* newCol = new Column(QString::number(i+1), AbstractColumn::Numeric); newCol->setPlotDesignation(AbstractColumn::Y); newCol->insertRows(0, m_spreadsheet->rowCount()); //insert the new column before the column next to the last selected column m_spreadsheet->insertChildBefore(newCol, nextCol); } } else { for (int i = 0; i < count; ++i) { Column* newCol = new Column(QString::number(i+1), AbstractColumn::Numeric); newCol->setPlotDesignation(AbstractColumn::Y); newCol->insertRows(0, m_spreadsheet->rowCount()); //last column selected, no next column available -> add/append a new column m_spreadsheet->addChild(newCol); } } } else { if (m_spreadsheet->columnCount()>0) { for (int i = 0; i < count; ++i) { Column* newCol = new Column(QString::number(i+1), AbstractColumn::Numeric); newCol->setPlotDesignation(AbstractColumn::Y); newCol->insertRows(0, m_spreadsheet->rowCount()); //columns available but no columns selected -> append the new column at the very end m_spreadsheet->addChild(newCol); } } else { //no columns available anymore -> resize the spreadsheet and the new column to the default size KConfigGroup group = KSharedConfig::openConfig()->group(QLatin1String("Spreadsheet")); const int rows = group.readEntry(QLatin1String("RowCount"), 100); m_spreadsheet->setRowCount(rows); for (int i = 0; i < count; ++i) { Column* newCol = new Column(QString::number(i+1), AbstractColumn::Numeric); (i == 0) ? newCol->setPlotDesignation(AbstractColumn::X) : newCol->setPlotDesignation(AbstractColumn::Y); newCol->insertRows(0, rows); //add/append a new column m_spreadsheet->addChild(newCol); } } } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::removeSelectedColumns() { WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: remove selected columns", m_spreadsheet->name())); for (auto* column : selectedColumns()) m_spreadsheet->removeChild(column); m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::clearSelectedColumns() { WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: clear selected columns", m_spreadsheet->name())); if (formulaModeActive()) { for (auto* col : selectedColumns()) { col->setSuppressDataChangedSignal(true); col->clearFormulas(); col->setSuppressDataChangedSignal(false); col->setChanged(); } } else { for (auto* col : selectedColumns()) { col->setSuppressDataChangedSignal(true); col->clear(); col->setSuppressDataChangedSignal(false); col->setChanged(); } } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::setSelectionAs() { QVector columns = selectedColumns(); if (!columns.size()) return; m_spreadsheet->beginMacro(i18n("%1: set plot designation", m_spreadsheet->name())); QAction* action = dynamic_cast(QObject::sender()); if (!action) return; AbstractColumn::PlotDesignation pd = (AbstractColumn::PlotDesignation)action->data().toInt(); for (auto* col : columns) col->setPlotDesignation(pd); m_spreadsheet->endMacro(); } /*! * add, subtract, multiply, divide */ void SpreadsheetView::modifyValues() { if (selectedColumnCount() < 1) return; const QAction* action = dynamic_cast(QObject::sender()); AddSubtractValueDialog::Operation op = (AddSubtractValueDialog::Operation)action->data().toInt(); auto* dlg = new AddSubtractValueDialog(m_spreadsheet, op); dlg->setColumns(selectedColumns()); dlg->exec(); } void SpreadsheetView::reverseColumns() { WAIT_CURSOR; QVector cols = selectedColumns(); m_spreadsheet->beginMacro(i18np("%1: reverse column", "%1: reverse columns", m_spreadsheet->name(), cols.size())); for (auto* col : cols) { if (col->columnMode() != AbstractColumn::Numeric) continue; auto* data = static_cast* >(col->data()); QVector new_data(*data); std::reverse(new_data.begin(), new_data.end()); col->replaceValues(0, new_data); } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::dropColumnValues() { if (selectedColumnCount() < 1) return; auto* dlg = new DropValuesDialog(m_spreadsheet); dlg->setColumns(selectedColumns()); dlg->exec(); } void SpreadsheetView::maskColumnValues() { if (selectedColumnCount() < 1) return; auto* dlg = new DropValuesDialog(m_spreadsheet, true); dlg->setColumns(selectedColumns()); dlg->exec(); } void SpreadsheetView::joinColumns() { //TODO } void SpreadsheetView::normalizeSelectedColumns() { WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: normalize columns", m_spreadsheet->name())); for (auto* col : selectedColumns()) { if (col->columnMode() == AbstractColumn::Numeric) { col->setSuppressDataChangedSignal(true); double max = col->maximum(); if (max != 0.0) {// avoid division by zero for (int row = 0; row < col->rowCount(); row++) col->setValueAt(row, col->valueAt(row) / max); } col->setSuppressDataChangedSignal(false); col->setChanged(); } } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::normalizeSelection() { WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: normalize selection", m_spreadsheet->name())); double max = 0.0; for (int col = firstSelectedColumn(); col <= lastSelectedColumn(); col++) if (m_spreadsheet->column(col)->columnMode() == AbstractColumn::Numeric) for (int row = 0; row < m_spreadsheet->rowCount(); row++) { if (isCellSelected(row, col) && m_spreadsheet->column(col)->valueAt(row) > max) max = m_spreadsheet->column(col)->valueAt(row); } if (max != 0.0) { // avoid division by zero //TODO setSuppressDataChangedSignal for (int col = firstSelectedColumn(); col <= lastSelectedColumn(); col++) if (m_spreadsheet->column(col)->columnMode() == AbstractColumn::Numeric) for (int row = 0; row < m_spreadsheet->rowCount(); row++) { if (isCellSelected(row, col)) m_spreadsheet->column(col)->setValueAt(row, m_spreadsheet->column(col)->valueAt(row) / max); } } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::sortSelectedColumns() { sortDialog(selectedColumns()); } void SpreadsheetView::showAllColumnsStatistics() { showColumnStatistics(true); } void SpreadsheetView::showColumnStatistics(bool forAll) { QString dlgTitle(m_spreadsheet->name() + " column statistics"); auto* dlg = new StatisticsDialog(dlgTitle); QVector columns; if (!forAll) dlg->setColumns(selectedColumns()); else if (forAll) { for (int col = 0; col < m_spreadsheet->columnCount(); ++col) { if (m_spreadsheet->column(col)->columnMode() == AbstractColumn::Numeric) columns << m_spreadsheet->column(col); } dlg->setColumns(columns); } if (dlg->exec() == QDialog::Accepted) { if (forAll) columns.clear(); } } void SpreadsheetView::showRowStatistics() { QString dlgTitle(m_spreadsheet->name() + " row statistics"); auto* dlg = new StatisticsDialog(dlgTitle); QVector columns; for (int i = 0; i < m_spreadsheet->rowCount(); ++i) { if (isRowSelected(i)) { QVector rowValues; for (int j = 0; j < m_spreadsheet->columnCount(); ++j) rowValues << m_spreadsheet->column(j)->valueAt(i); columns << new Column(QString::number(i+1), rowValues); } } dlg->setColumns(columns); if (dlg->exec() == QDialog::Accepted) { qDeleteAll(columns); columns.clear(); } } /*! Insert an empty row above(=before) the first selected row */ void SpreadsheetView::insertRowAbove() { insertRowsAbove(1); } /*! Insert multiple empty rows above(=before) the first selected row */ void SpreadsheetView::insertRowsAbove() { bool ok = false; int count = QInputDialog::getInt(nullptr, i18n("Insert multiple rows"), i18n("Enter the number of rows to insert"), 1/*value*/, 1/*min*/, 1000000/*max*/, 1/*step*/, &ok); if (ok) insertRowsAbove(count); } /*! * private helper function doing the actual insertion of rows above */ void SpreadsheetView::insertRowsAbove(int count) { int first = firstSelectedRow(); if (first < 0) return; WAIT_CURSOR; m_spreadsheet->beginMacro(i18np("%1: insert empty row", "%1: insert empty rows", m_spreadsheet->name(), count )); m_spreadsheet->insertRows(first, count); m_spreadsheet->endMacro(); RESET_CURSOR; } /*! Insert an empty row below the last selected row */ void SpreadsheetView::insertRowBelow() { insertRowsBelow(1); } /*! Insert an empty row below the last selected row */ void SpreadsheetView::insertRowsBelow() { bool ok = false; int count = QInputDialog::getInt(nullptr, i18n("Insert multiple rows"), i18n("Enter the number of rows to insert"), 1/*value*/, 1/*min*/, 1000000/*max*/, 1/*step*/, &ok); if (ok) insertRowsBelow(count); } /*! * private helper function doing the actual insertion of rows below */ void SpreadsheetView::insertRowsBelow(int count) { int last = lastSelectedRow(); if (last < 0) return; WAIT_CURSOR; m_spreadsheet->beginMacro(i18np("%1: insert empty row", "%1: insert empty rows", m_spreadsheet->name(), count )); if (last < m_spreadsheet->rowCount() - 1) m_spreadsheet->insertRows(last + 1, count); //insert before the next to the last selected row else m_spreadsheet->appendRows(count); //append new rows at the end m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::removeSelectedRows() { if (firstSelectedRow() < 0) return; WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: remove selected rows", m_spreadsheet->name())); //TODO setSuppressDataChangedSignal for (const auto& i : selectedRows().intervals()) m_spreadsheet->removeRows(i.start(), i.size()); m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::clearSelectedRows() { if (firstSelectedRow() < 0) return; WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: clear selected rows", m_spreadsheet->name())); for (auto* col : selectedColumns()) { col->setSuppressDataChangedSignal(true); if (formulaModeActive()) { for (const auto& i : selectedRows().intervals()) col->setFormula(i, QString()); } else { for (const auto& i : selectedRows().intervals()) { if (i.end() == col->rowCount()-1) col->removeRows(i.start(), i.size()); else { QVector empties; for (int j = 0; j < i.size(); j++) empties << QString(); col->asStringColumn()->replaceTexts(i.start(), empties); } } } col->setSuppressDataChangedSignal(false); col->setChanged(); } m_spreadsheet->endMacro(); RESET_CURSOR; //selected rows were deleted but the view selection is still in place -> reset the selection in the view m_tableView->clearSelection(); } void SpreadsheetView::clearSelectedCells() { int first = firstSelectedRow(); int last = lastSelectedRow(); if (first < 0) return; //don't try to clear values if the selected cells don't have any values at all bool empty = true; for (auto* column : selectedColumns()) { for (int row = last; row >= first; row--) { if (column->isValid(row)) { empty = false; break; } } if (!empty) break; } if (empty) return; WAIT_CURSOR; m_spreadsheet->beginMacro(i18n("%1: clear selected cells", m_spreadsheet->name())); for (auto* column : selectedColumns()) { column->setSuppressDataChangedSignal(true); if (formulaModeActive()) { int col = m_spreadsheet->indexOfChild(column); for (int row = last; row >= first; row--) if (isCellSelected(row, col)) column->setFormula(row, QString()); } else { int col = m_spreadsheet->indexOfChild(column); for (int row = last; row >= first; row--) if (isCellSelected(row, col)) { if (row < column->rowCount()) column->asStringColumn()->setTextAt(row, QString()); } } column->setSuppressDataChangedSignal(false); column->setChanged(); } m_spreadsheet->endMacro(); RESET_CURSOR; } void SpreadsheetView::goToCell() { bool ok; int col = QInputDialog::getInt(nullptr, i18n("Go to Cell"), i18n("Enter column"), 1, 1, m_spreadsheet->columnCount(), 1, &ok); if (!ok) return; int row = QInputDialog::getInt(nullptr, i18n("Go to Cell"), i18n("Enter row"), 1, 1, m_spreadsheet->rowCount(), 1, &ok); if (!ok) return; goToCell(row-1, col-1); } //! Open the sort dialog for the given columns void SpreadsheetView::sortDialog(QVector cols) { if (cols.isEmpty()) return; for (auto* col : cols) col->setSuppressDataChangedSignal(true); auto* dlg = new SortDialog(); connect(dlg, SIGNAL(sort(Column*,QVector,bool)), m_spreadsheet, SLOT(sortColumns(Column*,QVector,bool))); dlg->setColumns(cols); int rc = dlg->exec(); for (auto* col : cols) { col->setSuppressDataChangedSignal(false); if (rc == QDialog::Accepted) col->setChanged(); } } void SpreadsheetView::sortColumnAscending() { QVector cols = selectedColumns(); for (auto* col : cols) col->setSuppressDataChangedSignal(true); m_spreadsheet->sortColumns(cols.first(), cols, true); for (auto* col : cols) { col->setSuppressDataChangedSignal(false); col->setChanged(); } } void SpreadsheetView::sortColumnDescending() { QVector cols = selectedColumns(); for (auto* col : cols) col->setSuppressDataChangedSignal(true); m_spreadsheet->sortColumns(cols.first(), cols, false); for (auto* col : cols) { col->setSuppressDataChangedSignal(false); col->setChanged(); } } /*! Cause a repaint of the header. */ void SpreadsheetView::updateHeaderGeometry(Qt::Orientation o, int first, int last) { Q_UNUSED(first) Q_UNUSED(last) //TODO if (o != Qt::Horizontal) return; m_tableView->horizontalHeader()->setStretchLastSection(true); // ugly hack (flaw in Qt? Does anyone know a better way?) m_tableView->horizontalHeader()->updateGeometry(); m_tableView->horizontalHeader()->setStretchLastSection(false); // ugly hack part 2 } /*! selects the column \c column in the speadsheet view . */ void SpreadsheetView::selectColumn(int column) { QItemSelection selection(m_model->index(0, column), m_model->index(m_spreadsheet->rowCount()-1, column) ); m_suppressSelectionChangedEvent = true; m_tableView->selectionModel()->select(selection, QItemSelectionModel::Select); m_suppressSelectionChangedEvent = false; } /*! deselects the column \c column in the speadsheet view . */ void SpreadsheetView::deselectColumn(int column) { QItemSelection selection(m_model->index(0, column), m_model->index(m_spreadsheet->rowCount()-1, column) ); m_suppressSelectionChangedEvent = true; m_tableView->selectionModel()->select(selection, QItemSelectionModel::Deselect); m_suppressSelectionChangedEvent = false; } /*! called when a column in the speadsheet view was clicked (click in the header). Propagates the selection of the column to the \c Spreadsheet object (a click in the header always selects the column). */ void SpreadsheetView::columnClicked(int column) { m_spreadsheet->setColumnSelectedInView(column, true); } /*! called on selections changes. Propagates the selection/deselection of columns to the \c Spreadsheet object. */ void SpreadsheetView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(selected); Q_UNUSED(deselected); if (m_suppressSelectionChangedEvent) return; QItemSelectionModel* selModel = m_tableView->selectionModel(); for (int i = 0; i < m_spreadsheet->columnCount(); i++) m_spreadsheet->setColumnSelectedInView(i, selModel->isColumnSelected(i, QModelIndex())); } bool SpreadsheetView::exportView() { auto* dlg = new ExportSpreadsheetDialog(this); dlg->setFileName(m_spreadsheet->name()); dlg->setExportTo(QStringList() << i18n("FITS image") << i18n("FITS table")); for (int i = 0; i < m_spreadsheet->columnCount(); ++i) { if (m_spreadsheet->column(i)->columnMode() != AbstractColumn::Numeric) { dlg->setExportToImage(false); break; } } if (selectedColumnCount() == 0) dlg->setExportSelection(false); bool ret; if ((ret = dlg->exec()) == QDialog::Accepted) { const QString path = dlg->path(); const bool exportHeader = dlg->exportHeader(); WAIT_CURSOR; switch (dlg->format()) { case ExportSpreadsheetDialog::ASCII: { const QString separator = dlg->separator(); const QLocale::Language format = dlg->numberFormat(); exportToFile(path, exportHeader, separator, format); break; } case ExportSpreadsheetDialog::Binary: break; case ExportSpreadsheetDialog::LaTeX: { const bool exportLatexHeader = dlg->exportLatexHeader(); const bool gridLines = dlg->gridLines(); const bool captions = dlg->captions(); const bool skipEmptyRows = dlg->skipEmptyRows(); const bool exportEntire = dlg->entireSpreadheet(); exportToLaTeX(path, exportHeader, gridLines, captions, exportLatexHeader, skipEmptyRows, exportEntire); break; } case ExportSpreadsheetDialog::FITS: { const int exportTo = dlg->exportToFits(); const bool commentsAsUnits = dlg->commentsAsUnitsFits(); exportToFits(path, exportTo, commentsAsUnits); break; } case ExportSpreadsheetDialog::SQLite: exportToSQLite(path); break; } RESET_CURSOR; } delete dlg; return ret; } bool SpreadsheetView::printView() { QPrinter printer; auto* dlg = new QPrintDialog(&printer, this); dlg->setWindowTitle(i18nc("@title:window", "Print Spreadsheet")); bool ret; if ((ret = dlg->exec()) == QDialog::Accepted) { print(&printer); } delete dlg; return ret; } bool SpreadsheetView::printPreview() { QPrintPreviewDialog* dlg = new QPrintPreviewDialog(this); connect(dlg, &QPrintPreviewDialog::paintRequested, this, &SpreadsheetView::print); return dlg->exec(); } /*! prints the complete spreadsheet to \c printer. */ void SpreadsheetView::print(QPrinter* printer) const { WAIT_CURSOR; QPainter painter (printer); const int dpiy = printer->logicalDpiY(); const int margin = (int) ( (1/2.54)*dpiy ); // 1 cm margins QHeaderView *hHeader = m_tableView->horizontalHeader(); QHeaderView *vHeader = m_tableView->verticalHeader(); const int rows = m_spreadsheet->rowCount(); const int cols = m_spreadsheet->columnCount(); int height = margin; const int vertHeaderWidth = vHeader->width(); int columnsPerTable = 0; int headerStringWidth = 0; int firstRowStringWidth = 0; bool tablesNeeded = false; for (int col = 0; col < cols; ++col) { headerStringWidth += m_tableView->columnWidth(col); firstRowStringWidth += m_spreadsheet->column(col)->asStringColumn()->textAt(0).length(); if ((headerStringWidth >= printer->pageRect().width() -2*margin) || (firstRowStringWidth >= printer->pageRect().width() - 2*margin)) { tablesNeeded = true; break; } columnsPerTable++; } int tablesCount = (columnsPerTable != 0) ? cols/columnsPerTable : 0; const int remainingColumns = (columnsPerTable != 0) ? cols % columnsPerTable : cols; if (!tablesNeeded) { tablesCount = 1; columnsPerTable = cols; } if (remainingColumns > 0) tablesCount++; //Paint the horizontal header first for (int table = 0; table < tablesCount; ++table) { int right = margin + vertHeaderWidth; painter.setFont(hHeader->font()); QString headerString = m_tableView->model()->headerData(0, Qt::Horizontal).toString(); QRect br; br = painter.boundingRect(br, Qt::AlignCenter, headerString); QRect tr(br); if (table != 0) height += tr.height(); painter.drawLine(right, height, right, height+br.height()); int i = table * columnsPerTable; int toI = table * columnsPerTable + columnsPerTable; if ((remainingColumns > 0) && (table == tablesCount-1)) { i = (tablesCount-1)*columnsPerTable; toI = (tablesCount-1)* columnsPerTable + remainingColumns; } for (; imodel()->headerData(i, Qt::Horizontal).toString(); const int w = m_tableView->columnWidth(i); tr.setTopLeft(QPoint(right,height)); tr.setWidth(w); tr.setHeight(br.height()); painter.drawText(tr, Qt::AlignCenter, headerString); right += w; painter.drawLine(right, height, right, height+tr.height()); } painter.drawLine(margin + vertHeaderWidth, height, right-1, height);//first horizontal line height += tr.height(); painter.drawLine(margin, height, right-1, height); // print table values QString cellText; for (i = 0; i < rows; ++i) { right = margin; cellText = m_tableView->model()->headerData(i, Qt::Vertical).toString()+'\t'; tr = painter.boundingRect(tr, Qt::AlignCenter, cellText); painter.drawLine(right, height, right, height+tr.height()); br.setTopLeft(QPoint(right,height)); br.setWidth(vertHeaderWidth); br.setHeight(tr.height()); painter.drawText(br, Qt::AlignCenter, cellText); right += vertHeaderWidth; painter.drawLine(right, height, right, height+tr.height()); int j = table * columnsPerTable; int toJ = table * columnsPerTable + columnsPerTable; if ((remainingColumns > 0) && (table == tablesCount-1)) { j = (tablesCount-1)*columnsPerTable; toJ = (tablesCount-1)* columnsPerTable + remainingColumns; } for (; j < toJ; j++) { int w = m_tableView->columnWidth(j); cellText = m_spreadsheet->column(j)->isValid(i) ? m_spreadsheet->text(i,j)+'\t': QLatin1String("- \t"); tr = painter.boundingRect(tr,Qt::AlignCenter,cellText); br.setTopLeft(QPoint(right,height)); br.setWidth(w); br.setHeight(tr.height()); painter.drawText(br, Qt::AlignCenter, cellText); right += w; painter.drawLine(right, height, right, height+tr.height()); } height += br.height(); painter.drawLine(margin, height, right-1, height); if (height >= printer->height() - margin) { printer->newPage(); height = margin; painter.drawLine(margin, height, right, height); } } } RESET_CURSOR; } void SpreadsheetView::registerShortcuts() { action_clear_selection->setShortcut(QKeySequence::Delete); } void SpreadsheetView::unregisterShortcuts() { action_clear_selection->setShortcut(QKeySequence()); } void SpreadsheetView::exportToFile(const QString& path, const bool exportHeader, const QString& separator, QLocale::Language language) const { QFile file(path); if (!file.open(QFile::WriteOnly | QFile::Truncate)) return; PERFTRACE("export spreadsheet to file"); QTextStream out(&file); const int cols = m_spreadsheet->columnCount(); QString sep = separator; sep = sep.replace(QLatin1String("TAB"), QLatin1String("\t"), Qt::CaseInsensitive); sep = sep.replace(QLatin1String("SPACE"), QLatin1String(" "), Qt::CaseInsensitive); //export header (column names) if (exportHeader) { for (int j = 0; j < cols; ++j) { out << m_spreadsheet->column(j)->name(); if (j != cols - 1) out << sep; } out << '\n'; } //export values QLocale locale(language); for (int i = 0; i < m_spreadsheet->rowCount(); ++i) { for (int j = 0; j < cols; ++j) { Column* col = m_spreadsheet->column(j); if (col->columnMode() == AbstractColumn::Numeric) { const Double2StringFilter* out_fltr = static_cast(col->outputFilter()); out << locale.toString(col->valueAt(i), out_fltr->numericFormat(), 16); // export with max. precision } else out << col->asStringColumn()->textAt(i); if (j != cols - 1) out << sep; } out << '\n'; } } void SpreadsheetView::exportToLaTeX(const QString & path, const bool exportHeaders, const bool gridLines, const bool captions, const bool latexHeaders, const bool skipEmptyRows, const bool exportEntire) const { QFile file(path); if (!file.open(QFile::WriteOnly | QFile::Truncate)) return; QList toExport; int cols; int totalRowCount = 0; if (exportEntire) { cols = const_cast(this)->m_spreadsheet->columnCount(); totalRowCount = m_spreadsheet->rowCount(); for (int col = 0; col < cols; ++col) toExport << m_spreadsheet->column(col); } else { cols = const_cast(this)->selectedColumnCount(); const int firtsSelectedCol = const_cast(this)->firstSelectedColumn(); bool rowsCalculated = false; for (int col = firtsSelectedCol; col < firtsSelectedCol + cols; ++col) { QVector textData; for (int row = 0; row < m_spreadsheet->rowCount(); ++row) { if (const_cast(this)->isRowSelected(row)) { textData << m_spreadsheet->column(col)->asStringColumn()->textAt(row); if (!rowsCalculated) totalRowCount++; } } if (!rowsCalculated) rowsCalculated = true; Column* column = new Column(m_spreadsheet->column(col)->name(), textData); toExport << column; } } int columnsStringSize = 0; int columnsPerTable = 0; for (int i = 0; i < cols; ++i) { int maxSize = -1; for (int j = 0; j < toExport.at(i)->asStringColumn()->rowCount(); ++j) { if (toExport.at(i)->asStringColumn()->textAt(j).size() > maxSize) maxSize = toExport.at(i)->asStringColumn()->textAt(j).size(); } columnsStringSize += maxSize; if (!toExport.at(i)->isValid(0)) columnsStringSize += 3; if (columnsStringSize > 65) break; ++columnsPerTable; } const int tablesCount = (columnsPerTable != 0) ? cols/columnsPerTable : 0; const int remainingColumns = (columnsPerTable != 0) ? cols % columnsPerTable : cols; bool columnsSeparating = (cols > columnsPerTable); QTextStream out(&file); QProcess tex; tex.start("latex", QStringList() << "--version", QProcess::ReadOnly); tex.waitForFinished(500); QString texVersionOutput = QString(tex.readAllStandardOutput()); texVersionOutput = texVersionOutput.split('\n')[0]; int yearidx = -1; for (int i = texVersionOutput.size() - 1; i >= 0; --i) { if (texVersionOutput.at(i) == QChar('2')) { yearidx = i; break; } } if (texVersionOutput.at(yearidx+1) == QChar('/')) yearidx -= 3; bool ok; texVersionOutput.midRef(yearidx, 4).toInt(&ok); int version = -1; if (ok) version = texVersionOutput.midRef(yearidx, 4).toInt(&ok); if (latexHeaders) { out << QLatin1String("\\documentclass[11pt,a4paper]{article} \n"); out << QLatin1String("\\usepackage{geometry} \n"); out << QLatin1String("\\usepackage{xcolor,colortbl} \n"); if (version >= 2015) out << QLatin1String("\\extrafloats{1280} \n"); out << QLatin1String("\\definecolor{HeaderBgColor}{rgb}{0.81,0.81,0.81} \n"); out << QLatin1String("\\geometry{ \n"); out << QLatin1String("a4paper, \n"); out << QLatin1String("total={170mm,257mm}, \n"); out << QLatin1String("left=10mm, \n"); out << QLatin1String("top=10mm } \n"); out << QLatin1String("\\begin{document} \n"); out << QLatin1String("\\title{LabPlot Spreadsheet Export to \\LaTeX{} } \n"); out << QLatin1String("\\author{LabPlot} \n"); out << QLatin1String("\\date{\\today} \n"); } QString endTabularTable ("\\end{tabular} \n \\end{table} \n"); QString tableCaption ("\\caption{"+ m_spreadsheet->name()+ "} \n"); QString beginTable ("\\begin{table}[ht] \n"); int rowCount = 0; const int maxRows = 45; bool captionRemoved = false; if (columnsSeparating) { QVector emptyRowIndices; for (int table = 0; table < tablesCount; ++table) { QStringList textable; captionRemoved = false; textable << beginTable; if (captions) textable << tableCaption; textable << QLatin1String("\\centering \n"); textable << QLatin1String("\\begin{tabular}{") << (gridLines ? QStringLiteral("|") : QString()); for (int i = 0; i < columnsPerTable; ++i) textable << ( gridLines ? QLatin1String(" c |") : QLatin1String(" c ") ); textable << QLatin1String("} \n"); if (gridLines) textable << QLatin1String("\\hline \n"); if (exportHeaders) { if (latexHeaders) textable << QLatin1String("\\rowcolor{HeaderBgColor} \n"); for (int col = table*columnsPerTable; col < (table * columnsPerTable) + columnsPerTable; ++col) { textable << toExport.at(col)->name(); if (col != ((table * columnsPerTable)+ columnsPerTable)-1) textable << QLatin1String(" & "); } textable << QLatin1String("\\\\ \n"); if (gridLines) textable << QLatin1String("\\hline \n"); } for (const auto& s : textable) out << s; QStringList values; for (int row = 0; row < totalRowCount; ++row) { values.clear(); bool notEmpty = false; for (int col = table*columnsPerTable; col < (table * columnsPerTable) + columnsPerTable; ++col ) { if (toExport.at(col)->isValid(row)) { notEmpty = true; values << toExport.at(col)->asStringColumn()->textAt(row); } else values << QLatin1String("-"); if (col != ((table * columnsPerTable)+ columnsPerTable)-1) values << QLatin1String(" & "); } if (!notEmpty && skipEmptyRows) { if (!emptyRowIndices.contains(row)) emptyRowIndices << row; } if (emptyRowIndices.contains(row) && notEmpty) emptyRowIndices.remove(emptyRowIndices.indexOf(row)); if (notEmpty || !skipEmptyRows) { for (const auto& s : values) out << s; out << QLatin1String("\\\\ \n"); if (gridLines) out << QLatin1String("\\hline \n"); rowCount++; if (rowCount == maxRows) { out << endTabularTable; out << QLatin1String("\\newpage \n"); if (captions) if (!captionRemoved) textable.removeAt(1); for (const auto& s : textable) out << s; rowCount = 0; if (!captionRemoved) captionRemoved = true; } } } out << endTabularTable; } //new table for the remaining columns QStringList remainingTable; remainingTable << beginTable; if (captions) remainingTable << tableCaption; remainingTable << QLatin1String("\\centering \n"); remainingTable << QLatin1String("\\begin{tabular}{") << (gridLines ? QStringLiteral("|") : QString()); for (int c = 0; c < remainingColumns; ++c) remainingTable << ( gridLines ? QLatin1String(" c |") : QLatin1String(" c ") ); remainingTable << QLatin1String("} \n"); if (gridLines) remainingTable << QLatin1String("\\hline \n"); if (exportHeaders) { if (latexHeaders) remainingTable << QLatin1String("\\rowcolor{HeaderBgColor} \n"); for (int col = 0; col < remainingColumns; ++col) { remainingTable << toExport.at(col + (tablesCount * columnsPerTable))->name(); if (col != remainingColumns-1) remainingTable << QLatin1String(" & "); } remainingTable << QLatin1String("\\\\ \n"); if (gridLines) remainingTable << QLatin1String("\\hline \n"); } for (const auto& s : remainingTable) out << s; QStringList values; captionRemoved = false; for (int row = 0; row < totalRowCount; ++row) { values.clear(); bool notEmpty = false; for (int col = 0; col < remainingColumns; ++col ) { if (toExport.at(col + (tablesCount * columnsPerTable))->isValid(row)) { notEmpty = true; values << toExport.at(col + (tablesCount * columnsPerTable))->asStringColumn()->textAt(row); } else values << QLatin1String("-"); if (col != remainingColumns-1) values << QLatin1String(" & "); } if (!emptyRowIndices.contains(row) && !notEmpty) notEmpty = true; if (notEmpty || !skipEmptyRows) { for (const auto& s : values) out << s; out << QLatin1String("\\\\ \n"); if (gridLines) out << QLatin1String("\\hline \n"); rowCount++; if (rowCount == maxRows) { out << endTabularTable; out << QLatin1String("\\pagebreak[4] \n"); if (captions) if (!captionRemoved) remainingTable.removeAt(1); for (const auto& s : remainingTable) out << s; rowCount = 0; if (!captionRemoved) captionRemoved = true; } } } out << endTabularTable; } else { QStringList textable; textable << beginTable; if (captions) textable << tableCaption; textable << QLatin1String("\\centering \n"); textable << QLatin1String("\\begin{tabular}{") << (gridLines ? QStringLiteral("|") : QString()); for (int c = 0; c < cols; ++c) textable << ( gridLines ? QLatin1String(" c |") : QLatin1String(" c ") ); textable << QLatin1String("} \n"); if (gridLines) textable << QLatin1String("\\hline \n"); if (exportHeaders) { if (latexHeaders) textable << QLatin1String("\\rowcolor{HeaderBgColor} \n"); for (int col = 0; col < cols; ++col) { textable << toExport.at(col)->name(); if (col != cols-1) textable << QLatin1String(" & "); } textable << QLatin1String("\\\\ \n"); if (gridLines) textable << QLatin1String("\\hline \n"); } for (const auto& s : textable) out << s; QStringList values; captionRemoved = false; for (int row = 0; row < totalRowCount; ++row) { values.clear(); bool notEmpty = false; for (int col = 0; col < cols; ++col ) { if (toExport.at(col)->isValid(row)) { notEmpty = true; values << toExport.at(col)->asStringColumn()->textAt(row); } else values << "-"; if (col != cols-1) values << " & "; } if (notEmpty || !skipEmptyRows) { for (const auto& s : values) out << s; out << QLatin1String("\\\\ \n"); if (gridLines) out << QLatin1String("\\hline \n"); rowCount++; if (rowCount == maxRows) { out << endTabularTable; out << QLatin1String("\\newpage \n"); if (captions) if (!captionRemoved) textable.removeAt(1); for (const auto& s : textable) out << s; rowCount = 0; if (!captionRemoved) captionRemoved = true; } } } out << endTabularTable; } if (latexHeaders) out << QLatin1String("\\end{document} \n"); if (!exportEntire) { qDeleteAll(toExport); toExport.clear(); } else toExport.clear(); } void SpreadsheetView::exportToFits(const QString &fileName, const int exportTo, const bool commentsAsUnits) const { auto* filter = new FITSFilter; filter->setExportTo(exportTo); filter->setCommentsAsUnits(commentsAsUnits); filter->write(fileName, m_spreadsheet); delete filter; } void SpreadsheetView::exportToSQLite(const QString& path) const { QFile file(path); if (!file.open(QFile::WriteOnly | QFile::Truncate)) return; PERFTRACE("export spreadsheet to SQLite database"); QApplication::processEvents(QEventLoop::AllEvents, 0); //create database const QStringList& drivers = QSqlDatabase::drivers(); QString driver; if (drivers.contains(QLatin1String("QSQLITE3"))) driver = QLatin1String("QSQLITE3"); else driver = QLatin1String("QSQLITE"); QSqlDatabase db = QSqlDatabase::addDatabase(driver); db.setDatabaseName(path); if (!db.open()) { RESET_CURSOR; KMessageBox::error(nullptr, i18n("Couldn't create the SQLite database %1.", path)); } //create table const int cols = m_spreadsheet->columnCount(); QString query = QLatin1String("create table ") + m_spreadsheet->name() + QLatin1String(" ("); for (int i = 0; i < cols; ++i) { Column* col = m_spreadsheet->column(i); if (i != 0) query += QLatin1String(", "); query += QLatin1String("\"") + col->name() + QLatin1String("\" "); switch (col->columnMode()) { case AbstractColumn::Numeric: query += QLatin1String("REAL"); break; case AbstractColumn::Integer: query += QLatin1String("INTEGER"); break; case AbstractColumn::Text: case AbstractColumn::Month: case AbstractColumn::Day: case AbstractColumn::DateTime: query += QLatin1String("TEXT"); break; } } query += QLatin1Char(')'); QSqlQuery q; if (!q.exec(query)) { RESET_CURSOR; KMessageBox::error(nullptr, i18n("Failed to create table in the SQLite database %1.", path) + '\n' + q.lastError().databaseText()); db.close(); return; } //create bulk insert statement { PERFTRACE("Create the bulk insert statement"); q.exec(QLatin1String("BEGIN TRANSACTION;")); query = "INSERT INTO '" + m_spreadsheet->name() + "' ("; for (int i = 0; i < cols; ++i) { if (i != 0) query += QLatin1String(", "); query += QLatin1Char('\'') + m_spreadsheet->column(i)->name() + QLatin1Char('\''); } query += QLatin1String(") VALUES "); for (int i = 0; i < m_spreadsheet->rowCount(); ++i) { if (i != 0) query += QLatin1String(","); query += QLatin1Char('('); for (int j = 0; j < cols; ++j) { Column* col = m_spreadsheet->column(j); if (j != 0) query += QLatin1String(", "); query += QLatin1Char('\'') + col->asStringColumn()->textAt(i) + QLatin1Char('\''); } query += QLatin1String(")"); } query += QLatin1Char(';'); } //insert values if (!q.exec(query)) { RESET_CURSOR; KMessageBox::error(nullptr, i18n("Failed to insert values into the table.")); QDEBUG("bulk insert error " << q.lastError().databaseText()); } else q.exec(QLatin1String("COMMIT TRANSACTION;")); //close the database db.close(); }