/*************************************************************************** * Copyright (c) 2015 Eivind Kvedalen * * * * 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" #ifndef _PreComp_ #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Sheet.h" #include "SheetObserver.h" #include "Utils.h" #include "SheetPy.h" #include #include #include #include #include #include FC_LOG_LEVEL_INIT("Spreadsheet",true,true) using namespace Base; using namespace App; using namespace Spreadsheet; PROPERTY_SOURCE(Spreadsheet::Sheet, App::DocumentObject) typedef boost::adjacency_list < boost::vecS, // class OutEdgeListS : a Sequence or an AssociativeContainer boost::vecS, // class VertexListS : a Sequence or a RandomAccessContainer boost::directedS, // class DirectedS : This is a directed graph boost::no_property, // class VertexProperty: boost::no_property, // class EdgeProperty: boost::no_property, // class GraphProperty: boost::listS // class EdgeListS: > DependencyList; typedef boost::graph_traits Traits; typedef Traits::vertex_descriptor Vertex; typedef Traits::edge_descriptor Edge; /** * Construct a new Sheet object. */ Sheet::Sheet() : DocumentObject() , props(PropertyContainer::dynamicProps) , cells(this) { ADD_PROPERTY_TYPE(cells, (), "Spreadsheet", (PropertyType)(Prop_Hidden), "Cell contents"); ADD_PROPERTY_TYPE(columnWidths, (), "Spreadsheet", (PropertyType)(Prop_ReadOnly|Prop_Hidden|Prop_Output), "Column widths"); ADD_PROPERTY_TYPE(rowHeights, (), "Spreadsheet", (PropertyType)(Prop_ReadOnly|Prop_Hidden|Prop_Output), "Row heights"); ADD_PROPERTY_TYPE(rowHeights, (), "Spreadsheet", (PropertyType)(Prop_ReadOnly|Prop_Hidden), "Row heights"); ExpressionEngine.expressionChanged.connect([this](const App::ObjectIdentifier &) { this->updateBindings(); }); } /** * @brief Sheet::~Sheet * * The destructor; clear properties to release all memory. * */ Sheet::~Sheet() { clearAll(); } /** * Clear all cells in the sheet. */ void Sheet::clearAll() { cells.clear(); std::vector propNames = props.getDynamicPropertyNames(); for (std::vector::const_iterator i = propNames.begin(); i != propNames.end(); ++i) this->removeDynamicProperty((*i).c_str()); propAddress.clear(); cellErrors.clear(); columnWidths.clear(); rowHeights.clear(); for (ObserverMap::iterator i = observers.begin(); i != observers.end(); ++i) delete i->second; observers.clear(); } //validate import/export parameters bool Sheet::getCharsFromPrefs(char &delim, char "e, char &escape, std::string &errMsg){ bool isValid = true; ParameterGrp::handle group = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Spreadsheet"); QString delimiter = QString::fromStdString(group->GetASCII("ImportExportDelimiter","tab")); QString quoteChar = QString::fromStdString(group->GetASCII("ImportExportQuoteCharacter","\"")); QString escapeChar = QString::fromStdString(group->GetASCII("ImportExportEscapeCharacter","\\")); delim = delimiter.size() == 1 ? delimiter[0].toLatin1() : '\0'; if (delimiter.compare(QLatin1String("tab"), Qt::CaseInsensitive) == 0 || delimiter.compare(QLatin1String("\\t"),Qt::CaseInsensitive) == 0){ delim = '\t'; } else if (delimiter.compare(QLatin1String("comma"), Qt::CaseInsensitive) == 0){ delim = ','; } else if (delimiter.compare(QLatin1String("semicolon"), Qt::CaseInsensitive) == 0){ delim = ';'; } if(delim != '\0' && quoteChar.size() == 1 && escapeChar.size() == 1){ quote = quoteChar[0].toLatin1(); escape = escapeChar[0].toLatin1(); } else { isValid = false; std::string importExport = errMsg; std::stringstream errStream; errStream << "Invalid spreadsheet Import/Export parameter.\n"; if (delim == '\0') { errStream << "Unrecognized delimiter: " << delimiter.toStdString() << " (recognized tokens: \\t, tab, semicolon, comma, or any single character)\n"; } if (quoteChar.size() != 1){ errStream << "Invalid quote character: " << quoteChar.toStdString() << " (quote character must be one single character)\n"; } if (escapeChar.size() != 1){ errStream << "Invalid escape character: " << escapeChar.toStdString() << " (escape character must be one single character)\n"; } errStream << importExport << " not done.\n"; errMsg = errStream.str(); } return isValid; } /** * Import a file into the spreadsheet object. * * @param filename Name of file to import * @param delimiter The field delimiter character used. * @param quoteChar Quote character, if any (set to '\0' to disable). * @param escapeChar The escape character used, if any (set to '0' to disable). * * @returns True if successful, false if something failed. */ bool Sheet::importFromFile(const std::string &filename, char delimiter, char quoteChar, char escapeChar) { Base::FileInfo fi(filename); Base::ifstream file(fi, std::ios::in); int row = 0; PropertySheet::AtomicPropertyChange signaller(cells); clearAll(); if (file.is_open()) { std::string line; while (std::getline(file, line)) { using namespace boost; try { escaped_list_separator e; int col = 0; if (quoteChar) e = escaped_list_separator(escapeChar, delimiter, quoteChar); else e = escaped_list_separator('\0', delimiter, '\0'); tokenizer > tok(line, e); for(tokenizer >::iterator i = tok.begin(); i != tok.end();++i) { if ((*i).size() > 0) setCell(CellAddress(row, col), (*i).c_str()); col++; } } catch (...) { signaller.tryInvoke(); return false; } ++row; } file.close(); signaller.tryInvoke(); return true; } else return false; } /** * Write an escaped version of the string \a s to the stream \a out. * * @param s The string to write. * @param quoteChar The quote character. * @param escapeChar The escape character. * @param stream The stream to output the escaped string to. * */ static void writeEscaped(std::string const& s, char quoteChar, char escapeChar, std::ostream & out) { out << quoteChar; for (std::string::const_iterator i = s.begin(), end = s.end(); i != end; ++i) { unsigned char c = *i; if (c != quoteChar) out << c; else { out << escapeChar; out << c; } } out << quoteChar; } /** * Export spreadsheet data to file. * * @param filename Filename to store data to. * @param delimiter Field delimiter * @param quoteChar Quote character ('\0' to disable) * @param escapeChar Escape character ('\0' to disable) * * @returns True if store is successful, false if something failed. * */ bool Sheet::exportToFile(const std::string &filename, char delimiter, char quoteChar, char escapeChar) const { std::ofstream file; int prevRow = -1, prevCol = -1; file.open(filename.c_str(), std::ios::out | std::ios::ate | std::ios::binary); if (!file.is_open()) return false; std::set usedCells = cells.getUsedCells(); std::set::const_iterator i = usedCells.begin(); while (i != usedCells.end()) { Property * prop = getProperty(*i); if (prevRow != -1 && prevRow != i->row()) { for (int j = prevRow; j < i->row(); ++j) file << std::endl; prevCol = usedCells.begin()->col(); } if (prevCol != -1 && i->col() != prevCol) { for (int j = prevCol; j < i->col(); ++j) file << delimiter; } std::stringstream field; if (prop->isDerivedFrom((PropertyQuantity::getClassTypeId()))) field << static_cast(prop)->getValue(); else if (prop->isDerivedFrom((PropertyFloat::getClassTypeId()))) field << static_cast(prop)->getValue(); else if (prop->isDerivedFrom((PropertyInteger::getClassTypeId()))) field << static_cast(prop)->getValue(); else if (prop->isDerivedFrom((PropertyString::getClassTypeId()))) field << static_cast(prop)->getValue(); else assert(0); std::string str = field.str(); if (quoteChar && str.find(quoteChar) != std::string::npos) writeEscaped(str, quoteChar, escapeChar, file); else file << str; prevRow = i->row(); prevCol = i->col(); ++i; } file << std::endl; file.close(); return true; } /** * Merge a rectangle specified by \a range into one logical cell. * Data in all but the upper right cell are cleared when the cells are merged. * * @param range Range to merge. * @returns True if the cells were merged, false if the merge was unsuccessful. * */ bool Sheet::mergeCells(const Range & range) { return cells.mergeCells(range.from(), range.to()); } /** * Split a previously merged cell specified by \a address into its individual cells. * The address can point to any of the cells that make up the merged cell. * * @param address Address of cell to split * */ void Sheet::splitCell(CellAddress address) { cells.splitCell(address); } /** * Get contents of the cell specified by \a address, or 0 if it is not defined * * @returns A CellContent object or 0. */ Cell *Sheet::getCell(CellAddress address) { return cells.getValue(address); } /** * Get cell contents specified by \a address. * * @param address */ Cell *Sheet::getNewCell(CellAddress address) { Cell * cell = getCell(address); if (cell == nullptr) cell = cells.createCell(address); return cell; } /** * Set cell given by \a address to \a contents. * * @param address Address of cell to set. * @param contents New contents of given cell. * */ void Sheet::setCell(const char * address, const char * contents) { assert(address != nullptr && contents != nullptr); setCell(CellAddress(address), contents); } /** * Set cell at \a address to \a value. If a merged cell is specified, the upper right corner of the * merged cell must be specified. * * @param address Row position of cell. * @param value String value of expression. * */ void Sheet::setCell(CellAddress address, const char * value) { assert(value != nullptr); if (*value == '\0') { clear(address, false); return; } setContent(address, value); } /** * Get the Python object for the Sheet. * * @returns The Python object. */ PyObject *Sheet::getPyObject(void) { if (PythonObject.is(Py::_None())){ // ref counter is set to 1 PythonObject = Py::Object(new SheetPy(this),true); } return Py::new_reference_to(PythonObject); } /** * Get the Cell Property for the cell at \a key. * * @returns The Property object. * */ Property * Sheet::getProperty(CellAddress key) const { return props.getDynamicPropertyByName(key.toString(CellAddress::Cell::ShowRowColumn).c_str()); } /** * @brief Get a dynamic property. * @param addr Name of dynamic propeerty. * @return Pointer to property, or 0 if it does not exist. */ Property * Sheet::getProperty(const char * addr) const { return props.getDynamicPropertyByName(addr); } /** * Get the address as \a address of the Property \a prop. This function * throws an exception if the property is not found. * */ bool Sheet::getCellAddress(const Property *prop, CellAddress & address) { std::map::const_iterator i = propAddress.find(prop); if (i != propAddress.end()) { address = i->second; return true; } return false; } App::CellAddress Sheet::getCellAddress(const char *name, bool silent) const { return cells.getCellAddress(name,silent); } App::Range Sheet::getRange(const char *name, bool silent) const { return cells.getRange(name,silent); } /** * @brief Get a map with column indices and widths. * @return Map with results. */ std::map Sheet::getColumnWidths() const { return columnWidths.getValues(); } /** * @brief Get a map with row indices and heights. * @return Map with results */ std::map Sheet::getRowHeights() const { return rowHeights.getValues(); } /** * Update internal structure when document is set for this property. */ void Sheet::onSettingDocument() { cells.documentSet(); } /** * Set the property for cell \p key to a PropertyFloat with the value \a value. * If the Property exists, but of wrong type, the previous Property is destroyed and recreated as the correct type. * * @param key The address of the cell we want to create a Property for * @param value The value we want to assign to the Property. * */ Property * Sheet::setFloatProperty(CellAddress key, double value) { std::string name = key.toString(CellAddress::Cell::ShowRowColumn); Property * prop = props.getDynamicPropertyByName(name.c_str()); PropertyFloat * floatProp; if (!prop || prop->getTypeId() != PropertyFloat::getClassTypeId()) { if (prop) { this->removeDynamicProperty(name.c_str()); propAddress.erase(prop); } floatProp = freecad_dynamic_cast(addDynamicProperty("App::PropertyFloat", name.c_str(), nullptr, nullptr, Prop_ReadOnly | Prop_Hidden | Prop_NoPersist)); } else floatProp = static_cast(prop); propAddress[floatProp] = key; floatProp->setValue(value); return floatProp; } Property * Sheet::setIntegerProperty(CellAddress key, long value) { std::string name = key.toString(CellAddress::Cell::ShowRowColumn); Property * prop = props.getDynamicPropertyByName(name.c_str()); PropertyInteger * intProp; if (!prop || prop->getTypeId() != PropertyInteger::getClassTypeId()) { if (prop) { this->removeDynamicProperty(name.c_str()); propAddress.erase(prop); } intProp = freecad_dynamic_cast(addDynamicProperty( "App::PropertyInteger", name.c_str(), nullptr, nullptr, Prop_ReadOnly | Prop_Hidden | Prop_NoPersist)); } else intProp = static_cast(prop); propAddress[intProp] = key; intProp->setValue(value); return intProp; } /** * Set the property for cell \p key to a PropertyQuantity with \a value and \a unit. * If the Property exists, but of wrong type, the previous Property is destroyed and recreated as the correct type. * * @param key The address of the cell we want to create a Property for * @param value The value we want to assign to the Property. * @param unit The associated unit for \a value. * */ Property * Sheet::setQuantityProperty(CellAddress key, double value, const Base::Unit & unit) { std::string name = key.toString(CellAddress::Cell::ShowRowColumn); Property * prop = props.getDynamicPropertyByName(name.c_str()); PropertySpreadsheetQuantity * quantityProp; if (!prop || prop->getTypeId() != PropertySpreadsheetQuantity::getClassTypeId()) { if (prop) { this->removeDynamicProperty(name.c_str()); propAddress.erase(prop); } Property * p = addDynamicProperty("Spreadsheet::PropertySpreadsheetQuantity", name.c_str(), nullptr, nullptr, Prop_ReadOnly | Prop_Hidden | Prop_NoPersist); quantityProp = freecad_dynamic_cast(p); } else quantityProp = static_cast(prop); propAddress[quantityProp] = key; quantityProp->setValue(value); quantityProp->setUnit(unit); cells.setComputedUnit(key, unit); return quantityProp; } /** * Set the property for cell \p key to a PropertyString with \a value. * If the Property exists, but of wrong type, the previous Property is destroyed and recreated as the correct type. * * @param key The address of the cell we want to create a Property for * @param value The value we want to assign to the Property. * */ Property * Sheet::setStringProperty(CellAddress key, const std::string & value) { std::string name = key.toString(CellAddress::Cell::ShowRowColumn); Property * prop = props.getDynamicPropertyByName(name.c_str()); PropertyString * stringProp = freecad_dynamic_cast(prop); if (!stringProp) { if (prop) { this->removeDynamicProperty(name.c_str()); propAddress.erase(prop); } stringProp = freecad_dynamic_cast(addDynamicProperty("App::PropertyString", name.c_str(), nullptr, nullptr, Prop_ReadOnly | Prop_Hidden | Prop_NoPersist)); } propAddress[stringProp] = key; stringProp->setValue(value.c_str()); return stringProp; } Property * Sheet::setObjectProperty(CellAddress key, Py::Object object) { std::string name = key.toString(CellAddress::Cell::ShowRowColumn); Property * prop = props.getDynamicPropertyByName(name.c_str()); PropertyPythonObject * pyProp = freecad_dynamic_cast(prop); if (!pyProp) { if (prop) { this->removeDynamicProperty(name.c_str()); propAddress.erase(prop); } pyProp = freecad_dynamic_cast(addDynamicProperty("App::PropertyPythonObject", name.c_str(), nullptr, nullptr, Prop_ReadOnly | Prop_Hidden | Prop_NoPersist)); } propAddress[pyProp] = key; pyProp->setValue(object); return pyProp; } struct CurrentAddressLock { CurrentAddressLock(int &r, int &c, const CellAddress &addr) :row(r),col(c) { row = addr.row(); col = addr.col(); } ~CurrentAddressLock() { row = -1; col = -1; } int &row; int &col; }; /** * Update the Property given by \a key. This will also eventually trigger recomputations of cells depending on \a key. * * @param key The address of the cell we want to recompute. * */ void Sheet::updateProperty(CellAddress key) { Cell * cell = getCell(key); if (cell != nullptr) { std::unique_ptr output; const Expression * input = cell->getExpression(); if (input) { CurrentAddressLock lock(currentRow,currentCol,key); output.reset(input->eval()); } else { std::string s; if (cell->getStringContent(s)) output.reset(new StringExpression(this, s)); else output.reset(new StringExpression(this, "")); } /* Eval returns either NumberExpression or StringExpression, or * PyObjectExpression objects */ auto number = freecad_dynamic_cast(output.get()); if(number) { long l; auto constant = freecad_dynamic_cast(output.get()); if(constant && !constant->isNumber()) { Base::PyGILStateLocker lock; setObjectProperty(key, constant->getPyValue()); } else if (!number->getUnit().isEmpty()) setQuantityProperty(key, number->getValue(), number->getUnit()); else if(number->isInteger(&l)) setIntegerProperty(key,l); else setFloatProperty(key, number->getValue()); }else{ auto str_expr = freecad_dynamic_cast(output.get()); if(str_expr) setStringProperty(key, str_expr->getText().c_str()); else { Base::PyGILStateLocker lock; auto py_expr = freecad_dynamic_cast(output.get()); if(py_expr) setObjectProperty(key, py_expr->getPyValue()); else setObjectProperty(key, Py::Object()); } } } else clear(key); cellUpdated(key); } /** * Retrieve a specific Property given by \a name. * This function might throw an exception if something fails, but might also * return 0 in case the property is not found. * * @returns The property, or 0 if not found. * */ Property *Sheet::getPropertyByName(const char* name) const { CellAddress addr = getCellAddress(name,true); Property *prop = nullptr; if(addr.isValid()) prop = getProperty(addr); if (prop) return prop; else return DocumentObject::getPropertyByName(name); } Property *Sheet::getDynamicPropertyByName(const char* name) const { CellAddress addr = getCellAddress(name,true); Property *prop = nullptr; if(addr.isValid()) prop = getProperty(addr); if (prop) return prop; else return DocumentObject::getDynamicPropertyByName(name); } void Sheet::getPropertyNamedList(std::vector > &List) const { DocumentObject::getPropertyNamedList(List); List.reserve(List.size()+cells.aliasProp.size()); for(auto &v : cells.aliasProp) { auto prop = getProperty(v.first); if(prop) List.emplace_back(v.second.c_str(),prop); } } void Sheet::touchCells(Range range) { do { cells.setDirty(*range); }while(range.next()); } void Sheet::recomputeCells(Range range) { do { recomputeCell(*range); }while(range.next()); } /** * @brief Recompute cell at address \a p. * @param p Address of cell. */ void Sheet::recomputeCell(CellAddress p) { Cell * cell = cells.getValue(p); try { if (cell && cell->hasException()) { std::string content; cell->getStringContent(content); cell->setContent(content.c_str()); } updateProperty(p); if(!cell || !cell->hasException()) { cells.clearDirty(p); cellErrors.erase(p); } } catch (const Base::Exception & e) { QString msg = QString::fromUtf8("ERR: %1").arg(QString::fromUtf8(e.what())); setStringProperty(p, Base::Tools::toStdString(msg)); if (cell) cell->setException(e.what()); else e.ReportException(); // Mark as erroneous cellErrors.insert(p); cellUpdated(p); if(e.isDerivedFrom(Base::AbortException::getClassTypeId())) throw; } if (!cell || cell->spansChanged()) 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; } static inline unsigned _getBorder( const std::vector &ranges, const App::CellAddress &address) { unsigned flags = 0; for(auto &range : ranges) { 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 |= Sheet::BorderTop; if(address.row() == to.row()) flags |= Sheet::BorderBottom; if(address.col() == from.col()) flags |= Sheet::BorderLeft; if(address.col() == to.col()) flags |= Sheet::BorderRight; if(flags == Sheet::BorderAll) break; } return flags; } unsigned Sheet::getCellBindingBorder(App::CellAddress address) const { return _getBorder(boundRanges, address); } void Sheet::updateBindings() { std::set oldRangeSet(boundRanges.begin(), boundRanges.end()); std::set newRangeSet; std::set rangeSet; boundRanges.clear(); for(auto &v : ExpressionEngine.getExpressions()) { CellAddress from,to; if(!cells.isBindingPath(v.first,&from,&to)) continue; App::Range range(from,to); if(!oldRangeSet.erase(range)) newRangeSet.insert(range); rangeSet.insert(range); } boundRanges.reserve(rangeSet.size()); boundRanges.insert(boundRanges.end(),rangeSet.begin(),rangeSet.end()); for(auto &range : oldRangeSet) rangeUpdated(range); for(auto &range : newRangeSet) rangeUpdated(range); } /** * Update the document properties. * */ DocumentObjectExecReturn *Sheet::execute(void) { updateBindings(); // Get dirty cells that we have to recompute std::set dirtyCells = cells.getDirty(); // Always recompute cells that have failed for (std::set::const_iterator i = cellErrors.begin(); i != cellErrors.end(); ++i) { cells.recomputeDependencies(*i); dirtyCells.insert(*i); } DependencyList graph; std::map VertexList; std::map VertexIndexList; std::deque workQueue(dirtyCells.begin(),dirtyCells.end()); while(workQueue.size()) { CellAddress currPos = workQueue.front(); workQueue.pop_front(); // Insert into map of CellPos -> Index, if it doesn't exist already auto res = VertexList.emplace(currPos,Vertex()); if(res.second) { res.first->second = add_vertex(graph); VertexIndexList[res.first->second] = currPos; } // Process cells that depend on the current cell for(auto &dep : providesTo(currPos)) { auto resDep = VertexList.emplace(dep,Vertex()); if(resDep.second) { resDep.first->second = add_vertex(graph); VertexIndexList[resDep.first->second] = dep; if(dirtyCells.insert(dep).second) workQueue.push_back(dep); } // Add edge to graph to signal dependency add_edge(res.first->second, resDep.first->second, graph); } } // Compute cells std::list make_order; // Sort graph topologically to find evaluation order try { boost::topological_sort(graph, std::front_inserter(make_order)); // Recompute cells FC_LOG("recomputing " << getFullName()); for(auto &pos : make_order) { const auto &addr = VertexIndexList[pos]; FC_LOG(addr.toString()); recomputeCell(addr); } } catch (std::exception &) { for(auto &v : VertexList) { Cell * cell = cells.getValue(v.first); // Mark as erroneous if(cell) { cellErrors.insert(v.first); cell->setException("Pending computation due to cyclic dependency",true); cellUpdated(v.first); } } // Try to be more user friendly by finding individual loops while(dirtyCells.size()) { std::deque workQueue; DependencyList graph; std::map VertexList; std::map VertexIndexList; CellAddress currentAddr = *dirtyCells.begin(); workQueue.push_back(currentAddr); dirtyCells.erase(dirtyCells.begin()); while (workQueue.size() > 0) { CellAddress currPos = workQueue.front(); workQueue.pop_front(); // Insert into map of CellPos -> Index, if it doesn't exist already auto res = VertexList.emplace(currPos,Vertex()); if(res.second) { res.first->second = add_vertex(graph); VertexIndexList[res.first->second] = currPos; } // Process cells that depend on the current cell for(auto &dep : providesTo(currPos)) { auto resDep = VertexList.emplace(dep,Vertex()); if(resDep.second) { resDep.first->second = add_vertex(graph); VertexIndexList[resDep.first->second] = dep; workQueue.push_back(dep); dirtyCells.erase(dep); } // Add edge to graph to signal dependency add_edge(res.first->second, resDep.first->second, graph); } } std::list make_order; try { boost::topological_sort(graph, std::front_inserter(make_order)); } catch (std::exception&) { // Cycle detected; flag all with errors std::ostringstream ss; ss << "Cyclic dependency"; int count = 0; for(auto &v : VertexList) { if(count++%20 == 0) ss << std::endl; else ss << ", "; ss << v.first.toString(); } std::string msg = ss.str(); for(auto &v : VertexList) { Cell * cell = cells.getValue(v.first); if (cell) { cell->setException(msg.c_str(),true); cellUpdated(v.first); } } } } } // Signal update of column widths const std::set & dirtyColumns = columnWidths.getDirty(); for (std::set::const_iterator i = dirtyColumns.begin(); i != dirtyColumns.end(); ++i) columnWidthChanged(*i, columnWidths.getValue(*i)); // Signal update of row heights const std::set & dirtyRows = rowHeights.getDirty(); for (std::set::const_iterator i = dirtyRows.begin(); i != dirtyRows.end(); ++i) rowHeightChanged(*i, rowHeights.getValue(*i)); //cells.clearDirty(); rowHeights.clearDirty(); columnWidths.clearDirty(); if (cellErrors.size() == 0) return DocumentObject::StdReturn; else return new DocumentObjectExecReturn("One or more cells failed contains errors.", this); } /** * Determine whether this object needs to be executed to update internal structures. * */ short Sheet::mustExecute(void) const { if (cellErrors.size() > 0 || cells.isDirty()) return 1; return DocumentObject::mustExecute(); } /** * Clear the cell at \a address. If \a all is false, only the text or expression * contents are cleared. If \a all is true everything in the cell * is cleared, including color, style, etc. * * @param address Address of cell to clear * @param all Whether we should clear all or not. * */ void Sheet::clear(CellAddress address, bool /*all*/) { Cell * cell = getCell(address); std::string addr = address.toString(); Property * prop = props.getDynamicPropertyByName(addr.c_str()); // Remove alias, if defined std::string aliasStr; if (cell && cell->getAlias(aliasStr)) this->removeDynamicProperty(aliasStr.c_str()); cells.clear(address); propAddress.erase(prop); this->removeDynamicProperty(addr.c_str()); } /** * Get row an column span for the cell at (row, col). * * @param address Address of cell * @param rows The number of unit cells this cell spans * @param cols The number of unit rows this cell spans * */ void Sheet::getSpans(CellAddress address, int &rows, int &cols) const { return cells.getSpans(address, rows, cols); } /** * Determine whether this cell is merged with another or not. * * @param address * * @returns True if cell is merged, false if not. * */ bool Sheet::isMergedCell(CellAddress address) const { return cells.isMergedCell(address); } App::CellAddress Spreadsheet::Sheet::getAnchor(App::CellAddress address) const { return cells.getAnchor(address); } /** * @brief Set column with of column \a col to \a width- * @param col Index of column. * @param width New width of column. */ void Sheet::setColumnWidth(int col, int width) { columnWidths.setValue(col, width); } /** * @brief Get column with of column at index \a col. * @param col * @return */ int Sheet::getColumnWidth(int col) const { return columnWidths.getValue(col); } /** * @brief Set row height of row given by index in \p row to \a height. * @param row Row index. * @param height New height of row. */ void Sheet::setRowHeight(int row, int height) { rowHeights.setValue(row, height); } /** * @brief Get height of row at index \a row. * @param row Index of row. * @return Height */ int Sheet::getRowHeight(int row) const { return rowHeights.getValue(row); } /** * Get a vector of strings with addresses of all used cells. * * @returns vector of strings. * */ std::vector Sheet::getUsedCells() const { std::vector usedCells; // Insert int usedSet std::set usedSet = cells.getUsedCells(); for (std::set::const_iterator i = usedSet.begin(); i != usedSet.end(); ++i) usedCells.push_back(i->toString()); return usedCells; } void Sheet::updateColumnsOrRows(bool horizontal, int section, int count) { const auto &sizes = horizontal?columnWidths.getValues():rowHeights.getValues(); auto iter = sizes.lower_bound(section); if(iter!=sizes.end()) { std::map newsizes(sizes.begin(),iter); if(count>0) { for(;iter!=sizes.end();++iter) newsizes.emplace(iter->first + count, iter->second); } else { iter = sizes.lower_bound(section-count); if(iter!=sizes.end()) { for(;iter!=sizes.end();++iter) newsizes.emplace(iter->first+count, iter->second); } } if(horizontal) { columnWidths.setValues(newsizes); } else { rowHeights.setValues(newsizes); } } } /** * Insert \a count columns at before column \a col in the spreadsheet. * * @param col Address of column where the columns are inserted. * @param count Number of columns to insert * */ void Sheet::insertColumns(int col, int count) { cells.insertColumns(col, count); updateColumnsOrRows(true,col,count); } /** * Remove \a count columns at column \a col. * * @param col Address of first column to remove. * @param count Number of columns to remove. * */ void Sheet::removeColumns(int col, int count) { // Remove aliases, if defined for (auto address : cells.getColumns(col, count)) { auto cell = getCell(address); std::string aliasStr; if (cell && cell->getAlias(aliasStr)) removeDynamicProperty(aliasStr.c_str()); } cells.removeColumns(col, count); updateColumnsOrRows(true,col,-count); } /** * Inser \a count rows at row \a row. * * @param row Address of row where the rows are inserted. * @param count Number of rows to insert. * */ void Sheet::insertRows(int row, int count) { cells.insertRows(row, count); updateColumnsOrRows(false,row,count); } /** * Remove \a count rows starting at \a row from the spreadsheet. * * @param row Address of first row to remove. * @param count Number of rows to remove. * */ void Sheet::removeRows(int row, int count) { // Remove aliases, if defined for (auto address : cells.getRows(row, count)) { auto cell = getCell(address); std::string aliasStr; if (cell && cell->getAlias(aliasStr)) removeDynamicProperty(aliasStr.c_str()); } cells.removeRows(row, count); updateColumnsOrRows(false,row,-count); } /** * @brief Set content of cell at \a address to \a value. * @param address Address of cell * @param value New value */ void Sheet::setContent(CellAddress address, const char *value) { cells.setContent(address, value); } /** * @brief Set alignment of content in cell at \a address to \a alignment. * @param address Address of cell * @param alignment New alignment */ void Sheet::setAlignment(CellAddress address, int alignment) { cells.setAlignment(address, alignment); } /** * @brief Set style of cell at \a address to \a style. * @param address Address of cell * @param style New style */ void Sheet::setStyle(CellAddress address, const std::set &style) { cells.setStyle(address, style); } /** * @brief Set foreground (text color) of cell at address \a address to \a color. * @param address Address of cell * @param color New color */ void Sheet::setForeground(CellAddress address, const Color &color) { cells.setForeground(address, color); } /** * @brief Set background color of cell at address \a address to \a color. * @param address Address of cell * @param color New color */ void Sheet::setBackground(CellAddress address, const Color &color) { cells.setBackground(address, color); } /** * @brief Set display unit of cell at address \a address to \a unit. * @param address Address of cell * @param unit New unit */ void Sheet::setDisplayUnit(CellAddress address, const std::string &unit) { cells.setDisplayUnit(address, unit); } /** * @brief Set computed unit for cell at address \a address to \a unit. * @param address Address of cell * @param unit New unit. */ void Sheet::setComputedUnit(CellAddress address, const Base::Unit &unit) { cells.setComputedUnit(address, unit); } /** * @brief Set alias for cell at address \a address to \a alias. If the alias * is an empty string, the existing alias is removed. * @param address Address of cell * @param alias New alias. */ void Sheet::setAlias(CellAddress address, const std::string &alias) { std::string existingAlias = getAddressFromAlias(alias); if (existingAlias.size() > 0) { if (existingAlias == address.toString()) // Same as old? return; else throw Base::ValueError("Alias already defined"); } else if (alias.size() == 0) // Empty? cells.setAlias(address, ""); else if (isValidAlias(alias)) // Valid? cells.setAlias(address, alias); else throw Base::ValueError("Invalid alias"); } /** * @brief Get cell given an alias string * @param alias Alias for cell * * @returns Name of cell, or empty string if not defined */ std::string Sheet::getAddressFromAlias(const std::string &alias) const { const Cell * cell = cells.getValueFromAlias(alias); if (cell) return cell->getAddress().toString(); else return std::string(); } /** * @brief Determine whether a given alias candidate is valid or not. * * A candidate is valid is the string is syntactically correct, * and the alias does not conflict with an existing property. * */ bool Sheet::isValidAlias(const std::string & candidate) { // Valid syntactically? if (!cells.isValidAlias(candidate)) return false; // Existing alias? Then it's ok if (getAddressFromAlias(candidate).size() > 0 ) return true; // Check to see that is does not crash with any other property in the Sheet object. if (getPropertyByName(candidate.c_str())) return false; else return true; } /** * @brief Set row and column span for the cell at address \a address to \a rows and \a columns. * @param address Address to upper right corner of cell * @param rows Rows to span * @param columns Columns to span */ void Sheet::setSpans(CellAddress address, int rows, int columns) { cells.setSpans(address, rows, columns); } /** * @brief Return a set of dependencies links for cell at \a address. * @param address Address of cell * @return Set of dependencies. */ std::set Sheet::dependsOn(CellAddress address) const { return cells.getDeps(address); } /** * @brief Compute links to cells that cell at \a address provides input to. * @param address Address of cell * @param result Set of links. */ void Sheet::providesTo(CellAddress address, std::set & result) const { std::string fullName = getFullName() + "."; std::set tmpResult = cells.getDeps(fullName + address.toString()); for (std::set::const_iterator i = tmpResult.begin(); i != tmpResult.end(); ++i) result.insert(fullName + i->toString()); } /** * @brief Compute links to cells that cell at \a address provides input to. * @param address Address of cell * @param result Set of links. */ std::set Sheet::providesTo(CellAddress address) const { return cells.getDeps(getFullName()+"."+address.toString()); } void Sheet::onDocumentRestored() { auto ret = execute(); if(ret!=DocumentObject::StdReturn) { FC_ERR("Failed to restore " << getFullName() << ": " << ret->Why); delete ret; } } /** * @brief Create a document observer for this sheet. Used to track changes. * @param document document to observer. */ void Sheet::observeDocument(Document * document) { // observer is no longer required as PropertySheet is now derived from // PropertyLinkBase and will handle all the link related behavior #if 1 (void)document; #else ObserverMap::const_iterator it = observers.find(document->getName()); if (it != observers.end()) { // An observer already exists, increase reference counter for it it->second->ref(); } else { // Create a new observer SheetObserver * observer = new SheetObserver(document, &cells); observers[document->getName()] = observer; } #endif } void Sheet::renameObjectIdentifiers(const std::map &paths) { DocumentObject::renameObjectIdentifiers(paths); cells.renameObjectIdentifiers(paths); } bool Sheet::hasCell(const std::vector &ranges) const { for(auto range : ranges) { do { if(cells.getValue(*range)) return true; }while(range.next()); } return false; } std::string Sheet::getRow(int offset) const { if(currentRow < 0) throw Base::RuntimeError("No current row"); int row = currentRow + offset; if(row<0 || row>CellAddress::MAX_ROWS) throw Base::ValueError("Out of range"); return std::to_string(row+1); } std::string Sheet::getColumn(int offset) const { if(currentCol < 0) throw Base::RuntimeError("No current column"); int col = currentCol + offset; if(col<0 || col>CellAddress::MAX_COLUMNS) throw Base::ValueError("Out of range"); if (col < 26) { char txt[2]; txt[0] = (char)('A' + col); txt[1] = 0; return txt; } col -= 26; char txt[3]; txt[0] = (char)('A' + (col / 26)); txt[1] = (char)('A' + (col % 26)); txt[2] = 0; return txt; } void Sheet::onChanged(const App::Property *prop) { if(prop == &cells) { decltype(copyCutRanges) tmp; tmp.swap(copyCutRanges); for(auto &range : tmp) rangeUpdated(range); } App::DocumentObject::onChanged(prop); } void Sheet::setCopyOrCutRanges(const std::vector &ranges, bool copy) { std::set rangeSet(copyCutRanges.begin(), copyCutRanges.end()); copyCutRanges = ranges; rangeSet.insert(copyCutRanges.begin(), copyCutRanges.end()); for(auto range : rangeSet) rangeUpdated(range); hasCopyRange = copy; } const std::vector &Sheet::getCopyOrCutRange(bool copy) const { static const std::vector nullRange; if(hasCopyRange != copy) return nullRange; return copyCutRanges; } unsigned Sheet::getCopyOrCutBorder(CellAddress address, bool copy) const { if(hasCopyRange != copy) return 0; return _getBorder(copyCutRanges, address); } /////////////////////////////////////////////////////////////////////////////// TYPESYSTEM_SOURCE(Spreadsheet::PropertySpreadsheetQuantity, App::PropertyQuantity) Property *PropertySpreadsheetQuantity::Copy() const { PropertySpreadsheetQuantity * obj = new PropertySpreadsheetQuantity(); obj->_dValue = _dValue; obj->_Unit = _Unit; return obj; } void PropertySpreadsheetQuantity::Paste(const Property &from) { const auto &src = dynamic_cast(from); aboutToSetValue(); _dValue = src._dValue; _Unit = src._Unit; hasSetValue(); } // Python sheet feature --------------------------------------------------------- namespace App { /// @cond DOXERR PROPERTY_SOURCE_TEMPLATE(Spreadsheet::SheetPython, Spreadsheet::Sheet) template<> const char* Spreadsheet::SheetPython::getViewProviderName(void) const { return "SpreadsheetGui::ViewProviderSheet"; } template<> PyObject* Spreadsheet::SheetPython::getPyObject(void) { if (PythonObject.is(Py::_None())) { // ref counter is set to 1 PythonObject = Py::Object(new FeaturePythonPyT(this),true); } return Py::new_reference_to(PythonObject); } /// @endcond // explicit template instantiation template class SpreadsheetExport FeaturePythonT; }