diff --git a/src/App/Expression.cpp b/src/App/Expression.cpp index f00dd4950c..66e698ac9f 100644 --- a/src/App/Expression.cpp +++ b/src/App/Expression.cpp @@ -1140,12 +1140,13 @@ Expression* Expression::eval() const { return expressionFromPy(owner,getPyValue()); } -bool Expression::isSame(const Expression &other) const { +bool Expression::isSame(const Expression &other, bool checkComment) const { if(&other == this) return true; if(getTypeId()!=other.getTypeId()) return false; - return comment==other.comment && toString(true,true) == other.toString(true,true); + return (!checkComment || comment==other.comment) + && toString(true,true) == other.toString(true,true); } std::string Expression::toString(bool persistent, bool checkPriority, int indent) const { diff --git a/src/App/Expression.h b/src/App/Expression.h index 8d394db807..926e72c99c 100644 --- a/src/App/Expression.h +++ b/src/App/Expression.h @@ -185,7 +185,7 @@ public: Py::Object getPyValue() const; - bool isSame(const Expression &other) const; + bool isSame(const Expression &other, bool checkComment=true) const; friend ExpressionVisitor; diff --git a/src/App/ExpressionParser.h b/src/App/ExpressionParser.h index 726220c80c..a10c1e8212 100644 --- a/src/App/ExpressionParser.h +++ b/src/App/ExpressionParser.h @@ -301,6 +301,9 @@ public: static Py::Object evaluate(const Expression *owner, int type, const std::vector &args); + Function getFunction() const {return f;} + const std::vector &getArgs() const {return args;} + protected: static Py::Object evalAggregate(const Expression *owner, int type, const std::vector &args); virtual Py::Object _getPyValue() const override; diff --git a/src/App/PropertyExpressionEngine.cpp b/src/App/PropertyExpressionEngine.cpp index 57cdc5cb62..e316c0310d 100644 --- a/src/App/PropertyExpressionEngine.cpp +++ b/src/App/PropertyExpressionEngine.cpp @@ -28,7 +28,7 @@ #include #include #include -#include "Expression.h" +#include "ExpressionParser.h" #include "ExpressionVisitors.h" #include "PropertyExpressionEngine.h" #include "PropertyStandard.h" @@ -477,8 +477,13 @@ void PropertyExpressionEngine::setValue(const ObjectIdentifier & path, std::shar // Check if the current expression equals the new one and do nothing if so to reduce unneeded computations ExpressionMap::iterator it = expressions.find(usePath); - if(it != expressions.end() && expr == it->second.expression) + if(it != expressions.end() + && (expr == it->second.expression || + (expr && it->second.expression + && expr->isSame(*it->second.expression)))) + { return; + } if (expr) { std::string error = validateExpression(usePath, expr); @@ -488,9 +493,9 @@ void PropertyExpressionEngine::setValue(const ObjectIdentifier & path, std::shar expressions[usePath] = ExpressionInfo(expr); expressionChanged(usePath); signaller.tryInvoke(); - } else { + } else if (it != expressions.end()) { AtomicPropertyChange signaller(*this); - expressions.erase(usePath); + expressions.erase(it); expressionChanged(usePath); signaller.tryInvoke(); } diff --git a/src/App/PropertyExpressionEngine.h b/src/App/PropertyExpressionEngine.h index 926ff856d3..9a4982d2d2 100644 --- a/src/App/PropertyExpressionEngine.h +++ b/src/App/PropertyExpressionEngine.h @@ -176,7 +176,8 @@ private: typedef boost::adjacency_list< boost::listS, boost::vecS, boost::directedS > DiGraph; typedef std::pair Edge; - typedef boost::unordered_map ExpressionMap; + // Note: use std::map instead of unordered_map to keep the binding order stable + typedef std::map ExpressionMap; std::vector computeEvaluationOrder(ExecuteOption option); diff --git a/src/App/Range.cpp b/src/App/Range.cpp index f7e5445253..0e4f4651af 100644 --- a/src/App/Range.cpp +++ b/src/App/Range.cpp @@ -149,7 +149,7 @@ int App::validRow(const std::string &rowstr) char * end; int i = strtol(rowstr.c_str(), &end, 10); - if (i <0 || i >= CellAddress::MAX_ROWS || *end) + if (i <=0 || i > CellAddress::MAX_ROWS || *end) return -1; return i - 1; @@ -234,24 +234,28 @@ App::CellAddress App::stringToAddress(const char * strAddress, bool silent) * @returns Address given as a string. */ -std::string App::CellAddress::toString(bool noAbsolute) const +std::string App::CellAddress::toString(bool noAbsolute, bool r, bool c) const { std::stringstream s; - if(_absCol && !noAbsolute) - s << '$'; - if (col() < 26) - s << (char)('A' + col()); - else { - int colnum = col() - 26; + if(c) { + if(_absCol && !noAbsolute) + s << '$'; + if (col() < 26) + s << (char)('A' + col()); + else { + int colnum = col() - 26; - s << (char)('A' + (colnum / 26)); - s << (char)('A' + (colnum % 26)); + s << (char)('A' + (colnum / 26)); + s << (char)('A' + (colnum % 26)); + } } - if(_absRow && !noAbsolute) - s << '$'; - s << (row() + 1); + if(r) { + if(_absRow && !noAbsolute) + s << '$'; + s << (row() + 1); + } return s.str(); } diff --git a/src/App/Range.h b/src/App/Range.h index a9182b7466..6fb914c869 100644 --- a/src/App/Range.h +++ b/src/App/Range.h @@ -58,9 +58,9 @@ struct AppExport CellAddress { inline int col() const { return _col; } - void setRow(int r) { _row = r; } + void setRow(int r, bool clip=false) { _row = (clip && r>=MAX_ROWS) ? MAX_ROWS-1 : r; } - void setCol(int c) { _col = c; } + void setCol(int c, bool clip=false) { _col = (clip && c>=MAX_COLUMNS) ? MAX_COLUMNS-1 : c; } inline bool operator<(const CellAddress & other) const { return asInt() < other.asInt(); } @@ -74,7 +74,7 @@ struct AppExport CellAddress { inline bool isAbsoluteCol() const { return _absCol; } - std::string toString(bool noAbsolute=false) const; + std::string toString(bool noAbsolute=false, bool row=true, bool col=true) const; // Static members diff --git a/src/Gui/ExpressionCompleter.cpp b/src/Gui/ExpressionCompleter.cpp index 6fbe8e5be1..697ff6aad5 100644 --- a/src/Gui/ExpressionCompleter.cpp +++ b/src/Gui/ExpressionCompleter.cpp @@ -54,22 +54,21 @@ using namespace Gui; class ExpressionCompleterModel: public QAbstractItemModel { public: - ExpressionCompleterModel(QObject *parent, const App::DocumentObject *obj, bool noProperty) + ExpressionCompleterModel(QObject *parent, bool noProperty) :QAbstractItemModel(parent), noProperty(noProperty) { - setDocumentObject(obj); } void setNoProperty(bool enabled) { noProperty = enabled; } - void setDocumentObject(const App::DocumentObject *obj) { + void setDocumentObject(const App::DocumentObject *obj, bool checkInList) { beginResetModel(); if(obj) { currentDoc = obj->getDocument()->getName(); currentObj = obj->getNameInDocument(); - if(!noProperty) + if(!noProperty && checkInList) inList = obj->getInListEx(true); } else { currentDoc.clear(); @@ -340,9 +339,10 @@ private: * @param parent Parent object owning the completer. */ -ExpressionCompleter::ExpressionCompleter(const App::DocumentObject * currentDocObj, - QObject *parent, bool noProperty) - : QCompleter(parent), currentObj(currentDocObj), noProperty(noProperty) +ExpressionCompleter::ExpressionCompleter(const App::DocumentObject * currentDocObj, + QObject *parent, bool noProperty, bool checkInList) + : QCompleter(parent), currentObj(currentDocObj) + , noProperty(noProperty), checkInList(checkInList) { setCaseSensitivity(Qt::CaseInsensitive); } @@ -351,18 +351,21 @@ void ExpressionCompleter::init() { if(model()) return; - setModel(new ExpressionCompleterModel(this,currentObj.getObject(),noProperty)); + auto m = new ExpressionCompleterModel(this,noProperty); + m->setDocumentObject(currentObj.getObject(),checkInList); + setModel(m); } -void ExpressionCompleter::setDocumentObject(const App::DocumentObject *obj) { +void ExpressionCompleter::setDocumentObject(const App::DocumentObject *obj, bool _checkInList) { if(!obj || !obj->getNameInDocument()) currentObj = App::DocumentObjectT(); else currentObj = obj; setCompletionPrefix(QString()); + checkInList = _checkInList; auto m = model(); if(m) - static_cast(m)->setDocumentObject(obj); + static_cast(m)->setDocumentObject(obj, checkInList); } void ExpressionCompleter::setNoProperty(bool enabled) { @@ -372,10 +375,6 @@ void ExpressionCompleter::setNoProperty(bool enabled) { static_cast(m)->setNoProperty(enabled); } -void ExpressionCompleter::setRequireLeadingEqualSign(bool enabled) { - requireLeadingEqualSign = enabled; -} - QString ExpressionCompleter::pathFromIndex ( const QModelIndex & index ) const { auto m = model(); @@ -459,12 +458,6 @@ void ExpressionCompleter::slotUpdate(const QString & prefix, int pos) // Compute start; if prefix starts with =, start parsing from offset 1. int start = (prefix.size() > 0 && prefix.at(0) == QChar::fromLatin1('=')) ? 1 : 0; - if (requireLeadingEqualSign && start != 1) { - if (auto p = popup()) - p->setVisible(false); - return; - } - std::string expression = Base::Tools::toStdString(prefix.mid(start)); // Tokenize prefix @@ -564,28 +557,33 @@ void ExpressionCompleter::slotUpdate(const QString & prefix, int pos) } } -ExpressionLineEdit::ExpressionLineEdit(QWidget *parent, bool noProperty, bool requireLeadingEqualSign) +ExpressionLineEdit::ExpressionLineEdit(QWidget *parent, bool noProperty, char checkPrefix, bool checkInList) : QLineEdit(parent) , completer(nullptr) , block(true) , noProperty(noProperty) , exactMatch(false) - , requireLeadingEqualSign(requireLeadingEqualSign) + , checkInList(checkInList) + , checkPrefix(checkPrefix) { connect(this, SIGNAL(textEdited(const QString&)), this, SLOT(slotTextChanged(const QString&))); } -void ExpressionLineEdit::setDocumentObject(const App::DocumentObject * currentDocObj) +void ExpressionLineEdit::setPrefix(char prefix) { + checkPrefix = prefix; +} + +void ExpressionLineEdit::setDocumentObject(const App::DocumentObject * currentDocObj, bool _checkInList) { + checkInList = _checkInList; if (completer) { - completer->setDocumentObject(currentDocObj); + completer->setDocumentObject(currentDocObj, checkInList); return; } if (currentDocObj != 0) { - completer = new ExpressionCompleter(currentDocObj, this, noProperty); + completer = new ExpressionCompleter(currentDocObj, this, noProperty, checkInList); completer->setWidget(this); completer->setCaseSensitivity(Qt::CaseInsensitive); - completer->setRequireLeadingEqualSign(requireLeadingEqualSign); if (!exactMatch) completer->setFilterMode(Qt::MatchContains); connect(completer, SIGNAL(activated(QString)), this, SLOT(slotCompleteText(QString))); @@ -621,6 +619,8 @@ void ExpressionLineEdit::hideCompleter() void ExpressionLineEdit::slotTextChanged(const QString & text) { if (!block) { + if(!text.size() || (checkPrefix && text[0]!=QLatin1Char(checkPrefix))) + return; Q_EMIT textChanged2(text,cursorPosition()); } } diff --git a/src/Gui/ExpressionCompleter.h b/src/Gui/ExpressionCompleter.h index 70169bb792..3f76bd49d4 100644 --- a/src/Gui/ExpressionCompleter.h +++ b/src/Gui/ExpressionCompleter.h @@ -50,8 +50,8 @@ class GuiExport ExpressionCompleter : public QCompleter { Q_OBJECT public: - ExpressionCompleter(const App::DocumentObject * currentDocObj, - QObject *parent = nullptr, bool noProperty = false); + ExpressionCompleter(const App::DocumentObject * currentDocObj, + QObject *parent = nullptr, bool noProperty = false, bool checkInList = true); void getPrefixRange(int &start, int &end) const { start = prefixStart; @@ -62,10 +62,9 @@ public: prefixEnd = end; } - void setDocumentObject(const App::DocumentObject*); + void setDocumentObject(const App::DocumentObject*, bool checkInList=true); void setNoProperty(bool enabled=true); - void setRequireLeadingEqualSign(bool enabled); public Q_SLOTS: void slotUpdate(const QString &prefix, int pos); @@ -77,18 +76,19 @@ private: int prefixStart = 0; int prefixEnd = 0; - bool requireLeadingEqualSign = false; App::DocumentObjectT currentObj; bool noProperty; - + bool checkInList; }; class GuiExport ExpressionLineEdit : public QLineEdit { Q_OBJECT public: - ExpressionLineEdit(QWidget *parent = nullptr, bool noProperty = false, bool requireLeadingEqualSign = false); - void setDocumentObject(const App::DocumentObject *currentDocObj); + ExpressionLineEdit(QWidget *parent = 0, bool noProperty=false, + char checkPrefix=0, bool checkInList=true); + void setDocumentObject(const App::DocumentObject *currentDocObj, bool checkInList=true); + void setPrefix(char prefix); bool completerActive() const; void hideCompleter(); void setNoProperty(bool enabled=true); @@ -106,7 +106,8 @@ private: bool block; bool noProperty; bool exactMatch; - bool requireLeadingEqualSign; + bool checkInList; + char checkPrefix; }; class GuiExport ExpressionTextEdit : public QPlainTextEdit { diff --git a/src/Mod/Spreadsheet/App/PropertySheet.cpp b/src/Mod/Spreadsheet/App/PropertySheet.cpp index cef6b26087..7c7715beee 100644 --- a/src/Mod/Spreadsheet/App/PropertySheet.cpp +++ b/src/Mod/Spreadsheet/App/PropertySheet.cpp @@ -1216,7 +1216,8 @@ void PropertySheet::recomputeDependants(const App::DocumentObject *owner, const // protected by cyclic dependency checking, we need to take special // care to prevent it from misbehave. Sheet *sheet = Base::freecad_dynamic_cast(getContainer()); - if(!sheet || sheet->testStatus(App::ObjectStatus::Recompute2)) + if(!sheet || sheet->testStatus(App::ObjectStatus::Recompute2) + || !owner || owner->testStatus(App::ObjectStatus::Recompute2)) return; } @@ -1434,6 +1435,36 @@ void PropertySheet::setPyObject(PyObject *obj) { Paste(*static_cast(obj)->getPropertySheetPtr()); } +PyObject *PropertySheet::getPyValue(PyObject *key) { + assert(key); + + PY_TRY { + std::string addr = Py::Object(key).as_string(); + CellAddress caddr = getCellAddress(addr.c_str(),true); + if(caddr.isValid()) { + auto prop = owner->getPropertyByName(caddr.toString().c_str()); + if(prop) + return prop->getPyObject(); + Py_Return; + } + + Range range = getRange(Py::Object(key).as_string().c_str()); + if(!range.from().isValid() || !range.to().isValid()) + return Py::new_reference_to(Py::Tuple()); + + Py::Tuple res(range.size()); + int i = 0; + do { + addr = range.address(); + auto prop = owner->getPropertyByName(addr.c_str()); + res.setItem(i++,prop?Py::asObject(prop->getPyObject()):Py::Object()); + } while(range.next()); + + return Py::new_reference_to(res); + + } PY_CATCH +} + void PropertySheet::afterRestore() { Base::FlagToggler flag(restoring); @@ -1621,3 +1652,231 @@ void PropertySheet::setExpressions( } signaller.tryInvoke(); } + +App::CellAddress PropertySheet::getCellAddress(const char *addr, bool silent) const { + assert(addr); + CellAddress caddr; + const Cell * cell = getValueFromAlias(addr); + if(cell) + return cell->getAddress(); + else + return stringToAddress(addr,silent); +} + +App::Range PropertySheet::getRange(const char *range, bool silent) const { + assert(range); + const char *sep = strchr(range,':'); + CellAddress from,to; + if(!sep) + from = to = getCellAddress(range,silent); + else { + std::string addr(range,sep); + + auto findCell = [this, &addr](CellAddress caddr, int r, int c) -> CellAddress { + if(!getValue(caddr)) + return CellAddress(); + if(addr == "-") + r = 0; + else + c = 0; + for(;;) { + caddr.setRow(caddr.row()+r); + caddr.setCol(caddr.col()+c); + if(!caddr.isValid() || !getValue(caddr)) + break; + } + caddr.setRow(caddr.row()-r); + caddr.setCol(caddr.col()-c); + return caddr; + }; + + if(addr == "-" || addr == "|") { + to = getCellAddress(sep+1,silent); + return Range(findCell(to,-1,-1), from); + } else { + from = getCellAddress(addr.c_str(),silent); + addr = sep+1; + if(addr == "-" || addr == "|") + return Range(from, findCell(from,1,1)); + to = getCellAddress(addr.c_str(),silent); + } + } + + if(!from.isValid() || !to.isValid()) + return App::Range(App::CellAddress(),App::CellAddress()); + return App::Range(from,to); +} + +bool PropertySheet::isBindingPath(const ObjectIdentifier &path, + CellAddress *from, CellAddress *to, bool *href) const +{ + const auto &comps = path.getComponents(); + if (comps.size()!=4 + || !comps[2].isSimple() + || !comps[3].isSimple() + || (comps[1].getName()!="Bind" + && comps[1].getName()!="BindHREF" + && comps[1].getName()!="BindHiddenRef") + || path.getProperty() != this) + { + return false; + } + if(href) + *href = (comps[1].getName()=="BindHREF" || comps[1].getName()=="BindHiddenRef"); + if(from) + *from = CellAddress(comps[2].getName()); + if(to) + *to = CellAddress(comps[3].getName()); + return true; +} + +PropertySheet::BindingType PropertySheet::getBinding( + const Range &range, ExpressionPtr *pStart, ExpressionPtr *pEnd) const +{ + if(!owner) + return BindingNone; + + for(int href=0;href<2;++href) { + ObjectIdentifier path(*this); + path << ObjectIdentifier::SimpleComponent(href?"BindHiddenRef":"Bind"); + path << ObjectIdentifier::SimpleComponent(range.from().toString().c_str()); + path << ObjectIdentifier::SimpleComponent(range.to().toString().c_str()); + auto res = owner->getExpression(path); + if(res.expression && res.expression->isDerivedFrom(FunctionExpression::getClassTypeId())) + { + auto expr = static_cast(res.expression.get()); + if(href) { + if((expr->getFunction()!=FunctionExpression::HIDDENREF + && expr->getFunction()!=FunctionExpression::HREF) + || expr->getArgs().size()!=1 + || !expr->getArgs().front()->isDerivedFrom(FunctionExpression::getClassTypeId())) + continue; + expr = static_cast(expr->getArgs().front()); + } + + if(expr->getFunction() == FunctionExpression::TUPLE && expr->getArgs().size()==3) { + if(pStart) + pStart->reset(expr->getArgs()[1]->copy()); + if(pEnd) + pEnd->reset(expr->getArgs()[2]->copy()); + return href?BindingHiddenRef:BindingNormal; + } + } + } + return BindingNone; +} + +void PropertySheet::setPathValue(const ObjectIdentifier &path, const boost::any &value) +{ + if(!owner) + FC_THROWM(Base::RuntimeError, "Invalid state"); + + bool href = false; + CellAddress from,to; + if(!isBindingPath(path,&from,&to,&href)) { + FC_THROWM(Base::IndexError, "Invalid binding of '" << path.toString() + << "' in " << getFullName()); + } + + Base::PyGILStateLocker lock; + Py::Object pyValue = pyObjectFromAny(value); + + if(pyValue.isSequence()) { + Py::Sequence seq(pyValue); + if(seq.size()==3 + && PyObject_TypeCheck(seq[0].ptr(),&PropertySheetPy::Type) + && Py::Object(seq[1].ptr()).isString() + && Py::Object(seq[2].ptr()).isString()) + { + AtomicPropertyChange signaller(*this,false); + auto other = static_cast(seq[0].ptr())->getPropertySheetPtr(); + auto otherOwner = Base::freecad_dynamic_cast(other->getContainer()); + if(!otherOwner) + FC_THROWM(Base::RuntimeError, "Invalid binding of '" << other->getFullName() + << " in " << getFullName()); + + App::CellAddress targetFrom = other->getCellAddress( + Py::Object(seq[1].ptr()).as_string().c_str()); + + App::CellAddress targetTo = other->getCellAddress( + Py::Object(seq[2].ptr()).as_string().c_str()); + + App::Range range(from,to); + App::Range rangeTarget(targetFrom,targetTo); + + std::string expr(href?"href(":""); + if(other != this) { + if(otherOwner->getDocument() == owner->getDocument()) + expr = otherOwner->getNameInDocument(); + else + expr = otherOwner->getFullName(); + } + expr += "."; + std::size_t exprSize = expr.size(); + + do { + CellAddress target(*rangeTarget); + CellAddress source(*range); + if(other == this && source.row() >= targetFrom.row() + && source.row() <= targetTo.row() + && source.col() >= targetFrom.col() + && source.col() <= targetTo.col()) + continue; + + Cell *dst = other->getValue(target); + Cell *src = getValue(source); + if(!dst) { + if(src) { + signaller.aboutToChange(); + owner->clear(source); + owner->cellUpdated(source); + } + continue; + } + + if(!src) { + signaller.aboutToChange(); + src = createCell(source); + } + + std::string alias; + if(this!=other && dst->getAlias(alias)) { + auto *oldCell = getValueFromAlias(alias); + if(oldCell && oldCell!=dst) { + signaller.aboutToChange(); + oldCell->setAlias(""); + } + std::string oldAlias; + if(!src->getAlias(oldAlias) || oldAlias!=alias) { + signaller.aboutToChange(); + setAlias(source,alias); + } + } + + expr.resize(exprSize); + expr += rangeTarget.address(); + if(href) + expr += ")"; + auto e = App::ExpressionPtr(App::Expression::parse(owner,expr)); + auto e2 = src->getExpression(); + if(!e2 || !e->isSame(*e2,false)) { + signaller.aboutToChange(); + src->setExpression(std::move(e)); + } + + } while(range.next() && rangeTarget.next()); + owner->rangeUpdated(range); + signaller.tryInvoke(); + return; + } + } + + FC_THROWM(Base::TypeError, "Invalid path value '" + << "' for " << getFullName()); +} + +const boost::any PropertySheet::getPathValue(const App::ObjectIdentifier & path) const { + if(isBindingPath(path)) + return boost::any(); + return path.getValue(); +} diff --git a/src/Mod/Spreadsheet/App/PropertySheet.h b/src/Mod/Spreadsheet/App/PropertySheet.h index a98fee3087..8a181fec0d 100644 --- a/src/Mod/Spreadsheet/App/PropertySheet.h +++ b/src/Mod/Spreadsheet/App/PropertySheet.h @@ -163,6 +163,8 @@ public: PyObject *getPyObject(void) override; void setPyObject(PyObject *) override; + PyObject *getPyValue(PyObject *key); + void invalidateDependants(const App::DocumentObject *docObj); void renamedDocumentObject(const App::DocumentObject *docObj); @@ -176,6 +178,22 @@ public: std::string getColumn(int offset=0) const; + virtual void setPathValue(const App::ObjectIdentifier & path, const boost::any & value) override; + virtual const boost::any getPathValue(const App::ObjectIdentifier & path) const override; + + unsigned getBindingBorder(App::CellAddress address) const; + + bool isBindingPath(const App::ObjectIdentifier &path, + App::CellAddress *from=0, App::CellAddress *to=0, bool *href=0) const; + + enum BindingType { + BindingNone, + BindingNormal, + BindingHiddenRef, + }; + BindingType getBinding(const App::Range &range, + App::ExpressionPtr *pStart=0, App::ExpressionPtr *pEnd=0) const; + protected: virtual void hasSetValue() override; virtual void hasSetChildValue(App::Property &prop) override; diff --git a/src/Mod/Spreadsheet/App/Sheet.cpp b/src/Mod/Spreadsheet/App/Sheet.cpp index 61e4c2b17b..9f576d8faa 100644 --- a/src/Mod/Spreadsheet/App/Sheet.cpp +++ b/src/Mod/Spreadsheet/App/Sheet.cpp @@ -844,6 +844,52 @@ void Sheet::recomputeCell(CellAddress p) cellSpanChanged(p); } +PropertySheet::BindingType Sheet::getCellBinding(Range &range, + ExpressionPtr *pStart, ExpressionPtr *pEnd) const +{ + do { + CellAddress addr = *range; + for(auto &r : boundRanges) { + if(addr.row()>=r.from().row() + && addr.row()<=r.to().row() + && addr.col()>=r.from().col() + && addr.col()<=r.to().col()) + { + auto res = cells.getBinding(r,pStart,pEnd); + if(res != PropertySheet::BindingNone) { + range = r; + return res; + } + } + } + } while(range.next()); + return PropertySheet::BindingNone; +} + +unsigned Sheet::getCellBindingBorder(App::CellAddress address) const { + unsigned flags = 0; + for(auto &range : boundRanges) { + auto from = range.from(); + auto to = range.to(); + if(address.row() < from.row() + || address.row() > to.row() + || address.col() < from.col() + || address.col() > to.col()) + continue; + if(address.row() == from.row()) + flags |= BorderTop; + if(address.row() == to.row()) + flags |= BorderBottom; + if(address.col() == from.col()) + flags |= BorderLeft; + if(address.col() == to.col()) + flags |= BorderRight; + if(flags == BorderAll) + break; + } + return flags; +} + /** * Update the document properties. * @@ -853,6 +899,13 @@ DocumentObjectExecReturn *Sheet::execute(void) { // Remove all aliases first removeAliases(); + boundRanges.clear(); + for(auto &v : ExpressionEngine.getExpressions()) { + CellAddress from,to; + if(!cells.isBindingPath(v.first,&from,&to)) + continue; + boundRanges.emplace_back(from,to); + } // Get dirty cells that we have to recompute std::set dirtyCells = cells.getDirty(); diff --git a/src/Mod/Spreadsheet/App/Sheet.h b/src/Mod/Spreadsheet/App/Sheet.h index d1e7324773..fe5096121e 100644 --- a/src/Mod/Spreadsheet/App/Sheet.h +++ b/src/Mod/Spreadsheet/App/Sheet.h @@ -98,6 +98,18 @@ public: Cell *getNewCell(App::CellAddress address); + enum Border { + BorderTop = 1, + BorderLeft = 2, + BorderBottom = 4, + BorderRight = 8, + BorderAll = 15, + }; + unsigned getCellBindingBorder(App::CellAddress address) const; + + PropertySheet::BindingType getCellBinding(App::Range &range, + App::ExpressionPtr *pStart=0, App::ExpressionPtr *pEnd=0) const; + void setCell(const char *address, const char *value); void setCell(App::CellAddress address, const char *value); @@ -185,6 +197,8 @@ public: boost::signals2::signal cellUpdated; + boost::signals2::signal rangeUpdated; + boost::signals2::signal cellSpanChanged; boost::signals2::signal columnWidthChanged; @@ -259,6 +273,8 @@ protected: int currentRow = -1; int currentCol = -1; + std::vector boundRanges; + friend class SheetObserver; friend class PropertySheet; diff --git a/src/Mod/Spreadsheet/Gui/CMakeLists.txt b/src/Mod/Spreadsheet/Gui/CMakeLists.txt index cbffc4e9b3..5f6a54b3cf 100644 --- a/src/Mod/Spreadsheet/Gui/CMakeLists.txt +++ b/src/Mod/Spreadsheet/Gui/CMakeLists.txt @@ -51,6 +51,7 @@ endif() set(SpreadsheetGui_UIC_SRCS Sheet.ui PropertiesDialog.ui + DlgBindSheet.ui ) if(BUILD_QT5) @@ -89,6 +90,8 @@ SET(SpreadsheetGui_SRCS qtcolorpicker.cpp PropertiesDialog.h PropertiesDialog.cpp + DlgBindSheet.h + DlgBindSheet.cpp ${SpreadsheetGui_UIC_HDRS} ) diff --git a/src/Mod/Spreadsheet/Gui/DlgBindSheet.cpp b/src/Mod/Spreadsheet/Gui/DlgBindSheet.cpp new file mode 100644 index 0000000000..d72f0822c0 --- /dev/null +++ b/src/Mod/Spreadsheet/Gui/DlgBindSheet.cpp @@ -0,0 +1,202 @@ +/**************************************************************************** + * Copyright (c) 2019 Zheng, Lei (realthunder) * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ****************************************************************************/ + +#include "PreCompiled.h" +#include +#include +#include "DlgBindSheet.h" +#include +#include +#include +#include +#include +#include +#include "ui_DlgBindSheet.h" + +using namespace App; +using namespace Spreadsheet; +using namespace SpreadsheetGui; + +DlgBindSheet::DlgBindSheet(Sheet *sheet, const std::vector &ranges, QWidget *parent) + : QDialog(parent), sheet(sheet), range(ranges.front()), ui(new Ui::DlgBindSheet) +{ + ui->setupUi(this); + + std::string toStart,toEnd; + ExpressionPtr pStart, pEnd; + PropertySheet::BindingType type = sheet->getCellBinding(range,&pStart,&pEnd); + if(type == PropertySheet::BindingNone) { + if(ranges.size()>1) { + toStart = ranges.back().from().toString(); + toEnd = ranges.back().to().toString(); + } else { + CellAddress target(range.to().row()?0:range.to().row()+1,range.from().col()); + toStart = target.toString(); + target.setRow(target.row() + range.to().row() - range.from().row()); + target.setCol(target.col() + range.to().col() - range.from().col()); + toEnd = target.toString(); + } + } else { + ui->lineEditFromStart->setReadOnly(true); + ui->lineEditFromEnd->setReadOnly(true); + ui->checkBoxHREF->setChecked(type==PropertySheet::BindingHiddenRef); + assert(pStart && pEnd); + if(!pStart->hasComponent() && pStart->isDerivedFrom(StringExpression::getClassTypeId())) + toStart = static_cast(pStart.get())->getText(); + else { + toStart = "="; + toStart += pStart->toString(); + } + if(!pEnd->hasComponent() && pEnd->isDerivedFrom(StringExpression::getClassTypeId())) + toEnd = static_cast(pEnd.get())->getText(); + else { + toEnd = "="; + toEnd += pEnd->toString(); + } + } + + ui->lineEditFromStart->setText(QString::fromLatin1(range.from().toString().c_str())); + ui->lineEditFromEnd->setText(QString::fromLatin1(range.to().toString().c_str())); + + ui->lineEditToStart->setDocumentObject(sheet,false); + ui->lineEditToStart->setPrefix('='); + ui->lineEditToEnd->setDocumentObject(sheet,false); + ui->lineEditToEnd->setPrefix('='); + + ui->lineEditToStart->setText(QLatin1String(toStart.c_str())); + ui->lineEditToEnd->setText(QLatin1String(toEnd.c_str())); + + ui->comboBox->addItem(QString::fromLatin1(". (%1)").arg( + QString::fromUtf8(sheet->Label.getValue())), QByteArray("")); + + for(auto obj : sheet->getDocument()->getObjectsOfType()) { + if(obj == sheet) + continue; + QString label; + if(obj->Label.getStrValue() != obj->getNameInDocument()) + label = QString::fromLatin1("%1 (%2)").arg( + QString::fromLatin1(obj->getNameInDocument()), + QString::fromUtf8(obj->Label.getValue())); + else + label = QLatin1String(obj->getNameInDocument()); + ui->comboBox->addItem(label, QByteArray(obj->getNameInDocument())); + } + for(auto doc : GetApplication().getDocuments()) { + if(doc == sheet->getDocument()) + continue; + for(auto obj : sheet->getDocument()->getObjectsOfType()) { + if(obj == sheet) + continue; + std::string fullname = obj->getFullName(); + QString label; + if(obj->Label.getStrValue() != obj->getNameInDocument()) + label = QString::fromLatin1("%1 (%2)").arg( + QString::fromLatin1(fullname.c_str()), + QString::fromUtf8(obj->Label.getValue())); + else + label = QLatin1String(fullname.c_str()); + ui->comboBox->addItem(label, QByteArray(fullname.c_str())); + } + } + + connect(ui->btnDiscard, SIGNAL(clicked()), this, SLOT(onDiscard())); +} + +DlgBindSheet::~DlgBindSheet() +{ + delete ui; +} + +void DlgBindSheet::accept() +{ + bool commandActive = false; + try { + const char *ref = ui->comboBox->itemData(ui->comboBox->currentIndex()).toByteArray().constData(); + auto obj = sheet; + if(ref[0]) { + const char *sep = strchr(ref,'#'); + if(sep) { + std::string docname(ref,sep); + auto doc = GetApplication().getDocument(docname.c_str()); + if(!doc) + FC_THROWM(Base::RuntimeError, "Cannot find document " << docname); + obj = Base::freecad_dynamic_cast(doc->getObject(sep+1)); + } else + obj = Base::freecad_dynamic_cast(sheet->getDocument()->getObject(ref)); + if(!obj) + FC_THROWM(Base::RuntimeError, "Cannot find Spreadsheet '" << ref << "'"); + } + + std::string fromStart(ui->lineEditFromStart->text().trimmed().toLatin1().constData()); + std::string fromEnd(ui->lineEditFromEnd->text().trimmed().toLatin1().constData()); + + std::string toStart(ui->lineEditToStart->text().trimmed().toLatin1().constData()); + if(boost::starts_with(toStart,"=")) + toStart.erase(toStart.begin()); + else + toStart = std::string("<<") + toStart + ">>"; + + std::string toEnd(ui->lineEditToEnd->text().trimmed().toLatin1().constData()); + if(boost::starts_with(toEnd,"=")) + toEnd.erase(toEnd.begin()); + else + toEnd = std::string("<<") + toEnd + ">>"; + + Gui::Command::openCommand("Bind cells"); + commandActive = true; + + if(ui->checkBoxHREF->isChecked()) + Gui::cmdAppObjectArgs(sheet, + "setExpression('.cells.BindHiddenRef.%s.%s', 'hiddenref(tuple(%s.cells, %s, %s))')", + fromStart, fromEnd, ref, toStart, toEnd); + else + Gui::cmdAppObjectArgs(sheet, + "setExpression('.cells.Bind.%s.%s', 'tuple(%s.cells, %s, %s)')", + fromStart, fromEnd, ref, toStart, toEnd); + Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); + Gui::Command::commitCommand(); + QDialog::accept(); + } catch(Base::Exception &e) { + e.ReportException(); + QMessageBox::critical(this, tr("Bind cells"), QString::fromUtf8(e.what())); + if(commandActive) + Gui::Command::abortCommand(); + } +} + +void DlgBindSheet::onDiscard() { + try { + std::string fromStart(ui->lineEditFromStart->text().trimmed().toLatin1().constData()); + std::string fromEnd(ui->lineEditFromEnd->text().trimmed().toLatin1().constData()); + Gui::Command::openCommand("Unbind cells"); + Gui::cmdAppObjectArgs(sheet, "setExpression('.cells.Bind.%s.%s', None)", fromStart, fromEnd); + Gui::Command::doCommand(Gui::Command::Doc, "App.ActiveDocument.recompute()"); + Gui::Command::commitCommand(); + reject(); + } catch(Base::Exception &e) { + e.ReportException(); + QMessageBox::critical(this, tr("Unbind cells"), QString::fromUtf8(e.what())); + Gui::Command::abortCommand(); + } +} + +#include "moc_DlgBindSheet.cpp" diff --git a/src/Mod/Spreadsheet/Gui/DlgBindSheet.h b/src/Mod/Spreadsheet/Gui/DlgBindSheet.h new file mode 100644 index 0000000000..9afe922e3f --- /dev/null +++ b/src/Mod/Spreadsheet/Gui/DlgBindSheet.h @@ -0,0 +1,56 @@ +/**************************************************************************** + * Copyright (c) 2019 Zheng, Lei (realthunder) * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library 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 Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ****************************************************************************/ + +#ifndef DLG_BINDSHEET_H +#define DLG_BINDSHEET_H + +#include +#include + +namespace Ui { +class DlgBindSheet; +} + +namespace SpreadsheetGui { + +class DlgBindSheet : public QDialog +{ + Q_OBJECT + +public: + explicit DlgBindSheet(Spreadsheet::Sheet *sheet, const std::vector &range, QWidget *parent = 0); + ~DlgBindSheet(); + + void accept(); + +public Q_SLOTS: + void onDiscard(); + +private: + Spreadsheet::Sheet * sheet; + App::Range range; + Ui::DlgBindSheet *ui; +}; + +} + +#endif // DLG_BINDSHEET_H diff --git a/src/Mod/Spreadsheet/Gui/DlgBindSheet.ui b/src/Mod/Spreadsheet/Gui/DlgBindSheet.ui new file mode 100644 index 0000000000..79eb651311 --- /dev/null +++ b/src/Mod/Spreadsheet/Gui/DlgBindSheet.ui @@ -0,0 +1,169 @@ + + + DlgBindSheet + + + + 0 + 0 + 387 + 176 + + + + Bind Spreadsheet Cells + + + + + + From cells: + + + + + + + Binding cell range start + + + + + + + Binding cell range end + + + + + + + + To cells: + + + + + + + Starting cell address to bind to. Type '=' if you want to use expression. +The expression must evaluates to a string of some cell address. + + + + + + + Ending cell address to bind to. Type '=' if you want to use expression. +The expression must evaluates to a string of some cell address. + + + + + + + Sheet: + + + + + + + Use hidden reference not avoid creating a depdenecy with the referenced object. Use with caution! + + + Use hidden reference + + + + + + + + + Unbind + + + + + + + Cancel + + + + + + + OK + + + true + + + + + + + + + Select which spread sheet to bind to. + + + + + + + + Gui::ExpressionLineEdit + QLineEdit +
Gui/ExpressionCompleter.h
+
+
+ + lineEditFromStart + lineEditFromEnd + lineEditToStart + lineEditToEnd + comboBox + checkBoxHREF + btnOk + btnCancel + btnDiscard + + + + + btnOk + clicked() + DlgBindSheet + accept() + + + 334 + 152 + + + 193 + 87 + + + + + btnCancel + clicked() + DlgBindSheet + reject() + + + 243 + 152 + + + 193 + 87 + + + + +
diff --git a/src/Mod/Spreadsheet/Gui/SheetModel.cpp b/src/Mod/Spreadsheet/Gui/SheetModel.cpp index c289dcc121..7047f004e7 100644 --- a/src/Mod/Spreadsheet/Gui/SheetModel.cpp +++ b/src/Mod/Spreadsheet/Gui/SheetModel.cpp @@ -50,6 +50,7 @@ SheetModel::SheetModel(Sheet *_sheet, QObject *parent) , sheet(_sheet) { cellUpdatedConnection = sheet->cellUpdated.connect(bind(&SheetModel::cellUpdated, this, bp::_1)); + rangeUpdatedConnection = sheet->rangeUpdated.connect(bind(&SheetModel::rangeUpdated, this, bp::_1)); ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Spreadsheet"); aliasBgColor = QColor(Base::Tools::fromStdString(hGrp->GetASCII("AliasedCellBackgroundColor", "#feff9e"))); @@ -61,6 +62,7 @@ SheetModel::SheetModel(Sheet *_sheet, QObject *parent) SheetModel::~SheetModel() { cellUpdatedConnection.disconnect(); + rangeUpdatedConnection.disconnect(); } int SheetModel::rowCount(const QModelIndex &parent) const @@ -594,4 +596,12 @@ void SheetModel::cellUpdated(CellAddress address) dataChanged(i, i); } +void SheetModel::rangeUpdated(const Range &range) +{ + QModelIndex i = index(range.from().row(), range.from().col()); + QModelIndex j = index(range.to().row(), range.to().col()); + + dataChanged(i, j); +} + #include "moc_SheetModel.cpp" diff --git a/src/Mod/Spreadsheet/Gui/SheetModel.h b/src/Mod/Spreadsheet/Gui/SheetModel.h index 5e418ac067..f6caf69a26 100644 --- a/src/Mod/Spreadsheet/Gui/SheetModel.h +++ b/src/Mod/Spreadsheet/Gui/SheetModel.h @@ -50,8 +50,10 @@ public: private: void cellUpdated(App::CellAddress address); + void rangeUpdated(const App::Range &range); boost::signals2::scoped_connection cellUpdatedConnection; + boost::signals2::scoped_connection rangeUpdatedConnection; Spreadsheet::Sheet * sheet; QColor aliasBgColor; QColor textFgColor; diff --git a/src/Mod/Spreadsheet/Gui/SheetTableView.cpp b/src/Mod/Spreadsheet/Gui/SheetTableView.cpp index b77e0d12a4..5a7a2f673c 100644 --- a/src/Mod/Spreadsheet/Gui/SheetTableView.cpp +++ b/src/Mod/Spreadsheet/Gui/SheetTableView.cpp @@ -47,6 +47,7 @@ #include "SheetTableView.h" #include "LineEdit.h" #include "PropertiesDialog.h" +#include "DlgBindSheet.h" using namespace SpreadsheetGui; using namespace Spreadsheet; @@ -178,6 +179,13 @@ SheetTableView::SheetTableView(QWidget *parent) connect(recompute, SIGNAL(triggered()), this, SLOT(onRecompute())); contextMenu->addAction(recompute); + actionBind = new QAction(tr("Bind..."),this); + connect(actionBind, SIGNAL(triggered()), this, SLOT(onBind())); + contextMenu->addAction(actionBind); + + horizontalHeader()->addAction(actionBind); + verticalHeader()->addAction(actionBind); + contextMenu->addSeparator(); actionMerge = contextMenu->addAction(tr("Merge cells")); connect(actionMerge,SIGNAL(triggered()), this, SLOT(mergeCells())); @@ -206,6 +214,14 @@ void SheetTableView::onRecompute() { Gui::Command::commitCommand(); } +void SheetTableView::onBind() { + auto ranges = selectedRanges(); + if(ranges.size()>=1 && ranges.size()<=2) { + DlgBindSheet dlg(sheet,ranges,this); + dlg.exec(); + } +} + void SheetTableView::cellProperties() { std::unique_ptr dialog(new PropertiesDialog(sheet, selectedRanges(), this)); @@ -909,6 +925,9 @@ void SheetTableView::contextMenuEvent(QContextMenuEvent *) actionMerge->setEnabled(true); } + auto ranges = selectedRanges(); + actionBind->setEnabled(ranges.size()>=1 && ranges.size()<=2); + contextMenu->exec(QCursor::pos()); } diff --git a/src/Mod/Spreadsheet/Gui/SheetTableView.h b/src/Mod/Spreadsheet/Gui/SheetTableView.h index b8e6c62522..47ae6534e0 100644 --- a/src/Mod/Spreadsheet/Gui/SheetTableView.h +++ b/src/Mod/Spreadsheet/Gui/SheetTableView.h @@ -81,6 +81,7 @@ protected Q_SLOTS: void removeColumns(); void cellProperties(); void onRecompute(); + void onBind(); protected: bool edit(const QModelIndex &index, EditTrigger trigger, QEvent *event); @@ -102,6 +103,7 @@ protected: QAction *actionPaste; QAction *actionCut; QAction *actionDel; + QAction *actionBind; boost::signals2::scoped_connection cellSpanChangedConnection; }; diff --git a/src/Mod/Spreadsheet/Gui/SpreadsheetDelegate.cpp b/src/Mod/Spreadsheet/Gui/SpreadsheetDelegate.cpp index 6ced58e311..60e554fc7a 100644 --- a/src/Mod/Spreadsheet/Gui/SpreadsheetDelegate.cpp +++ b/src/Mod/Spreadsheet/Gui/SpreadsheetDelegate.cpp @@ -24,8 +24,8 @@ #include "PreCompiled.h" #ifndef _PreComp_ -#include -#include +# include +# include #endif #include "SpreadsheetDelegate.h" @@ -33,11 +33,13 @@ #include #include #include +#include "DlgBindSheet.h" +using namespace Spreadsheet; using namespace SpreadsheetGui; SpreadsheetDelegate::SpreadsheetDelegate(Spreadsheet::Sheet * _sheet, QWidget *parent) - : QItemDelegate(parent) + : QStyledItemDelegate(parent) , sheet(_sheet) { } @@ -46,7 +48,13 @@ QWidget *SpreadsheetDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const { - Q_UNUSED(index) + App::CellAddress addr(index.row(),index.column()); + App::Range range(addr,addr); + if(sheet && sheet->getCellBinding(range)) { + FC_ERR("Bound cell " << addr.toString() << " cannot be edited"); + return 0; + } + SpreadsheetGui::LineEdit *editor = new SpreadsheetGui::LineEdit(parent); editor->setDocumentObject(sheet); connect(editor, &SpreadsheetGui::LineEdit::finishedWithKey, this, &SpreadsheetDelegate::on_editorFinishedWithKey); @@ -85,5 +93,32 @@ QSize SpreadsheetDelegate::sizeHint(const QStyleOptionViewItem & option, const Q return QSize(); } +void SpreadsheetDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + QStyledItemDelegate::paint(painter, option, index); + if(!sheet) + return; + unsigned flags = sheet->getCellBindingBorder(App::CellAddress(index.row(),index.column())); + if(!flags) + return; + QPen pen(Qt::blue); + pen.setWidth(1); + pen.setStyle(Qt::SolidLine); + painter->setPen(pen); + if(flags == Sheet::BorderAll) { + painter->drawRect(option.rect); + return; + } + if(flags & Sheet::BorderLeft) + painter->drawLine(option.rect.topLeft(), option.rect.bottomLeft()); + if(flags & Sheet::BorderTop) + painter->drawLine(option.rect.topLeft(), option.rect.topRight()); + if(flags & Sheet::BorderRight) + painter->drawLine(option.rect.topRight(), option.rect.bottomRight()); + if(flags & Sheet::BorderBottom) + painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight()); +} + #include "moc_SpreadsheetDelegate.cpp" diff --git a/src/Mod/Spreadsheet/Gui/SpreadsheetDelegate.h b/src/Mod/Spreadsheet/Gui/SpreadsheetDelegate.h index fe4efc7855..2a477b1cb1 100644 --- a/src/Mod/Spreadsheet/Gui/SpreadsheetDelegate.h +++ b/src/Mod/Spreadsheet/Gui/SpreadsheetDelegate.h @@ -25,7 +25,7 @@ #ifndef SPREADSHEETDELEGATE_H #define SPREADSHEETDELEGATE_H -#include +#include namespace Spreadsheet { class Sheet; @@ -33,7 +33,7 @@ class Sheet; namespace SpreadsheetGui { -class SpreadsheetDelegate : public QItemDelegate +class SpreadsheetDelegate : public QStyledItemDelegate { Q_OBJECT public: @@ -45,8 +45,11 @@ public: const QModelIndex &index) const; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const; + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const; + Q_SIGNALS: void finishedWithKey(int key, Qt::KeyboardModifiers modifiers); + private Q_SLOTS: void on_editorFinishedWithKey(int key, Qt::KeyboardModifiers modifiers); private: