Files
create/src/Mod/Spreadsheet/App/Sheet.cpp
2022-10-25 23:54:35 +02:00

1639 lines
49 KiB
C++

/***************************************************************************
* Copyright (c) 2015 Eivind Kvedalen <eivind@kvedalen.name> *
* *
* 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_
# include <deque>
# include <memory>
# include <sstream>
# include <boost/tokenizer.hpp>
#endif
#include <App/Application.h>
#include <App/Document.h>
#include <App/DynamicProperty.h>
#include <App/ExpressionParser.h>
#include <App/FeaturePythonPyImp.h>
#include <Base/Exception.h>
#include <Base/FileInfo.h>
#include <Base/Reader.h>
#include <Base/Stream.h>
#include <Base/Tools.h>
#include "Sheet.h"
#include "SheetObserver.h"
#include "SheetPy.h"
FC_LOG_LEVEL_INIT("Spreadsheet", true, true)
using namespace Base;
using namespace App;
using namespace Spreadsheet;
PROPERTY_SOURCE(Spreadsheet::Sheet, App::DocumentObject)
using DependencyList = 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:
>;
using Traits = boost::graph_traits<DependencyList>;
using Vertex = Traits::vertex_descriptor;
using Edge = Traits::edge_descriptor;
/**
* 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<std::string> propNames = props.getDynamicPropertyNames();
for (std::vector<std::string>::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 &quote, 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<char> e;
int col = 0;
if (quoteChar)
e = escaped_list_separator<char>(escapeChar, delimiter, quoteChar);
else
e = escaped_list_separator<char>('\0', delimiter, '\0');
tokenizer<escaped_list_separator<char> > tok(line, e);
for(tokenizer<escaped_list_separator<char> >::iterator i = tok.begin(); i != tok.end();++i) {
if (!i->empty())
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
{
Base::ofstream file;
int prevRow = -1, prevCol = -1;
Base::FileInfo fi(filename);
file.open(fi, std::ios::out | std::ios::ate | std::ios::binary);
if (!file.is_open())
return false;
auto usedCells = cells.getNonEmptyCells();
auto 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<PropertyQuantity*>(prop)->getValue();
else if (prop->isDerivedFrom((PropertyFloat::getClassTypeId())))
field << static_cast<PropertyFloat*>(prop)->getValue();
else if (prop->isDerivedFrom((PropertyInteger::getClassTypeId())))
field << static_cast<PropertyInteger*>(prop)->getValue();
else if (prop->isDerivedFrom((PropertyString::getClassTypeId())))
field << static_cast<PropertyString*>(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)
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 && contents);
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);
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 Property*, CellAddress >::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<int, int> Sheet::getColumnWidths() const
{
return columnWidths.getValues();
}
/**
* @brief Get a map with row indices and heights.
* @return Map with results
*/
std::map<int, int> 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<PropertyFloat>(addDynamicProperty("App::PropertyFloat", name.c_str(), nullptr, nullptr, Prop_ReadOnly | Prop_Hidden | Prop_NoPersist));
}
else
floatProp = static_cast<PropertyFloat*>(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<PropertyInteger>(addDynamicProperty(
"App::PropertyInteger", name.c_str(), nullptr, nullptr,
Prop_ReadOnly | Prop_Hidden | Prop_NoPersist));
}
else
intProp = static_cast<PropertyInteger*>(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<PropertySpreadsheetQuantity>(p);
}
else
quantityProp = static_cast<PropertySpreadsheetQuantity*>(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<PropertyString>(prop);
if (!stringProp) {
if (prop) {
this->removeDynamicProperty(name.c_str());
propAddress.erase(prop);
}
stringProp = freecad_dynamic_cast<PropertyString>(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<PropertyPythonObject>(prop);
if (!pyProp) {
if (prop) {
this->removeDynamicProperty(name.c_str());
propAddress.erase(prop);
}
pyProp = freecad_dynamic_cast<PropertyPythonObject>(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) {
std::unique_ptr<Expression> 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) && !s.empty())
output.reset(new StringExpression(this, s));
else {
this->removeDynamicProperty(key.toString().c_str());
return;
}
}
/* Eval returns either NumberExpression or StringExpression, or
* PyObjectExpression objects */
auto number = freecad_dynamic_cast<NumberExpression>(output.get());
if(number) {
long l;
auto constant = freecad_dynamic_cast<ConstantExpression>(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<StringExpression>(output.get());
if(str_expr)
setStringProperty(key, str_expr->getText().c_str());
else {
Base::PyGILStateLocker lock;
auto py_expr = freecad_dynamic_cast<PyObjectExpression>(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<std::pair<const char*,Property*> > &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;
}
}
PropertySheet::BindingType
Sheet::getCellBinding(Range &range,
ExpressionPtr *pStart,
ExpressionPtr *pEnd,
App::ObjectIdentifier *pTarget) const
{
range.normalize();
do {
CellAddress addr = *range;
for(const 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,pTarget);
if(res != PropertySheet::BindingNone) {
range = r;
return res;
}
}
}
} while(range.next());
return PropertySheet::BindingNone;
}
static inline unsigned _getBorder(const Sheet *sheet,
const std::vector<App::Range> &ranges,
const App::CellAddress &address)
{
unsigned flags = 0;
int rows, cols;
sheet->getSpans(address, rows, cols);
--rows;
--cols;
for(auto &range : ranges) {
auto from = range.from();
auto to = range.to();
if(address.row() < from.row()
|| address.row() + rows > to.row()
|| address.col() < from.col()
|| address.col() + cols > to.col())
continue;
if(address.row() == from.row())
flags |= Sheet::BorderTop;
if(address.row() == to.row() || address.row() + rows == to.row())
flags |= Sheet::BorderBottom;
if(address.col() == from.col())
flags |= Sheet::BorderLeft;
if(address.col() == to.col() || address.col() + cols == to.col())
flags |= Sheet::BorderRight;
if(flags == Sheet::BorderAll)
break;
}
return flags;
}
unsigned Sheet::getCellBindingBorder(App::CellAddress address) const {
return _getBorder(this, boundRanges, address);
}
void Sheet::updateBindings()
{
std::set<Range> oldRangeSet(boundRanges.begin(), boundRanges.end());
std::set<Range> newRangeSet;
std::set<Range> rangeSet;
boundRanges.clear();
for(const auto &v : ExpressionEngine.getExpressions()) {
CellAddress from,to;
if(!cells.isBindingPath(v.first,&from,&to))
continue;
App::Range range(from,to,true);
if(!oldRangeSet.erase(range))
newRangeSet.insert(range);
rangeSet.insert(range);
}
boundRanges.reserve(rangeSet.size());
boundRanges.insert(boundRanges.end(),rangeSet.begin(),rangeSet.end());
for(const auto &range : oldRangeSet)
rangeUpdated(range);
for(const auto &range : newRangeSet)
rangeUpdated(range);
}
/**
* Update the document properties.
*
*/
DocumentObjectExecReturn *Sheet::execute(void)
{
updateBindings();
// Get dirty cells that we have to recompute
std::set<CellAddress> dirtyCells = cells.getDirty();
// Always recompute cells that have failed
for (std::set<CellAddress>::const_iterator i = cellErrors.begin(); i != cellErrors.end(); ++i) {
cells.recomputeDependencies(*i);
dirtyCells.insert(*i);
}
DependencyList graph;
std::map<CellAddress, Vertex> VertexList;
std::map<Vertex, CellAddress> VertexIndexList;
std::deque<CellAddress> workQueue(dirtyCells.begin(),dirtyCells.end());
while(!workQueue.empty()) {
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<Vertex> 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_TRACE(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.empty()) {
std::deque<CellAddress> workQueue;
DependencyList graph;
std::map<CellAddress, Vertex> VertexList;
std::map<Vertex, CellAddress> VertexIndexList;
CellAddress currentAddr = *dirtyCells.begin();
workQueue.push_back(currentAddr);
dirtyCells.erase(dirtyCells.begin());
while (!workQueue.empty()) {
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<Vertex> 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<int> & dirtyColumns = columnWidths.getDirty();
for (std::set<int>::const_iterator i = dirtyColumns.begin(); i != dirtyColumns.end(); ++i)
columnWidthChanged(*i, columnWidths.getValue(*i));
// Signal update of row heights
const std::set<int> & dirtyRows = rowHeights.getDirty();
for (std::set<int>::const_iterator i = dirtyRows.begin(); i != dirtyRows.end(); ++i)
rowHeightChanged(*i, rowHeights.getValue(*i));
//cells.clearDirty();
rowHeights.clearDirty();
columnWidths.clearDirty();
if (cellErrors.empty())
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.empty() || 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*/)
{
if (auto cell = getCell(address)) {
// Remove alias, if defined
std::string aliasStr;
if (cell->getAlias(aliasStr))
this->removeDynamicProperty(aliasStr.c_str());
cells.clear(address);
}
std::string addr = address.toString();
if (auto prop = props.getDynamicPropertyByName(addr.c_str())) {
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<std::string> Sheet::getUsedCells() const
{
std::vector<std::string> usedCells;
for (const auto &addr : cells.getUsedCells())
usedCells.push_back(addr.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<int,int> 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);
}
/**
* Insert \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<std::string> &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.empty()) {
if (existingAlias == address.toString()) // Same as old?
return;
else
throw Base::ValueError("Alias already defined");
}
else if (alias.empty()) // 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).empty() )
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<std::string> 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<std::string> & result) const
{
std::string fullName = getFullName() + ".";
std::set<CellAddress> tmpResult = cells.getDeps(fullName + address.toString());
for (std::set<CellAddress>::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<CellAddress> 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<ObjectIdentifier, ObjectIdentifier> &paths)
{
DocumentObject::renameObjectIdentifiers(paths);
cells.renameObjectIdentifiers(paths);
}
bool Sheet::hasCell(const std::vector<App::Range> &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);
}
else
cells.slotChangedObject(*this, *prop);
App::DocumentObject::onChanged(prop);
}
void Sheet::setCopyOrCutRanges(const std::vector<App::Range> &ranges, bool copy)
{
std::set<Range> rangeSet(copyCutRanges.begin(), copyCutRanges.end());
copyCutRanges = ranges;
rangeSet.insert(copyCutRanges.begin(), copyCutRanges.end());
for(auto range : rangeSet)
rangeUpdated(range);
hasCopyRange = copy;
}
const std::vector<Range> &Sheet::getCopyOrCutRange(bool copy) const
{
static const std::vector<Range> nullRange;
if(hasCopyRange != copy)
return nullRange;
return copyCutRanges;
}
unsigned Sheet::getCopyOrCutBorder(CellAddress address, bool copy) const
{
if(hasCopyRange != copy)
return 0;
return _getBorder(this, 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<const PropertySpreadsheetQuantity&>(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() const {
return "SpreadsheetGui::ViewProviderSheet";
}
template<> PyObject* Spreadsheet::SheetPython::getPyObject() {
if (PythonObject.is(Py::_None())) {
// ref counter is set to 1
PythonObject = Py::Object(new FeaturePythonPyT<Spreadsheet::SheetPy>(this),true);
}
return Py::new_reference_to(PythonObject);
}
/// @endcond
// explicit template instantiation
template class SpreadsheetExport FeaturePythonT<Spreadsheet::Sheet>;
}