diff --git a/cMake/FreeCAD_Helpers/SetupQt.cmake b/cMake/FreeCAD_Helpers/SetupQt.cmake
index 34c454adba..bbe1316b0a 100644
--- a/cMake/FreeCAD_Helpers/SetupQt.cmake
+++ b/cMake/FreeCAD_Helpers/SetupQt.cmake
@@ -10,6 +10,10 @@ if(BUILD_GUI)
if (FREECAD_QT_MAJOR_VERSION EQUAL 6)
list (APPEND FREECAD_QT_COMPONENTS GuiTools)
list (APPEND FREECAD_QT_COMPONENTS SvgWidgets)
+ elseif (FREECAD_QT_MAJOR_VERSION EQUAL 5)
+ if (WIN32)
+ list (APPEND FREECAD_QT_COMPONENTS WinExtras)
+ endif()
endif()
list (APPEND FREECAD_QT_COMPONENTS OpenGL PrintSupport Svg UiTools Widgets)
if (BUILD_WEB)
diff --git a/src/3rdParty/libE57Format/src/CompressedVectorWriterImpl.cpp b/src/3rdParty/libE57Format/src/CompressedVectorWriterImpl.cpp
index 7f5b30690e..c57da72d63 100644
--- a/src/3rdParty/libE57Format/src/CompressedVectorWriterImpl.cpp
+++ b/src/3rdParty/libE57Format/src/CompressedVectorWriterImpl.cpp
@@ -342,6 +342,7 @@ namespace e57
/// zero after write, if have too much data)
}
+#ifdef E57_MAX_VERBOSE
///??? useful?
/// Get approximation of number of bytes per record of CompressedVector
/// and total of bytes used
@@ -351,7 +352,6 @@ namespace e57
totalBitsPerRecord += bytestream->bitsPerRecord();
}
-#ifdef E57_MAX_VERBOSE
const float totalBytesPerRecord = std::max( totalBitsPerRecord / 8, 0.1F ); //??? trust
std::cout << " totalBytesPerRecord=" << totalBytesPerRecord << std::endl; //???
diff --git a/src/App/CMakeLists.txt b/src/App/CMakeLists.txt
index 02ebe92de2..918ced0a6f 100644
--- a/src/App/CMakeLists.txt
+++ b/src/App/CMakeLists.txt
@@ -261,6 +261,7 @@ SET(FreeCADApp_CPP_SRCS
ComplexGeoData.cpp
ComplexGeoDataPyImp.cpp
Enumeration.cpp
+ IndexedName.cpp
Material.cpp
MaterialPyImp.cpp
Metadata.cpp
@@ -277,6 +278,7 @@ SET(FreeCADApp_HPP_SRCS
ColorModel.h
ComplexGeoData.h
Enumeration.h
+ IndexedName.h
Material.h
Metadata.h
)
diff --git a/src/App/Document.h b/src/App/Document.h
index f09a7310d0..ad202a31d3 100644
--- a/src/App/Document.h
+++ b/src/App/Document.h
@@ -71,13 +71,6 @@ public:
IgnoreErrorOnRecompute = 12, // Don't report errors if the recompute failed
};
- enum class NotificationType {
- Information,
- Warning,
- Error,
- Critical,
- };
-
/** @name Properties */
//@{
/// holds the long name of the document (utf-8 coded)
@@ -177,8 +170,6 @@ public:
boost::signals2::signal&)> signalSkipRecompute;
boost::signals2::signal signalFinishRestoreObject;
boost::signals2::signal signalChangePropertyEditor;
- // signal user message
- boost::signals2::signal signalUserMessage;
//@}
boost::signals2::signal signalLinkXsetValue;
diff --git a/src/App/Expression.cpp b/src/App/Expression.cpp
index 786de7a870..6b253e1619 100644
--- a/src/App/Expression.cpp
+++ b/src/App/Expression.cpp
@@ -106,17 +106,19 @@ FC_LOG_LEVEL_INIT("Expression", true, true)
_e.raiseException();\
}while(0)
-#define EXPR_PY_THROW(_expr) _EXPR_PY_THROW("",_expr)
+#define EXPR_PY_THROW(_expr) _EXPR_PY_THROW("", _expr)
-#define EXPR_THROW(_msg) _EXPR_THROW(_msg,this)
+#define EXPR_THROW(_msg) _EXPR_THROW(_msg, this)
-#define RUNTIME_THROW(_msg) __EXPR_THROW(Base::RuntimeError,_msg, static_cast(nullptr))
+#define ARGUMENT_THROW(_msg) EXPR_THROW("Invalid number of arguments: " _msg)
-#define TYPE_THROW(_msg) __EXPR_THROW(Base::TypeError,_msg, static_cast(nullptr))
+#define RUNTIME_THROW(_msg) __EXPR_THROW(Base::RuntimeError, _msg, static_cast(nullptr))
-#define PARSER_THROW(_msg) __EXPR_THROW(Base::ParserError,_msg, static_cast(nullptr))
+#define TYPE_THROW(_msg) __EXPR_THROW(Base::TypeError, _msg, static_cast(nullptr))
-#define PY_THROW(_msg) __EXPR_THROW(Py::RuntimeError,_msg, static_cast(nullptr))
+#define PARSER_THROW(_msg) __EXPR_THROW(Base::ParserError, _msg, static_cast(nullptr))
+
+#define PY_THROW(_msg) __EXPR_THROW(Py::RuntimeError, _msg, static_cast(nullptr))
static inline std::ostream &operator<<(std::ostream &os, const App::Expression *expr) {
if(expr) {
@@ -1729,60 +1731,91 @@ FunctionExpression::FunctionExpression(const DocumentObject *_owner, Function _f
, args(_args)
{
switch (f) {
+ case ABS:
case ACOS:
case ASIN:
case ATAN:
- case ABS:
- case EXP:
- case LOG:
- case LOG10:
- case SIN:
- case SINH:
- case TAN:
- case TANH:
- case SQRT:
case CBRT:
+ case CEIL:
case COS:
case COSH:
- case ROUND:
- case TRUNC:
- case CEIL:
+ case EXP:
case FLOOR:
- case MINVERT:
- case STR:
case HIDDENREF:
case HREF:
+ case LOG:
+ case LOG10:
+ case MINVERT:
+ case ROTATIONX:
+ case ROTATIONY:
+ case ROTATIONZ:
+ case ROUND:
+ case SIN:
+ case SINH:
+ case SQRT:
+ case STR:
+ case TAN:
+ case TANH:
+ case TRUNC:
if (args.size() != 1)
- EXPR_THROW("Invalid number of arguments: exactly one required.");
+ ARGUMENT_THROW("exactly one required.");
+ break;
+ case PLACEMENT:
+ if (args.size() > 3)
+ ARGUMENT_THROW("exactly one, two, or three required.");
+ break;
+ case TRANSLATIONM:
+ if (args.size() != 1 && args.size() != 3)
+ ARGUMENT_THROW("exactly one or three required.");
break;
- case MOD:
case ATAN2:
+ case MOD:
+ case MROTATEX:
+ case MROTATEY:
+ case MROTATEZ:
case POW:
if (args.size() != 2)
- EXPR_THROW("Invalid number of arguments: exactly two required.");
+ ARGUMENT_THROW("exactly two required.");
break;
- case HYPOT:
case CATH:
+ case HYPOT:
+ case ROTATION:
if (args.size() < 2 || args.size() > 3)
- EXPR_THROW("Invalid number of arguments: exactly two, or three required.");
+ ARGUMENT_THROW("exactly two, or three required.");
+ break;
+ case MTRANSLATE:
+ case MSCALE:
+ if (args.size() != 2 && args.size() != 4)
+ ARGUMENT_THROW("exactly two or four required.");
+ break;
+ case MROTATE:
+ if (args.size() < 2 || args.size() > 4)
+ ARGUMENT_THROW("exactly two, three, or four required.");
+ break;
+ case VECTOR:
+ if (args.size() != 3)
+ ARGUMENT_THROW("exactly three required.");
+ break;
+ case MATRIX:
+ if (args.size() > 16)
+ ARGUMENT_THROW("exactly 16 or less required.");
break;
- case STDDEV:
- case SUM:
case AVERAGE:
case COUNT:
- case MIN:
- case MAX:
case CREATE:
- case MSCALE:
+ case MAX:
+ case MIN:
+ case STDDEV:
+ case SUM:
if (args.empty())
- EXPR_THROW("Invalid number of arguments: at least one required.");
+ ARGUMENT_THROW("at least one required.");
break;
case LIST:
case TUPLE:
break;
- case NONE:
case AGGREGATES:
case LAST:
+ case NONE:
default:
PARSER_THROW("Unknown function");
break;
@@ -1998,6 +2031,76 @@ Py::Object FunctionExpression::evalAggregate(
return pyFromQuantity(c->getQuantity());
}
+Base::Vector3d FunctionExpression::evaluateSecondVectorArgument(const Expression *expression, const std::vector &arguments)
+{
+ Py::Tuple vectorValues;
+ Py::Object secondParameter = arguments[1]->getPyValue();
+
+ if (arguments.size() == 2) {
+ if (!secondParameter.isSequence())
+ _EXPR_THROW("Second parameter is not a sequence type: '" << secondParameter.as_string() << "'.", expression);
+ if (PySequence_Size(secondParameter.ptr()) != 3)
+ _EXPR_THROW("Second parameter provided has " << PySequence_Size(secondParameter.ptr()) << " elements instead of 3.", expression);
+
+ vectorValues = Py::Tuple(Py::Sequence(secondParameter));
+ } else {
+ vectorValues = Py::Tuple(3);
+ vectorValues.setItem(0, secondParameter);
+ vectorValues.setItem(1, arguments[2]->getPyValue());
+ vectorValues.setItem(2, arguments[3]->getPyValue());
+ }
+
+ Vector3d vector;
+ if (!PyArg_ParseTuple(vectorValues.ptr(), "ddd", &vector.x, &vector.y, &vector.z)) {
+ PyErr_Clear();
+ _EXPR_THROW("Error parsing scale values.", expression);
+ }
+
+ return vector;
+}
+
+void FunctionExpression::initialiseObject(const Py::Object *object, const std::vector &arguments, const unsigned long offset)
+{
+ if (arguments.size() > offset) {
+ Py::Tuple constructorArguments(arguments.size() - offset);
+ for (unsigned i = offset; i < arguments.size(); ++i)
+ constructorArguments.setItem(i - offset, arguments[i]->getPyValue());
+ Py::Dict kwd;
+ PyObjectBase::__PyInit(object->ptr(), constructorArguments.ptr(), kwd.ptr());
+ }
+}
+
+Py::Object FunctionExpression::transformFirstArgument(
+ const Expression* expression,
+ const std::vector &arguments,
+ const Base::Matrix4D* transformationMatrix
+)
+{
+ Py::Object target = arguments[0]->getPyValue();
+
+ if (PyObject_TypeCheck(target.ptr(), &Base::MatrixPy::Type)) {
+ Base::Matrix4D matrix = static_cast(target.ptr())->value();
+ return Py::asObject(new Base::MatrixPy(*transformationMatrix * matrix));
+ } else if (PyObject_TypeCheck(target.ptr(), &Base::PlacementPy::Type)) {
+ Base::Matrix4D placementMatrix =
+ static_cast(target.ptr())->getPlacementPtr()->toMatrix();
+ return Py::asObject(new Base::PlacementPy(Base::Placement(*transformationMatrix * placementMatrix)));
+ } else if (PyObject_TypeCheck(target.ptr(), &Base::RotationPy::Type)) {
+ Base::Matrix4D rotatioMatrix;
+ static_cast(target.ptr())->getRotationPtr()->getValue(rotatioMatrix);
+ return Py::asObject(new Base::RotationPy(Base::Rotation(*transformationMatrix * rotatioMatrix)));
+ }
+
+ _EXPR_THROW("Function requires the first argument to be either Matrix, Placement or Rotation.", expression);
+}
+
+Py::Object FunctionExpression::translationMatrix(double x, double y, double z)
+{
+ Base::Matrix4D matrix;
+ matrix.move(x, y, z);
+ return Py::asObject(new Base::MatrixPy(matrix));
+}
+
Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std::vector &args)
{
if(!expr || !expr->getOwner())
@@ -2007,103 +2110,163 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
if (f > AGGREGATES)
return evalAggregate(expr, f, args);
- if(f == LIST) {
- if(args.size() == 1 && args[0]->isDerivedFrom(RangeExpression::getClassTypeId()))
+ switch (f) {
+ case LIST: {
+ if (args.size() == 1 && args[0]->isDerivedFrom(RangeExpression::getClassTypeId()))
return args[0]->getPyValue();
Py::List list(args.size());
- int i=0;
- for(auto &arg : args)
- list.setItem(i++,arg->getPyValue());
+ int i = 0;
+ for (auto &arg : args)
+ list.setItem(i++, arg->getPyValue());
return list;
- } else if (f == TUPLE) {
- if(args.size() == 1 && args[0]->isDerivedFrom(RangeExpression::getClassTypeId()))
+ }
+ case TUPLE: {
+ if (args.size() == 1 && args[0]->isDerivedFrom(RangeExpression::getClassTypeId()))
return Py::Tuple(args[0]->getPyValue());
Py::Tuple tuple(args.size());
- int i=0;
- for(auto &arg : args)
- tuple.setItem(i++,arg->getPyValue());
+ int i = 0;
+ for (auto &arg : args)
+ tuple.setItem(i++, arg->getPyValue());
return tuple;
- } else if (f == MSCALE) {
- if(args.size() < 2)
- _EXPR_THROW("Function requires at least two arguments.",expr);
- Py::Object pymat = args[0]->getPyValue();
- Py::Object pyscale;
- if(PyObject_TypeCheck(pymat.ptr(),&Base::MatrixPy::Type)) {
- if(args.size() == 2) {
- Py::Object obj = args[1]->getPyValue();
- if(obj.isSequence() && PySequence_Size(obj.ptr())==3)
- pyscale = Py::Tuple(Py::Sequence(obj));
- } else if(args.size() == 4) {
- Py::Tuple tuple(3);
- tuple.setItem(0,args[1]->getPyValue());
- tuple.setItem(1,args[2]->getPyValue());
- tuple.setItem(2,args[3]->getPyValue());
- pyscale = tuple;
- }
- }
- if(!pyscale.isNone()) {
- Base::Vector3d vec;
- if (!PyArg_ParseTuple(pyscale.ptr(), "ddd", &vec.x,&vec.y,&vec.z))
- PyErr_Clear();
- else {
- auto mat = static_cast(pymat.ptr())->value();
- mat.scale(vec);
- return Py::asObject(new Base::MatrixPy(mat));
- }
- }
- _EXPR_THROW("Function requires arguments to be either "
- "(matrix,vector) or (matrix,number,number,number).", expr);
+ }
}
if(args.empty())
_EXPR_THROW("Function requires at least one argument.",expr);
- if (f == MINVERT) {
+ switch (f) {
+ case MINVERT: {
Py::Object pyobj = args[0]->getPyValue();
- if (PyObject_TypeCheck(pyobj.ptr(),&Base::MatrixPy::Type)) {
+ if (PyObject_TypeCheck(pyobj.ptr(), &Base::MatrixPy::Type)) {
auto m = static_cast(pyobj.ptr())->value();
if (fabs(m.determinant()) <= DBL_EPSILON)
- _EXPR_THROW("Cannot invert singular matrix.",expr);
+ _EXPR_THROW("Cannot invert singular matrix.", expr);
m.inverseGauss();
return Py::asObject(new Base::MatrixPy(m));
-
- } else if (PyObject_TypeCheck(pyobj.ptr(),&Base::PlacementPy::Type)) {
+ } else if (PyObject_TypeCheck(pyobj.ptr(), &Base::PlacementPy::Type)) {
const auto &pla = *static_cast(pyobj.ptr())->getPlacementPtr();
return Py::asObject(new Base::PlacementPy(pla.inverse()));
-
- } else if (PyObject_TypeCheck(pyobj.ptr(),&Base::RotationPy::Type)) {
+ } else if (PyObject_TypeCheck(pyobj.ptr(), &Base::RotationPy::Type)) {
const auto &rot = *static_cast(pyobj.ptr())->getRotationPtr();
return Py::asObject(new Base::RotationPy(rot.inverse()));
}
- _EXPR_THROW("Function requires the first argument to be either Matrix, Placement or Rotation.",expr);
+ _EXPR_THROW(
+ "Function requires the first argument to be either Matrix, Placement or Rotation.",
+ expr);
+ break;
+ }
+ case MROTATE: {
+ Py::Object rotationObject = args[1]->getPyValue();
+ if (!PyObject_TypeCheck(rotationObject.ptr(), &Base::RotationPy::Type))
+ {
+ rotationObject = Py::asObject(new Base::RotationPy(Base::Rotation()));
+ initialiseObject(&rotationObject, args, 1);
+ }
- } else if (f == CREATE) {
+ Base::Matrix4D rotationMatrix;
+ static_cast(rotationObject.ptr())->getRotationPtr()->getValue(rotationMatrix);
+
+ return transformFirstArgument(expr, args, &rotationMatrix);
+ }
+ case MROTATEX:
+ case MROTATEY:
+ case MROTATEZ:
+ {
+ Py::Object rotationAngleParameter = args[1]->getPyValue();
+ Quantity rotationAngle = pyToQuantity(rotationAngleParameter, expr, "Invalid rotation angle.");
+
+ if (!(rotationAngle.isDimensionlessOrUnit(Unit::Angle)))
+ _EXPR_THROW("Unit must be either empty or an angle.", expr);
+
+ Rotation rotation = Base::Rotation(
+ Vector3d(static_cast(f == MROTATEX), static_cast(f == MROTATEY), static_cast(f == MROTATEZ)),
+ rotationAngle.getValue() * M_PI / 180.0);
+ Base::Matrix4D rotationMatrix;
+ rotation.getValue(rotationMatrix);
+
+ return transformFirstArgument(expr, args, &rotationMatrix);
+ }
+ case MSCALE: {
+ Vector3d scaleValues = evaluateSecondVectorArgument(expr, args);
+
+ Base::Matrix4D scaleMatrix;
+ scaleMatrix.scale(scaleValues);
+
+ return transformFirstArgument(expr, args, &scaleMatrix);
+ }
+ case MTRANSLATE: {
+ Vector3d translateValues = evaluateSecondVectorArgument(expr, args);
+
+ Base::Matrix4D translateMatrix;
+ translateMatrix.move(translateValues);
+
+ Py::Object target = args[0]->getPyValue();
+ if (PyObject_TypeCheck(target.ptr(), &Base::RotationPy::Type)) {
+ Base::Matrix4D targetRotatioMatrix;
+ static_cast(target.ptr())->getRotationPtr()->getValue(targetRotatioMatrix);
+ return Py::asObject(new Base::PlacementPy(Base::Placement(translateMatrix * targetRotatioMatrix)));
+ }
+
+ return transformFirstArgument(expr, args, &translateMatrix);
+ }
+ case CREATE: {
Py::Object pytype = args[0]->getPyValue();
- if(!pytype.isString())
- _EXPR_THROW("Function requires the first argument to be a string.",expr);
+ if (!pytype.isString())
+ _EXPR_THROW("Function requires the first argument to be a string.", expr);
std::string type(pytype.as_string());
Py::Object res;
- if(boost::iequals(type,"matrix"))
+ if (boost::iequals(type, "matrix"))
res = Py::asObject(new Base::MatrixPy(Base::Matrix4D()));
- else if(boost::iequals(type,"vector"))
+ else if (boost::iequals(type, "vector"))
res = Py::asObject(new Base::VectorPy(Base::Vector3d()));
- else if(boost::iequals(type,"placement"))
+ else if (boost::iequals(type, "placement"))
res = Py::asObject(new Base::PlacementPy(Base::Placement()));
- else if(boost::iequals(type,"rotation"))
+ else if (boost::iequals(type, "rotation"))
res = Py::asObject(new Base::RotationPy(Base::Rotation()));
else
- _EXPR_THROW("Unknown type '" << type << "'.",expr);
- if(args.size()>1) {
- Py::Tuple tuple(args.size()-1);
- for(unsigned i=1;igetPyValue());
- Py::Dict dict;
- PyObjectBase::__PyInit(res.ptr(),tuple.ptr(),dict.ptr());
- }
+ _EXPR_THROW("Unknown type '" << type << "'.", expr);
+ initialiseObject(&res, args, 1);
return res;
- } else if (f == STR) {
+ }
+ case MATRIX: {
+ Py::Object matrix = Py::asObject(new Base::MatrixPy(Base::Matrix4D()));
+ initialiseObject(&matrix, args);
+ return matrix;
+ }
+ case PLACEMENT: {
+ Py::Object placement = Py::asObject(new Base::PlacementPy(Base::Placement()));
+ initialiseObject(&placement, args);
+ return placement;
+ }
+ case ROTATION: {
+ Py::Object rotation = Py::asObject(new Base::RotationPy(Base::Rotation()));
+ initialiseObject(&rotation, args);
+ return rotation;
+ }
+ case STR:
return Py::String(args[0]->getPyValue().as_string());
- } else if (f == HIDDENREF || f == HREF) {
+ case TRANSLATIONM: {
+ if (args.size() != 1)
+ break; // Break and proceed to 3 size version.
+ Py::Object parameter = args[0]->getPyValue();
+ if (!parameter.isSequence())
+ _EXPR_THROW("Not sequence type: '" << parameter.as_string() << "'.", expr);
+ if (PySequence_Size(parameter.ptr()) != 3)
+ _EXPR_THROW("Sequence provided has " << PySequence_Size(parameter.ptr()) << " elements instead of 3.", expr);
+ double x, y, z;
+ if (!PyArg_ParseTuple(Py::Tuple(Py::Sequence(parameter)).ptr(), "ddd", &x, &y, &z)) {
+ PyErr_Clear();
+ _EXPR_THROW("Error parsing sequence.", expr);
+ }
+ return translationMatrix(x, y, z);
+ }
+ case VECTOR: {
+ Py::Object vector = Py::asObject(new Base::VectorPy(Base::Vector3d()));
+ initialiseObject(&vector, args);
+ return vector;
+ }
+ case HIDDENREF:
+ case HREF:
return args[0]->getPyValue();
}
@@ -2111,13 +2274,13 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
Quantity v1 = pyToQuantity(e1,expr,"Invalid first argument.");
Py::Object e2;
Quantity v2;
- if(args.size()>1) {
+ if (args.size() > 1) {
e2 = args[1]->getPyValue();
v2 = pyToQuantity(e2,expr,"Invalid second argument.");
}
Py::Object e3;
Quantity v3;
- if(args.size()>2) {
+ if (args.size() > 2) {
e3 = args[2]->getPyValue();
v3 = pyToQuantity(e3,expr,"Invalid third argument.");
}
@@ -2133,8 +2296,11 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
case COS:
case SIN:
case TAN:
- if (!(v1.getUnit() == Unit::Angle || v1.getUnit().isEmpty()))
- _EXPR_THROW("Unit must be either empty or an angle.",expr);
+ case ROTATIONX:
+ case ROTATIONY:
+ case ROTATIONZ:
+ if (!(v1.isDimensionlessOrUnit(Unit::Angle)))
+ _EXPR_THROW("Unit must be either empty or an angle.", expr);
// Convert value to radians
value *= M_PI / 180.0;
@@ -2143,8 +2309,8 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
case ACOS:
case ASIN:
case ATAN:
- if (!v1.getUnit().isEmpty())
- _EXPR_THROW("Unit must be empty.",expr);
+ if (!v1.isDimensionless())
+ _EXPR_THROW("Unit must be empty.", expr);
unit = Unit::Angle;
scaler = 180.0 / M_PI;
break;
@@ -2154,7 +2320,7 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
case SINH:
case TANH:
case COSH:
- if (!v1.getUnit().isEmpty())
+ if (!v1.isDimensionless())
_EXPR_THROW("Unit must be empty.",expr);
unit = Unit();
break;
@@ -2233,12 +2399,12 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
if (e2.isNone())
_EXPR_THROW("Invalid second argument.",expr);
- if (!v2.getUnit().isEmpty())
+ if (!v2.isDimensionless())
_EXPR_THROW("Exponent is not allowed to have a unit.",expr);
// Compute new unit for exponentiation
double exponent = v2.getValue();
- if (!v1.getUnit().isEmpty()) {
+ if (!v1.isDimensionless()) {
if (exponent - boost::math::round(exponent) < 1e-9)
unit = v1.getUnit().pow(exponent);
else
@@ -2261,6 +2427,10 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
}
unit = v1.getUnit();
break;
+ case TRANSLATIONM:
+ if (v1.isDimensionlessOrUnit(Unit::Length) && v2.isDimensionlessOrUnit(Unit::Length) && v3.isDimensionlessOrUnit(Unit::Length))
+ break;
+ _EXPR_THROW("Translation units must be a length or dimensionless.", expr);
default:
_EXPR_THROW("Unknown function: " << f,0);
}
@@ -2344,6 +2514,14 @@ Py::Object FunctionExpression::evaluate(const Expression *expr, int f, const std
case FLOOR:
output = floor(value);
break;
+ case ROTATIONX:
+ case ROTATIONY:
+ case ROTATIONZ:
+ return Py::asObject(new Base::RotationPy(Base::Rotation(
+ Vector3d(static_cast(f == ROTATIONX), static_cast(f == ROTATIONY), static_cast(f == ROTATIONZ)),
+ value)));
+ case TRANSLATIONM:
+ return translationMatrix(v1.getValue(), v2.getValue(), v3.getValue());
default:
_EXPR_THROW("Unknown function: " << f,0);
}
@@ -2397,82 +2575,108 @@ Expression *FunctionExpression::simplify() const
void FunctionExpression::_toString(std::ostream &ss, bool persistent,int) const
{
switch (f) {
+ case ABS:
+ ss << "abs("; break;;
case ACOS:
ss << "acos("; break;;
case ASIN:
ss << "asin("; break;;
case ATAN:
ss << "atan("; break;;
- case ABS:
- ss << "abs("; break;;
- case EXP:
- ss << "exp("; break;;
- case LOG:
- ss << "log("; break;;
- case LOG10:
- ss << "log10("; break;;
- case SIN:
- ss << "sin("; break;;
- case SINH:
- ss << "sinh("; break;;
- case TAN:
- ss << "tan("; break;;
- case TANH:
- ss << "tanh("; break;;
- case SQRT:
- ss << "sqrt("; break;;
+ case ATAN2:
+ ss << "atan2("; break;;
+ case CATH:
+ ss << "cath("; break;;
case CBRT:
ss << "cbrt("; break;;
+ case CEIL:
+ ss << "ceil("; break;;
case COS:
ss << "cos("; break;;
case COSH:
ss << "cosh("; break;;
- case MOD:
- ss << "mod("; break;;
- case ATAN2:
- ss << "atan2("; break;;
- case POW:
- ss << "pow("; break;;
- case HYPOT:
- ss << "hypot("; break;;
- case CATH:
- ss << "cath("; break;;
- case ROUND:
- ss << "round("; break;;
- case TRUNC:
- ss << "trunc("; break;;
- case CEIL:
- ss << "ceil("; break;;
+ case EXP:
+ ss << "exp("; break;;
case FLOOR:
ss << "floor("; break;;
- case SUM:
- ss << "sum("; break;;
- case COUNT:
- ss << "count("; break;;
- case AVERAGE:
- ss << "average("; break;;
- case STDDEV:
- ss << "stddev("; break;;
- case MIN:
- ss << "min("; break;;
- case MAX:
- ss << "max("; break;;
- case LIST:
- ss << "list("; break;;
- case TUPLE:
- ss << "tuple("; break;;
- case MSCALE:
- ss << "mscale("; break;;
+ case HYPOT:
+ ss << "hypot("; break;;
+ case LOG:
+ ss << "log("; break;;
+ case LOG10:
+ ss << "log10("; break;;
+ case MOD:
+ ss << "mod("; break;;
+ case POW:
+ ss << "pow("; break;;
+ case ROUND:
+ ss << "round("; break;;
+ case SIN:
+ ss << "sin("; break;;
+ case SINH:
+ ss << "sinh("; break;;
+ case SQRT:
+ ss << "sqrt("; break;;
+ case TAN:
+ ss << "tan("; break;;
+ case TANH:
+ ss << "tanh("; break;;
+ case TRUNC:
+ ss << "trunc("; break;;
case MINVERT:
ss << "minvert("; break;;
+ case MROTATE:
+ ss << "mrotate("; break;;
+ case MROTATEX:
+ ss << "mrotatex("; break;;
+ case MROTATEY:
+ ss << "mrotatey("; break;;
+ case MROTATEZ:
+ ss << "mrotatez("; break;;
+ case MSCALE:
+ ss << "mscale("; break;;
+ case MTRANSLATE:
+ ss << "mtranslate("; break;;
case CREATE:
ss << "create("; break;;
+ case LIST:
+ ss << "list("; break;;
+ case MATRIX:
+ ss << "matrix("; break;;
+ case PLACEMENT:
+ ss << "placement("; break;;
+ case ROTATION:
+ ss << "rotation("; break;;
+ case ROTATIONX:
+ ss << "rotationx("; break;;
+ case ROTATIONY:
+ ss << "rotationy("; break;;
+ case ROTATIONZ:
+ ss << "rotationz("; break;;
case STR:
ss << "str("; break;;
+ case TRANSLATIONM:
+ ss << "translationm("; break;;
+ case TUPLE:
+ ss << "tuple("; break;;
+ case VECTOR:
+ ss << "vector("; break;;
case HIDDENREF:
ss << "hiddenref("; break;;
case HREF:
ss << "href("; break;;
+ case AVERAGE:
+ ss << "average("; break;;
+ case COUNT:
+ ss << "count("; break;;
+ case MAX:
+ ss << "max("; break;;
+ case MIN:
+ ss << "min("; break;;
+ case STDDEV:
+ ss << "stddev("; break;;
+ case SUM:
+ ss << "sum("; break;;
default:
ss << fname << "("; break;;
}
@@ -3267,46 +3471,62 @@ static void initParser(const App::DocumentObject *owner)
unitExpression = valueExpression = false;
if (!has_registered_functions) {
+ registered_functions["abs"] = FunctionExpression::ABS;
registered_functions["acos"] = FunctionExpression::ACOS;
registered_functions["asin"] = FunctionExpression::ASIN;
registered_functions["atan"] = FunctionExpression::ATAN;
- registered_functions["abs"] = FunctionExpression::ABS;
- registered_functions["exp"] = FunctionExpression::EXP;
- registered_functions["log"] = FunctionExpression::LOG;
- registered_functions["log10"] = FunctionExpression::LOG10;
- registered_functions["sin"] = FunctionExpression::SIN;
- registered_functions["sinh"] = FunctionExpression::SINH;
- registered_functions["tan"] = FunctionExpression::TAN;
- registered_functions["tanh"] = FunctionExpression::TANH;
- registered_functions["sqrt"] = FunctionExpression::SQRT;
+ registered_functions["atan2"] = FunctionExpression::ATAN2;
+ registered_functions["cath"] = FunctionExpression::CATH;
registered_functions["cbrt"] = FunctionExpression::CBRT;
+ registered_functions["ceil"] = FunctionExpression::CEIL;
registered_functions["cos"] = FunctionExpression::COS;
registered_functions["cosh"] = FunctionExpression::COSH;
- registered_functions["atan2"] = FunctionExpression::ATAN2;
+ registered_functions["exp"] = FunctionExpression::EXP;
+ registered_functions["floor"] = FunctionExpression::FLOOR;
+ registered_functions["hypot"] = FunctionExpression::HYPOT;
+ registered_functions["log"] = FunctionExpression::LOG;
+ registered_functions["log10"] = FunctionExpression::LOG10;
registered_functions["mod"] = FunctionExpression::MOD;
registered_functions["pow"] = FunctionExpression::POW;
registered_functions["round"] = FunctionExpression::ROUND;
+ registered_functions["sin"] = FunctionExpression::SIN;
+ registered_functions["sinh"] = FunctionExpression::SINH;
+ registered_functions["sqrt"] = FunctionExpression::SQRT;
+ registered_functions["tan"] = FunctionExpression::TAN;
+ registered_functions["tanh"] = FunctionExpression::TANH;
registered_functions["trunc"] = FunctionExpression::TRUNC;
- registered_functions["ceil"] = FunctionExpression::CEIL;
- registered_functions["floor"] = FunctionExpression::FLOOR;
- registered_functions["hypot"] = FunctionExpression::HYPOT;
- registered_functions["cath"] = FunctionExpression::CATH;
- registered_functions["list"] = FunctionExpression::LIST;
- registered_functions["tuple"] = FunctionExpression::TUPLE;
- registered_functions["mscale"] = FunctionExpression::MSCALE;
+
registered_functions["minvert"] = FunctionExpression::MINVERT;
+ registered_functions["mrotate"] = FunctionExpression::MROTATE;
+ registered_functions["mrotatex"] = FunctionExpression::MROTATEX;
+ registered_functions["mrotatey"] = FunctionExpression::MROTATEY;
+ registered_functions["mrotatez"] = FunctionExpression::MROTATEZ;
+ registered_functions["mscale"] = FunctionExpression::MSCALE;
+ registered_functions["mtranslate"] = FunctionExpression::MTRANSLATE;
+
registered_functions["create"] = FunctionExpression::CREATE;
+ registered_functions["list"] = FunctionExpression::LIST;
+ registered_functions["matrix"] = FunctionExpression::MATRIX;
+ registered_functions["placement"] = FunctionExpression::PLACEMENT;
+ registered_functions["rotation"] = FunctionExpression::ROTATION;
+ registered_functions["rotationx"] = FunctionExpression::ROTATIONX;
+ registered_functions["rotationy"] = FunctionExpression::ROTATIONY;
+ registered_functions["rotationz"] = FunctionExpression::ROTATIONZ;
registered_functions["str"] = FunctionExpression::STR;
+ registered_functions["translationm"] = FunctionExpression::TRANSLATIONM;
+ registered_functions["tuple"] = FunctionExpression::TUPLE;
+ registered_functions["vector"] = FunctionExpression::VECTOR;
+
registered_functions["hiddenref"] = FunctionExpression::HIDDENREF;
registered_functions["href"] = FunctionExpression::HREF;
// Aggregates
- registered_functions["sum"] = FunctionExpression::SUM;
- registered_functions["count"] = FunctionExpression::COUNT;
registered_functions["average"] = FunctionExpression::AVERAGE;
- registered_functions["stddev"] = FunctionExpression::STDDEV;
- registered_functions["min"] = FunctionExpression::MIN;
+ registered_functions["count"] = FunctionExpression::COUNT;
registered_functions["max"] = FunctionExpression::MAX;
+ registered_functions["min"] = FunctionExpression::MIN;
+ registered_functions["stddev"] = FunctionExpression::STDDEV;
+ registered_functions["sum"] = FunctionExpression::SUM;
has_registered_functions = true;
}
diff --git a/src/App/ExpressionParser.h b/src/App/ExpressionParser.h
index f115ad2736..c985093435 100644
--- a/src/App/ExpressionParser.h
+++ b/src/App/ExpressionParser.h
@@ -26,7 +26,9 @@
#define EXPRESSION_PARSER_H
#include "Expression.h"
+#include
#include
+#include
namespace App {
@@ -244,48 +246,66 @@ public:
NONE,
// Normal functions taking one or two arguments
+ ABS,
ACOS,
ASIN,
ATAN,
- ABS,
- EXP,
- LOG,
- LOG10,
- SIN,
- SINH,
- TAN,
- TANH,
- SQRT,
+ ATAN2,
+ CATH,
CBRT,
+ CEIL,
COS,
COSH,
- ATAN2,
+ EXP,
+ FLOOR,
+ HYPOT,
+ LOG,
+ LOG10,
MOD,
POW,
ROUND,
+ SIN,
+ SINH,
+ SQRT,
+ TAN,
+ TANH,
TRUNC,
- CEIL,
- FLOOR,
- HYPOT,
- CATH,
- LIST,
- TUPLE,
- MSCALE, // matrix scale by vector
+
+ // Matrix
MINVERT, // invert matrix/placement/rotation
- CREATE, // create new object of a given type
+ MROTATE, // Rotate matrix/placement/rotation around axis, by rotation object, or by euler angles.
+ MROTATEX, // Rotate matrix/placement/rotation around x-axis.
+ MROTATEY, // Rotate matrix/placement/rotation around y-axis.
+ MROTATEZ, // Rotate matrix/placement/rotation around z-axis.
+ MSCALE, // matrix scale by vector
+ MTRANSLATE, // Translate matrix/placement.
+
+ // Object creation
+ CREATE, // Create new object of a given type.
+ LIST, // Create Python list.
+ MATRIX, // Create matrix object.
+ PLACEMENT, // Create placement object.
+ ROTATION, // Create rotation object.
+ ROTATIONX, // Create x-axis rotation object.
+ ROTATIONY, // Create y-axis rotation object.
+ ROTATIONZ, // Create z-axis rotation object.
STR, // stringify
+ TRANSLATIONM, // Create translation matrix object.
+ TUPLE, // Create Python tuple.
+ VECTOR, // Create vector object.
+
HIDDENREF, // hidden reference that has no dependency check
HREF, // deprecated alias of HIDDENREF
// Aggregates
AGGREGATES,
- SUM,
AVERAGE,
- STDDEV,
COUNT,
- MIN,
MAX,
+ MIN,
+ STDDEV,
+ SUM,
// Last one
LAST,
@@ -307,6 +327,13 @@ public:
protected:
static Py::Object evalAggregate(const Expression *owner, int type, const std::vector &args);
+ static Base::Vector3d evaluateSecondVectorArgument(const Expression *expression, const std::vector &arguments);
+ static void initialiseObject(const Py::Object *object, const std::vector &arguments, const unsigned long offset = 0);
+ static Py::Object transformFirstArgument(
+ const Expression *expression,
+ const std::vector &arguments,
+ const Base::Matrix4D *transformationMatrix);
+ static Py::Object translationMatrix(double x, double y, double z);
Py::Object _getPyValue() const override;
Expression * _copy() const override;
void _visit(ExpressionVisitor & v) override;
diff --git a/src/App/FreeCADInit.py b/src/App/FreeCADInit.py
index e00275344a..9670c56db7 100644
--- a/src/App/FreeCADInit.py
+++ b/src/App/FreeCADInit.py
@@ -63,16 +63,6 @@ def setupSearchPaths(PathExtension):
# new paths must be prepended to avoid to load a wrong version of a library
try:
os.environ["PATH"] = PathEnvironment + os.environ["PATH"]
- except UnicodeDecodeError:
- # See #0002238. FIXME: check again once ported to Python 3.x
- Log('UnicodeDecodeError was raised when concatenating unicode string with PATH. Try to remove non-ascii paths...\n')
- path = os.environ["PATH"].split(os.pathsep)
- cleanpath=[]
- for i in path:
- if test_ascii(i):
- cleanpath.append(i)
- os.environ["PATH"] = PathEnvironment + os.pathsep.join(cleanpath)
- Log('done\n')
except UnicodeEncodeError:
Log('UnicodeEncodeError was raised when concatenating unicode string with PATH. Try to replace non-ascii chars...\n')
os.environ["PATH"] = PathEnvironment.encode(errors='replace') + os.environ["PATH"]
@@ -305,7 +295,6 @@ Wrn = FreeCAD.Console.PrintWarning
Crt = FreeCAD.Console.PrintCritical
Ntf = FreeCAD.Console.PrintNotification
Tnf = FreeCAD.Console.PrintTranslatedNotification
-test_ascii = lambda s: all(ord(c) < 128 for c in s)
#store the cmake variales
App.__cmake__ = cmake;
@@ -962,6 +951,5 @@ App.ReturnType = ReturnType
# clean up namespace
del(InitApplications)
-del(test_ascii)
Log ('Init: App::FreeCADInit.py done\n')
diff --git a/src/App/IndexedName.cpp b/src/App/IndexedName.cpp
new file mode 100644
index 0000000000..ebe9bf2ddb
--- /dev/null
+++ b/src/App/IndexedName.cpp
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+/****************************************************************************
+ * Copyright (c) 2022 Zheng, Lei (realthunder) *
+ * Copyright (c) 2023 FreeCAD Project Association *
+ * *
+ * This file is part of FreeCAD. *
+ * *
+ * FreeCAD is free software: you can redistribute it and/or modify it *
+ * under the terms of the GNU Lesser General Public License as *
+ * published by the Free Software Foundation, either version 2.1 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * FreeCAD 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 *
+ * Lesser General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Lesser General Public *
+ * License along with FreeCAD. If not, see *
+ * . *
+ * *
+ ***************************************************************************/
+
+// NOLINTNEXTLINE
+#include "PreCompiled.h"
+
+#ifndef _PreComp_
+# include
+# include
+#endif
+
+#include "IndexedName.h"
+
+using namespace Data;
+
+/// Check whether the input character is an underscore or an ASCII letter a-Z or A-Z
+inline bool isInvalidChar(char test)
+{
+ return test != '_' && (test < 'a' || test > 'z' ) && (test < 'A' || test > 'Z');
+}
+
+/// Get the integer suffix of name. Returns a tuple of (suffix, suffixPosition). Calling code
+/// should check to ensure that suffixPosition is not equal to nameLength (in which case there was no
+/// suffix).
+///
+/// \param name The name to check
+/// \param nameLength The length of the string in name
+/// \returns An integer pair of the suffix itself and the position of that suffix in name
+std::pair getIntegerSuffix(const char *name, int nameLength)
+{
+ int suffixPosition {nameLength - 1};
+
+ for (; suffixPosition >= 0; --suffixPosition) {
+ // When we support C++20 we can use std::span<> to eliminate the clang-tidy warning
+ // NOLINTNEXTLINE cppcoreguidelines-pro-bounds-pointer-arithmetic
+ if (!isdigit(name[suffixPosition])) {
+ break;
+ }
+ }
+ ++suffixPosition;
+ int suffix {0};
+ if (suffixPosition < nameLength) {
+ // When we support C++20 we can use std::span<> to eliminate the clang-tidy warning
+ // NOLINTNEXTLINE cppcoreguidelines-pro-bounds-pointer-arithmetic
+ suffix = std::atoi(name + suffixPosition);
+ }
+ return std::make_pair(suffix, suffixPosition);
+}
+
+void IndexedName::set(
+ const char* name,
+ int length,
+ const std::vector& allowedNames,
+ bool allowOthers)
+{
+ // Storage for names that we weren't given external storage for
+ static std::unordered_set NameSet;
+
+ if (length < 0) {
+ length = static_cast(std::strlen(name));
+ }
+ // Name typically ends with an integer: find that integer
+ auto [suffix, suffixPosition] = getIntegerSuffix(name, length);
+ if (suffixPosition < length) {
+ this->index = suffix;
+ }
+
+ // Make sure that every character is either an ASCII letter (upper or lowercase), or an
+ // underscore. If any other character appears, reject the entire string.
+ // When we support C++20 we can use std::span<> to eliminate the clang-tidy warning
+ // NOLINTNEXTLINE cppcoreguidelines-pro-bounds-pointer-arithmetic
+ if (std::any_of(name, name+suffixPosition, isInvalidChar)) {
+ this->type = "";
+ return;
+ }
+
+ // If a list of allowedNames was provided, see if our set name matches one of those allowedNames: if it
+ // does, reference that memory location and return.
+ for (const auto *typeName : allowedNames) {
+ if (std::strncmp(name, typeName, suffixPosition) == 0) {
+ this->type = typeName;
+ return;
+ }
+ }
+
+ // If the type was NOT in the list of allowedNames, but the caller has set the allowOthers flag to
+ // true, then add the new type to the static NameSet (if it is not already there).
+ if (allowOthers) {
+ auto res = NameSet.insert(ByteArray(QByteArray::fromRawData(name, suffixPosition)));
+ if (res.second /*The insert succeeded (the type was new)*/) {
+ // Make sure that the data in the set is a unique (unshared) copy of the text
+ res.first->ensureUnshared();
+ }
+ this->type = res.first->bytes.constData();
+ }
+ else {
+ // The passed-in type is not in the allowed list, and allowOthers was not true, so don't
+ // store the type
+ this->type = "";
+ }
+}
diff --git a/src/App/IndexedName.h b/src/App/IndexedName.h
new file mode 100644
index 0000000000..85282b9545
--- /dev/null
+++ b/src/App/IndexedName.h
@@ -0,0 +1,338 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+/****************************************************************************
+ * Copyright (c) 2022 Zheng, Lei (realthunder) *
+ * Copyright (c) 2023 FreeCAD Project Association *
+ * *
+ * This file is part of FreeCAD. *
+ * *
+ * FreeCAD is free software: you can redistribute it and/or modify it *
+ * under the terms of the GNU Lesser General Public License as *
+ * published by the Free Software Foundation, either version 2.1 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * FreeCAD 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 *
+ * Lesser General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Lesser General Public *
+ * License along with FreeCAD. If not, see *
+ * . *
+ * *
+ ***************************************************************************/
+
+#ifndef APP_INDEXEDNAME_H
+#define APP_INDEXEDNAME_H
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "FCGlobal.h"
+
+
+namespace Data
+{
+
+/// The IndexedName class provides a very memory-efficient data structure to hold a name and an index
+/// value, and to perform various comparisons and validations of those values. The name must only
+/// consist of upper- and lower-case ASCII characters and the underscore ('_') character. The index
+/// must be a positive integer. The string representation of this IndexedName is the name followed by
+/// the index, with no spaces between: an IndexedName may be constructed from this string. For
+/// example "EDGE1" or "FACE345" might be the names of elements that use an IndexedName. If there is
+/// then an "EDGE2", only a pointer to the original stored name "EDGE" is retained.
+///
+/// The memory efficiency of the class comes from re-using the same character storage for names that
+/// match, while retaining their differing indices. This is achieved by either using user-provided
+/// const char * names (provided as a list of typeNames and presumed to never be deallocated), or by
+/// maintaining an internal list of names that have been used before, and can be re-used later.
+class AppExport IndexedName {
+public:
+
+ /// Construct from a name and an optional index. If the name contains an index it is read, but
+ /// is used as the index *only* if _index parameter is unset. If the _index parameter is given
+ /// it overrides any trailing integer in the name. Index must be positive, and name must contain
+ /// only ASCII letters and the underscore character. If these conditions are not met, name is
+ /// set to the empty string, and isNull() will return true.
+ ///
+ /// \param name The new name - ASCII letters and underscores only, with optional integer suffix.
+ /// This memory will be copied into a new internal storage location and need not be persistent.
+ /// \param _index The new index - if provided, it overrides any suffix provided by name
+ explicit IndexedName(const char *name = nullptr, int _index = 0)
+ : index(0)
+ {
+ assert(_index >= 0);
+ if (!name) {
+ this->type = "";
+ }
+ else {
+ set(name);
+ if (_index > 0) {
+ this->index = _index;
+ }
+ }
+ }
+
+ /// Create an indexed name that is restricted to a list of preset type names. If it appears in
+ /// that list, only a pointer to the character storage in the list is retained: the memory
+ /// locations pointed at by the list must never be destroyed once they have been used to create
+ /// names. If allowOthers is true (the default) then a requested name that is not in the list
+ /// will be added to a static internal storage table, and its memory then re-used for later
+ /// objects with the same name. If allowOthers is false, then the name request is rejected, and
+ /// the name is treated as null.
+ ///
+ /// \param name The new name - ASCII letters and underscores only, with optional integer suffix
+ /// \param allowedTypeNames A vector of allowed names. Storage locations must persist for the
+ /// entire run of the program.
+ /// \param allowOthers Whether a name not in allowedTypeNames is permitted. If true (the
+ /// default) then a name not in allowedTypeNames is added to a static internal storage vector
+ /// so that it can be re-used later without additional memory allocation.
+ IndexedName(const char *name,
+ const std::vector & allowedTypeNames,
+ bool allowOthers=true) : type(""), index(0)
+ {
+ set(name, -1, allowedTypeNames, allowOthers);
+ }
+
+ /// Construct from a QByteArray, but explicitly making a copy of the name on its first
+ /// occurrence. If this is a name that has already been stored internally, no additional copy
+ /// is made.
+ ///
+ /// \param data The QByteArray to copy the data from
+ explicit IndexedName(const QByteArray & data) : type(""), index(0)
+ {
+ set(data.constData(), data.size());
+ }
+
+ /// Given constant name and an index, re-use the existing memory for the name, not making a copy
+ /// of it, or scanning any existing storage for it. The name must never become invalid for the
+ /// lifetime of the object it names. This memory will never be re-used by another object.
+ ///
+ /// \param name The name of the object. This memory is NOT copied and must be persistent.
+ /// \param index A positive, non-zero integer
+ /// \return An IndexedName with the given name and index, re-using the existing memory for name
+ static IndexedName fromConst(const char *name, int index) {
+ assert (index >= 0);
+ IndexedName res;
+ res.type = name;
+ res.index = index;
+ return res;
+ }
+
+ /// Given an existing std::string, *append* this name to it. If index is not zero, this will
+ /// include the index.
+ ///
+ /// \param buffer A (possibly non-empty) string buffer to append the name to.
+ void appendToStringBuffer(std::string & buffer) const
+ {
+ buffer += this->type;
+ if (this->index > 0) {
+ buffer += std::to_string(this->index);
+ }
+ }
+
+ /// Create and return a new std::string with this name in it.
+ ///
+ /// \return A newly-created string with the IndexedName in it (e.g. "EDGE42")
+ std::string toString() const
+ {
+ std::string result;
+ this->appendToStringBuffer(result);
+ return result;
+ }
+
+ /// An indexedName is represented as the simple concatenation of the name and its index, e.g.
+ /// "EDGE1" or "FACE42".
+ friend std::ostream & operator<<(std::ostream & stream, const IndexedName & indexedName)
+ {
+ stream << indexedName.type;
+ if (indexedName.index > 0) {
+ stream << indexedName.index;
+ }
+ return stream;
+ }
+
+ /// True only if both the name and index compare exactly equal.
+ bool operator==(const IndexedName & other) const
+ {
+ return this->index == other.index
+ && (this->type == other.type
+ || std::strcmp(this->type, other.type)==0);
+ }
+
+ /// Increments the index by the given offset. Does not affect the text part of the name.
+ IndexedName & operator+=(int offset)
+ {
+ this->index += offset;
+ assert(this->index >= 0);
+ return *this;
+ }
+
+ /// Pre-increment operator: increases the index of this element by one.
+ IndexedName & operator++()
+ {
+ ++this->index;
+ return *this;
+ }
+
+ /// Pre-decrement operator: decreases the index of this element by one. Must not make the index
+ /// negative (only checked when compiled in debug mode).
+ IndexedName & operator--()
+ {
+ --this->index;
+ assert(this->index >= 0);
+ return *this;
+ }
+
+ /// True if either the name or the index compare not equal.
+ bool operator!=(const IndexedName & other) const
+ {
+ return !(this->operator==(other));
+ }
+
+ /// Equivalent to C++20's operator <=>
+ int compare(const IndexedName & other) const
+ {
+ int res = std::strcmp(this->type, other.type);
+ if (res != 0) {
+ return res;
+ }
+ if (this->index < other.index) {
+ return -1;
+ }
+ if (this->index > other.index) {
+ return 1;
+ }
+ return 0;
+ }
+
+ /// Provided to enable sorting operations: the comparison is first lexicographical for the text
+ /// element of the names, then numerical for the indices.
+ bool operator<(const IndexedName & other) const
+ {
+ return compare(other) < 0;
+ }
+
+ /// Allow direct memory access to the individual characters of the text portion of the name.
+ /// NOTE: input is not range-checked when compiled in release mode.
+ char operator[](int input) const
+ {
+ assert(input >= 0);
+ assert(input < static_cast(std::strlen(this->type)));
+ // When we support C++20 we can use std::span<> to eliminate the clang-tidy warning
+ // NOLINTNEXTLINE cppcoreguidelines-pro-bounds-pointer-arithmetic
+ return this->type[input];
+ }
+
+ /// Get a pointer to text part of the name - does NOT make a copy, returns direct memory access
+ const char * getType() const { return this->type; }
+
+ /// Get the numerical part of the name
+ int getIndex() const { return this->index; }
+
+ /// Set the numerical part of the name (note that there is no equivalent function to allow
+ /// changing the text part of the name, which is immutable once created).
+ ///
+ /// \param input The new index. Must be a positive non-zero integer
+ void setIndex(int input) { assert(input>=0); this->index = input; }
+
+ /// A name is considered "null" if its text component is an empty string.
+ // When we support C++20 we can use std::span<> to eliminate the clang-tidy warning
+ // NOLINTNEXTLINE cppcoreguidelines-pro-bounds-pointer-arithmetic
+ bool isNull() const { return this->type[0] == '\0'; }
+
+ /// Boolean conversion provides the opposite of isNull(), yielding true when the text part of
+ /// the name is NOT the empty string.
+ explicit operator bool() const { return !isNull(); }
+
+protected:
+ /// Apply the IndexedName rules and either store the characters of a new type or a reference to
+ /// the characters in a type named in types, or stored statically within this function. If len
+ /// is not set, or set to -1 (the default), then the provided string in name is scanned for its
+ /// length using strlen (e.g. it must be null-terminated).
+ ///
+ /// \param name The new name. If necessary a copy is made, this char * need not be persistent
+ /// \param length The length of name
+ /// \param allowedNames A vector of storage locations of allowed names. These storage locations
+ /// must be persistent for the duration of the program run.
+ /// \param allowOthers If true (the default), then if name is not in allowedNames it is allowed,
+ /// and it is added to internal storage (making a copy of the name if this is its first
+ /// occurrence).
+ void set(const char *name,
+ int length = -1,
+ const std::vector & allowedNames = {},
+ bool allowOthers = true);
+
+private:
+ const char * type;
+ int index;
+};
+
+
+/// A thin wrapper around a QByteArray providing the ability to force a copy of the data at any
+/// time, even if it isn't being written to. The standard assignment operator for this class *does*
+/// make a copy of the data, unlike the standard assignment operator for QByteArray.
+struct ByteArray
+{
+ explicit ByteArray(QByteArray other)
+ :bytes(std::move(other))
+ {}
+
+ ByteArray(const ByteArray& other) = default;
+
+ ByteArray(ByteArray&& other) noexcept
+ :bytes(std::move(other.bytes))
+ {}
+
+ ~ByteArray() = default;
+
+ /// Guarantee that the stored QByteArray does not share its memory with another instance.
+ void ensureUnshared() const
+ {
+ QByteArray copy;
+ copy.append(bytes.constData(), bytes.size());
+ bytes = copy;
+ }
+
+ bool operator==(const ByteArray& other) const {
+ return bytes == other.bytes;
+ }
+
+ ByteArray &operator=(const ByteArray & other) {
+ bytes.clear();
+ bytes.append(other.bytes.constData(), other.bytes.size());
+ return *this;
+ }
+
+ ByteArray &operator= (ByteArray&& other) noexcept
+ {
+ bytes = std::move(other.bytes);
+ return *this;
+ }
+
+ mutable QByteArray bytes;
+};
+
+
+struct ByteArrayHasher
+{
+ std::size_t operator()(const ByteArray& bytes) const
+ {
+ return qHash(bytes.bytes);
+ }
+
+ std::size_t operator()(const QByteArray& bytes) const
+ {
+ return qHash(bytes);
+ }
+};
+
+}
+
+#endif // APP_INDEXEDNAME_H
diff --git a/src/Base/CMakeLists.txt b/src/Base/CMakeLists.txt
index 38609caf05..a82dd77b2d 100644
--- a/src/Base/CMakeLists.txt
+++ b/src/Base/CMakeLists.txt
@@ -231,6 +231,7 @@ SET(FreeCADBase_CPP_SRCS
MatrixPyImp.cpp
MemDebug.cpp
Mutex.cpp
+ Observer.cpp
Parameter.xsd
Parameter.cpp
ParameterPy.cpp
diff --git a/src/Base/Factory.h b/src/Base/Factory.h
index 035a78b173..76d4e7a792 100644
--- a/src/Base/Factory.h
+++ b/src/Base/Factory.h
@@ -49,8 +49,8 @@ public:
/** Base class of all factories
- * This class has the purpose to produce at runtime instances
- * of classes not known at compile time. It holds a map of so called
+ * This class has the purpose to produce instances of classes at runtime
+ * that are unknown at compile time. It holds a map of so called
* producers which are able to produce an instance of a special class.
* Producer can be registered at runtime through e.g. application modules
*/
diff --git a/src/Base/Observer.cpp b/src/Base/Observer.cpp
new file mode 100644
index 0000000000..26813b28ff
--- /dev/null
+++ b/src/Base/Observer.cpp
@@ -0,0 +1,34 @@
+/***************************************************************************
+ * Copyright (c) 2023 Abdullah Tahiri *
+ * *
+ * This file is part of the FreeCAD CAx development system. *
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU Library General Public License (LGPL) *
+ * as published by the Free Software Foundation; either version 2 of *
+ * the License, or (at your option) any later version. *
+ * for detail see the LICENCE text file. *
+ * *
+ * FreeCAD 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 FreeCAD; if not, write to the Free Software *
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
+ * USA *
+ * *
+ ***************************************************************************/
+
+#include "PreCompiled.h"
+
+#include "Observer.h"
+
+
+namespace Base {
+
+template class BaseExport Observer;
+template class BaseExport Subject;
+
+} //namespace Base
diff --git a/src/Base/Observer.h b/src/Base/Observer.h
index 00de45e81e..494c1d23cc 100644
--- a/src/Base/Observer.h
+++ b/src/Base/Observer.h
@@ -216,16 +216,17 @@ protected:
std::set *> _ObserverSet;
};
-#if defined (FC_OS_WIN32) || defined(FC_OS_CYGWIN)
-# ifdef FCBase
-template class BaseExport Observer;
-template class BaseExport Subject;
-# else
-extern template class BaseExport Observer;
-extern template class BaseExport Subject;
-# endif
+// Workaround for MSVC
+#if defined (FreeCADBase_EXPORTS) && defined(_MSC_VER)
+# define Base_EXPORT
+#else
+# define Base_EXPORT BaseExport
#endif
+extern template class Base_EXPORT Observer;
+extern template class Base_EXPORT Subject;
+
+
} //namespace Base
diff --git a/src/Base/ProgressIndicatorPy.cpp b/src/Base/ProgressIndicatorPy.cpp
index a3a4a47405..a5c0dfcfe2 100644
--- a/src/Base/ProgressIndicatorPy.cpp
+++ b/src/Base/ProgressIndicatorPy.cpp
@@ -43,6 +43,21 @@ void ProgressIndicatorPy::init_type()
add_varargs_method("stop",&ProgressIndicatorPy::stop,"stop()");
}
+Py::PythonType& ProgressIndicatorPy::behaviors()
+{
+ return Py::PythonExtension::behaviors();
+}
+
+PyTypeObject* ProgressIndicatorPy::type_object()
+{
+ return Py::PythonExtension::type_object();
+}
+
+bool ProgressIndicatorPy::check(PyObject* p)
+{
+ return Py::PythonExtension::check(p);
+}
+
PyObject *ProgressIndicatorPy::PyMake(struct _typeobject *, PyObject *, PyObject *)
{
return new ProgressIndicatorPy();
diff --git a/src/Base/ProgressIndicatorPy.h b/src/Base/ProgressIndicatorPy.h
index 556fc97bdc..249068b02f 100644
--- a/src/Base/ProgressIndicatorPy.h
+++ b/src/Base/ProgressIndicatorPy.h
@@ -35,6 +35,9 @@ class BaseExport ProgressIndicatorPy : public Py::PythonExtension
-
-
-
- QDialogButtonBox::Close
-
-
-
@@ -531,21 +524,5 @@
-
- buttonBox
- rejected()
- Gui::Dialog::DlgDisplayProperties
- reject()
-
-
- 150
- 461
-
-
- 144
- 243
-
-
-
diff --git a/src/Gui/DlgDisplayPropertiesImp.cpp b/src/Gui/DlgDisplayPropertiesImp.cpp
index a6d9f87e49..14f8874169 100644
--- a/src/Gui/DlgDisplayPropertiesImp.cpp
+++ b/src/Gui/DlgDisplayPropertiesImp.cpp
@@ -192,11 +192,6 @@ void DlgDisplayPropertiesImp::setupConnections()
connect(d->ui.buttonColorPlot, &ColorButton::clicked, this, &DlgDisplayPropertiesImp::onButtonColorPlotClicked);
}
-void DlgDisplayPropertiesImp::showDefaultButtons(bool ok)
-{
- d->ui.buttonBox->setVisible(ok);
-}
-
void DlgDisplayPropertiesImp::changeEvent(QEvent *e)
{
if (e->type() == QEvent::LanguageChange) {
@@ -630,7 +625,6 @@ TaskDisplayProperties::TaskDisplayProperties()
{
this->setButtonPosition(TaskDisplayProperties::North);
widget = new DlgDisplayPropertiesImp(false);
- widget->showDefaultButtons(false);
taskbox = new Gui::TaskView::TaskBox(QPixmap(), widget->windowTitle(),true, nullptr);
taskbox->groupLayout()->addWidget(widget);
Content.push_back(taskbox);
diff --git a/src/Gui/DlgPreferencePackManagementImp.cpp b/src/Gui/DlgPreferencePackManagementImp.cpp
index 54365de62a..b956965e79 100644
--- a/src/Gui/DlgPreferencePackManagementImp.cpp
+++ b/src/Gui/DlgPreferencePackManagementImp.cpp
@@ -70,7 +70,7 @@ void DlgPreferencePackManagementImp::showEvent(QShowEvent* event)
for (const auto& mod : fs::directory_iterator(modDirectory)) {
auto packs = getPacksFromDirectory(mod);
if (!packs.empty()) {
- auto modName = mod.path().leaf().string();
+ auto modName = mod.path().filename().string();
installedPacks.emplace(modName, packs);
}
}
diff --git a/src/Gui/DlgSettingsNotificationArea.cpp b/src/Gui/DlgSettingsNotificationArea.cpp
new file mode 100644
index 0000000000..cc1daec243
--- /dev/null
+++ b/src/Gui/DlgSettingsNotificationArea.cpp
@@ -0,0 +1,102 @@
+/***************************************************************************
+ * Copyright (c) 2023 Abdullah Tahiri *
+ * *
+ * 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
+#endif
+
+#include "DlgSettingsNotificationArea.h"
+#include "ui_DlgSettingsNotificationArea.h"
+
+
+using namespace Gui::Dialog;
+
+/* TRANSLATOR Gui::Dialog::DlgSettingsNotificationArea */
+
+DlgSettingsNotificationArea::DlgSettingsNotificationArea(QWidget* parent)
+ : PreferencePage(parent)
+ , ui(new Ui_DlgSettingsNotificationArea)
+{
+ ui->setupUi(this);
+
+ connect(ui->NotificationAreaEnabled, &QCheckBox::stateChanged, [this](int state) {
+ if(state == Qt::CheckState::Checked) {
+ ui->NonIntrusiveNotificationsEnabled->setEnabled(true);
+ ui->maxDuration->setEnabled(true);
+ ui->maxDuration->setEnabled(true);
+ ui->minDuration->setEnabled(true);
+ ui->maxNotifications->setEnabled(true);
+ ui->maxWidgetMessages->setEnabled(true);
+ ui->autoRemoveUserNotifications->setEnabled(true);
+ QMessageBox::information(this, tr("Notification Area"),
+ tr("Activation of the Notification Area only takes effect after an application restart."));
+ }
+ else {
+ ui->NonIntrusiveNotificationsEnabled->setEnabled(false);
+ ui->maxDuration->setEnabled(false);
+ ui->maxDuration->setEnabled(false);
+ ui->minDuration->setEnabled(false);
+ ui->maxNotifications->setEnabled(false);
+ ui->maxWidgetMessages->setEnabled(false);
+ ui->autoRemoveUserNotifications->setEnabled(false);
+ // N.B: Deactivation is handled by the Notification Area itself, as it listens to all its configuration parameters.
+ }
+ });
+}
+
+DlgSettingsNotificationArea::~DlgSettingsNotificationArea()
+{
+}
+
+void DlgSettingsNotificationArea::saveSettings()
+{
+ ui->NotificationAreaEnabled->onSave();
+ ui->NonIntrusiveNotificationsEnabled->onSave();
+ ui->maxDuration->onSave();
+ ui->minDuration->onSave();
+ ui->maxNotifications->onSave();
+ ui->maxWidgetMessages->onSave();
+ ui->autoRemoveUserNotifications->onSave();
+}
+
+void DlgSettingsNotificationArea::loadSettings()
+{
+ ui->NotificationAreaEnabled->onRestore();
+ ui->NonIntrusiveNotificationsEnabled->onRestore();
+ ui->maxDuration->onRestore();
+ ui->minDuration->onRestore();
+ ui->maxNotifications->onRestore();
+ ui->maxWidgetMessages->onRestore();
+ ui->autoRemoveUserNotifications->onRestore();
+}
+
+void DlgSettingsNotificationArea::changeEvent(QEvent *e)
+{
+ if (e->type() == QEvent::LanguageChange) {
+ ui->retranslateUi(this);
+ }
+ QWidget::changeEvent(e);
+}
+
+#include "moc_DlgSettingsNotificationArea.cpp"
diff --git a/src/Gui/DlgSettingsNotificationArea.h b/src/Gui/DlgSettingsNotificationArea.h
new file mode 100644
index 0000000000..4aac834a3b
--- /dev/null
+++ b/src/Gui/DlgSettingsNotificationArea.h
@@ -0,0 +1,59 @@
+/***************************************************************************
+ * Copyright (c) 2023 Abdullah Tahiri *
+ * *
+ * 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 GUI_DIALOG_DLGSETTINGSNOTIFICATIONAREA_H
+#define GUI_DIALOG_DLGSETTINGSNOTIFICATIONAREA_H
+
+#include "PropertyPage.h"
+#include
+
+namespace Gui {
+namespace Dialog {
+class Ui_DlgSettingsNotificationArea;
+
+/**
+ * The DlgSettingsNotificationArea class implements a preference page to change settings
+ * for the Notification Area.
+ */
+class DlgSettingsNotificationArea : public PreferencePage
+{
+ Q_OBJECT
+
+public:
+ explicit DlgSettingsNotificationArea(QWidget* parent = nullptr);
+ ~DlgSettingsNotificationArea() override;
+
+ void saveSettings() override;
+ void loadSettings() override;
+
+protected:
+ void changeEvent(QEvent *e) override;
+
+private:
+ std::unique_ptr ui;
+};
+
+} // namespace Dialog
+} // namespace Gui
+
+#endif // GUI_DIALOG_DLGSETTINGSNOTIFICATIONAREA_H
diff --git a/src/Gui/DlgSettingsNotificationArea.ui b/src/Gui/DlgSettingsNotificationArea.ui
new file mode 100644
index 0000000000..d430c5ee94
--- /dev/null
+++ b/src/Gui/DlgSettingsNotificationArea.ui
@@ -0,0 +1,240 @@
+
+
+ Gui::Dialog::DlgSettingsNotificationArea
+
+
+
+ 0
+ 0
+ 654
+ 356
+
+
+
+ Notification Area
+
+
+
+
+
+ Non-Intrusive Notifications
+
+
+
+
+
+ Minimum Duration:
+
+
+
+
+
+
+
+
+
+ Maximum Duration:
+
+
+
+
+
+
+ Duration during which the notification will be shown (unless mouse buttons are clicked)
+
+
+ s
+
+
+ 0
+
+
+ 120
+
+
+ 20
+
+
+ NotificationTime
+
+
+ NotificationArea
+
+
+
+
+
+
+ Minimum duration during which the notification will be shown (unless notification clicked)
+
+
+ s
+
+
+ 5
+
+
+ MinimumOnScreenTime
+
+
+ NotificationArea
+
+
+
+
+
+
+ Maximum Number of Notifications:
+
+
+
+
+
+
+ Maximum number of notifications that will be simultaneously present on the screen
+
+
+ 15
+
+
+ MaxOpenNotifications
+
+
+ NotificationArea
+
+
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
+ The Notification area will appear in the status bar
+
+
+ Enable Notification Area
+
+
+ true
+
+
+ NotificationAreaEnabled
+
+
+ NotificationArea
+
+
+
+
+
+
+ Non-intrusive notifications will appear next to the notification area in the status bar
+
+
+ Enable non-intrusive notifications
+
+
+ true
+
+
+ NonIntrusiveNotificationsEnabled
+
+
+ NotificationArea
+
+
+
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 63
+
+
+
+
+
+
+
+ Message List
+
+
+
+
+
+ Limit the number of messages that will be kept in the list. If 0 there is no limit.
+
+
+ 10000
+
+
+ 1000
+
+
+ MaxWidgetMessages
+
+
+ NotificationArea
+
+
+
+
+
+
+ Maximum Messages (0 = no limit):
+
+
+
+
+
+
+ Removes the user notifications from the message list after the non-intrusive maximum duration has lapsed.
+
+
+ Auto-remove User Notifications
+
+
+ true
+
+
+ AutoRemoveUserNotifications
+
+
+ NotificationArea
+
+
+
+
+
+
+
+
+
+
+ Gui::PrefCheckBox
+ QCheckBox
+ Gui/PrefWidgets.h
+
+
+ Gui::PrefSpinBox
+ QSpinBox
+ Gui/PrefWidgets.h
+
+
+
+
+
diff --git a/src/Gui/Document.cpp b/src/Gui/Document.cpp
index 2a4f9e8d22..5ff1c4c583 100644
--- a/src/Gui/Document.cpp
+++ b/src/Gui/Document.cpp
@@ -30,6 +30,7 @@
# include
# include
# include
+# include
# include
# include
#endif
@@ -55,6 +56,7 @@
#include "FileDialog.h"
#include "MainWindow.h"
#include "MDIView.h"
+#include "NotificationArea.h"
#include "Selection.h"
#include "Thumbnail.h"
#include "Tree.h"
@@ -72,175 +74,6 @@ namespace bp = boost::placeholders;
namespace Gui {
-/** This class is an implementation only class to handle user notifications offered by App::Document.
- *
- * It provides a mechanism requiring confirmation for critical notifications only during User initiated restore/document loading ( it
- * does not require confirmation for macro/Python initiated restore, not to interfere with automations).
- *
- * Additionally, it provides a mechanism to show autoclosing non-modal user notifications in a non-intrusive way.
- **/
-class MessageManager {
-public:
- MessageManager() = default;
- ~MessageManager();
-
- void setDocument(Gui::Document * pDocument);
- void slotUserMessage(const App::DocumentObject&, const QString &, App::Document::NotificationType);
-
-
-private:
- void reorderAutoClosingMessages();
- QMessageBox* createNonModalMessage(const QString & msg, App::Document::NotificationType notificationtype);
- void pushAutoClosingMessage(const QString & msg, App::Document::NotificationType notificationtype);
- void pushAutoClosingMessageTooManyMessages();
-
-private:
- using Connection = boost::signals2::connection;
- Gui::Document * pDoc;
- Connection connectUserMessage;
- bool requireConfirmationCriticalMessageDuringRestoring = true;
- std::vector openAutoClosingMessages;
- std::mutex mutexAutoClosingMessages;
- const int autoClosingTimeout = 5000; // ms
- const int autoClosingMessageStackingOffset = 10;
- const unsigned int maxNumberOfOpenAutoClosingMessages = 3;
- bool maxNumberOfOpenAutoClosingMessagesLimitReached = false;
-};
-
-MessageManager::~MessageManager(){
- connectUserMessage.disconnect();
-}
-
-void MessageManager::setDocument(Gui::Document * pDocument)
-{
-
- pDoc = pDocument;
-
- connectUserMessage = pDoc->getDocument()->signalUserMessage.connect
- (boost::bind(&Gui::MessageManager::slotUserMessage, this, bp::_1, bp::_2, bp::_3));
-
-}
-
-void MessageManager::slotUserMessage(const App::DocumentObject& obj, const QString & msg, App::Document::NotificationType notificationtype)
-{
- (void) obj;
-
- auto userInitiatedRestore = Application::Instance->testStatus(Gui::Application::UserInitiatedOpenDocument);
-
- if(notificationtype == App::Document::NotificationType::Critical && userInitiatedRestore && requireConfirmationCriticalMessageDuringRestoring) {
- auto confirmMsg = msg + QStringLiteral("\n\n") + QObject::tr("Do you want to skip confirmation of further critical message notifications while loading the file?");
- auto button = QMessageBox::critical(pDoc->getActiveView(), QObject::tr("Critical Message"), confirmMsg, QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
-
- if(button == QMessageBox::Yes)
- requireConfirmationCriticalMessageDuringRestoring = false;
- }
- else { // Non-critical errors and warnings - auto-closing non-blocking message box
-
- auto messageNumber = openAutoClosingMessages.size();
-
- // Not opening more than the number of maximum autoclosing messages
- // If maximum reached, the mechanism only resets after all present messages are auto-closed
- if( messageNumber < maxNumberOfOpenAutoClosingMessages) {
- if(messageNumber == 0 && maxNumberOfOpenAutoClosingMessagesLimitReached) {
- maxNumberOfOpenAutoClosingMessagesLimitReached = false;
- }
-
- if(!maxNumberOfOpenAutoClosingMessagesLimitReached) {
- pushAutoClosingMessage(msg, notificationtype);
- }
- }
- else {
- if(!maxNumberOfOpenAutoClosingMessagesLimitReached)
- pushAutoClosingMessageTooManyMessages();
-
- maxNumberOfOpenAutoClosingMessagesLimitReached = true;
- }
- }
-}
-
-void MessageManager::pushAutoClosingMessage(const QString & msg, App::Document::NotificationType notificationtype)
-{
- std::lock_guard g(mutexAutoClosingMessages); // guard to avoid creating new messages while closing old messages (via timer)
-
- auto msgBox = createNonModalMessage(msg, notificationtype);
-
- msgBox->show();
-
- int numberOpenAutoClosingMessages = openAutoClosingMessages.size();
-
- openAutoClosingMessages.push_back(msgBox);
-
- reorderAutoClosingMessages();
-
- QTimer::singleShot(autoClosingTimeout*numberOpenAutoClosingMessages, [msgBox, this](){
- std::lock_guard g(mutexAutoClosingMessages); // guard to avoid closing old messages while creating new ones
- if(msgBox) {
- msgBox->done(0);
- openAutoClosingMessages.erase(
- std::remove(openAutoClosingMessages.begin(), openAutoClosingMessages.end(), msgBox),
- openAutoClosingMessages.end());
-
- reorderAutoClosingMessages();
- }
- });
-}
-
-void MessageManager::pushAutoClosingMessageTooManyMessages()
-{
- pushAutoClosingMessage(QObject::tr("Too many message notifications. Notification temporarily stopped. Look at the report view for more information."), App::Document::NotificationType::Warning);
-}
-
-
-QMessageBox* MessageManager::createNonModalMessage(const QString & msg, App::Document::NotificationType notificationtype)
-{
- auto parent = pDoc->getActiveView();
-
- QMessageBox* msgBox = new QMessageBox(parent);
- msgBox->setAttribute(Qt::WA_DeleteOnClose); // msgbox deleted automatically upon closed
- msgBox->setStandardButtons(QMessageBox::NoButton);
- msgBox->setWindowFlag(Qt::FramelessWindowHint,true);
- msgBox->setText(msg);
-
- if(notificationtype == App::Document::NotificationType::Error) {
- msgBox->setWindowTitle(QObject::tr("Error"));
- msgBox->setIcon(QMessageBox::Critical);
- }
- else if(notificationtype == App::Document::NotificationType::Warning) {
- msgBox->setWindowTitle(QObject::tr("Warning"));
- msgBox->setIcon(QMessageBox::Warning);
- }
- else if(notificationtype == App::Document::NotificationType::Information) {
- msgBox->setWindowTitle(QObject::tr("Information"));
- msgBox->setIcon(QMessageBox::Information);
- }
- else if(notificationtype == App::Document::NotificationType::Critical) {
- msgBox->setWindowTitle(QObject::tr("Critical"));
- msgBox->setIcon(QMessageBox::Critical);
- }
-
- msgBox->setModal( false ); // if you want it non-modal
-
- return msgBox;
-}
-
-void MessageManager::reorderAutoClosingMessages()
-{
- auto parent = pDoc->getActiveView();
-
- int numberOpenAutoClosingMessages = openAutoClosingMessages.size();
-
- auto x = parent->width() / 2;
- auto y = parent->height() / 7;
-
- int posindex = numberOpenAutoClosingMessages - 1;
- for (auto rit = openAutoClosingMessages.rbegin(); rit != openAutoClosingMessages.rend(); ++rit, posindex--) {
- int xw = x - (*rit)->width() / 2 + autoClosingMessageStackingOffset*posindex;;
- int yw = y + autoClosingMessageStackingOffset*posindex;
- (*rit)->move(xw, yw);
- (*rit)->raise();
- }
-}
-
// Pimpl class
struct DocumentP
{
@@ -301,8 +134,6 @@ struct DocumentP
using ConnectionBlock = boost::signals2::shared_connection_block;
ConnectionBlock connectActObjectBlocker;
ConnectionBlock connectChangeDocumentBlocker;
-
- MessageManager messageManager;
};
} // namespace Gui
@@ -384,7 +215,6 @@ Document::Document(App::Document* pcDocument,Application * app)
d->connectTransactionRemove = pcDocument->signalTransactionRemove.connect
(boost::bind(&Gui::Document::slotTransactionRemove, this, bp::_1, bp::_2));
- d->messageManager.setDocument(this);
// pointer to the python class
// NOTE: As this Python object doesn't get returned to the interpreter we
// mustn't increment it (Werner Jan-12-2006)
diff --git a/src/Gui/GuiConsole.cpp b/src/Gui/GuiConsole.cpp
index 3cb10b2882..764343979c 100644
--- a/src/Gui/GuiConsole.cpp
+++ b/src/Gui/GuiConsole.cpp
@@ -83,6 +83,8 @@ GUIConsole::~GUIConsole (void)
void GUIConsole::SendLog(const std::string& notifiername, const std::string& msg, Base::LogStyle level)
{
+ (void) notifiername;
+
int color = -1;
switch(level){
case Base::LogStyle::Warning:
@@ -100,6 +102,8 @@ void GUIConsole::SendLog(const std::string& notifiername, const std::string& msg
case Base::LogStyle::Critical:
color = FOREGROUND_RED | FOREGROUND_GREEN;
break;
+ default:
+ break;
}
::SetConsoleTextAttribute(::GetStdHandle(STD_OUTPUT_HANDLE), color);
@@ -115,7 +119,7 @@ GUIConsole::~GUIConsole () {}
void GUIConsole::SendLog(const std::string& notifiername, const std::string& msg, Base::LogStyle level)
{
(void) notifiername;
-
+
switch(level){
case Base::LogStyle::Warning:
std::cerr << "Warning: " << msg;
diff --git a/src/Gui/Icons/InTray.svg b/src/Gui/Icons/InTray.svg
new file mode 100644
index 0000000000..e9d0340b08
--- /dev/null
+++ b/src/Gui/Icons/InTray.svg
@@ -0,0 +1,487 @@
+
+
+
+
diff --git a/src/Gui/Icons/Warning.svg b/src/Gui/Icons/Warning.svg
new file mode 100644
index 0000000000..9aab3a965a
--- /dev/null
+++ b/src/Gui/Icons/Warning.svg
@@ -0,0 +1,121 @@
+
+
+
+
diff --git a/src/Gui/Icons/critical-info.svg b/src/Gui/Icons/critical-info.svg
new file mode 100644
index 0000000000..a5236e240b
--- /dev/null
+++ b/src/Gui/Icons/critical-info.svg
@@ -0,0 +1,111 @@
+
+
+
+
diff --git a/src/Gui/Icons/info.svg b/src/Gui/Icons/info.svg
new file mode 100644
index 0000000000..2fe7a0d9bf
--- /dev/null
+++ b/src/Gui/Icons/info.svg
@@ -0,0 +1,111 @@
+
+
+
+
diff --git a/src/Gui/Icons/resource.qrc b/src/Gui/Icons/resource.qrc
index ccd411b1c5..39df69cbc5 100644
--- a/src/Gui/Icons/resource.qrc
+++ b/src/Gui/Icons/resource.qrc
@@ -74,6 +74,8 @@
edit-undo.svgedit-edit.svgedit-cleartext.svg
+ info.svg
+ critical-info.svgtree-item-drag.svgtree-goto-sel.svgtree-rec-sel.svg
@@ -98,6 +100,7 @@
accessories-text-editor.svgaccessories-calculator.svginternet-web-browser.svg
+ InTray.svgview-select.svgview-unselectable.svgview-refresh.svg
@@ -251,6 +254,7 @@
Std_UserEditModeTransform.svgStd_UserEditModeCutting.svgStd_UserEditModeColor.svg
+ Warning.svg
diff --git a/src/Gui/MainWindow.cpp b/src/Gui/MainWindow.cpp
index 3f1ee3ad7b..cd038999d0 100644
--- a/src/Gui/MainWindow.cpp
+++ b/src/Gui/MainWindow.cpp
@@ -80,6 +80,7 @@
#include "DownloadManager.h"
#include "FileDialog.h"
#include "MenuManager.h"
+#include "NotificationArea.h"
#include "ProgressBar.h"
#include "PropertyView.h"
#include "PythonConsole.h"
@@ -251,7 +252,6 @@ protected:
} // namespace Gui
-
/* TRANSLATOR Gui::MainWindow */
MainWindow::MainWindow(QWidget * parent, Qt::WindowFlags f)
@@ -304,6 +304,17 @@ MainWindow::MainWindow(QWidget * parent, Qt::WindowFlags f)
statusBar()->addPermanentWidget(progressBar, 0);
statusBar()->addPermanentWidget(d->sizeLabel, 0);
+ auto hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/NotificationArea");
+
+ auto notificationAreaEnabled = hGrp->GetBool("NotificationAreaEnabled", true);
+
+ if(notificationAreaEnabled) {
+ NotificationArea* notificationArea = new NotificationArea(statusBar());
+ notificationArea->setObjectName(QString::fromLatin1("notificationArea"));
+ notificationArea->setIcon(QIcon(QString::fromLatin1(":/icons/InTray.svg")));
+ notificationArea->setStyleSheet(QStringLiteral("text-align:left;"));
+ statusBar()->addPermanentWidget(notificationArea);
+ }
// clears the action label
d->actionTimer = new QTimer( this );
d->actionTimer->setObjectName(QString::fromLatin1("actionTimer"));
diff --git a/src/Gui/NotificationArea.cpp b/src/Gui/NotificationArea.cpp
new file mode 100644
index 0000000000..84d943e6f2
--- /dev/null
+++ b/src/Gui/NotificationArea.cpp
@@ -0,0 +1,993 @@
+/***************************************************************************
+ * Copyright (c) 2022 Abdullah Tahiri *
+ * *
+ * 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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#endif
+
+#include
+#include
+
+#include "Application.h"
+#include "BitmapFactory.h"
+#include "MDIView.h"
+#include "MainWindow.h"
+#include "NotificationBox.h"
+
+#include "NotificationArea.h"
+
+using namespace Gui;
+
+using Connection = boost::signals2::connection;
+
+namespace bp = boost::placeholders;
+
+class NotificationAreaObserver;
+
+namespace Gui
+{
+/** PImpl idiom structure having all data necessary for the notification area */
+struct NotificationAreaP
+{
+ // Structure holding all variables necessary for the Notification Area.
+ // Preference parameters are updated by NotificationArea::ParameterObserver
+
+ /** @name Non-intrusive notifications parameters */
+ //@{
+ /// Parameter controlled
+ int maxOpenNotifications = 15;
+ /// Parameter controlled
+ unsigned int notificationExpirationTime = 10000;
+ /// minimum time that the notification will remain unclosed
+ unsigned int minimumOnScreenTime = 5000;
+ /// Parameter controlled
+ bool notificationsDisabled = false;
+ /// Control of confirmation mechanism (for Critical Messages)
+ bool requireConfirmationCriticalMessageDuringRestoring = true;
+ //@}
+
+ /** @name Widget parameters */
+ //@{
+ /// Parameter controlled - maximum number of message allowed in the notification area widget (0
+ /// means no limit)
+ int maxWidgetMessages = 1000;
+ /// User notifications get automatically removed from the Widget after the non-intrusive
+ /// notification expiration time
+ bool autoRemoveUserNotifications;
+ //@}
+
+ /** @name Notification rate control */
+ //@{
+ /* Control rate of updates of non-intrusive messages (avoids visual artifacts when messages are
+ * constantly being received) */
+ // Timer to delay notification until a minimum time between two consecutive messages have lapsed
+ QTimer inhibitTimer;
+ // The time between two consecutive messages forced by the inhibitTimer
+ const unsigned int inhibitNotificationTime = 250;
+ //@}
+
+ // Access control
+ std::mutex mutexNotification;
+
+ // Pointers to widgets (no ownership)
+ QMenu* menu;
+ QWidgetAction* notificationaction;
+
+ /** @name Resources */
+ //@{
+ /// Base::Console Message observer
+ std::unique_ptr observer;
+ Connection finishRestoreDocumentConnection;
+ /// Preference Parameter observer
+ std::unique_ptr parameterObserver;
+ //@}
+};
+
+}// namespace Gui
+
+
+/******************* Resource Management *****************************************/
+
+/** Simple class to manage Notification Area Resources*/
+class ResourceManager
+{
+
+private:
+ ResourceManager()
+ {
+ error = BitmapFactory().pixmapFromSvg(":/icons/edit_Cancel.svg", QSize(16, 16));
+ warning = BitmapFactory().pixmapFromSvg(":/icons/Warning.svg", QSize(16, 16));
+ critical = BitmapFactory().pixmapFromSvg(":/icons/critical-info.svg", QSize(16, 16));
+ info = BitmapFactory().pixmapFromSvg(":/icons/info.svg", QSize(16, 16));
+ }
+
+ inline static const auto& getResourceManager()
+ {
+ static ResourceManager manager;
+
+ return manager;
+ }
+
+public:
+ inline static auto ErrorPixmap()
+ {
+ auto rm = getResourceManager();
+ return rm.error;
+ }
+
+ inline static auto WarningPixmap()
+ {
+ auto rm = getResourceManager();
+ return rm.warning;
+ }
+
+ inline static auto CriticalPixmap()
+ {
+ auto rm = getResourceManager();
+ return rm.critical;
+ }
+
+ inline static auto InfoPixmap()
+ {
+ auto rm = getResourceManager();
+ return rm.info;
+ }
+
+private:
+ QPixmap error;
+ QPixmap warning;
+ QPixmap critical;
+ QPixmap info;
+};
+
+/******************** Console Messages Observer (Console Interface) ************************/
+
+/** This class listens to all messages sent via the console interface and
+ feeds the non-intrusive notification system and the notifications widget */
+class NotificationAreaObserver: public Base::ILogger
+{
+public:
+ NotificationAreaObserver(NotificationArea* notificationarea);
+ ~NotificationAreaObserver() override;
+
+ /// Function that is called by the console interface for this observer with the message
+ /// information
+ void SendLog(const std::string& notifiername, const std::string& msg,
+ Base::LogStyle level) override;
+
+ /// Name of the observer
+ const char* Name() override
+ {
+ return "NotificationAreaObserver";
+ }
+
+private:
+ NotificationArea* notificationArea;
+};
+
+NotificationAreaObserver::NotificationAreaObserver(NotificationArea* notificationarea)
+ : notificationArea(notificationarea)
+{
+ Base::Console().AttachObserver(this);
+ bLog = false; // ignore log messages
+ bMsg = false; // ignore messages
+ bNotification = true; // activate user notifications
+ bTranslatedNotification = true;// activate translated user notifications
+}
+
+NotificationAreaObserver::~NotificationAreaObserver()
+{
+ Base::Console().DetachObserver(this);
+}
+
+void NotificationAreaObserver::SendLog(const std::string& notifiername, const std::string& msg,
+ Base::LogStyle level)
+{
+ // 1. As notification system is shared with report view and others, the expectation is that any
+ // individual error and warning message will end in "\n". This means the string must be stripped
+ // of this character for representation in the notification area.
+ // 2. However, any message marked with the QT_TRANSLATE_NOOT macro with the "Notifications"
+ // context, shall not include
+ // "\n", as this generates problems with the translation system. Then the string must be
+ // stripped of "\n" before translation.
+
+ auto simplifiedstring =
+ QString::fromStdString(msg)
+ .trimmed();// remove any leading and trailing whitespace character ('\n')
+
+ // avoid processing empty strings
+ if(simplifiedstring.isEmpty())
+ return;
+
+ if (level == Base::LogStyle::TranslatedNotification) {
+ notificationArea->pushNotification(
+ QString::fromStdString(notifiername), simplifiedstring, level);
+ }
+ else {
+ notificationArea->pushNotification(
+ QString::fromStdString(notifiername),
+ QCoreApplication::translate("Notifications", simplifiedstring.toUtf8()),
+ level);
+ }
+}
+
+
+/******************* Notification Widget *******************************************************/
+
+/** Specialised Item class for the Widget notifications/errors/warnings
+ It holds all item specific data, including visualisation data and controls how
+ the item should appear in the widget.
+*/
+class NotificationItem: public QTreeWidgetItem
+{
+public:
+ NotificationItem(Base::LogStyle notificationtype, QString notifiername, QString message)
+ : notificationType(notificationtype),
+ notifierName(std::move(notifiername)),
+ msg(std::move(message))
+ {}
+
+ QVariant data(int column, int role) const override
+ {
+ // strings that will be displayed for each column of the widget
+ if (role == Qt::DisplayRole) {
+ switch (column) {
+ case 1:
+ return notifierName;
+ break;
+ case 2:
+ return msg;
+ break;
+ }
+ }
+ else if (column == 0 && role == Qt::DecorationRole) {
+ // Icons to be displayed for the first row
+ if (notificationType == Base::LogStyle::Error) {
+ return std::move(ResourceManager::ErrorPixmap());
+ }
+ else if (notificationType == Base::LogStyle::Warning) {
+ return std::move(ResourceManager::WarningPixmap());
+ }
+ else if (notificationType == Base::LogStyle::Critical) {
+ return std::move(ResourceManager::CriticalPixmap());
+ }
+ else {
+ return std::move(ResourceManager::InfoPixmap());
+ }
+ }
+ else if (role == Qt::FontRole) {
+ // Visualisation control of unread messages
+ static QFont font;
+ static QFont boldFont(font.family(), font.pointSize(), QFont::Bold);
+
+ if (unread) {
+ return boldFont;
+ }
+
+ return font;
+ }
+
+ return QVariant();
+ }
+
+ Base::LogStyle notificationType;
+ QString notifierName;
+ QString msg;
+
+ bool unread = true; // item is unread in the Notification Area Widget
+ bool notifying = true;// item is to be notified or being notified as non-intrusive message
+ bool shown = false; // item is already being notified (it is onScreen)
+};
+
+/** Drop menu Action containing the notifications widget.
+ * It stores all the notification item information in the form
+ * of NotificationItems (QTreeWidgetItem). This information is used
+ * by the Widget and by the non-intrusive messages.
+ * It owns the notification resources and is responsible for the release
+ * of the memory resources, either directly for the intermediate fast cache
+ * or indirectly via QT for the case of the QTreeWidgetItems.
+ */
+class NotificationsAction: public QWidgetAction
+{
+public:
+ NotificationsAction(QWidget* parent)
+ : QWidgetAction(parent)
+ {}
+
+ ~NotificationsAction()
+ {
+ for (auto* item : pushedItems) {
+ if (item) {
+ delete item;
+ }
+ }
+ }
+
+public:
+ /// deletes only notifications (messages of type Notification and TranslatedNotification)
+ void deleteNotifications()
+ {
+ if (tableWidget) {
+ for (int i = tableWidget->topLevelItemCount() - 1; i >= 0; i--) {
+ auto* item = static_cast(tableWidget->topLevelItem(i));
+ if (item->notificationType == Base::LogStyle::Notification
+ || item->notificationType == Base::LogStyle::TranslatedNotification) {
+ delete item;
+ }
+ }
+ }
+ for (int i = pushedItems.size() - 1; i >= 0; i--) {
+ auto* item = static_cast(pushedItems.at(i));
+ if (item->notificationType == Base::LogStyle::Notification
+ || item->notificationType == Base::LogStyle::TranslatedNotification) {
+ delete pushedItems.takeAt(i);
+ }
+ }
+ }
+
+ /// deletes all notifications, errors and warnings
+ void deleteAll()
+ {
+ if (tableWidget) {
+ tableWidget->clear();// the parent will delete the items.
+ }
+ while (!pushedItems.isEmpty())
+ delete pushedItems.takeFirst();
+ }
+
+ /// returns the amount of unread notifications, errors and warnings
+ inline int getUnreadCount() const
+ {
+ return getCurrently([](auto* item) {
+ return item->unread;
+ });
+ }
+
+ /// returns the amount of notifications, errors and warnings currently being notified
+ inline int getCurrentlyNotifyingCount() const
+ {
+ return getCurrently([](auto* item) {
+ return item->notifying;
+ });
+ }
+
+ /// returns the amount of notifications, errors and warnings currently being shown as
+ /// non-intrusive messages (on-screen)
+ inline int getShownCount() const
+ {
+ return getCurrently([](auto* item) {
+ return item->shown;
+ });
+ }
+
+ /// marks all notifications, errors and warnings as read
+ void clearUnreadFlag()
+ {
+ for (auto i = 0; i < tableWidget->topLevelItemCount();
+ i++) {// all messages were read, so clear the unread flag
+ auto* item = static_cast(tableWidget->topLevelItem(i));
+ item->unread = false;
+ }
+ }
+
+ /// pushes all Notification Items to the Widget, so that they can be shown
+ void synchroniseWidget()
+ {
+ tableWidget->insertTopLevelItems(0, pushedItems);
+ pushedItems.clear();
+ }
+
+ /** pushes all Notification Items to the fast cache (this also prevents all unnecessary
+ * signaling from parents) and allows to accelerate insertions and deletions
+ */
+ void shiftToCache()
+ {
+ tableWidget->blockSignals(true);
+ tableWidget->clearSelection();
+ while (tableWidget->topLevelItemCount() > 0) {
+ auto* item = tableWidget->takeTopLevelItem(0);
+ pushedItems.push_back(item);
+ }
+ tableWidget->blockSignals(false);
+ }
+
+ /// returns if there are no notifications, errors and warnings at all
+ bool isEmpty() const
+ {
+ return tableWidget->topLevelItemCount() == 0 && pushedItems.isEmpty();
+ }
+
+ /// returns the total amount of notifications, errors and warnings currently stored
+ auto count() const
+ {
+ return tableWidget->topLevelItemCount() + pushedItems.count();
+ }
+
+ /// retrieves a pointer to a given notification from storage.
+ auto getItem(int index) const
+ {
+ if (index < pushedItems.count()) {
+ return pushedItems.at(index);
+ }
+ else {
+ return tableWidget->topLevelItem(index - pushedItems.count());
+ }
+ }
+
+ /// deletes a given notification, errors or warnings by index
+ void deleteItem(int index)
+ {
+ if (index < pushedItems.count()) {
+ delete pushedItems.takeAt(index);
+ }
+ else {
+ delete tableWidget->topLevelItem(index - pushedItems.count());
+ }
+ }
+
+ /// deletes a given notification, errors or warnings by pointer
+ void deleteItem(NotificationItem* item)
+ {
+ for (int i = 0; i < count(); i++) {
+ if (getItem(i) == item) {
+ deleteItem(i);
+ return;
+ }
+ }
+ }
+
+ /// deletes the last Notification Item
+ void deleteLastItem()
+ {
+ deleteItem(count() - 1);
+ }
+
+ /// pushes a notification item to the front
+ void push_front(NotificationItem* item)
+ {
+ pushedItems.push_front(item);
+ }
+
+protected:
+ /// creates the Notifications Widget
+ QWidget* createWidget(QWidget* parent) override
+ {
+ QWidget* notificationsWidget = new QWidget(parent);
+
+ QHBoxLayout* layout = new QHBoxLayout(notificationsWidget);
+ notificationsWidget->setLayout(layout);
+
+ tableWidget = new QTreeWidget(parent);
+ tableWidget->setColumnCount(3);
+
+ QStringList headers;
+ headers << QObject::tr("Type") << QObject::tr("Notifier") << QObject::tr("Message");
+ tableWidget->setHeaderLabels(headers);
+
+ layout->addWidget(tableWidget);
+
+ tableWidget->setMaximumSize(1200, 600);
+ tableWidget->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
+ tableWidget->header()->setStretchLastSection(false);
+ tableWidget->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+
+ tableWidget->setSelectionMode(QAbstractItemView::ExtendedSelection);
+ tableWidget->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ // context menu on any item (row) of the widget
+ QObject::connect(
+ tableWidget, &QTreeWidget::customContextMenuRequested, [&](const QPoint& pos) {
+ // auto item = tableWidget->itemAt(pos);
+ auto selectedItems = tableWidget->selectedItems();
+
+ QMenu menu;
+
+ QAction* del = menu.addAction(tr("Delete"), this, [&]() {
+ for (auto it : selectedItems) {
+ delete it;
+ }
+ });
+
+ del->setEnabled(!selectedItems.isEmpty());
+
+ menu.addSeparator();
+
+ QAction* delnotifications =
+ menu.addAction(tr("Delete user notifications"),
+ this,
+ &NotificationsAction::deleteNotifications);
+
+ delnotifications->setEnabled(tableWidget->topLevelItemCount() > 0);
+
+ QAction* delall =
+ menu.addAction(tr("Delete All"), this, &NotificationsAction::deleteAll);
+
+ delall->setEnabled(tableWidget->topLevelItemCount() > 0);
+
+ menu.setDefaultAction(del);
+
+ menu.exec(tableWidget->mapToGlobal(pos));
+ });
+
+ return notificationsWidget;
+ }
+
+private:
+ /// utility function to return the number of Notification Items meeting the functor/lambda
+ /// criteria
+ int getCurrently(std::function F) const
+ {
+ int instate = 0;
+ for (auto i = 0; i < tableWidget->topLevelItemCount(); i++) {
+ auto* item = static_cast(tableWidget->topLevelItem(i));
+ if (F(item)) {
+ instate++;
+ }
+ }
+ for (auto i = 0; i < pushedItems.count(); i++) {
+ auto* item = static_cast(pushedItems.at(i));
+ if (F(item)) {
+ instate++;
+ }
+ }
+ return instate;
+ }
+
+private:
+ QTreeWidget* tableWidget;
+ // Intermediate storage
+ // Note: QTreeWidget is helplessly slow to single insertions, QTreeWidget is actually only
+ // necessary when showing the widget. A single QList insertion into a QTreeWidget is actually
+ // not that slow. The use of this intermediate storage substantially accelerates non-intrusive
+ // notifications.
+ QList pushedItems;
+};
+
+/************ Parameter Observer (preferences) **************************************/
+
+NotificationArea::ParameterObserver::ParameterObserver(NotificationArea* notificationarea)
+ : notificationArea(notificationarea)
+{
+ hGrp = App::GetApplication().GetParameterGroupByPath(
+ "User parameter:BaseApp/Preferences/NotificationArea");
+
+ parameterMap = {
+ {"NotificationAreaEnabled",
+ [this](const std::string& string) {
+ auto enabled = hGrp->GetBool(string.c_str(), true);
+ if (!enabled)
+ notificationArea->deleteLater();
+ }},
+ {"NonIntrusiveNotificationsEnabled",
+ [this](const std::string& string) {
+ auto enabled = hGrp->GetBool(string.c_str(), true);
+ notificationArea->pImp->notificationsDisabled = !enabled;
+ }},
+ {"NotificationTime",
+ [this](const std::string& string) {
+ auto time = hGrp->GetInt(string.c_str(), 20) * 1000;
+ if (time < 0)
+ time = 0;
+ notificationArea->pImp->notificationExpirationTime = static_cast(time);
+ }},
+ {"MinimumOnScreenTime",
+ [this](const std::string& string) {
+ auto time = hGrp->GetInt(string.c_str(), 5) * 1000;
+ if (time < 0)
+ time = 0;
+ notificationArea->pImp->minimumOnScreenTime = static_cast(time);
+ }},
+ {"MaxOpenNotifications",
+ [this](const std::string& string) {
+ auto limit = hGrp->GetInt(string.c_str(), 15);
+ if (limit < 0)
+ limit = 0;
+ notificationArea->pImp->maxOpenNotifications = static_cast(limit);
+ }},
+ {"MaxWidgetMessages",
+ [this](const std::string& string) {
+ auto limit = hGrp->GetInt(string.c_str(), 1000);
+ if (limit < 0)
+ limit = 0;
+ notificationArea->pImp->maxWidgetMessages = static_cast(limit);
+ }},
+ {"AutoRemoveUserNotifications",
+ [this](const std::string& string) {
+ auto enabled = hGrp->GetBool(string.c_str(), true);
+ notificationArea->pImp->autoRemoveUserNotifications = enabled;
+ }},
+ };
+
+ for (auto& val : parameterMap) {
+ auto string = val.first;
+ auto update = val.second;
+
+ update(string);
+ }
+
+ hGrp->Attach(this);
+}
+
+NotificationArea::ParameterObserver::~ParameterObserver()
+{
+ hGrp->Detach(this);
+}
+
+void NotificationArea::ParameterObserver::OnChange(Base::Subject& rCaller,
+ const char* sReason)
+{
+ (void)rCaller;
+
+ auto key = parameterMap.find(sReason);
+
+ if (key != parameterMap.end()) {
+ auto string = key->first;
+ auto update = key->second;
+
+ update(string);
+ }
+}
+
+/************************* Notification Area *****************************************/
+
+NotificationArea::NotificationArea(QWidget* parent)
+ : QPushButton(parent)
+{
+ // QPushButton appearance
+ setText(QString());
+ setFlat(true);
+
+ // Initialisation of pImpl structure
+ pImp = std::make_unique();
+
+ pImp->observer = std::make_unique(this);
+ pImp->parameterObserver = std::make_unique(this);
+
+ pImp->menu = new QMenu(parent);
+ setMenu(pImp->menu);
+
+ auto na = new NotificationsAction(pImp->menu);
+
+ pImp->menu->addAction(na);
+
+ pImp->notificationaction = na;
+
+ // Signals for synchronisation of storage before showing/hiding the widget
+ QObject::connect(pImp->menu, &QMenu::aboutToHide, [&]() {
+ std::lock_guard g(pImp->mutexNotification);
+ static_cast(pImp->notificationaction)->clearUnreadFlag();
+ static_cast(pImp->notificationaction)->shiftToCache();
+ });
+
+ QObject::connect(pImp->menu, &QMenu::aboutToShow, [this]() {
+ std::lock_guard g(
+ pImp->mutexNotification);// guard to avoid modifying the notification list and indices
+ // while creating the tooltip
+ setText(QString::number(0)); // no unread notifications
+ static_cast(pImp->notificationaction)->synchroniseWidget();
+
+ // the position of the action has already been calculated (for a non-synchronised widget),
+ // the size could be recalculated here, but not the position according to the previous size.
+ // This resize event forces this recalculation.
+ QResizeEvent re(pImp->menu->size(), pImp->menu->size());
+ qApp->sendEvent(pImp->menu, &re);
+ });
+
+
+ // Connection to the finish restore signal to rearm Critical messages modal mode when action is
+ // user initiated
+ pImp->finishRestoreDocumentConnection =
+ App::GetApplication().signalFinishRestoreDocument.connect(
+ boost::bind(&Gui::NotificationArea::slotRestoreFinished, this, bp::_1));
+
+ // Initialisation of the timer to inhibit continuous updates of the notification system in case
+ // clusters of messages arrive (so as to delay the actual notification until the whole cluster
+ // has arrived)
+ pImp->inhibitTimer.setSingleShot(true);
+
+ pImp->inhibitTimer.callOnTimeout([this, na]() {
+ setText(QString::number(na->getUnreadCount()));
+ showInNotificationArea();
+ });
+}
+
+NotificationArea::~NotificationArea()
+{
+ pImp->finishRestoreDocumentConnection.disconnect();
+}
+
+void NotificationArea::mousePressEvent(QMouseEvent* e)
+{
+ // Contextual menu (e.g. to delete Notifications or all messages (Notifications, Errors,
+ // Warnings and Critical messages)
+ if (e->button() == Qt::RightButton && hitButton(e->pos())) {
+ QMenu menu;
+
+ NotificationsAction* na = static_cast(pImp->notificationaction);
+
+ QAction* delnotifications = menu.addAction(tr("Delete user notifications"), [&]() {
+ std::lock_guard g(
+ pImp->mutexNotification);// guard to avoid modifying the notification list and
+ // indices while creating the tooltip
+ na->deleteNotifications();
+ setText(QString::number(na->getUnreadCount()));
+ });
+
+ delnotifications->setEnabled(!na->isEmpty());
+
+ QAction* delall = menu.addAction(tr("Delete All"), [&]() {
+ std::lock_guard g(
+ pImp->mutexNotification);// guard to avoid modifying the notification list and
+ // indices while creating the tooltip
+ na->deleteAll();
+ setText(QString::number(0));
+ });
+
+ delall->setEnabled(!na->isEmpty());
+
+ menu.setDefaultAction(delall);
+
+ menu.exec(this->mapToGlobal(e->pos()));
+ }
+ QPushButton::mousePressEvent(e);
+}
+
+void NotificationArea::pushNotification(const QString& notifiername, const QString& message,
+ Base::LogStyle level)
+{
+ auto* item = new NotificationItem(level, notifiername, message);
+
+ bool confirmation = confirmationRequired(level);
+
+ if (confirmation) {
+ showConfirmationDialog(notifiername, message);
+ }
+
+ std::lock_guard g(
+ pImp->mutexNotification);// guard to avoid modifying the notification list and indices while
+ // creating the tooltip
+
+ NotificationsAction* na = static_cast(pImp->notificationaction);
+
+ // Limit the maximum number of messages stored in the widget (0 means no limit)
+ if (pImp->maxWidgetMessages != 0 && na->count() > pImp->maxWidgetMessages) {
+ na->deleteLastItem();
+ }
+
+ na->push_front(item);
+
+ // If the non-intrusive notifications are disabled then stop here (messages added to the widget
+ // only)
+ if (pImp->notificationsDisabled) {
+ item->notifying =
+ false;// avoid mass of old notifications if feature is activated afterwards
+ setText(QString::number(
+ static_cast(pImp->notificationaction)->getUnreadCount()));
+ return;
+ }
+
+ // start or restart rate control (the timer is rearmed if not yet expired, expiration triggers
+ // showing of the notification messages)
+ //
+ // NOTE: The inhibition timer is created in the main thread and cannot be restarted from another
+ // QThread. A QTimer can be moved to another QThread (from the QThread in which it is). However,
+ // it can only be create in a QThread, not in any other thread.
+ //
+ // For this reason, the timer is only triggered if this QThread is the QTimer thread.
+ //
+ // But I want my message from my thread to appear in the notification area. Fine, then configure
+ // Console not to use the direct connection mode, but the Queued one:
+ // Base::Console().SetConnectionMode(ConnectionMode::Queued);
+
+ auto timer_thread = pImp->inhibitTimer.thread();
+ auto current_thread = QThread::currentThread();
+
+ if(timer_thread == current_thread)
+ pImp->inhibitTimer.start(pImp->inhibitNotificationTime);
+}
+
+bool NotificationArea::confirmationRequired(Base::LogStyle level)
+{
+ auto userInitiatedRestore =
+ Application::Instance->testStatus(Gui::Application::UserInitiatedOpenDocument);
+
+ return (level == Base::LogStyle::Critical && userInitiatedRestore
+ && pImp->requireConfirmationCriticalMessageDuringRestoring);
+}
+
+void NotificationArea::showConfirmationDialog(const QString& notifiername, const QString& message)
+{
+ auto confirmMsg = QObject::tr("Notifier: ") + notifiername + QStringLiteral("\n\n") + message
+ + QStringLiteral("\n\n")
+ + QObject::tr("Do you want to skip confirmation of further critical message notifications "
+ "while loading the file?");
+
+ auto button = QMessageBox::critical(getMainWindow()->activeWindow(),
+ QObject::tr("Critical Message"),
+ confirmMsg,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No);
+
+ if (button == QMessageBox::Yes)
+ pImp->requireConfirmationCriticalMessageDuringRestoring = false;
+}
+
+void NotificationArea::showInNotificationArea()
+{
+ std::lock_guard g(
+ pImp->mutexNotification);// guard to avoid modifying the notification list and indices while
+ // creating the tooltip
+
+ NotificationsAction* na = static_cast(pImp->notificationaction);
+
+ if (!NotificationBox::isVisible()) {
+ // The Notification Box may have been closed (by the user by popping it out or by left mouse
+ // button) ensure that old notifications are not shown again, even if the timer has not
+ // lapsed
+ int i = 0;
+ while (i < na->count() && static_cast(na->getItem(i))->notifying) {
+ NotificationItem* item = static_cast(na->getItem(i));
+
+ if (item->shown) {
+ item->notifying = false;
+ item->shown = false;
+ }
+
+ i++;
+ }
+ }
+
+ auto currentlyshown = na->getShownCount();
+
+ // If we cannot show more messages, we do no need to update the non-intrusive notification
+ if (currentlyshown < pImp->maxOpenNotifications) {
+ // There is space for at least one more notification
+ // We update the message with the most recent up to maxOpenNotifications
+
+ QString msgw =
+ QString::fromLatin1(
+ " \
+
")
+ .arg(QObject::tr("Too many opened non-intrusive notifications. Notifications "
+ "are being omitted!"));
+ }
+
+ int i = 0;
+
+ while (i < na->count() && static_cast(na->getItem(i))->notifying) {
+
+ if (i < pImp->maxOpenNotifications) {// show the first up to maxOpenNotifications
+ NotificationItem* item = static_cast(na->getItem(i));
+
+ QString iconstr;
+ if (item->notificationType == Base::LogStyle::Error) {
+ iconstr = QStringLiteral(":/icons/edit_Cancel.svg");
+ }
+ else if (item->notificationType == Base::LogStyle::Warning) {
+ iconstr = QStringLiteral(":/icons/Warning.svg");
+ }
+ else if (item->notificationType == Base::LogStyle::Critical) {
+ iconstr = QStringLiteral(":/icons/critical-info.svg");
+ }
+ else {
+ iconstr = QStringLiteral(":/icons/info.svg");
+ }
+
+ msgw +=
+ QString::fromLatin1(
+ " \
+
\
+
\
+
%2
\
+
%3
\
+
")
+ .arg(iconstr)
+ .arg(item->notifierName)
+ .arg(item->msg);
+
+ // start a timer for each of these notifications that was not previously shown
+ if (!item->shown) {
+ QTimer::singleShot(pImp->notificationExpirationTime, [this, item]() {
+ std::lock_guard g(
+ pImp->mutexNotification);// guard to avoid modifying the notification
+ // start index while creating the tooltip
+
+ if (item) {
+ item->shown = false;
+ item->notifying = false;
+
+ if (pImp->autoRemoveUserNotifications) {
+ if (item->notificationType == Base::LogStyle::Notification
+ || item->notificationType
+ == Base::LogStyle::TranslatedNotification) {
+
+ static_cast(pImp->notificationaction)
+ ->deleteItem(item);
+ }
+ }
+ }
+ });
+ }
+
+ // We update the status to shown
+ item->shown = true;
+ }
+ else {// We do not have more space and older notifications will be too old
+ static_cast(na->getItem(i))->notifying = false;
+ static_cast(na->getItem(i))->shown = false;
+ }
+
+ i++;
+ }
+
+ msgw += QString::fromLatin1("
");
+
+
+ NotificationBox::showText(this->mapToGlobal(QPoint()),
+ msgw,
+ pImp->notificationExpirationTime,
+ pImp->minimumOnScreenTime);
+ }
+}
+
+void NotificationArea::slotRestoreFinished(const App::Document&)
+{
+ // Re-arm on restore critical message modal notifications if another document is loaded
+ pImp->requireConfirmationCriticalMessageDuringRestoring = true;
+}
diff --git a/src/Gui/NotificationArea.h b/src/Gui/NotificationArea.h
new file mode 100644
index 0000000000..133cb41737
--- /dev/null
+++ b/src/Gui/NotificationArea.h
@@ -0,0 +1,78 @@
+/***************************************************************************
+ * Copyright (c) 2022 Abdullah Tahiri *
+ * *
+ * 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 GUI_NOTIFICATIONAREA_H
+#define GUI_NOTIFICATIONAREA_H
+
+#include
+#include
+
+#include