Rebased ref, commits from common ancestor: commit ca8bc047298e59e598c631d4c93d374793054067 Author: Sarper Akdemir <q.sarperakde...@gmail.com> AuthorDate: Thu Aug 13 03:40:25 2020 +0300 Commit: Sarper Akdemir <q.sarperakde...@gmail.com> CommitDate: Thu Aug 13 06:40:50 2020 +0300
make physics animations handle sequential animations correctly Change-Id: Ie8bb8b32588f4c7bf16317b5229adc5b0334d192 diff --git a/slideshow/source/engine/animationfactory.cxx b/slideshow/source/engine/animationfactory.cxx index bc1848f68435..d6198439b327 100644 --- a/slideshow/source/engine/animationfactory.cxx +++ b/slideshow/source/engine/animationfactory.cxx @@ -323,7 +323,9 @@ namespace slideshow::internal { mpShapeManager->notifyShapeUpdate( mpShape ); if ( mpBox2DWorld->isInitialized() ) - mpBox2DWorld->queuePositionUpdate( mpShape->getXShape(), rOutPos ); + { + mpBox2DWorld->queueShapePathAnimationUpdate( mpShape->getXShape(), mpAttrLayer ); + } } return true; diff --git a/slideshow/source/engine/box2dtools.cxx b/slideshow/source/engine/box2dtools.cxx index 68ab37fb6868..0dbc7be63f2a 100644 --- a/slideshow/source/engine/box2dtools.cxx +++ b/slideshow/source/engine/box2dtools.cxx @@ -203,7 +203,8 @@ box2DWorld::box2DWorld(const ::basegfx::B2DVector& rSlideSize) , mbShapesInitialized(false) , mbHasWorldStepper(false) , mpXShapeToBodyMap() - , maShapeUpdateQueue() + , maShapeParallelUpdateQueue() + , maShapeSequentialUpdate() { } @@ -253,6 +254,13 @@ void box2DWorld::createStaticFrameAroundSlide(const ::basegfx::B2DVector& rSlide pStaticBody->CreateFixture(&aFixtureDef); } +void box2DWorld::setShapePosition(const css::uno::Reference<com::sun::star::drawing::XShape> xShape, + const basegfx::B2DPoint& rOutPos) +{ + Box2DBodySharedPtr pBox2DBody = mpXShapeToBodyMap.find(xShape)->second; + pBox2DBody->setPosition(rOutPos); +} + void box2DWorld::setShapePositionByLinearVelocity( const css::uno::Reference<com::sun::star::drawing::XShape> xShape, const basegfx::B2DPoint& rOutPos, const double fPassedTime) @@ -274,6 +282,13 @@ void box2DWorld::setShapeLinearVelocity( pBox2DBody->setLinearVelocity(rVelocity); } +void box2DWorld::setShapeAngle(const css::uno::Reference<com::sun::star::drawing::XShape> xShape, + const double fAngle) +{ + Box2DBodySharedPtr pBox2DBody = mpXShapeToBodyMap.find(xShape)->second; + pBox2DBody->setAngle(fAngle); +} + void box2DWorld::setShapeAngleByAngularVelocity( const css::uno::Reference<com::sun::star::drawing::XShape> xShape, const double fAngle, const double fPassedTime) @@ -305,41 +320,75 @@ void box2DWorld::setShapeCollision( void box2DWorld::processUpdateQueue(const double fPassedTime) { - while (!maShapeUpdateQueue.empty()) + if (maShapeSequentialUpdate.empty()) { - Box2DShapeUpdateInformation& aQueueElement = maShapeUpdateQueue.front(); - - if (aQueueElement.mnDelayForSteps > 0) + while (!maShapeParallelUpdateQueue.empty()) { - // it was queued as a delayed action, skip it, don't pop - aQueueElement.mnDelayForSteps--; + Box2DDynamicUpdateInformation& aQueueElement = maShapeParallelUpdateQueue.front(); + + if (aQueueElement.mnDelayForSteps > 0) + { + // it was queued as a delayed action, skip it, don't pop + aQueueElement.mnDelayForSteps--; + } + else + { + switch (aQueueElement.meUpdateType) + { + default: + case BOX2D_UPDATE_POSITION: + setShapePositionByLinearVelocity(aQueueElement.mxShape, + aQueueElement.maPosition, fPassedTime); + break; + case BOX2D_UPDATE_ANGLE: + setShapeAngleByAngularVelocity(aQueueElement.mxShape, aQueueElement.mfAngle, + fPassedTime); + break; + case BOX2D_UPDATE_SIZE: + break; + case BOX2D_UPDATE_VISIBILITY: + setShapeCollision(aQueueElement.mxShape, aQueueElement.mbVisibility); + break; + case BOX2D_UPDATE_LINEAR_VELOCITY: + setShapeLinearVelocity(aQueueElement.mxShape, aQueueElement.maVelocity); + break; + case BOX2D_UPDATE_ANGULAR_VELOCITY: + setShapeAngularVelocity(aQueueElement.mxShape, + aQueueElement.mfAngularVelocity); + } + maShapeParallelUpdateQueue.pop(); + } } - else + } + else + { + // clear the Parallel Update Queue since the updates in it + // are not relevant now - if there's any + maShapeParallelUpdateQueue = {}; + + for (auto aIt : maShapeSequentialUpdate) { - switch (aQueueElement.meUpdateType) + css::uno::Reference<css::drawing::XShape> xShape = aIt.first.first; + box2DNonsimulatedShapeUpdateType eUpdateType = aIt.first.second; + Box2DStaticUpdateInformation& aUpdateInformation = aIt.second; + + switch (eUpdateType) { default: case BOX2D_UPDATE_POSITION: - setShapePositionByLinearVelocity(aQueueElement.mxShape, - aQueueElement.maPosition, fPassedTime); + setShapePosition(xShape, aUpdateInformation.maPosition); break; case BOX2D_UPDATE_ANGLE: - setShapeAngleByAngularVelocity(aQueueElement.mxShape, aQueueElement.mfAngle, - fPassedTime); - break; - case BOX2D_UPDATE_SIZE: + setShapeAngle(xShape, aUpdateInformation.mfAngle); break; case BOX2D_UPDATE_VISIBILITY: - setShapeCollision(aQueueElement.mxShape, aQueueElement.mbVisibility); - break; - case BOX2D_UPDATE_LINEAR_VELOCITY: - setShapeLinearVelocity(aQueueElement.mxShape, aQueueElement.maVelocity); + setShapeCollision(xShape, aUpdateInformation.mbVisibility); break; - case BOX2D_UPDATE_ANGULAR_VELOCITY: - setShapeAngularVelocity(aQueueElement.mxShape, aQueueElement.mfAngularVelocity); } - maShapeUpdateQueue.pop(); } + + // After applying all required updates empty map + maShapeSequentialUpdate.clear(); } } @@ -376,47 +425,58 @@ void box2DWorld::setHasWorldStepper(const bool bHasWorldStepper) mbHasWorldStepper = bHasWorldStepper; } -void box2DWorld::queuePositionUpdate( +void box2DWorld::queueDynamicPositionUpdate( const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, const basegfx::B2DPoint& rOutPos) { - Box2DShapeUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_POSITION }; + Box2DDynamicUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_POSITION }; aQueueElement.maPosition = rOutPos; - maShapeUpdateQueue.push(aQueueElement); + maShapeParallelUpdateQueue.push(aQueueElement); } void box2DWorld::queueLinearVelocityUpdate( const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, - const basegfx::B2DVector& rVelocity) + const basegfx::B2DVector& rVelocity, const int nDelayForSteps) { - Box2DShapeUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_LINEAR_VELOCITY, 1 }; + Box2DDynamicUpdateInformation aQueueElement + = { xShape, {}, BOX2D_UPDATE_LINEAR_VELOCITY, nDelayForSteps }; aQueueElement.maVelocity = rVelocity; - maShapeUpdateQueue.push(aQueueElement); + maShapeParallelUpdateQueue.push(aQueueElement); } -void box2DWorld::queueRotationUpdate( +void box2DWorld::queueDynamicRotationUpdate( const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, const double fAngle) { - Box2DShapeUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_ANGLE }; + Box2DDynamicUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_ANGLE }; aQueueElement.mfAngle = fAngle; - maShapeUpdateQueue.push(aQueueElement); + maShapeParallelUpdateQueue.push(aQueueElement); } void box2DWorld::queueAngularVelocityUpdate( const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, - const double fAngularVelocity) + const double fAngularVelocity, const int nDelayForSteps) { - Box2DShapeUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_ANGULAR_VELOCITY, 1 }; + Box2DDynamicUpdateInformation aQueueElement + = { xShape, {}, BOX2D_UPDATE_ANGULAR_VELOCITY, nDelayForSteps }; aQueueElement.mfAngularVelocity = fAngularVelocity; - maShapeUpdateQueue.push(aQueueElement); + maShapeParallelUpdateQueue.push(aQueueElement); } void box2DWorld::queueShapeVisibilityUpdate( const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, const bool bVisibility) { - Box2DShapeUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_VISIBILITY }; + Box2DDynamicUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_VISIBILITY }; aQueueElement.mbVisibility = bVisibility; - maShapeUpdateQueue.push(aQueueElement); + maShapeParallelUpdateQueue.push(aQueueElement); +} + +void box2DWorld::queueShapePathAnimationUpdate( + const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, + const slideshow::internal::ShapeAttributeLayerSharedPtr& pAttrLayer) +{ + // Workaround for PathAnimations since they do not have their own AttributeType + // - using PosX makes it register a DynamicPositionUpdate - + queueShapeAnimationUpdate(xShape, pAttrLayer, slideshow::internal::AttributeType::PosX); } void box2DWorld::queueShapeAnimationUpdate( @@ -424,20 +484,50 @@ void box2DWorld::queueShapeAnimationUpdate( const slideshow::internal::ShapeAttributeLayerSharedPtr& pAttrLayer, const slideshow::internal::AttributeType eAttrType) { - switch (eAttrType) + if (mbHasWorldStepper) // if there's a physics animation going on { - case slideshow::internal::AttributeType::Visibility: - queueShapeVisibilityUpdate(xShape, pAttrLayer->getVisibility()); - return; - case slideshow::internal::AttributeType::Rotate: - queueRotationUpdate(xShape, pAttrLayer->getRotationAngle()); - return; - case slideshow::internal::AttributeType::PosX: - case slideshow::internal::AttributeType::PosY: - queuePositionUpdate(xShape, { pAttrLayer->getPosX(), pAttrLayer->getPosY() }); - return; - default: - return; + switch (eAttrType) + { + case slideshow::internal::AttributeType::Visibility: + queueShapeVisibilityUpdate(xShape, pAttrLayer->getVisibility()); + return; + case slideshow::internal::AttributeType::Rotate: + queueDynamicRotationUpdate(xShape, pAttrLayer->getRotationAngle()); + return; + case slideshow::internal::AttributeType::PosX: + case slideshow::internal::AttributeType::PosY: + queueDynamicPositionUpdate(xShape, + { pAttrLayer->getPosX(), pAttrLayer->getPosY() }); + return; + default: + return; + } + } + else + { + Box2DStaticUpdateInformation aStaticUpdateInformation; + switch (eAttrType) + { + case slideshow::internal::AttributeType::Visibility: + aStaticUpdateInformation.mbVisibility = pAttrLayer->getVisibility(); + maShapeSequentialUpdate[std::make_pair(xShape, BOX2D_UPDATE_VISIBILITY)] + = aStaticUpdateInformation; + return; + case slideshow::internal::AttributeType::Rotate: + aStaticUpdateInformation.mfAngle = pAttrLayer->getRotationAngle(); + maShapeSequentialUpdate[std::make_pair(xShape, BOX2D_UPDATE_ANGLE)] + = aStaticUpdateInformation; + return; + case slideshow::internal::AttributeType::PosX: + case slideshow::internal::AttributeType::PosY: + aStaticUpdateInformation.maPosition + = basegfx::B2DPoint(pAttrLayer->getPosX(), pAttrLayer->getPosY()); + maShapeSequentialUpdate[std::make_pair(xShape, BOX2D_UPDATE_POSITION)] + = aStaticUpdateInformation; + return; + default: + return; + } } } @@ -448,11 +538,11 @@ void box2DWorld::queueShapeAnimationEndUpdate( switch (eAttrType) { case slideshow::internal::AttributeType::Rotate: - queueAngularVelocityUpdate(xShape, 0.0f); + queueAngularVelocityUpdate(xShape, 0.0, 1); return; case slideshow::internal::AttributeType::PosX: case slideshow::internal::AttributeType::PosY: - queueLinearVelocityUpdate(xShape, { 0, 0 }); + queueLinearVelocityUpdate(xShape, { 0, 0 }, 1); return; default: return; @@ -617,6 +707,12 @@ box2DBody::box2DBody(std::shared_ptr<b2Body> pBox2DBody, double fScaleFactor) return ::basegfx::B2DPoint(fX, fY); } +void box2DBody::setPosition(const basegfx::B2DPoint& rPos) +{ + mpBox2DBody->SetTransform(convertB2DPointToBox2DVec2(rPos, mfScaleFactor), + mpBox2DBody->GetAngle()); +} + void box2DBody::setPositionByLinearVelocity(const basegfx::B2DPoint& rDesiredPos, const double fPassedTime) { @@ -675,6 +771,11 @@ double box2DBody::getAngle() return ::basegfx::rad2deg(-fAngle); } +void box2DBody::setAngle(double fAngle) +{ + mpBox2DBody->SetTransform(mpBox2DBody->GetPosition(), ::basegfx::deg2rad(-fAngle)); +} + void box2DBody::setType(box2DBodyType eType) { mpBox2DBody->SetType(getBox2DInternalBodyType(eType)); diff --git a/slideshow/source/inc/box2dtools.hxx b/slideshow/source/inc/box2dtools.hxx index 7f01f09eb607..e5d9c94577e5 100644 --- a/slideshow/source/inc/box2dtools.hxx +++ b/slideshow/source/inc/box2dtools.hxx @@ -12,6 +12,7 @@ #include "shape.hxx" #include "shapeattributelayer.hxx" #include "attributemap.hxx" +#include <map> #include <unordered_map> #include <queue> @@ -50,7 +51,7 @@ enum box2DNonsimulatedShapeUpdateType /// Holds required information to perform an update to box2d /// body of a shape that was altered by an animation effect -struct Box2DShapeUpdateInformation +struct Box2DDynamicUpdateInformation { css::uno::Reference<css::drawing::XShape> mxShape; union { @@ -64,6 +65,13 @@ struct Box2DShapeUpdateInformation int mnDelayForSteps = 0; }; +union Box2DStaticUpdateInformation { + ::basegfx::B2DPoint maPosition; + bool mbVisibility; + double mfAngle; + Box2DStaticUpdateInformation() {} +}; + /** Class that manages the Box2D World This class is used when there's a simulated animation going on, @@ -78,15 +86,25 @@ private: /// Scale factor for conversions between LO user space coordinates to Box2D World coordinates double mfScaleFactor; bool mbShapesInitialized; + /// Holds whether or not there is a Physics Animation node that + /// is stepping the Box2D World bool mbHasWorldStepper; std::unordered_map<css::uno::Reference<css::drawing::XShape>, Box2DBodySharedPtr> mpXShapeToBodyMap; /// Holds any information needed to keep LO animations and Box2D world in sync - std::queue<Box2DShapeUpdateInformation> maShapeUpdateQueue; + /// if they are ongoing on parallel + std::queue<Box2DDynamicUpdateInformation> maShapeParallelUpdateQueue; + /// Holds necessary information if a shape was altered by an animation update + /// while there was no Physics animation going on in parallel + std::map<std::pair<css::uno::Reference<css::drawing::XShape>, box2DNonsimulatedShapeUpdateType>, + Box2DStaticUpdateInformation> + maShapeSequentialUpdate; /// Creates a static frame in Box2D world that corresponds to the slide borders void createStaticFrameAroundSlide(const ::basegfx::B2DVector& rSlideSize); + void setShapePosition(const css::uno::Reference<css::drawing::XShape> xShape, + const ::basegfx::B2DPoint& rOutPos); /** Sets shape's corresponding Box2D body to specified position Sets shape's corresponding Box2D body to specified position as if @@ -108,6 +126,8 @@ private: void setShapeLinearVelocity(const css::uno::Reference<com::sun::star::drawing::XShape> xShape, const basegfx::B2DVector& rVelocity); + void setShapeAngle(const css::uno::Reference<com::sun::star::drawing::XShape> xShape, + const double fAngle); /** Sets shape's corresponding Box2D body to specified angle Sets shape's corresponding Box2D body to specified angle as if @@ -157,14 +177,15 @@ private: const int nPositionIterations = 2); /// Queue a rotation update on the next step of the box2DWorld for the corresponding body - void queueRotationUpdate(const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, - const double fAngle); + void + queueDynamicRotationUpdate(const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, + const double fAngle); /// Queue an angular velocity update for the corresponding body /// to take place after the next step of the box2DWorld void queueAngularVelocityUpdate(const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, - const double fAngularVelocity); + const double fAngularVelocity, const int nDelayForSteps = 0); /// Queue an update that changes collision of the corresponding body /// on the next step of the box2DWorld, used for animations that change visibility @@ -244,19 +265,24 @@ public: void setHasWorldStepper(const bool bHasWorldStepper); /// Queue a position update the next step of the box2DWorld for the corresponding body - void queuePositionUpdate(const css::uno::Reference<css::drawing::XShape>& xShape, - const ::basegfx::B2DPoint& rOutPos); + void queueDynamicPositionUpdate(const css::uno::Reference<css::drawing::XShape>& xShape, + const ::basegfx::B2DPoint& rOutPos); /// Queue a linear velocity update for the corresponding body /// to take place after the next step of the box2DWorld void queueLinearVelocityUpdate(const css::uno::Reference<css::drawing::XShape>& xShape, - const ::basegfx::B2DVector& rVelocity); + const ::basegfx::B2DVector& rVelocity, + const int nDelayForSteps = 0); void queueShapeAnimationUpdate(const css::uno::Reference<css::drawing::XShape>& xShape, const slideshow::internal::ShapeAttributeLayerSharedPtr& pAttrLayer, const slideshow::internal::AttributeType eAttrType); + void queueShapePathAnimationUpdate( + const css::uno::Reference<com::sun::star::drawing::XShape>& xShape, + const slideshow::internal::ShapeAttributeLayerSharedPtr& pAttrLayer); + void queueShapeAnimationEndUpdate(const css::uno::Reference<css::drawing::XShape>& xShape, const slideshow::internal::AttributeType eAttrType); }; @@ -276,6 +302,8 @@ public: /// @return current position in LO user space coordinates ::basegfx::B2DPoint getPosition(); + void setPosition(const ::basegfx::B2DPoint& rPos); + /** Sets body to specified position Sets body to specified position as if the body had @@ -315,6 +343,8 @@ public: /// @return current angle of rotation of the body double getAngle(); + void setAngle(double fAngle); + /// Set type of the body void setType(box2DBodyType eType); commit 3da099c53bac119c8e9542d83891afa10e759d0f Author: Sarper Akdemir <q.sarperakde...@gmail.com> AuthorDate: Thu Aug 6 10:32:55 2020 +0300 Commit: Sarper Akdemir <q.sarperakde...@gmail.com> CommitDate: Thu Aug 13 06:40:49 2020 +0300 make physics based animation effects always processed last Change-Id: I92d436aced6ef3ee2c8b0bf0167c1f7e642ba3b5 diff --git a/slideshow/source/engine/activitiesqueue.cxx b/slideshow/source/engine/activitiesqueue.cxx index ba982385356e..38825c6b5f06 100644 --- a/slideshow/source/engine/activitiesqueue.cxx +++ b/slideshow/source/engine/activitiesqueue.cxx @@ -50,6 +50,8 @@ namespace slideshow::internal { for( const auto& pActivity : maCurrentActivitiesWaiting ) pActivity->dispose(); + for( const auto& pActivity : maCurrentActivitiesToBeProcessedLast ) + pActivity->dispose(); for( const auto& pActivity : maCurrentActivitiesReinsert ) pActivity->dispose(); } @@ -59,7 +61,7 @@ namespace slideshow::internal } } - bool ActivitiesQueue::addActivity( const ActivitySharedPtr& pActivity ) + bool ActivitiesQueue::addActivity( const ActivitySharedPtr& pActivity, const bool bProcessLast ) { OSL_ENSURE( pActivity, "ActivitiesQueue::addActivity: activity ptr NULL" ); @@ -67,7 +69,17 @@ namespace slideshow::internal return false; // add entry to waiting list - maCurrentActivitiesWaiting.push_back( pActivity ); + if( !bProcessLast ) + { + maCurrentActivitiesWaiting.push_back( pActivity ); + } + else + { + // Activities that should be processed last are kept in a different + // ActivityQueue, and later appended to the end of the maCurrentActivitiesWaiting + // on the beginning of ActivitiesQueue::process() + maCurrentActivitiesToBeProcessedLast.push_back( pActivity ); + } return true; } @@ -76,6 +88,12 @@ namespace slideshow::internal { SAL_INFO("slideshow.verbose", "ActivitiesQueue: outer loop heartbeat" ); + // If there are activities to be processed last append them to the end of the ActivitiesQueue + maCurrentActivitiesWaiting.insert( maCurrentActivitiesWaiting.end(), + maCurrentActivitiesToBeProcessedLast.begin(), + maCurrentActivitiesToBeProcessedLast.end() ); + maCurrentActivitiesToBeProcessedLast.clear(); + // accumulate time lag for all activities, and lag time // base if necessary: double fLag = 0.0; diff --git a/slideshow/source/engine/animationnodes/animationbasenode.cxx b/slideshow/source/engine/animationnodes/animationbasenode.cxx index 4dcb640795aa..7999b5a7654a 100644 --- a/slideshow/source/engine/animationnodes/animationbasenode.cxx +++ b/slideshow/source/engine/animationnodes/animationbasenode.cxx @@ -23,6 +23,7 @@ #include <com/sun/star/animations/Timing.hpp> #include <com/sun/star/animations/AnimationAdditiveMode.hpp> #include <com/sun/star/presentation/ShapeAnimationSubType.hpp> +#include <com/sun/star/animations/AnimationNodeType.hpp> #include "nodetools.hxx" #include <doctreenode.hxx> @@ -294,7 +295,10 @@ void AnimationBaseNode::activate_st() mpActivity->setTargets( getShape(), maAttributeLayerHolder.get() ); // add to activities queue - getContext().mrActivitiesQueue.addActivity( mpActivity ); + if( mxAnimateNode->getType() == css::animations::AnimationNodeType::ANIMATEPHYSICS ) + getContext().mrActivitiesQueue.addActivity(mpActivity, true); + else + getContext().mrActivitiesQueue.addActivity( mpActivity ); } else { // Actually, DO generate the event for empty activity, diff --git a/slideshow/source/inc/activitiesqueue.hxx b/slideshow/source/inc/activitiesqueue.hxx index b4f88b1b39d1..fff8b67bd779 100644 --- a/slideshow/source/inc/activitiesqueue.hxx +++ b/slideshow/source/inc/activitiesqueue.hxx @@ -57,7 +57,7 @@ namespace slideshow /** Add the given activity to the queue. */ - bool addActivity( const ActivitySharedPtr& pActivity ); + bool addActivity( const ActivitySharedPtr& pActivity, const bool bProcessLast = false ); /** Process the activities queue. @@ -96,6 +96,9 @@ namespace slideshow // await processing for this // round + ActivityQueue maCurrentActivitiesToBeProcessedLast; // activities that will be + // processed last in the queue + ActivityQueue maCurrentActivitiesReinsert; // currently running // activities, that are // already processed for _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits