diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /slideshow/source/engine/slide | |
parent | Initial commit. (diff) | |
download | libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.tar.xz libreoffice-ed5640d8b587fbcfed7dd7967f3de04b37a76f26.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
17 files changed, 8480 insertions, 0 deletions
diff --git a/slideshow/source/engine/slide/layer.cxx b/slideshow/source/engine/slide/layer.cxx new file mode 100644 index 000000000..01ce4efa4 --- /dev/null +++ b/slideshow/source/engine/slide/layer.cxx @@ -0,0 +1,272 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <basegfx/range/b2drange.hxx> +#include <basegfx/range/b1drange.hxx> +#include <basegfx/range/b2dpolyrange.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> +#include <osl/diagnose.h> + +#include "layer.hxx" + +using namespace ::com::sun::star; + +namespace slideshow::internal +{ + Layer::Layer( Dummy ) : + maViewEntries(), + maBounds(), + maNewBounds(), + mbBoundsDirty(false), + mbBackgroundLayer(true), + mbClipSet(false) + { + } + + Layer::Layer() : + maViewEntries(), + maBounds(), + maNewBounds(), + mbBoundsDirty(false), + mbBackgroundLayer(false), + mbClipSet(false) + { + } + + ViewLayerSharedPtr Layer::addView( const ViewSharedPtr& rNewView ) + { + OSL_ASSERT( rNewView ); + + ViewEntryVector::iterator aIter; + const ViewEntryVector::iterator aEnd( maViewEntries.end() ); + if( (aIter=std::find_if( maViewEntries.begin(), + aEnd, + [&rNewView]( const ViewEntry& rViewEntry ) + { return rViewEntry.getView() == rNewView; } ) ) != aEnd ) + { + // already added - just return existing layer + return aIter->mpViewLayer; + + } + + // not yet added - create new view layer + ViewLayerSharedPtr pNewLayer; + if( mbBackgroundLayer ) + pNewLayer = rNewView; + else + pNewLayer = rNewView->createViewLayer(maBounds); + + // add to local list + maViewEntries.emplace_back( rNewView, + pNewLayer ); + + return maViewEntries.back().mpViewLayer; + } + + ViewLayerSharedPtr Layer::removeView( const ViewSharedPtr& rView ) + { + OSL_ASSERT( rView ); + + ViewEntryVector::iterator aIter; + const ViewEntryVector::iterator aEnd( maViewEntries.end() ); + if( (aIter=std::find_if( maViewEntries.begin(), + aEnd, + [&rView]( const ViewEntry& rViewEntry ) + { return rViewEntry.getView() == rView; } ) ) == aEnd ) + { + // View was not added/is already removed + return ViewLayerSharedPtr(); + } + + OSL_ENSURE( std::count_if( maViewEntries.begin(), + aEnd, + [&rView]( const ViewEntry& rViewEntry ) + { return rViewEntry.getView() == rView; } ) == 1, + "Layer::removeView(): view added multiple times" ); + + ViewLayerSharedPtr pRet( aIter->mpViewLayer ); + maViewEntries.erase(aIter); + + return pRet; + } + + void Layer::setShapeViews( ShapeSharedPtr const& rShape ) const + { + rShape->clearAllViewLayers(); + + for( const auto& rViewEntry : maViewEntries ) + rShape->addViewLayer( rViewEntry.getViewLayer(), false ); + } + + void Layer::setPriority( const ::basegfx::B1DRange& rPrioRange ) + { + if( !mbBackgroundLayer ) + { + for( const auto& rViewEntry : maViewEntries ) + rViewEntry.getViewLayer()->setPriority( rPrioRange ); + } + } + + void Layer::addUpdateRange( ::basegfx::B2DRange const& rUpdateRange ) + { + // TODO(Q1): move this to B2DMultiRange + if( !rUpdateRange.isEmpty() ) + maUpdateAreas.appendElement( rUpdateRange, + basegfx::B2VectorOrientation::Positive ); + } + + void Layer::updateBounds( ShapeSharedPtr const& rShape ) + { + if( !mbBackgroundLayer ) + { + if( !mbBoundsDirty ) + maNewBounds.reset(); + + maNewBounds.expand( rShape->getUpdateArea() ); + } + + mbBoundsDirty = true; + } + + bool Layer::commitBounds() + { + mbBoundsDirty = false; + + if( mbBackgroundLayer ) + return false; + + if( maNewBounds == maBounds ) + return false; + + maBounds = maNewBounds; + if( std::count_if( maViewEntries.begin(), + maViewEntries.end(), + [this]( const ViewEntry& rViewEntry ) + { return rViewEntry.getViewLayer()->resize( this->maBounds ); } + ) == 0 ) + { + return false; + } + + // layer content invalid, update areas have wrong + // coordinates/not sensible anymore. + clearUpdateRanges(); + + return true; + } + + void Layer::clearUpdateRanges() + { + maUpdateAreas.clear(); + } + + void Layer::clearContent() + { + // clear content on all view layers + for( const auto& rViewEntry : maViewEntries ) + rViewEntry.getViewLayer()->clearAll(); + + // layer content cleared, update areas are not sensible + // anymore. + clearUpdateRanges(); + } + + class LayerEndUpdate + { + public: + LayerEndUpdate( const LayerEndUpdate& ) = delete; + LayerEndUpdate& operator=( const LayerEndUpdate& ) = delete; + explicit LayerEndUpdate( LayerSharedPtr const& rLayer ) : + mpLayer( rLayer ) + {} + + ~LayerEndUpdate() { if(mpLayer) mpLayer->endUpdate(); } + + private: + LayerSharedPtr mpLayer; + }; + + Layer::EndUpdater Layer::beginUpdate() + { + if( maUpdateAreas.count() ) + { + // perform proper layer update. That means, setup proper + // clipping, and render each shape that intersects with + // the calculated update area + ::basegfx::B2DPolyPolygon aClip( maUpdateAreas.solveCrossovers() ); + aClip = ::basegfx::utils::stripNeutralPolygons(aClip); + aClip = ::basegfx::utils::stripDispensablePolygons(aClip); + + // actually, if there happen to be shapes with zero + // update area in the maUpdateAreas vector, the + // resulting clip polygon will be empty. + if( aClip.count() ) + { + for( const auto& rViewEntry : maViewEntries ) + { + const ViewLayerSharedPtr& pViewLayer = rViewEntry.getViewLayer(); + + // set clip to all view layers and + pViewLayer->setClip( aClip ); + + // clear update area on all view layers + pViewLayer->clear(); + } + + mbClipSet = true; + } + } + + return std::make_shared<LayerEndUpdate>(shared_from_this()); + } + + void Layer::endUpdate() + { + if( mbClipSet ) + { + mbClipSet = false; + + basegfx::B2DPolyPolygon aEmptyClip; + for( const auto& rViewEntry : maViewEntries ) + rViewEntry.getViewLayer()->setClip( aEmptyClip ); + } + + clearUpdateRanges(); + } + + bool Layer::isInsideUpdateArea( ShapeSharedPtr const& rShape ) const + { + return maUpdateAreas.overlaps( rShape->getUpdateArea() ); + } + + LayerSharedPtr Layer::createBackgroundLayer() + { + return LayerSharedPtr(new Layer( BackgroundLayer )); + } + + LayerSharedPtr Layer::createLayer( ) + { + return LayerSharedPtr( new Layer ); + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/layer.hxx b/slideshow/source/engine/slide/layer.hxx new file mode 100644 index 000000000..73f3fcce9 --- /dev/null +++ b/slideshow/source/engine/slide/layer.hxx @@ -0,0 +1,263 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_LAYER_HXX +#define INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_LAYER_HXX + +#include <basegfx/range/b2dpolyrange.hxx> + +#include <shape.hxx> +#include <view.hxx> + +#include <vector> +#include <memory> + + +namespace slideshow::internal + { + class LayerEndUpdate; + class Layer; + typedef ::std::shared_ptr< Layer > LayerSharedPtr; + typedef ::std::weak_ptr< Layer > LayerWeakPtr; + + + /* Definition of Layer class */ + + /** This class represents one layer of output on a Slide. + + Layers group shapes for a certain depth region of a slide. + + Since slides have a notion of depth, i.e. shapes on it + have a certain order in which they lie upon each other, + this layering must be modelled. A prime example for this + necessity are animations of shapes lying behind other + shapes. Then, everything behind the animated shape will be + in a background layer, the shape itself will be in an + animation layer, and everything before it will be in a + foreground layer (these layers are most preferably + modelled as XSprite objects internally). + + @attention All methods of this class are only supposed to + be called from the LayerManager. Normally, it shouldn't be + possible to get hold of an instance of this class at all. + */ + class Layer : public std::enable_shared_from_this<Layer> + { + public: + typedef std::shared_ptr<LayerEndUpdate> EndUpdater; + + /// Forbid copy construction + Layer(const Layer&) = delete; + /// Forbid copy assignment + Layer& operator=(const Layer&) = delete; + + /** Create background layer + + This method will create a layer without a ViewLayer, + i.e. one that displays directly on the background. + */ + static LayerSharedPtr createBackgroundLayer(); + + /** Create non-background layer + + This method will create a layer in front of the + background, to contain shapes that should appear in + front of animated objects. + */ + static LayerSharedPtr createLayer(); + + + /** Predicate, whether this layer is the special + background layer + + This method is mostly useful for checking invariants. + */ + bool isBackgroundLayer() const { return mbBackgroundLayer; } + + /** Add a view to this layer. + + If the view is already added, this method does not add + it a second time, just returning the existing ViewLayer. + + @param rNewView + New view to add to this layer. + + @return the newly generated ViewLayer for this View + */ + ViewLayerSharedPtr addView( const ViewSharedPtr& rNewView ); + + /** Remove a view + + This method removes the view from this Layer and all + shapes included herein. + + @return the ViewLayer of the removed Layer, if + any. Otherwise, NULL is returned. + */ + ViewLayerSharedPtr removeView( const ViewSharedPtr& rView ); + + /** Init shape with this layer's views + + @param rShape + The shape, that will subsequently display on this + layer's views + */ + void setShapeViews( ShapeSharedPtr const& rShape ) const; + + + /** Change layer priority range. + + The layer priority affects the position of the layer + in the z direction (i.e. before/behind which other + layers this one appears). The higher the prio, the + further on top of the layer stack this one appears. + + @param rPrioRange + The priority range of differing layers must not + intersect + */ + void setPriority( const ::basegfx::B1DRange& rPrioRange ); + + /** Add an area that needs update + + @param rUpdateRange + Area on this layer that needs update + */ + void addUpdateRange( ::basegfx::B2DRange const& rUpdateRange ); + + /** Whether any update ranges have been added + + @return true, if any non-empty addUpdateRange() calls + have been made since the last render()/update() call. + */ + bool isUpdatePending() const { return maUpdateAreas.count()!=0; } + + /** Update layer bound rect from shape bounds + */ + void updateBounds( ShapeSharedPtr const& rShape ); + + /** Commit collected layer bounds to ViewLayer + + Call this method when you're done adding new shapes to + the layer. + + @return true, if layer needed a resize (which + invalidates its content - you have to repaint all + contained shapes!) + */ + bool commitBounds(); + + /** Clear all registered update ranges + + This method clears all update ranges that are + registered at this layer. + */ + void clearUpdateRanges(); + + /** Clear whole layer content + + This method clears the whole layer content. As a + byproduct, all update ranges are cleared as well. It + makes no sense to maintain them any further, since + they only serve for partial updates. + */ + void clearContent(); + + /** Init layer update. + + This method initializes a full layer update of the + update area. When the last copy of the returned + EndUpdater is destroyed, the Layer leaves update mode + again. + + @return an update end RAII object. + */ + EndUpdater beginUpdate(); + + /** Finish layer update + + Resets clipping and transformation to normal values + */ + void endUpdate(); + + /** Check whether given shape is inside current update area. + + @return true, if the given shape is at least partially + inside the current update area. + */ + bool isInsideUpdateArea( ShapeSharedPtr const& rShape ) const; + + private: + enum Dummy{ BackgroundLayer }; + + /** Create background layer + + This constructor will create a layer without a + ViewLayer, i.e. one that displays directly on the + background. + + @param eFlag + Dummy parameter, to disambiguate from normal layer + constructor + */ + explicit Layer( Dummy eFlag ); + + /** Create non-background layer + + This constructor will create a layer in front of the + background, to contain shapes that should appear in + front of animated objects. + */ + explicit Layer(); + + struct ViewEntry + { + ViewEntry( const ViewSharedPtr& rView, + const ViewLayerSharedPtr& rViewLayer ) : + mpView( rView ), + mpViewLayer( rViewLayer ) + {} + + ViewSharedPtr mpView; + ViewLayerSharedPtr mpViewLayer; + + // for generic algo access (which needs actual functions) + const ViewSharedPtr& getView() const { return mpView; } + const ViewLayerSharedPtr& getViewLayer() const { return mpViewLayer; } + }; + + typedef ::std::vector< ViewEntry > ViewEntryVector; + + ViewEntryVector maViewEntries; + basegfx::B2DPolyRange maUpdateAreas; + basegfx::B2DRange maBounds; + basegfx::B2DRange maNewBounds; + bool mbBoundsDirty; // true, if view layers need resize + bool mbBackgroundLayer; // true, if this + // layer is the + // special + // background layer + bool mbClipSet; // true, if beginUpdate set a clip + }; + +} + +#endif // INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_LAYER_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/layermanager.cxx b/slideshow/source/engine/slide/layermanager.cxx new file mode 100644 index 000000000..74a26d1e7 --- /dev/null +++ b/slideshow/source/engine/slide/layermanager.cxx @@ -0,0 +1,833 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <tools/diagnose_ex.h> +#include <basegfx/range/b1drange.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <cppcanvas/canvas.hxx> + +#include <functional> +#include <algorithm> + +#include "layermanager.hxx" + +using namespace ::com::sun::star; + +namespace +{ + // add operator!= for weak_ptr + bool notEqual( slideshow::internal::LayerWeakPtr const& rLHS, + slideshow::internal::LayerWeakPtr const& rRHS ) + { + return rLHS.lock().get() != rRHS.lock().get(); + } +} + +namespace slideshow::internal +{ + template<typename LayerFunc, + typename ShapeFunc> void LayerManager::manageViews( + LayerFunc layerFunc, + ShapeFunc shapeFunc ) + { + LayerSharedPtr pCurrLayer; + ViewLayerSharedPtr pCurrViewLayer; + for( const auto& rShape : maAllShapes ) + { + LayerSharedPtr pLayer = rShape.second.lock(); + if( pLayer && pLayer != pCurrLayer ) + { + pCurrLayer = pLayer; + pCurrViewLayer = layerFunc(pCurrLayer); + } + + if( pCurrViewLayer ) + shapeFunc(rShape.first,pCurrViewLayer); + } + } + + LayerManager::LayerManager( const UnoViewContainer& rViews, + bool bDisableAnimationZOrder ) : + mrViews(rViews), + maLayers(), + maXShapeHash( 101 ), + maAllShapes(), + maUpdateShapes(), + mnActiveSprites(0), + mbLayerAssociationDirty(false), + mbActive(false), + mbDisableAnimationZOrder(bDisableAnimationZOrder) + { + // prevent frequent resizes (won't have more than 4 layers + // for 99.9% of the cases) + maLayers.reserve(4); + + // create initial background layer + maLayers.push_back( Layer::createBackgroundLayer() ); + + // init views + for( const auto& rView : mrViews ) + viewAdded( rView ); + } + + void LayerManager::activate() + { + mbActive = true; + maUpdateShapes.clear(); // update gets forced via area, or + // has happened outside already + + // clear all possibly pending update areas - content + // is there, already + for( const auto& pLayer : maLayers ) + pLayer->clearUpdateRanges(); + + updateShapeLayers( true/*bSlideBackgroundPainted*/ ); + } + + void LayerManager::deactivate() + { + // TODO(F3): This is mostly a hack. Problem is, there's + // currently no smart way of telling shapes "remove your + // sprites". Others, like MediaShapes, listen to + // start/stop animation events, which is too much overhead + // for all shapes, though. + + const bool bMoreThanOneLayer(maLayers.size() > 1); + if (mnActiveSprites || bMoreThanOneLayer) + { + // clear all viewlayers, dump everything but the + // background layer - this will also remove all shape + // sprites + for (auto& rShape : maAllShapes) + rShape.first->clearAllViewLayers(); + + for (auto& rShape : maAllShapes) + rShape.second.reset(); + + if (bMoreThanOneLayer) + maLayers.erase(maLayers.begin() + 1, maLayers.end()); + + mbLayerAssociationDirty = true; + } + + mbActive = false; + + // only background layer left + OSL_ASSERT( maLayers.size() == 1 && maLayers.front()->isBackgroundLayer() ); + } + + void LayerManager::viewAdded( const UnoViewSharedPtr& rView ) + { + // view must be member of mrViews container + OSL_ASSERT( std::find(mrViews.begin(), + mrViews.end(), + rView) != mrViews.end() ); + + // init view content + if( mbActive ) + rView->clearAll(); + + // add View to all registered shapes + manageViews( + [&rView]( const LayerSharedPtr& pLayer ) + { return pLayer->addView( rView ); }, + []( const ShapeSharedPtr& pShape, const ViewLayerSharedPtr& pLayer ) + { return pShape->addViewLayer( pLayer, true ); } ); + + // in case we haven't reached all layers from the + // maAllShapes, issue addView again for good measure + for( const auto& pLayer : maLayers ) + pLayer->addView( rView ); + } + + void LayerManager::viewRemoved( const UnoViewSharedPtr& rView ) + { + // view must not be member of mrViews container anymore + OSL_ASSERT( std::find(mrViews.begin(), + mrViews.end(), + rView) == mrViews.end() ); + + // remove View from all registered shapes + manageViews( + [&rView]( const LayerSharedPtr& pLayer ) + { return pLayer->removeView( rView ); }, + []( const ShapeSharedPtr& pShape, const ViewLayerSharedPtr& pLayer ) + { return pShape->removeViewLayer( pLayer ); } ); + + // in case we haven't reached all layers from the + // maAllShapes, issue removeView again for good measure + for( const auto& pLayer : maLayers ) + pLayer->removeView( rView ); + } + + void LayerManager::viewChanged( const UnoViewSharedPtr& rView ) + { + // view must be member of mrViews container + OSL_ASSERT( std::find(mrViews.begin(), + mrViews.end(), + rView) != mrViews.end() ); + + // TODO(P2): selectively update only changed view + viewsChanged(); + } + + void LayerManager::viewsChanged() + { + if( !mbActive ) + return; + + // clear view area + for( const auto& pView : mrViews ) + pView->clearAll(); + + // TODO(F3): resize and repaint all layers + + // render all shapes + for( const auto& rShape : maAllShapes ) + rShape.first->render(); + } + + void LayerManager::addShape( const ShapeSharedPtr& rShape ) + { + OSL_ASSERT( !maLayers.empty() ); // always at least background layer + ENSURE_OR_THROW( rShape, "LayerManager::addShape(): invalid Shape" ); + + // add shape to XShape hash map + if( !maXShapeHash.emplace(rShape->getXShape(), + rShape).second ) + { + // entry already present, nothing to do + return; + } + + // add shape to appropriate layer + implAddShape( rShape ); + } + + void LayerManager::putShape2BackgroundLayer( LayerShapeMap::value_type& rShapeEntry ) + { + LayerSharedPtr& rBgLayer( maLayers.front() ); + rBgLayer->setShapeViews(rShapeEntry.first); + rShapeEntry.second = rBgLayer; + } + + void LayerManager::implAddShape( const ShapeSharedPtr& rShape ) + { + OSL_ASSERT( !maLayers.empty() ); // always at least background layer + ENSURE_OR_THROW( rShape, "LayerManager::implAddShape(): invalid Shape" ); + + LayerShapeMap::value_type aValue (rShape, LayerWeakPtr()); + + OSL_ASSERT( maAllShapes.find(rShape) == maAllShapes.end() ); // shape must not be added already + mbLayerAssociationDirty = true; + + if( mbDisableAnimationZOrder ) + putShape2BackgroundLayer( + *maAllShapes.insert(aValue).first ); + else + maAllShapes.insert(aValue); + + // update shape, it's just added and not yet painted + if( rShape->isVisible() ) + notifyShapeUpdate( rShape ); + } + + bool LayerManager::removeShape( const ShapeSharedPtr& rShape ) + { + // remove shape from XShape hash map + if( maXShapeHash.erase( rShape->getXShape() ) == 0 ) + return false; // shape not in map + + OSL_ASSERT( maAllShapes.find(rShape) != maAllShapes.end() ); + + implRemoveShape( rShape ); + + return true; + } + + void LayerManager::implRemoveShape( const ShapeSharedPtr& rShape ) + { + OSL_ASSERT( !maLayers.empty() ); // always at least background layer + ENSURE_OR_THROW( rShape, "LayerManager::implRemoveShape(): invalid Shape" ); + + const LayerShapeMap::iterator aShapeEntry( maAllShapes.find(rShape) ); + + if( aShapeEntry == maAllShapes.end() ) + return; + + const bool bShapeUpdateNotified = maUpdateShapes.erase( rShape ) != 0; + + // Enter shape area to the update area, but only if shape + // is visible and not in sprite mode (otherwise, updating + // the area doesn't do actual harm, but costs time) + // Actually, also add it if it was listed in + // maUpdateShapes (might have just gone invisible). + if( bShapeUpdateNotified || + (rShape->isVisible() && + !rShape->isBackgroundDetached()) ) + { + LayerSharedPtr pLayer = aShapeEntry->second.lock(); + if( pLayer ) + { + // store area early, once the shape is removed from + // the layers, it no longer has any view references + pLayer->addUpdateRange( rShape->getUpdateArea() ); + } + } + + rShape->clearAllViewLayers(); + maAllShapes.erase( aShapeEntry ); + + mbLayerAssociationDirty = true; + } + + ShapeSharedPtr LayerManager::lookupShape( const uno::Reference< drawing::XShape >& xShape ) const + { + ENSURE_OR_THROW( xShape.is(), "LayerManager::lookupShape(): invalid Shape" ); + + const XShapeToShapeMap::const_iterator aIter( maXShapeHash.find( xShape )); + if( aIter == maXShapeHash.end() ) + return ShapeSharedPtr(); // not found + + // found, return data part of entry pair. + return aIter->second; + } + + AttributableShapeSharedPtr LayerManager::getSubsetShape( const AttributableShapeSharedPtr& rOrigShape, + const DocTreeNode& rTreeNode ) + { + OSL_ASSERT( !maLayers.empty() ); // always at least background layer + + AttributableShapeSharedPtr pSubset; + + // shape already added? + if( rOrigShape->createSubset( pSubset, + rTreeNode ) ) + { + OSL_ENSURE( pSubset, "LayerManager::getSubsetShape(): failed to create subset" ); + + // don't add to shape hash, we're dupes to the + // original XShape anyway - all subset shapes return + // the same XShape as the original one. + + // add shape to corresponding layer + implAddShape( pSubset ); + + // update original shape, it now shows less content + // (the subset is removed from its displayed + // output). Subset shape is updated within + // implAddShape(). + if( rOrigShape->isVisible() ) + notifyShapeUpdate( rOrigShape ); + } + + return pSubset; + } + + const XShapeToShapeMap& LayerManager::getXShapeToShapeMap() const + { + return maXShapeHash; + } + + void LayerManager::revokeSubset( const AttributableShapeSharedPtr& rOrigShape, + const AttributableShapeSharedPtr& rSubsetShape ) + { + OSL_ASSERT( !maLayers.empty() ); // always at least background layer + + if( rOrigShape->revokeSubset( rSubsetShape ) ) + { + OSL_ASSERT( maAllShapes.find(rSubsetShape) != maAllShapes.end() ); + + implRemoveShape( rSubsetShape ); + + // update original shape, it now shows more content + // (the subset is added back to its displayed output) + if( rOrigShape->isVisible() ) + notifyShapeUpdate( rOrigShape ); + } + } + + void LayerManager::enterAnimationMode( const AnimatableShapeSharedPtr& rShape ) + { + OSL_ASSERT( !maLayers.empty() ); // always at least background layer + ENSURE_OR_THROW( rShape, "LayerManager::enterAnimationMode(): invalid Shape" ); + + const bool bPrevAnimState( rShape->isBackgroundDetached() ); + + rShape->enterAnimationMode(); + + // if this call _really_ enabled the animation mode at + // rShape, insert it to our enter animation queue, to + // perform the necessary layer reorg lazily on + // LayerManager::update()/render(). + if( bPrevAnimState != rShape->isBackgroundDetached() ) + { + ++mnActiveSprites; + mbLayerAssociationDirty = true; + + // area needs update (shape is removed from normal + // slide, and now rendered as an autonomous + // sprite). store in update set + if( rShape->isVisible() ) + addUpdateArea( rShape ); + } + + // TODO(P1): this can lead to potential wasted effort, if + // a shape gets toggled animated/unanimated a few times + // between two frames, returning to the original state. + } + + void LayerManager::leaveAnimationMode( const AnimatableShapeSharedPtr& rShape ) + { + ENSURE_OR_THROW( !maLayers.empty(), "LayerManager::leaveAnimationMode(): no layers" ); + ENSURE_OR_THROW( rShape, "LayerManager::leaveAnimationMode(): invalid Shape" ); + + const bool bPrevAnimState( rShape->isBackgroundDetached() ); + + rShape->leaveAnimationMode(); + + // if this call _really_ ended the animation mode at + // rShape, insert it to our leave animation queue, to + // perform the necessary layer reorg lazily on + // LayerManager::update()/render(). + if( bPrevAnimState != rShape->isBackgroundDetached() ) + { + --mnActiveSprites; + mbLayerAssociationDirty = true; + + // shape needs update, no previous rendering, fast + // update possible. + if( rShape->isVisible() ) + notifyShapeUpdate( rShape ); + } + + // TODO(P1): this can lead to potential wasted effort, if + // a shape gets toggled animated/unanimated a few times + // between two frames, returning to the original state. + } + + void LayerManager::notifyShapeUpdate( const ShapeSharedPtr& rShape ) + { + if( !mbActive || mrViews.empty() ) + return; + + // hidden sprite-shape needs render() call still, to hide sprite + if( rShape->isVisible() || rShape->isBackgroundDetached() ) + maUpdateShapes.insert( rShape ); + else + addUpdateArea( rShape ); + } + + bool LayerManager::isUpdatePending() const + { + if( !mbActive ) + return false; + + if( mbLayerAssociationDirty || !maUpdateShapes.empty() ) + return true; + + return std::any_of( maLayers.begin(), + maLayers.end(), + std::mem_fn(&Layer::isUpdatePending) ); + } + + bool LayerManager::updateSprites() + { + bool bRet(true); + + // send update() calls to every shape in the + // maUpdateShapes set, which is _animated_ (i.e. a + // sprite). + for( const auto& pShape : maUpdateShapes ) + { + if( pShape->isBackgroundDetached() ) + { + // can update shape directly, without + // affecting layer content (shape is + // currently displayed in a sprite) + if( !pShape->update() ) + bRet = false; // delay error exit + } + else + { + // TODO(P2): addUpdateArea() involves log(n) + // search for shape layer. Have a frequent + // shape/layer association cache, or ptr back to + // layer at the shape? + + // cannot update shape directly, it's not + // animated and update() calls will prolly + // overwrite other page content. + addUpdateArea( pShape ); + } + } + + maUpdateShapes.clear(); + + return bRet; + } + + bool LayerManager::update() + { + bool bRet = true; + + if( !mbActive ) + return bRet; + + // going to render - better flush any pending layer reorg + // now + updateShapeLayers(false); + + // all sprites + bRet = updateSprites(); + + // any non-sprite update areas left? + if( std::none_of( maLayers.begin(), + maLayers.end(), + std::mem_fn( &Layer::isUpdatePending ) ) ) + return bRet; // nope, done. + + // update each shape on each layer, that has + // isUpdatePending() + bool bIsCurrLayerUpdating(false); + Layer::EndUpdater aEndUpdater; + LayerSharedPtr pCurrLayer; + for( const auto& rShape : maAllShapes ) + { + LayerSharedPtr pLayer = rShape.second.lock(); + if( pLayer != pCurrLayer ) + { + pCurrLayer = pLayer; + bIsCurrLayerUpdating = pCurrLayer->isUpdatePending(); + + if( bIsCurrLayerUpdating ) + aEndUpdater = pCurrLayer->beginUpdate(); + } + + if( bIsCurrLayerUpdating && + !rShape.first->isBackgroundDetached() && + pCurrLayer->isInsideUpdateArea(rShape.first) ) + { + if( !rShape.first->render() ) + bRet = false; + } + } + + return bRet; + } + + namespace + { + /** Little wrapper around a Canvas, to render one-shot + into a canvas + */ + class DummyLayer : public ViewLayer + { + public: + explicit DummyLayer( const ::cppcanvas::CanvasSharedPtr& rCanvas ) : + mpCanvas( rCanvas ) + { + } + + virtual bool isOnView(ViewSharedPtr const& /*rView*/) const override + { + return true; // visible on all views + } + + virtual ::cppcanvas::CanvasSharedPtr getCanvas() const override + { + return mpCanvas; + } + + virtual void clear() const override + { + // NOOP + } + + virtual void clearAll() const override + { + // NOOP + } + + virtual ::cppcanvas::CustomSpriteSharedPtr createSprite( const ::basegfx::B2DSize& /*rSpriteSizePixel*/, + double /*nSpritePrio*/ ) const override + { + ENSURE_OR_THROW( false, + "DummyLayer::createSprite(): This method is not supposed to be called!" ); + return ::cppcanvas::CustomSpriteSharedPtr(); + } + + virtual void setPriority( const basegfx::B1DRange& /*rRange*/ ) override + { + OSL_FAIL( "BitmapView::setPriority(): This method is not supposed to be called!" ); + } + + virtual css::geometry::IntegerSize2D getTranslationOffset() const override + { + return geometry::IntegerSize2D(0,0); + } + + virtual ::basegfx::B2DHomMatrix getTransformation() const override + { + return mpCanvas->getTransformation(); + } + + virtual ::basegfx::B2DHomMatrix getSpriteTransformation() const override + { + OSL_FAIL( "BitmapView::getSpriteTransformation(): This method is not supposed to be called!" ); + return ::basegfx::B2DHomMatrix(); + } + + virtual void setClip( const ::basegfx::B2DPolyPolygon& /*rClip*/ ) override + { + OSL_FAIL( "BitmapView::setClip(): This method is not supposed to be called!" ); + } + + virtual bool resize( const ::basegfx::B2DRange& /*rArea*/ ) override + { + OSL_FAIL( "BitmapView::resize(): This method is not supposed to be called!" ); + return false; + } + + private: + ::cppcanvas::CanvasSharedPtr mpCanvas; + }; + } + + bool LayerManager::renderTo( const ::cppcanvas::CanvasSharedPtr& rTargetCanvas ) const + { + bool bRet( true ); + ViewLayerSharedPtr pTmpLayer = std::make_shared<DummyLayer>( rTargetCanvas ); + + for( const auto& rShape : maAllShapes ) + { + try + { + // forward to all shape's addViewLayer method (which + // we request to render the Shape on the new + // ViewLayer. Since we add the shapes in the + // maShapeSet order (which is also the render order), + // this is equivalent to a subsequent render() call) + rShape.first->addViewLayer( pTmpLayer, + true ); + + // and remove again, this is only temporary + rShape.first->removeViewLayer( pTmpLayer ); + } + catch( uno::Exception& ) + { + // TODO(E1): Might be superfluous. Nowadays, + // addViewLayer swallows all errors, anyway. + TOOLS_WARN_EXCEPTION( "slideshow", "" ); + // at least one shape could not be rendered + bRet = false; + } + } + + return bRet; + } + + void LayerManager::addUpdateArea( ShapeSharedPtr const& rShape ) + { + OSL_ASSERT( !maLayers.empty() ); // always at least background layer + ENSURE_OR_THROW( rShape, "LayerManager::addUpdateArea(): invalid Shape" ); + + const LayerShapeMap::const_iterator aShapeEntry( maAllShapes.find(rShape) ); + + if( aShapeEntry == maAllShapes.end() ) + return; + + LayerSharedPtr pLayer = aShapeEntry->second.lock(); + if( pLayer ) + pLayer->addUpdateRange( rShape->getUpdateArea() ); + } + + void LayerManager::commitLayerChanges( std::size_t nCurrLayerIndex, + LayerShapeMap::const_iterator aFirstLayerShape, + const LayerShapeMap::const_iterator& aEndLayerShapes ) + { + const bool bLayerExists( maLayers.size() > nCurrLayerIndex ); + if( !bLayerExists ) + return; + + const LayerSharedPtr& rLayer( maLayers.at(nCurrLayerIndex) ); + const bool bLayerResized( rLayer->commitBounds() ); + rLayer->setPriority( basegfx::B1DRange(nCurrLayerIndex, + nCurrLayerIndex+1) ); + + if( !bLayerResized ) + return; + + // need to re-render whole layer - start from + // clean state + rLayer->clearContent(); + + // render and remove from update set + while( aFirstLayerShape != aEndLayerShapes ) + { + maUpdateShapes.erase(aFirstLayerShape->first); + aFirstLayerShape->first->render(); + ++aFirstLayerShape; + } + } + + LayerSharedPtr LayerManager::createForegroundLayer() const + { + OSL_ASSERT( mbActive ); + + LayerSharedPtr pLayer( Layer::createLayer() ); + + // create ViewLayers for all registered views, and add to + // newly created layer. + for( const auto& rView : mrViews ) + pLayer->addView( rView ); + + return pLayer; + } + + void LayerManager::updateShapeLayers( bool bBackgroundLayerPainted ) + { + OSL_ASSERT( !maLayers.empty() ); // always at least background layer + OSL_ASSERT( mbActive ); + + // do we need to process shapes? + if( !mbLayerAssociationDirty ) + return; + + if( mbDisableAnimationZOrder ) + { + // layer setup happened elsewhere, is only bg layer + // anyway. + mbLayerAssociationDirty = false; + return; + } + + // scan through maAllShapes, and determine shape animation + // discontinuities: when a shape that has + // isBackgroundDetached() return false follows a shape + // with isBackgroundDetached() true, the former and all + // following ones must be moved into an own layer. + + // to avoid tons of temporaries, create weak_ptr to Layers + // beforehand + std::vector< LayerWeakPtr > aWeakLayers(maLayers.begin(),maLayers.end()); + + std::size_t nCurrLayerIndex(0); + bool bIsBackgroundLayer(true); + bool bLastWasBackgroundDetached(false); // last shape sprite state + LayerShapeMap::iterator aCurrShapeEntry( maAllShapes.begin() ); + LayerShapeMap::iterator aCurrLayerFirstShapeEntry( maAllShapes.begin() ); + const LayerShapeMap::iterator aEndShapeEntry ( maAllShapes.end() ); + while( aCurrShapeEntry != aEndShapeEntry ) + { + const ShapeSharedPtr pCurrShape( aCurrShapeEntry->first ); + const bool bThisIsBackgroundDetached( + pCurrShape->isBackgroundDetached() ); + + if( bLastWasBackgroundDetached && + !bThisIsBackgroundDetached ) + { + // discontinuity found - current shape needs to + // get into a new layer + + + // commit changes to previous layer + commitLayerChanges(nCurrLayerIndex, + aCurrLayerFirstShapeEntry, + aCurrShapeEntry); + aCurrLayerFirstShapeEntry=aCurrShapeEntry; + ++nCurrLayerIndex; + bIsBackgroundLayer = false; + + if( aWeakLayers.size() <= nCurrLayerIndex || + notEqual(aWeakLayers.at(nCurrLayerIndex), aCurrShapeEntry->second) ) + { + // no more layers left, or shape was not + // member of this layer - create a new one + maLayers.insert( maLayers.begin()+nCurrLayerIndex, + createForegroundLayer() ); + aWeakLayers.insert( aWeakLayers.begin()+nCurrLayerIndex, + maLayers[nCurrLayerIndex] ); + } + } + + OSL_ASSERT( maLayers.size() == aWeakLayers.size() ); + + // note: using indices here, since vector::insert + // above invalidates iterators + LayerSharedPtr& rCurrLayer( maLayers.at(nCurrLayerIndex) ); + LayerWeakPtr& rCurrWeakLayer( aWeakLayers.at(nCurrLayerIndex) ); + if( notEqual(rCurrWeakLayer, aCurrShapeEntry->second) ) + { + // mismatch: shape is not contained in current + // layer - move shape to that layer, then. + maLayers.at(nCurrLayerIndex)->setShapeViews( + pCurrShape ); + + // layer got new shape(s), need full repaint, if + // non-sprite shape + if( !bThisIsBackgroundDetached && pCurrShape->isVisible() ) + { + LayerSharedPtr pOldLayer( aCurrShapeEntry->second.lock() ); + if( pOldLayer ) + { + // old layer still valid? then we need to + // repaint former shape area + pOldLayer->addUpdateRange( + pCurrShape->getUpdateArea() ); + } + + // render on new layer (only if not + // explicitly disabled) + if( !(bBackgroundLayerPainted && bIsBackgroundLayer) ) + maUpdateShapes.insert( pCurrShape ); + } + + aCurrShapeEntry->second = rCurrWeakLayer; + } + + // update layerbounds regardless of the fact that the + // shape might be contained in said layer + // already. updateBounds() is dumb and needs to + // collect all shape bounds. + // of course, no need to expand layer bounds for + // shapes that reside in sprites themselves. + if( !bThisIsBackgroundDetached && !bIsBackgroundLayer ) + rCurrLayer->updateBounds( pCurrShape ); + + bLastWasBackgroundDetached = bThisIsBackgroundDetached; + ++aCurrShapeEntry; + } + + // commit very last layer data + commitLayerChanges(nCurrLayerIndex, + aCurrLayerFirstShapeEntry, + aCurrShapeEntry); + + // any layers left? Bin them! + if( maLayers.size() > nCurrLayerIndex+1 ) + maLayers.erase(maLayers.begin()+nCurrLayerIndex+1, + maLayers.end()); + + mbLayerAssociationDirty = false; + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/layermanager.hxx b/slideshow/source/engine/slide/layermanager.hxx new file mode 100644 index 000000000..1969f0ccc --- /dev/null +++ b/slideshow/source/engine/slide/layermanager.hxx @@ -0,0 +1,363 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_LAYERMANAGER_HXX +#define INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_LAYERMANAGER_HXX + +#include <unoviewcontainer.hxx> +#include <attributableshape.hxx> +#include "layer.hxx" +#include <tools.hxx> + +#include <memory> +#include <map> +#include <unordered_map> +#include <vector> + +namespace basegfx { + class B2DRange; +} + +namespace slideshow::internal + { + /** A hash map which maps the XShape to the corresponding Shape object. + + Provides quicker lookup than ShapeSet for simple mappings + */ + typedef std::unordered_map< + css::uno::Reference< css::drawing::XShape >, + ShapeSharedPtr, + hash< css::uno::Reference< css::drawing::XShape > > + > XShapeToShapeMap; + + /* Definition of Layermanager class */ + + /** This class manages all of a slide's layers (and shapes) + + Since layer content changes when animations start or end, + the layer manager keeps track of this and also handles + starting/stopping of Shape animations. Note that none of + the methods actually perform a screen update, this is + always delayed until the ActivitiesQueue explicitly + performs it. + + @see Layer + @see Shape + */ + class LayerManager + { + public: + /** Create a new layer manager for the given page bounds + + @param rViews + Views currently registered + + @param bDisableAnimationZOrder + When true, all sprite animations run in the + foreground. That is, no extra layers are created, and + the slideshow runs potentially faster. + */ + LayerManager( const UnoViewContainer& rViews, + bool bDisableAnimationZOrder ); + + /// Forbid copy construction + LayerManager(const LayerManager&) = delete; + + /// Forbid copy assignment + LayerManager& operator=(const LayerManager&) = delete; + + /** Activate the LayerManager + + This method activates the LayerManager. Prior to + activation, this instance will be passive, i.e. won't + render anything to any view. + */ + void activate(); + + /** Deactivate the LayerManager + + This method deactivates the LayerManager. After + deactivation, this instance will be passive, + i.e. don't render anything to any view. Furthermore, + if there's currently more than one Layer active, this + method also removes all but one. + */ + void deactivate(); + + // From ViewEventHandler, forwarded by SlideImpl + /// Notify new view added to UnoViewContainer + void viewAdded( const UnoViewSharedPtr& rView ); + /// Notify view removed from UnoViewContainer + void viewRemoved( const UnoViewSharedPtr& rView ); + void viewChanged( const UnoViewSharedPtr& rView ); + void viewsChanged(); + + /** Add the shape to this object + + This method adds a shape to the page. + */ + void addShape( const ShapeSharedPtr& rShape ); + + /** Remove shape from this object + + This method removes a shape from the shape. + */ + bool removeShape( const ShapeSharedPtr& rShape ); + + /** Lookup a Shape from an XShape model object + + This method looks up the internal shape map for one + representing the given XShape. + + @param xShape + The XShape object, for which the representing Shape + should be looked up. + */ + ShapeSharedPtr lookupShape( const css::uno::Reference< css::drawing::XShape >& xShape ) const; + + /** Query a subset of the given original shape + + This method queries a new (but not necessarily unique) + shape, which displays only the given subset of the + original one. + */ + AttributableShapeSharedPtr getSubsetShape( const AttributableShapeSharedPtr& rOrigShape, + const DocTreeNode& rTreeNode ); + + /** Get a map that maps all Shapes with their XShape reference as the key + * + * @return an unordered map that contains all shapes in the + * current page with their XShape reference as the key + */ + const XShapeToShapeMap& getXShapeToShapeMap() const; + + /** Revoke a previously queried subset shape. + + With this method, a previously requested subset shape + is revoked again. If the last client revokes a given + subset, it will cease to be displayed, and the + original shape will again show the subset data. + + @param rOrigShape + The shape the subset was created from + + @param rSubsetShape + The subset created from rOrigShape + */ + void revokeSubset( const AttributableShapeSharedPtr& rOrigShape, + const AttributableShapeSharedPtr& rSubsetShape ); + + /** Notify the LayerManager that the given Shape starts an + animation now. + + This method enters animation mode for the Shape on all + registered views. + */ + void enterAnimationMode( const AnimatableShapeSharedPtr& rShape ); + + /** Notify the LayerManager that the given Shape is no + longer animated. + + This methods ends animation mode for the given Shape + on all registered views. + */ + void leaveAnimationMode( const AnimatableShapeSharedPtr& rShape ); + + /** Notify that a shape needs an update + + This method notifies the layer manager that a shape + update is necessary. This is useful if, during + animation playback, changes occur to shapes which make + an update necessary on an update() call. Otherwise, + update() will not render anything, which is not + triggered by calling one of the other LayerManager + methods. + + @param rShape + Shape which needs an update + */ + void notifyShapeUpdate( const ShapeSharedPtr& rShape); + + /** Check whether any update operations are pending. + + @return true, if this LayerManager has any updates + pending, i.e. needs to repaint something for the next + frame. + */ + bool isUpdatePending() const; + + /** Update the content + + This method updates the content on all layers on all + registered views. It does not issues a + View::updateScreen() call on registered views. Please + note that this method only takes into account changes + to shapes induced directly by calling methods of the + LayerManager. If a shape needs an update, because of + some external event unknown to the LayerManager (most + notably running animations), you have to notify the + LayerManager via notifyShapeUpdate(). + + @see LayerManager::updateScreen() + + @return whether the update finished successfully. + */ + bool update(); + + /** Render the content to given canvas + + This is a one-shot operation, which simply draws all + shapes onto the given canvas, without any caching or + other fuzz. Don't use that for repeated output onto + the same canvas, the View concept is more optimal + then. + + @param rTargetCanvas + Target canvas to output onto. + */ + bool renderTo( const ::cppcanvas::CanvasSharedPtr& rTargetCanvas ) const; + + private: + + class ShapeComparator + { + public: + bool operator() (const ShapeSharedPtr& rpS1, const ShapeSharedPtr& rpS2 ) const + { + return Shape::lessThanShape::compare(rpS1.get(), rpS2.get()); + } + }; + /** Set of all shapes + */ + private: + typedef ::std::map< ShapeSharedPtr, LayerWeakPtr, ShapeComparator > LayerShapeMap; + + + /// Adds shape area to containing layer's damage area + void addUpdateArea( ShapeSharedPtr const& rShape ); + + LayerSharedPtr createForegroundLayer() const; + + /** Push changes from updateShapeLayerAssociations() to current layer + + Factored-out method that resizes layer, if necessary, + assigns correct layer priority, and repaints contained shapes. + + @param nCurrLayerIndex + Index of current layer in maLayers + + @param aFirstLayerShape + Valid iterator out of maAllShapes, denoting the first + shape from nCurrLayerIndex + + @param aEndLayerShapes + Valid iterator or end iterator out of maAllShapes, + denoting one-behind-the-last shape of nCurrLayerIndex + */ + void commitLayerChanges( std::size_t nCurrLayerIndex, + LayerShapeMap::const_iterator aFirstLayerShape, + const LayerShapeMap::const_iterator& aEndLayerShapes ); + + /** Init Shape layers with background layer. + */ + void putShape2BackgroundLayer( LayerShapeMap::value_type& rShapeEntry ); + + /** Commits any pending layer reorg, due to shapes either + entering or leaving animation mode + + @param bBackgroundLayerPainted + When true, LayerManager does not render anything on + the background layer. Use this, if background has been + updated by other means (e.g. slide transition) + */ + void updateShapeLayers( bool bBackgroundLayerPainted ); + + /** Common stuff when adding a shape + */ + void implAddShape( const ShapeSharedPtr& rShape ); + + /** Common stuff when removing a shape + */ + void implRemoveShape( const ShapeSharedPtr& rShape ); + + /** Add or remove views + + Sharing duplicate code from viewAdded and viewRemoved + method. The only point of variation at those places + are removal vs. adding. + */ + template<typename LayerFunc, + typename ShapeFunc> void manageViews( LayerFunc layerFunc, + ShapeFunc shapeFunc ); + + bool updateSprites(); + + /// Registered views + const UnoViewContainer& mrViews; + + /// All layers of this object. Vector owns the layers + ::std::vector< LayerSharedPtr > + maLayers; + + /** Contains all shapes with their XShape reference as the key + */ + XShapeToShapeMap maXShapeHash; + + /** Set of shapes this LayerManager own + + Contains the same set of shapes as XShapeHash, but is + sorted in z order, for painting and layer + association. Set entries are enriched with two flags + for buffering animation enable/disable changes, and + shape update requests. + */ + LayerShapeMap maAllShapes; + + /** Set of shapes that have requested an update + + When a shape is member of this set, its maShapes entry + has bNeedsUpdate set to true. We maintain this + redundant information for faster update processing. + */ + ::std::set< ShapeSharedPtr > + maUpdateShapes; + + /// Number of shape sprites currently active on this LayerManager + sal_Int32 mnActiveSprites; + + /// sal_True, if shapes might need to move to different layer + bool mbLayerAssociationDirty; + + /// sal_False when deactivated + bool mbActive; + + /** When true, all sprite animations run in the foreground. That + is, no extra layers are created, and the slideshow runs + potentially faster. + */ + bool mbDisableAnimationZOrder; + }; + + typedef ::std::shared_ptr< LayerManager > LayerManagerSharedPtr; + +} + +#endif // INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_LAYERMANAGER_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/shapemanagerimpl.cxx b/slideshow/source/engine/slide/shapemanagerimpl.cxx new file mode 100644 index 000000000..1c730970e --- /dev/null +++ b/slideshow/source/engine/slide/shapemanagerimpl.cxx @@ -0,0 +1,426 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <comphelper/processfactory.hxx> +#include <tools/diagnose_ex.h> +#include <com/sun/star/awt/MouseButton.hpp> +#include <com/sun/star/awt/SystemPointer.hpp> +#include <com/sun/star/system/SystemShellExecute.hpp> +#include <com/sun/star/system/SystemShellExecuteFlags.hpp> +#include <com/sun/star/system/XSystemShellExecute.hpp> +#include <svx/ImageMapInfo.hxx> + +#include "shapemanagerimpl.hxx" + +#include <functional> + +using namespace css; +using namespace css::uno; +using namespace css::drawing; +using namespace css::system; + +namespace slideshow::internal { + +ShapeManagerImpl::ShapeManagerImpl( EventMultiplexer& rMultiplexer, + LayerManagerSharedPtr const& rLayerManager, + CursorManager& rCursorManager, + const ShapeEventListenerMap& rGlobalListenersMap, + const ShapeCursorMap& rGlobalCursorMap, + const Reference<XDrawPage>& xDrawPage ): + mrMultiplexer(rMultiplexer), + mpLayerManager(rLayerManager), + mrCursorManager(rCursorManager), + mrGlobalListenersMap(rGlobalListenersMap), + mrGlobalCursorMap(rGlobalCursorMap), + maShapeListenerMap(), + maShapeCursorMap(), + maHyperlinkShapes(), + mbEnabled(false), + mxDrawPage(xDrawPage) +{ +} + +void ShapeManagerImpl::activate() +{ + if( mbEnabled ) + return; + + mbEnabled = true; + + // register this handler on EventMultiplexer. + // Higher prio (overrides other engine handlers) + mrMultiplexer.addMouseMoveHandler( shared_from_this(), 2.0 ); + mrMultiplexer.addClickHandler( shared_from_this(), 2.0 ); + mrMultiplexer.addShapeListenerHandler( shared_from_this() ); + + // clone listener map + for( const auto& rListener : mrGlobalListenersMap ) + listenerAdded( rListener.first ); + + // clone cursor map + for( const auto& rListener : mrGlobalCursorMap ) + cursorChanged( rListener.first, rListener.second ); + + if( mpLayerManager ) + mpLayerManager->activate(); +} + +void ShapeManagerImpl::deactivate() +{ + if( !mbEnabled ) + return; + + mbEnabled = false; + + if( mpLayerManager ) + mpLayerManager->deactivate(); + + maShapeListenerMap.clear(); + maShapeCursorMap.clear(); + + mrMultiplexer.removeShapeListenerHandler( shared_from_this() ); + mrMultiplexer.removeMouseMoveHandler( shared_from_this() ); + mrMultiplexer.removeClickHandler( shared_from_this() ); +} + +void ShapeManagerImpl::dispose() +{ + // remove listeners (EventMultiplexer holds shared_ptr on us) + deactivate(); + + maHyperlinkShapes.clear(); + maShapeCursorMap.clear(); + maShapeListenerMap.clear(); + mpLayerManager.reset(); +} + +bool ShapeManagerImpl::handleMousePressed( awt::MouseEvent const& ) +{ + // not used here + return false; // did not handle the event +} + +bool ShapeManagerImpl::handleMouseReleased( awt::MouseEvent const& e ) +{ + if( !mbEnabled || e.Buttons != awt::MouseButton::LEFT) + return false; + + basegfx::B2DPoint const aPosition( e.X, e.Y ); + + // first check for hyperlinks, because these have + // highest prio: + OUString const hyperlink( checkForHyperlink(aPosition) ); + if( !hyperlink.isEmpty() ) + { + mrMultiplexer.notifyHyperlinkClicked(hyperlink); + return true; // event consumed + } + + // tdf#74045 Handle ImageMaps + OUString const imageMapLink(checkForImageMap(e)); + if (!imageMapLink.isEmpty()) + { + Reference<XSystemShellExecute> exec( + SystemShellExecute::create(comphelper::getProcessComponentContext())); + exec->execute(imageMapLink, OUString(), SystemShellExecuteFlags::URIS_ONLY); + + return true; + } + + // find matching shape (scan reversely, to coarsely match + // paint order) + auto aCurrBroadcaster = std::find_if(maShapeListenerMap.rbegin(), maShapeListenerMap.rend(), + [&aPosition](const ShapeToListenersMap::value_type& rBroadcaster) { + // TODO(F2): Get proper geometry polygon from the + // shape, to avoid having areas outside the shape + // react on the mouse + return rBroadcaster.first->getBounds().isInside( aPosition ) + && rBroadcaster.first->isVisible(); + }); + if (aCurrBroadcaster != maShapeListenerMap.rend()) + { + // shape hit, and shape is visible. Raise + // event. + + std::shared_ptr<comphelper::OInterfaceContainerHelper3<css::presentation::XShapeEventListener>> const & pCont = + aCurrBroadcaster->second; + uno::Reference<drawing::XShape> const xShape( + aCurrBroadcaster->first->getXShape() ); + + // DON'T do anything with /this/ after this point! + pCont->forEach( + [&xShape, &e]( const uno::Reference< presentation::XShapeEventListener >& rListener ) + { return rListener->click( xShape, e ); } ); + + return true; // handled this event + } + + return false; // did not handle this event +} + +bool ShapeManagerImpl::handleMouseDragged( const awt::MouseEvent& ) +{ + // not used here + return false; // did not handle the event +} + +bool ShapeManagerImpl::handleMouseMoved( const awt::MouseEvent& e ) +{ + if( !mbEnabled ) + return false; + + // find hit shape in map + const ::basegfx::B2DPoint aPosition( e.X, e.Y ); + sal_Int16 nNewCursor(-1); + + if( !checkForHyperlink(aPosition).isEmpty() || !checkForImageMap(e).isEmpty() ) + { + nNewCursor = awt::SystemPointer::REFHAND; + } + else + { + // find matching shape (scan reversely, to coarsely match + // paint order) + auto aCurrCursor = std::find_if(maShapeCursorMap.rbegin(), maShapeCursorMap.rend(), + [&aPosition](const ShapeToCursorMap::value_type& rCursor) { + // TODO(F2): Get proper geometry polygon from the + // shape, to avoid having areas outside the shape + // react on the mouse + return rCursor.first->getBounds().isInside( aPosition ) + && rCursor.first->isVisible(); + }); + if (aCurrCursor != maShapeCursorMap.rend()) + { + // shape found, and it's visible. set + // requested cursor to shape's + nNewCursor = aCurrCursor->second; + } + } + + if( nNewCursor == -1 ) + mrCursorManager.resetCursor(); + else + mrCursorManager.requestCursor( nNewCursor ); + + return false; // we don't /eat/ this event. Lower prio + // handler should see it, too. +} + +bool ShapeManagerImpl::update() +{ + if( mbEnabled && mpLayerManager ) + return mpLayerManager->update(); + + return false; +} + +bool ShapeManagerImpl::needsUpdate() const +{ + if( mbEnabled && mpLayerManager ) + return mpLayerManager->isUpdatePending(); + + return false; +} + +void ShapeManagerImpl::enterAnimationMode( const AnimatableShapeSharedPtr& rShape ) +{ + if( mbEnabled && mpLayerManager ) + mpLayerManager->enterAnimationMode(rShape); +} + +void ShapeManagerImpl::leaveAnimationMode( const AnimatableShapeSharedPtr& rShape ) +{ + if( mbEnabled && mpLayerManager ) + mpLayerManager->leaveAnimationMode(rShape); +} + +void ShapeManagerImpl::notifyShapeUpdate( const ShapeSharedPtr& rShape ) +{ + if( mbEnabled && mpLayerManager ) + mpLayerManager->notifyShapeUpdate(rShape); +} + +ShapeSharedPtr ShapeManagerImpl::lookupShape( uno::Reference< drawing::XShape > const & xShape ) const +{ + if( mpLayerManager ) + return mpLayerManager->lookupShape(xShape); + + return ShapeSharedPtr(); +} + +const XShapeToShapeMap& ShapeManagerImpl::getXShapeToShapeMap() const +{ + assert( mpLayerManager ); + return mpLayerManager->getXShapeToShapeMap(); +} + +void ShapeManagerImpl::addHyperlinkArea( const HyperlinkAreaSharedPtr& rArea ) +{ + maHyperlinkShapes.insert(rArea); +} + +AttributableShapeSharedPtr ShapeManagerImpl::getSubsetShape( const AttributableShapeSharedPtr& rOrigShape, + const DocTreeNode& rTreeNode ) +{ + if( mpLayerManager ) + return mpLayerManager->getSubsetShape(rOrigShape,rTreeNode); + + return AttributableShapeSharedPtr(); +} + +void ShapeManagerImpl::revokeSubset( const AttributableShapeSharedPtr& rOrigShape, + const AttributableShapeSharedPtr& rSubsetShape ) +{ + if( mpLayerManager ) + mpLayerManager->revokeSubset(rOrigShape,rSubsetShape); +} + +bool ShapeManagerImpl::listenerAdded( + const uno::Reference<drawing::XShape>& xShape ) +{ + ShapeEventListenerMap::const_iterator aIter = mrGlobalListenersMap.find( xShape ); + if( aIter == mrGlobalListenersMap.end() ) + { + ENSURE_OR_RETURN_FALSE(false, + "ShapeManagerImpl::listenerAdded(): global " + "shape listener map inconsistency!"); + } + + // is this one of our shapes? other shapes are ignored. + ShapeSharedPtr pShape( lookupShape(xShape) ); + if( pShape ) + { + maShapeListenerMap.emplace(pShape, aIter->second); + } + + return true; +} + +bool ShapeManagerImpl::listenerRemoved( const uno::Reference<drawing::XShape>& xShape ) +{ + // shape really erased from map? maybe there are other listeners + // for the same shape pending... + if( mrGlobalListenersMap.find(xShape) == mrGlobalListenersMap.end() ) + { + // is this one of our shapes? other shapes are ignored. + ShapeSharedPtr pShape( lookupShape(xShape) ); + if( pShape ) + maShapeListenerMap.erase(pShape); + } + + return true; +} + +void ShapeManagerImpl::cursorChanged( const uno::Reference<drawing::XShape>& xShape, + sal_Int16 nCursor ) +{ + ShapeSharedPtr pShape( lookupShape(xShape) ); + + // is this one of our shapes? other shapes are ignored. + if( !pShape ) + return; + + if( mrGlobalCursorMap.find(xShape) == mrGlobalCursorMap.end() ) + { + // erased from global map - erase locally, too + maShapeCursorMap.erase(pShape); + } + else + { + // included in global map - update local one + ShapeToCursorMap::iterator aIter; + if( (aIter = maShapeCursorMap.find(pShape)) + == maShapeCursorMap.end() ) + { + maShapeCursorMap.emplace(pShape, nCursor); + } + else + { + aIter->second = nCursor; + } + } +} + +OUString ShapeManagerImpl::checkForHyperlink( basegfx::B2DPoint const& hitPos ) const +{ + // find matching region (scan reversely, to coarsely match + // paint order): set is ordered by priority + AreaSet::const_reverse_iterator iPos( maHyperlinkShapes.rbegin() ); + AreaSet::const_reverse_iterator const iEnd( maHyperlinkShapes.rend() ); + for( ; iPos != iEnd; ++iPos ) + { + HyperlinkAreaSharedPtr const& pArea = *iPos; + + HyperlinkArea::HyperlinkRegions const linkRegions( + pArea->getHyperlinkRegions() ); + + for( std::size_t i = linkRegions.size(); i--; ) + { + basegfx::B2DRange const& region = linkRegions[i].first; + if( region.isInside(hitPos) ) + return linkRegions[i].second; + } + } + + return OUString(); +} + +OUString ShapeManagerImpl::checkForImageMap( awt::MouseEvent const& evt ) const +{ + for (sal_Int32 i = 0; i < mxDrawPage->getCount(); i++) + { + Reference<XShape> xShape(mxDrawPage->getByIndex(i), UNO_QUERY_THROW); + SdrObject* pObj = SdrObject::getSdrObjectFromXShape(xShape); + if (!pObj) + continue; + const IMapObject* pIMapObj = SvxIMapInfo::GetHitIMapObject(pObj, Point(evt.X, evt.Y)); + if (pIMapObj && !pIMapObj->GetURL().isEmpty()) + { + return pIMapObj->GetURL(); + } + } + return OUString(); +} + +void ShapeManagerImpl::addIntrinsicAnimationHandler( const IntrinsicAnimationEventHandlerSharedPtr& rHandler ) +{ + maIntrinsicAnimationEventHandlers.add( rHandler ); +} + +void ShapeManagerImpl::removeIntrinsicAnimationHandler( const IntrinsicAnimationEventHandlerSharedPtr& rHandler ) +{ + maIntrinsicAnimationEventHandlers.remove( rHandler ); +} + +void ShapeManagerImpl::notifyIntrinsicAnimationsEnabled() +{ + maIntrinsicAnimationEventHandlers.applyAll( + std::mem_fn(&IntrinsicAnimationEventHandler::enableAnimations)); +} + +void ShapeManagerImpl::notifyIntrinsicAnimationsDisabled() +{ + maIntrinsicAnimationEventHandlers.applyAll( + std::mem_fn(&IntrinsicAnimationEventHandler::disableAnimations)); +} + + +} // namespace slideshow::internal + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/shapemanagerimpl.hxx b/slideshow/source/engine/slide/shapemanagerimpl.hxx new file mode 100644 index 000000000..20bbe0340 --- /dev/null +++ b/slideshow/source/engine/slide/shapemanagerimpl.hxx @@ -0,0 +1,189 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ +#ifndef INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_SHAPEMANAGERIMPL_HXX +#define INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_SHAPEMANAGERIMPL_HXX + +#include <com/sun/star/drawing/XDrawPage.hpp> +#include <com/sun/star/uno/Reference.hxx> + +#include <comphelper/interfacecontainer3.hxx> +#include <shape.hxx> +#include <subsettableshapemanager.hxx> +#include <eventmultiplexer.hxx> +#include "layermanager.hxx" +#include <viewupdate.hxx> +#include <shapemaps.hxx> +#include <cursormanager.hxx> +#include <hyperlinkarea.hxx> +#include <listenercontainer.hxx> +#include <shapelistenereventhandler.hxx> +#include <mouseeventhandler.hxx> + +#include <set> +#include <map> +#include <memory> + +namespace slideshow::internal { + +/** Listener class for shape events + + This helper class registers itself on each view, and + broadcasts the XShapeEventListener events. The mouse motion + events are needed for setting the shape cursor. +*/ +class ShapeManagerImpl : public SubsettableShapeManager, + public ShapeListenerEventHandler, + public MouseEventHandler, + public ViewUpdate, + public std::enable_shared_from_this<ShapeManagerImpl> +{ +public: + /** Create a shape event broadcaster + + @param rEventMultiplexer + The slideshow-global event source, where this class + registers its event handlers. + */ + ShapeManagerImpl( EventMultiplexer& rMultiplexer, + LayerManagerSharedPtr const& rLayerManager, + CursorManager& rCursorManager, + const ShapeEventListenerMap& rGlobalListenersMap, + const ShapeCursorMap& rGlobalCursorMap, + const css::uno::Reference<css::drawing::XDrawPage>& xDrawPage); + + /// Forbid copy construction + ShapeManagerImpl(const ShapeManagerImpl&) = delete; + + /// Forbid copy assignment + ShapeManagerImpl& operator=(const ShapeManagerImpl&) = delete; + + /** Enables event listening. + + The initial slide content on the background layer + is already rendered (e.g. from a previous slide + transition). + */ + void activate(); + + /** Disables event listening. + */ + void deactivate(); + + // Disposable interface + + + virtual void dispose() override; + +private: + + // MouseEventHandler interface + + + virtual bool handleMousePressed( + css::awt::MouseEvent const& evt ) override; + virtual bool handleMouseReleased( + css::awt::MouseEvent const& evt ) override; + virtual bool handleMouseDragged( + css::awt::MouseEvent const& evt ) override; + virtual bool handleMouseMoved( + css::awt::MouseEvent const& evt ) override; + + + // ViewUpdate interface + + + virtual bool update() override; + virtual bool needsUpdate() const override; + + + // ShapeManager interface + + + virtual void enterAnimationMode( const AnimatableShapeSharedPtr& rShape ) override; + virtual void leaveAnimationMode( const AnimatableShapeSharedPtr& rShape ) override; + virtual void notifyShapeUpdate( const ShapeSharedPtr& rShape ) override; + virtual ShapeSharedPtr lookupShape( + css::uno::Reference< css::drawing::XShape > const & xShape ) const override; + virtual const XShapeToShapeMap& getXShapeToShapeMap() const override; + virtual void addHyperlinkArea( const HyperlinkAreaSharedPtr& rArea ) override; + + + // SubsettableShapeManager interface + + + virtual AttributableShapeSharedPtr getSubsetShape( + const AttributableShapeSharedPtr& rOrigShape, + const DocTreeNode& rTreeNode ) override; + virtual void revokeSubset( + const AttributableShapeSharedPtr& rOrigShape, + const AttributableShapeSharedPtr& rSubsetShape ) override; + + virtual void addIntrinsicAnimationHandler( + const IntrinsicAnimationEventHandlerSharedPtr& rHandler ) override; + virtual void removeIntrinsicAnimationHandler( + const IntrinsicAnimationEventHandlerSharedPtr& rHandler ) override; + virtual void notifyIntrinsicAnimationsEnabled() override; + virtual void notifyIntrinsicAnimationsDisabled() override; + + + // ShapeListenerEventHandler + + + virtual bool listenerAdded( const css::uno::Reference< css::drawing::XShape>& xShape ) override; + + virtual bool listenerRemoved( const css::uno::Reference< css::drawing::XShape>& xShape ) override; + + void cursorChanged( const css::uno::Reference< css::drawing::XShape>& xShape, + sal_Int16 nCursor ); + + + OUString checkForHyperlink( ::basegfx::B2DPoint const& hitPos )const; + OUString checkForImageMap( css::awt::MouseEvent const& evt ) const; + + + typedef std::map<ShapeSharedPtr, + std::shared_ptr< ::comphelper::OInterfaceContainerHelper3<css::presentation::XShapeEventListener> >, + Shape::lessThanShape> ShapeToListenersMap; + typedef std::map<ShapeSharedPtr, sal_Int16, + Shape::lessThanShape> ShapeToCursorMap; + typedef std::set<HyperlinkAreaSharedPtr, + HyperlinkArea::lessThanArea> AreaSet; + + typedef ThreadUnsafeListenerContainer< + IntrinsicAnimationEventHandlerSharedPtr, + std::vector<IntrinsicAnimationEventHandlerSharedPtr> > ImplIntrinsicAnimationEventHandlers; + + EventMultiplexer& mrMultiplexer; + LayerManagerSharedPtr mpLayerManager; + CursorManager& mrCursorManager; + const ShapeEventListenerMap& mrGlobalListenersMap; + const ShapeCursorMap& mrGlobalCursorMap; + ShapeToListenersMap maShapeListenerMap; + ShapeToCursorMap maShapeCursorMap; + AreaSet maHyperlinkShapes; + ImplIntrinsicAnimationEventHandlers maIntrinsicAnimationEventHandlers; + bool mbEnabled; + const css::uno::Reference<css::drawing::XDrawPage> mxDrawPage; +}; + +} // namespace presentation::internal + +#endif // INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_SHAPEMANAGERIMPL_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/slideanimations.cxx b/slideshow/source/engine/slide/slideanimations.cxx new file mode 100644 index 000000000..d433b7af1 --- /dev/null +++ b/slideshow/source/engine/slide/slideanimations.cxx @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <tools/diagnose_ex.h> + +#include "slideanimations.hxx" +#include <animationnodefactory.hxx> + +using namespace ::com::sun::star; + +namespace slideshow::internal +{ + SlideAnimations::SlideAnimations( const SlideShowContext& rContext, + const ::basegfx::B2DVector& rSlideSize ) : + maContext( rContext ), + maSlideSize( rSlideSize ), + mpRootNode() + { + ENSURE_OR_THROW( maContext.mpSubsettableShapeManager, + "SlideAnimations::SlideAnimations(): Invalid SlideShowContext" ); + } + + SlideAnimations::~SlideAnimations() COVERITY_NOEXCEPT_FALSE + { + if( !mpRootNode ) + return; + + SHOW_NODE_TREE( mpRootNode ); + + try + { + mpRootNode->dispose(); + } + catch (uno::Exception &) + { + TOOLS_WARN_EXCEPTION( "slideshow", "" ); + } + } + + bool SlideAnimations::importAnimations( const uno::Reference< animations::XAnimationNode >& xRootAnimationNode ) + { + mpRootNode = AnimationNodeFactory::createAnimationNode( + xRootAnimationNode, + maSlideSize, + maContext ); + + SHOW_NODE_TREE( mpRootNode ); + + return static_cast< bool >(mpRootNode); + } + + bool SlideAnimations::isAnimated() const + { + if( !mpRootNode ) + return false; // no animations there + + // query root node about pending animations + return mpRootNode->hasPendingAnimation(); + } + + bool SlideAnimations::start() + { + if( !mpRootNode ) + return false; // no animations there + + // init all nodes + if( !mpRootNode->init() ) + return false; + + // resolve root node + if( !mpRootNode->resolve() ) + return false; + + return true; + } + + void SlideAnimations::end() + { + if( !mpRootNode ) + return; // no animations there + + // end root node + mpRootNode->deactivate(); + mpRootNode->end(); + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/slideanimations.hxx b/slideshow/source/engine/slide/slideanimations.hxx new file mode 100644 index 000000000..3ebc1b21b --- /dev/null +++ b/slideshow/source/engine/slide/slideanimations.hxx @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_SLIDEANIMATIONS_HXX +#define INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_SLIDEANIMATIONS_HXX + +#include <com/sun/star/uno/Reference.hxx> +#include <basegfx/vector/b2dvector.hxx> + +#include <slideshowcontext.hxx> +#include <animationnode.hxx> + +namespace com::sun::star::animations { class XAnimationNode; } + + +/* Definition of SlideAnimations class */ + +namespace slideshow::internal + { + /** This class generates and manages all animations of a slide. + + Provided with the root animation node, this class imports + the effect information and builds the event tree for all + of the slide's animations. + */ + class SlideAnimations + { + public: + /** Create an animation generator. + + @param rContext + Slide show context, passing on common parameters + */ + SlideAnimations( const SlideShowContext& rContext, + const ::basegfx::B2DVector& rSlideSize ); + ~SlideAnimations() COVERITY_NOEXCEPT_FALSE; + + /** Import animations from a SMIL root animation node. + + This method might take some time, depending on the + complexity of the SMIL animation network to be + imported. + + @param xRootAnimationNode + Root animation node for the effects to be + generated. This is typically obtained from the + XDrawPage's XAnimationNodeSupplier. + + */ + bool importAnimations( const css::uno::Reference< css::animations::XAnimationNode >& xRootAnimationNode ); + + /** Check, whether imported animations actually contain + any effects. + + @return true, if there are actual animations in the + imported node tree. + */ + bool isAnimated() const; + + /** Start the animations. + + This method creates the network of events and + activities for all animations. The events and + activities are inserted into the constructor-provided + queues. These queues are not explicitly cleared, if + you rely on this object's effects to run without + interference, you should clear the queues by yourself. + + @return true, if all events have been successfully + created. + */ + bool start(); + + /** End all animations. + + This method force-ends all animations. If a slide end + event has been registered, that is fired, too. + */ + void end(); + + private: + SlideShowContext maContext; + const basegfx::B2DVector maSlideSize; + AnimationNodeSharedPtr mpRootNode; + }; + +} + +#endif // INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_SLIDEANIMATIONS_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/slideimpl.cxx b/slideshow/source/engine/slide/slideimpl.cxx new file mode 100644 index 000000000..033af8756 --- /dev/null +++ b/slideshow/source/engine/slide/slideimpl.cxx @@ -0,0 +1,1130 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <osl/diagnose.hxx> +#include <tools/diagnose_ex.h> +#include <cppcanvas/basegfxfactory.hxx> + +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/point/b2dpoint.hxx> + +#include <com/sun/star/awt/SystemPointer.hpp> +#include <com/sun/star/drawing/XMasterPageTarget.hpp> +#include <com/sun/star/beans/XPropertySet.hpp> +#include <com/sun/star/presentation/ParagraphTarget.hpp> +#include <com/sun/star/presentation/EffectNodeType.hpp> + +#include <slide.hxx> +#include <slideshowcontext.hxx> +#include "slideanimations.hxx" +#include <doctreenode.hxx> +#include <screenupdater.hxx> +#include <cursormanager.hxx> +#include <shapeimporter.hxx> +#include <slideshowexceptions.hxx> +#include <eventqueue.hxx> +#include <activitiesqueue.hxx> +#include "layermanager.hxx" +#include "shapemanagerimpl.hxx" +#include <usereventqueue.hxx> +#include "userpaintoverlay.hxx" +#include "targetpropertiescreator.hxx" +#include <tools.hxx> +#include <box2dtools.hxx> +#include <vcl/graphicfilter.hxx> +#include <svx/svdograf.hxx> + +using namespace ::com::sun::star; + + +namespace slideshow::internal +{ +namespace +{ + +class SlideImpl : public Slide, + public CursorManager, + public ViewEventHandler, + public ::osl::DebugBase<SlideImpl> +{ +public: + SlideImpl( const uno::Reference<drawing::XDrawPage>& xDrawPage, + const uno::Reference<drawing::XDrawPagesSupplier>& xDrawPages, + const uno::Reference<animations::XAnimationNode>& xRootNode, + EventQueue& rEventQueue, + EventMultiplexer& rEventMultiplexer, + ScreenUpdater& rScreenUpdater, + ActivitiesQueue& rActivitiesQueue, + UserEventQueue& rUserEventQueue, + CursorManager& rCursorManager, + MediaFileManager& rMediaFileManager, + const UnoViewContainer& rViewContainer, + const uno::Reference<uno::XComponentContext>& xContext, + const ShapeEventListenerMap& rShapeListenerMap, + const ShapeCursorMap& rShapeCursorMap, + PolyPolygonVector&& rPolyPolygonVector, + RGBColor const& rUserPaintColor, + double dUserPaintStrokeWidth, + bool bUserPaintEnabled, + bool bIntrinsicAnimationsAllowed, + bool bDisableAnimationZOrder ); + + virtual ~SlideImpl() override; + + + // Slide interface + + + virtual void prefetch() override; + virtual void show( bool ) override; + virtual void hide() override; + + virtual basegfx::B2ISize getSlideSize() const override; + virtual uno::Reference<drawing::XDrawPage > getXDrawPage() const override; + virtual uno::Reference<animations::XAnimationNode> getXAnimationNode() const override; + virtual PolyPolygonVector getPolygons() override; + virtual void drawPolygons() const override; + virtual bool isPaintOverlayActive() const override; + virtual void enablePaintOverlay() override; + virtual void update_settings( bool bUserPaintEnabled, RGBColor const& aUserPaintColor, double dUserPaintStrokeWidth ) override; + + + // TODO(F2): Rework SlideBitmap to no longer be based on XBitmap, + // but on canvas-independent basegfx bitmaps + virtual SlideBitmapSharedPtr getCurrentSlideBitmap( const UnoViewSharedPtr& rView ) const override; + + +private: + // ViewEventHandler + virtual void viewAdded( const UnoViewSharedPtr& rView ) override; + virtual void viewRemoved( const UnoViewSharedPtr& rView ) override; + virtual void viewChanged( const UnoViewSharedPtr& rView ) override; + virtual void viewsChanged() override; + + // CursorManager + virtual bool requestCursor( sal_Int16 nCursorShape ) override; + virtual void resetCursor() override; + + void activatePaintOverlay(); + void deactivatePaintOverlay(); + + /** Query whether the slide has animations at all + + If the slide doesn't have animations, show() displays + only static content. If an event is registered with + registerSlideEndEvent(), this event will be + immediately activated at the end of the show() method. + + @return true, if this slide has animations, false + otherwise + */ + bool isAnimated(); + + /// Set all Shapes to their initial attributes for slideshow + bool applyInitialShapeAttributes( const css::uno::Reference< css::animations::XAnimationNode >& xRootAnimationNode ); + + /// Set shapes to attributes corresponding to initial or final state of slide + void applyShapeAttributes( + const css::uno::Reference< css::animations::XAnimationNode >& xRootAnimationNode, + bool bInitial) const; + + /// Renders current slide content to bitmap + SlideBitmapSharedPtr createCurrentSlideBitmap( + const UnoViewSharedPtr& rView, + ::basegfx::B2ISize const & rSlideSize ) const; + + /// Prefetch all shapes (not the animations) + bool loadShapes(); + + /// Retrieve slide size from XDrawPage + basegfx::B2ISize getSlideSizeImpl() const; + + /// Prefetch show, but don't call applyInitialShapeAttributes() + bool implPrefetchShow(); + + /// Add Polygons to the member maPolygons + void addPolygons(const PolyPolygonVector& rPolygons); + + // Types + // ===== + + enum SlideAnimationState + { + CONSTRUCTING_STATE=0, + INITIAL_STATE=1, + SHOWING_STATE=2, + FINAL_STATE=3, + SlideAnimationState_NUM_ENTRIES=4 + }; + + typedef std::vector< SlideBitmapSharedPtr > VectorOfSlideBitmaps; + /** Vector of slide bitmaps. + + Since the bitmap content is sensitive to animation + effects, we have an inner vector containing a distinct + bitmap for each of the SlideAnimationStates. + */ + typedef ::std::vector< std::pair< UnoViewSharedPtr, + VectorOfSlideBitmaps > > VectorOfVectorOfSlideBitmaps; + + + // Member variables + // ================ + + /// The page model object + uno::Reference< drawing::XDrawPage > mxDrawPage; + uno::Reference< drawing::XDrawPagesSupplier > mxDrawPagesSupplier; + uno::Reference< animations::XAnimationNode > mxRootNode; + + LayerManagerSharedPtr mpLayerManager; + std::shared_ptr<ShapeManagerImpl> mpShapeManager; + std::shared_ptr<SubsettableShapeManager> mpSubsettableShapeManager; + box2d::utils::Box2DWorldSharedPtr mpBox2DWorld; + + /// Contains common objects needed throughout the slideshow + SlideShowContext maContext; + + /// parent cursor manager + CursorManager& mrCursorManager; + + /// Handles the animation and event generation for us + SlideAnimations maAnimations; + PolyPolygonVector maPolygons; + + RGBColor maUserPaintColor; + double mdUserPaintStrokeWidth; + UserPaintOverlaySharedPtr mpPaintOverlay; + + /// Bitmaps with slide content at various states + mutable VectorOfVectorOfSlideBitmaps maSlideBitmaps; + + SlideAnimationState meAnimationState; + + const basegfx::B2ISize maSlideSize; + + sal_Int16 mnCurrentCursor; + + /// True, when intrinsic shape animations are allowed + bool mbIntrinsicAnimationsAllowed; + + /// True, when user paint overlay is enabled + bool mbUserPaintOverlayEnabled; + + /// True, if initial load of all page shapes succeeded + bool mbShapesLoaded; + + /// True, if initial load of all animation info succeeded + bool mbShowLoaded; + + /** True, if this slide is not static. + + If this slide has animated content, this variable will + be true, and false otherwise. + */ + bool mbHaveAnimations; + + /** True, if this slide has a main animation sequence. + + If this slide has animation content, which in turn has + a main animation sequence (which must be fully run + before EventMultiplexer::notifySlideAnimationsEnd() is + called), this member is true. + */ + bool mbMainSequenceFound; + + /// When true, show() was called. Slide hidden otherwise. + bool mbActive; + + /// When true, enablePaintOverlay was called and mbUserPaintOverlay = true + bool mbPaintOverlayActive; + + /// When true, final state attributes are already applied to shapes + bool mbFinalStateApplied; +}; + + +void slideRenderer( SlideImpl const * pSlide, const UnoViewSharedPtr& rView ) +{ + // fully clear view content to background color + rView->clearAll(); + + SlideBitmapSharedPtr pBitmap( pSlide->getCurrentSlideBitmap( rView ) ); + ::cppcanvas::CanvasSharedPtr pCanvas( rView->getCanvas() ); + + const ::basegfx::B2DHomMatrix aViewTransform( rView->getTransformation() ); + const ::basegfx::B2DPoint aOutPosPixel( aViewTransform * ::basegfx::B2DPoint() ); + + // setup a canvas with device coordinate space, the slide + // bitmap already has the correct dimension. + ::cppcanvas::CanvasSharedPtr pDevicePixelCanvas( pCanvas->clone() ); + pDevicePixelCanvas->setTransformation( ::basegfx::B2DHomMatrix() ); + + // render at given output position + pBitmap->move( aOutPosPixel ); + + // clear clip (might have been changed, e.g. from comb + // transition) + pBitmap->clip( ::basegfx::B2DPolyPolygon() ); + pBitmap->draw( pDevicePixelCanvas ); +} + + +SlideImpl::SlideImpl( const uno::Reference< drawing::XDrawPage >& xDrawPage, + const uno::Reference<drawing::XDrawPagesSupplier>& xDrawPages, + const uno::Reference< animations::XAnimationNode >& xRootNode, + EventQueue& rEventQueue, + EventMultiplexer& rEventMultiplexer, + ScreenUpdater& rScreenUpdater, + ActivitiesQueue& rActivitiesQueue, + UserEventQueue& rUserEventQueue, + CursorManager& rCursorManager, + MediaFileManager& rMediaFileManager, + const UnoViewContainer& rViewContainer, + const uno::Reference< uno::XComponentContext >& xComponentContext, + const ShapeEventListenerMap& rShapeListenerMap, + const ShapeCursorMap& rShapeCursorMap, + PolyPolygonVector&& rPolyPolygonVector, + RGBColor const& aUserPaintColor, + double dUserPaintStrokeWidth, + bool bUserPaintEnabled, + bool bIntrinsicAnimationsAllowed, + bool bDisableAnimationZOrder ) : + mxDrawPage( xDrawPage ), + mxDrawPagesSupplier( xDrawPages ), + mxRootNode( xRootNode ), + mpLayerManager( std::make_shared<LayerManager>( + rViewContainer, + bDisableAnimationZOrder) ), + mpShapeManager( std::make_shared<ShapeManagerImpl>( + rEventMultiplexer, + mpLayerManager, + rCursorManager, + rShapeListenerMap, + rShapeCursorMap, + xDrawPage)), + mpSubsettableShapeManager( mpShapeManager ), + mpBox2DWorld( std::make_shared<box2d::utils::box2DWorld>( + basegfx::B2DSize( getSlideSizeImpl() ) ) ), + maContext( mpSubsettableShapeManager, + rEventQueue, + rEventMultiplexer, + rScreenUpdater, + rActivitiesQueue, + rUserEventQueue, + *this, + rMediaFileManager, + rViewContainer, + xComponentContext, + mpBox2DWorld ), + mrCursorManager( rCursorManager ), + maAnimations( maContext, + basegfx::B2DSize( getSlideSizeImpl() ) ), + maPolygons(std::move(rPolyPolygonVector)), + maUserPaintColor(aUserPaintColor), + mdUserPaintStrokeWidth(dUserPaintStrokeWidth), + mpPaintOverlay(), + maSlideBitmaps(), + meAnimationState( CONSTRUCTING_STATE ), + maSlideSize(getSlideSizeImpl()), + mnCurrentCursor( awt::SystemPointer::ARROW ), + mbIntrinsicAnimationsAllowed( bIntrinsicAnimationsAllowed ), + mbUserPaintOverlayEnabled(bUserPaintEnabled), + mbShapesLoaded( false ), + mbShowLoaded( false ), + mbHaveAnimations( false ), + mbMainSequenceFound( false ), + mbActive( false ), + mbPaintOverlayActive( false ), + mbFinalStateApplied( false ) +{ + // clone already existing views for slide bitmaps + for( const auto& rView : rViewContainer ) + viewAdded( rView ); + + // register screen update (LayerManager needs to signal pending + // updates) + maContext.mrScreenUpdater.addViewUpdate(mpShapeManager); +} + +void SlideImpl::update_settings( bool bUserPaintEnabled, RGBColor const& aUserPaintColor, double dUserPaintStrokeWidth ) +{ + maUserPaintColor = aUserPaintColor; + mdUserPaintStrokeWidth = dUserPaintStrokeWidth; + mbUserPaintOverlayEnabled = bUserPaintEnabled; +} + +SlideImpl::~SlideImpl() +{ + if( mpShapeManager ) + { + maContext.mrScreenUpdater.removeViewUpdate(mpShapeManager); + mpShapeManager->dispose(); + + // TODO(Q3): Make sure LayerManager (and thus Shapes) dies + // first, because SlideShowContext has SubsettableShapeManager + // as reference member. + mpLayerManager.reset(); + } +} + +void SlideImpl::prefetch() +{ + if( !mxRootNode.is() ) + return; + + // Try to prefetch all graphics from the page. This will be done + // in threads to be more efficient than loading them on-demand one by one. + std::vector<Graphic*> graphics; + for (sal_Int32 i = 0; i < mxDrawPage->getCount(); i++) + { + com::sun::star::uno::Reference<com::sun::star::drawing::XShape> xShape(mxDrawPage->getByIndex(i), com::sun::star::uno::UNO_QUERY_THROW); + SdrObject* pObj = SdrObject::getSdrObjectFromXShape(xShape); + if (!pObj) + continue; + if( SdrGrafObj* grafObj = dynamic_cast<SdrGrafObj*>(pObj)) + if( !grafObj->GetGraphic().isAvailable()) + graphics.push_back( const_cast<Graphic*>(&grafObj->GetGraphic())); + } + if(graphics.size() > 1) // threading does not help with loading just one + GraphicFilter::GetGraphicFilter().MakeGraphicsAvailableThreaded( graphics ); + + applyInitialShapeAttributes(mxRootNode); +} + +void SlideImpl::show( bool bSlideBackgroundPainted ) +{ + if( mbActive ) + return; // already active + + if( !mpShapeManager || !mpLayerManager ) + return; // disposed + + // set initial shape attributes (e.g. hide shapes that have + // 'appear' effect set) + if( !applyInitialShapeAttributes(mxRootNode) ) + return; + + // activate and take over view - clears view, if necessary + mbActive = true; + requestCursor( mnCurrentCursor ); + + // enable shape management & event broadcasting for shapes of this + // slide. Also enables LayerManager to record updates. Currently, + // never let LayerManager render initial slide content, use + // buffered slide bitmaps instead. + mpShapeManager->activate(); + + + // render slide to screen, if requested + if( !bSlideBackgroundPainted ) + { + for( const auto& rContext : maContext.mrViewContainer ) + slideRenderer( this, rContext ); + + maContext.mrScreenUpdater.notifyUpdate(); + } + + + // fire up animations + const bool bIsAnimated( isAnimated() ); + if( bIsAnimated ) + maAnimations.start(); // feeds initial events into queue + + // NOTE: this looks slightly weird, but is indeed correct: + // as isAnimated() might return false, _although_ there is + // a main sequence (because the animation nodes don't + // contain any executable effects), we gotta check both + // conditions here. + if( !bIsAnimated || !mbMainSequenceFound ) + { + // manually trigger a slide animation end event (we don't have + // animations at all, or we don't have a main animation + // sequence, but if we had, it'd end now). Note that having + // animations alone does not matter here, as only main + // sequence animations prevents showing the next slide on + // nextEvent(). + maContext.mrEventMultiplexer.notifySlideAnimationsEnd(); + } + + // enable shape-intrinsic animations (drawing layer animations or + // GIF animations) + if( mbIntrinsicAnimationsAllowed ) + mpSubsettableShapeManager->notifyIntrinsicAnimationsEnabled(); + + // enable paint overlay, if maUserPaintColor is valid + activatePaintOverlay(); + + + // from now on, animations might be showing + meAnimationState = SHOWING_STATE; +} + +void SlideImpl::hide() +{ + if( !mbActive || !mpShapeManager ) + return; // already hidden/disposed + + + // from now on, all animations are stopped + meAnimationState = FINAL_STATE; + + + // disable user paint overlay under all circumstances, + // this slide now ceases to be active. + deactivatePaintOverlay(); + + + // switch off all shape-intrinsic animations. + mpSubsettableShapeManager->notifyIntrinsicAnimationsDisabled(); + + // force-end all SMIL animations, too + maAnimations.end(); + + + // disable shape management & event broadcasting for shapes of this + // slide. Also disables LayerManager. + mpShapeManager->deactivate(); + + // vanish from view + resetCursor(); + mbActive = false; +} + +basegfx::B2ISize SlideImpl::getSlideSize() const +{ + return maSlideSize; +} + +uno::Reference<drawing::XDrawPage > SlideImpl::getXDrawPage() const +{ + return mxDrawPage; +} + +uno::Reference<animations::XAnimationNode> SlideImpl::getXAnimationNode() const +{ + return mxRootNode; +} + +PolyPolygonVector SlideImpl::getPolygons() +{ + if(mbPaintOverlayActive) + maPolygons = mpPaintOverlay->getPolygons(); + return maPolygons; +} + +SlideBitmapSharedPtr SlideImpl::getCurrentSlideBitmap( const UnoViewSharedPtr& rView ) const +{ + // search corresponding entry in maSlideBitmaps (which + // contains the views as the key) + VectorOfVectorOfSlideBitmaps::iterator aIter; + const VectorOfVectorOfSlideBitmaps::iterator aEnd( maSlideBitmaps.end() ); + if( (aIter=std::find_if( maSlideBitmaps.begin(), + aEnd, + [&rView] + ( const VectorOfVectorOfSlideBitmaps::value_type& cp ) + { return rView == cp.first; } ) ) == aEnd ) + { + // corresponding view not found - maybe view was not + // added to Slide? + ENSURE_OR_THROW( false, + "SlideImpl::getInitialSlideBitmap(): view does not " + "match any of the added ones" ); + } + + // ensure that the show is loaded + if( !mbShowLoaded ) + { + // only prefetch and init shapes when not done already + // (otherwise, at least applyInitialShapeAttributes() will be + // called twice for initial slide rendering). Furthermore, + // applyInitialShapeAttributes() _always_ performs + // initializations, which would be highly unwanted during a + // running show. OTOH, a slide whose mbShowLoaded is false is + // guaranteed not be running a show. + + // set initial shape attributes (e.g. hide 'appear' effect + // shapes) + if( !const_cast<SlideImpl*>(this)->applyInitialShapeAttributes( mxRootNode ) ) + ENSURE_OR_THROW(false, + "SlideImpl::getCurrentSlideBitmap(): Cannot " + "apply initial attributes"); + } + + SlideBitmapSharedPtr& rBitmap( aIter->second.at( meAnimationState )); + const ::basegfx::B2ISize& rSlideSize( + getSlideSizePixel( ::basegfx::B2DSize( getSlideSize() ), + rView )); + + // is the bitmap valid (actually existent, and of correct + // size)? + if( !rBitmap || rBitmap->getSize() != rSlideSize ) + { + // no bitmap there yet, or wrong size - create one + rBitmap = createCurrentSlideBitmap(rView, rSlideSize); + } + + return rBitmap; +} + + +// private methods + + +void SlideImpl::viewAdded( const UnoViewSharedPtr& rView ) +{ + maSlideBitmaps.emplace_back( rView, + VectorOfSlideBitmaps(SlideAnimationState_NUM_ENTRIES) ); + + if( mpLayerManager ) + mpLayerManager->viewAdded( rView ); +} + +void SlideImpl::viewRemoved( const UnoViewSharedPtr& rView ) +{ + if( mpLayerManager ) + mpLayerManager->viewRemoved( rView ); + + const VectorOfVectorOfSlideBitmaps::iterator aEnd( maSlideBitmaps.end() ); + maSlideBitmaps.erase( + std::remove_if( maSlideBitmaps.begin(), + aEnd, + [&rView] + ( const VectorOfVectorOfSlideBitmaps::value_type& cp ) + { return rView == cp.first; } ), + aEnd ); +} + +void SlideImpl::viewChanged( const UnoViewSharedPtr& rView ) +{ + // nothing to do for the Slide - getCurrentSlideBitmap() lazily + // handles bitmap resizes + if( mbActive && mpLayerManager ) + mpLayerManager->viewChanged(rView); +} + +void SlideImpl::viewsChanged() +{ + // nothing to do for the Slide - getCurrentSlideBitmap() lazily + // handles bitmap resizes + if( mbActive && mpLayerManager ) + mpLayerManager->viewsChanged(); +} + +bool SlideImpl::requestCursor( sal_Int16 nCursorShape ) +{ + mnCurrentCursor = nCursorShape; + return mrCursorManager.requestCursor(mnCurrentCursor); +} + +void SlideImpl::resetCursor() +{ + mnCurrentCursor = awt::SystemPointer::ARROW; + mrCursorManager.resetCursor(); +} + +bool SlideImpl::isAnimated() +{ + // prefetch, but don't apply initial shape attributes + if( !implPrefetchShow() ) + return false; + + return mbHaveAnimations && maAnimations.isAnimated(); +} + +SlideBitmapSharedPtr SlideImpl::createCurrentSlideBitmap( const UnoViewSharedPtr& rView, + const ::basegfx::B2ISize& rBmpSize ) const +{ + ENSURE_OR_THROW( rView && rView->getCanvas(), + "SlideImpl::createCurrentSlideBitmap(): Invalid view" ); + ENSURE_OR_THROW( mpLayerManager, + "SlideImpl::createCurrentSlideBitmap(): Invalid layer manager" ); + ENSURE_OR_THROW( mbShowLoaded, + "SlideImpl::createCurrentSlideBitmap(): No show loaded" ); + + // tdf#96083 ensure end state settings are applied to shapes once when bitmap gets re-rendered + // in that state + if(!mbFinalStateApplied && FINAL_STATE == meAnimationState && mxRootNode.is()) + { + const_cast< SlideImpl* >(this)->mbFinalStateApplied = true; + applyShapeAttributes(mxRootNode, false); + } + + ::cppcanvas::CanvasSharedPtr pCanvas( rView->getCanvas() ); + + // create a bitmap of appropriate size + ::cppcanvas::BitmapSharedPtr pBitmap( + ::cppcanvas::BaseGfxFactory::createBitmap( + pCanvas, + rBmpSize ) ); + + ENSURE_OR_THROW( pBitmap, + "SlideImpl::createCurrentSlideBitmap(): Cannot create page bitmap" ); + + ::cppcanvas::BitmapCanvasSharedPtr pBitmapCanvas( pBitmap->getBitmapCanvas() ); + + ENSURE_OR_THROW( pBitmapCanvas, + "SlideImpl::createCurrentSlideBitmap(): Cannot create page bitmap canvas" ); + + // apply linear part of destination canvas transformation (linear means in this context: + // transformation without any translational components) + ::basegfx::B2DHomMatrix aLinearTransform( rView->getTransformation() ); + aLinearTransform.set( 0, 2, 0.0 ); + aLinearTransform.set( 1, 2, 0.0 ); + pBitmapCanvas->setTransformation( aLinearTransform ); + + // output all shapes to bitmap + initSlideBackground( pBitmapCanvas, rBmpSize ); + mpLayerManager->renderTo( pBitmapCanvas ); + + return std::make_shared<SlideBitmap>( pBitmap ); +} + +class MainSequenceSearcher +{ +public: + MainSequenceSearcher() + { + maSearchKey.Name = "node-type"; + maSearchKey.Value <<= presentation::EffectNodeType::MAIN_SEQUENCE; + } + + void operator()( const uno::Reference< animations::XAnimationNode >& xChildNode ) + { + uno::Sequence< beans::NamedValue > aUserData( xChildNode->getUserData() ); + + if( findNamedValue( aUserData, maSearchKey ) ) + { + maMainSequence = xChildNode; + } + } + + const uno::Reference< animations::XAnimationNode >& getMainSequence() const + { + return maMainSequence; + } + +private: + beans::NamedValue maSearchKey; + uno::Reference< animations::XAnimationNode > maMainSequence; +}; + +bool SlideImpl::implPrefetchShow() +{ + if( mbShowLoaded ) + return true; + + ENSURE_OR_RETURN_FALSE( mxDrawPage.is(), + "SlideImpl::implPrefetchShow(): Invalid draw page" ); + ENSURE_OR_RETURN_FALSE( mpLayerManager, + "SlideImpl::implPrefetchShow(): Invalid layer manager" ); + + // fetch desired page content + // ========================== + + if( !loadShapes() ) + return false; + + // New animations framework: import the shape effect info + // ====================================================== + + try + { + if( mxRootNode.is() ) + { + if( !maAnimations.importAnimations( mxRootNode ) ) + { + OSL_FAIL( "SlideImpl::implPrefetchShow(): have animation nodes, " + "but import animations failed." ); + + // could not import animation framework, + // _although_ some animation nodes are there - + // this is an error (not finding animations at + // all is okay - might be a static slide) + return false; + } + + // now check whether we've got a main sequence (if + // not, we must manually call + // EventMultiplexer::notifySlideAnimationsEnd() + // above, as e.g. interactive sequences alone + // don't block nextEvent() from issuing the next + // slide) + MainSequenceSearcher aSearcher; + if( for_each_childNode( mxRootNode, aSearcher ) ) + mbMainSequenceFound = aSearcher.getMainSequence().is(); + + // import successfully done + mbHaveAnimations = true; + } + } + catch( uno::RuntimeException& ) + { + throw; + } + catch( uno::Exception& ) + { + TOOLS_WARN_EXCEPTION( "slideshow", "" ); + // TODO(E2): Error handling. For now, bail out + } + + mbShowLoaded = true; + + return true; +} + +void SlideImpl::enablePaintOverlay() +{ + if( !mbUserPaintOverlayEnabled || !mbPaintOverlayActive ) + { + mbUserPaintOverlayEnabled = true; + activatePaintOverlay(); + } +} + +void SlideImpl::activatePaintOverlay() +{ + if( mbUserPaintOverlayEnabled || !maPolygons.empty() ) + { + mpPaintOverlay = UserPaintOverlay::create( maUserPaintColor, + mdUserPaintStrokeWidth, + maContext, + std::vector(maPolygons), + mbUserPaintOverlayEnabled ); + mbPaintOverlayActive = true; + } +} + +void SlideImpl::drawPolygons() const +{ + if( mpPaintOverlay ) + mpPaintOverlay->drawPolygons(); +} + +void SlideImpl::addPolygons(const PolyPolygonVector& rPolygons) +{ + maPolygons.insert(maPolygons.end(), rPolygons.begin(), rPolygons.end()); +} + +bool SlideImpl::isPaintOverlayActive() const +{ + return mbPaintOverlayActive; +} + +void SlideImpl::deactivatePaintOverlay() +{ + if(mbPaintOverlayActive) + maPolygons = mpPaintOverlay->getPolygons(); + + mpPaintOverlay.reset(); + mbPaintOverlayActive = false; +} + +void SlideImpl::applyShapeAttributes( + const css::uno::Reference< css::animations::XAnimationNode >& xRootAnimationNode, + bool bInitial) const +{ + const uno::Sequence< animations::TargetProperties > aProps( + TargetPropertiesCreator::createTargetProperties( xRootAnimationNode, bInitial ) ); + + // apply extracted values to our shapes + for( const auto& rProp : aProps ) + { + sal_Int16 nParaIndex( -1 ); + uno::Reference< drawing::XShape > xShape( rProp.Target, + uno::UNO_QUERY ); + + if( !xShape.is() ) + { + // not a shape target. Maybe a ParagraphTarget? + presentation::ParagraphTarget aParaTarget; + + if( rProp.Target >>= aParaTarget ) + { + // yep, ParagraphTarget found - extract shape + // and index + xShape = aParaTarget.Shape; + nParaIndex = aParaTarget.Paragraph; + } + } + + if( xShape.is() ) + { + ShapeSharedPtr pShape( mpLayerManager->lookupShape( xShape ) ); + + if( !pShape ) + { + OSL_FAIL( "SlideImpl::applyInitialShapeAttributes(): no shape found for given target" ); + continue; + } + + AttributableShapeSharedPtr pAttrShape( + ::std::dynamic_pointer_cast< AttributableShape >( pShape ) ); + + if( !pAttrShape ) + { + OSL_FAIL( "SlideImpl::applyInitialShapeAttributes(): shape found does not " + "implement AttributableShape interface" ); + continue; + } + + if( nParaIndex != -1 ) + { + // our target is a paragraph subset, thus look + // this up first. + const DocTreeNodeSupplier& rNodeSupplier( pAttrShape->getTreeNodeSupplier() ); + + if( rNodeSupplier.getNumberOfTreeNodes( + DocTreeNode::NodeType::LogicalParagraph ) <= nParaIndex ) + { + OSL_FAIL( "SlideImpl::applyInitialShapeAttributes(): shape found does not " + "provide a subset for requested paragraph index" ); + continue; + } + + pAttrShape = pAttrShape->getSubset( + rNodeSupplier.getTreeNode( + nParaIndex, + DocTreeNode::NodeType::LogicalParagraph ) ); + + if( !pAttrShape ) + { + OSL_FAIL( "SlideImpl::applyInitialShapeAttributes(): shape found does not " + "provide a subset for requested paragraph index" ); + continue; + } + } + + const uno::Sequence< beans::NamedValue >& rShapeProps( rProp.Properties ); + for( const auto& rShapeProp : rShapeProps ) + { + bool bVisible=false; + if( rShapeProp.Name.equalsIgnoreAsciiCase("visibility") && + extractValue( bVisible, + rShapeProp.Value, + pShape, + ::basegfx::B2DSize( getSlideSize() ) )) + { + pAttrShape->setVisibility( bVisible ); + } + else + { + OSL_FAIL( "SlideImpl::applyInitialShapeAttributes(): Unexpected " + "(and unimplemented) property encountered" ); + } + } + } + } +} + +bool SlideImpl::applyInitialShapeAttributes( + const uno::Reference< animations::XAnimationNode >& xRootAnimationNode ) +{ + if( !implPrefetchShow() ) + return false; + + if( !xRootAnimationNode.is() ) + { + meAnimationState = INITIAL_STATE; + + return true; // no animations - no attributes to apply - + // succeeded + } + + applyShapeAttributes(xRootAnimationNode, true); + + meAnimationState = INITIAL_STATE; + + return true; +} + +bool SlideImpl::loadShapes() +{ + if( mbShapesLoaded ) + return true; + + ENSURE_OR_RETURN_FALSE( mxDrawPage.is(), + "SlideImpl::loadShapes(): Invalid draw page" ); + ENSURE_OR_RETURN_FALSE( mpLayerManager, + "SlideImpl::loadShapes(): Invalid layer manager" ); + + // fetch desired page content + // ========================== + + // also take master page content + uno::Reference< drawing::XDrawPage > xMasterPage; + uno::Reference< drawing::XShapes > xMasterPageShapes; + sal_Int32 nCurrCount(0); + + uno::Reference< drawing::XMasterPageTarget > xMasterPageTarget( mxDrawPage, + uno::UNO_QUERY ); + if( xMasterPageTarget.is() ) + { + xMasterPage = xMasterPageTarget->getMasterPage(); + xMasterPageShapes = xMasterPage; + + if( xMasterPage.is() && xMasterPageShapes.is() ) + { + // TODO(P2): maybe cache master pages here (or treat the + // masterpage as a single metafile. At least currently, + // masterpages do not contain animation effects) + try + { + // load the masterpage shapes + + ShapeImporter aMPShapesFunctor( xMasterPage, + mxDrawPage, + mxDrawPagesSupplier, + maContext, + 0, /* shape num starts at 0 */ + true ); + + mpLayerManager->addShape( + aMPShapesFunctor.importBackgroundShape() ); + + while( !aMPShapesFunctor.isImportDone() ) + { + ShapeSharedPtr const& rShape( + aMPShapesFunctor.importShape() ); + if( rShape ) + { + rShape->setIsForeground(false); + mpLayerManager->addShape( rShape ); + } + } + addPolygons(aMPShapesFunctor.getPolygons()); + + nCurrCount = static_cast<sal_Int32>(aMPShapesFunctor.getImportedShapesCount()); + } + catch( uno::RuntimeException& ) + { + throw; + } + catch( ShapeLoadFailedException& ) + { + // TODO(E2): Error handling. For now, bail out + TOOLS_WARN_EXCEPTION( "slideshow", "SlideImpl::loadShapes(): caught ShapeLoadFailedException" ); + return false; + + } + catch( uno::Exception& ) + { + TOOLS_WARN_EXCEPTION( "slideshow", "" ); + return false; + } + } + } + + try + { + // load the normal page shapes + + + ShapeImporter aShapesFunctor( mxDrawPage, + mxDrawPage, + mxDrawPagesSupplier, + maContext, + nCurrCount, + false ); + + while( !aShapesFunctor.isImportDone() ) + { + ShapeSharedPtr const& rShape( + aShapesFunctor.importShape() ); + if( rShape ) + mpLayerManager->addShape( rShape ); + } + addPolygons(aShapesFunctor.getPolygons()); + } + catch( uno::RuntimeException& ) + { + throw; + } + catch( ShapeLoadFailedException& ) + { + // TODO(E2): Error handling. For now, bail out + TOOLS_WARN_EXCEPTION( "slideshow", "SlideImpl::loadShapes(): caught ShapeLoadFailedException" ); + return false; + } + catch( uno::Exception& ) + { + TOOLS_WARN_EXCEPTION( "slideshow", "" ); + return false; + } + + mbShapesLoaded = true; + + return true; +} + +basegfx::B2ISize SlideImpl::getSlideSizeImpl() const +{ + uno::Reference< beans::XPropertySet > xPropSet( + mxDrawPage, uno::UNO_QUERY_THROW ); + + sal_Int32 nDocWidth = 0; + sal_Int32 nDocHeight = 0; + xPropSet->getPropertyValue("Width") >>= nDocWidth; + xPropSet->getPropertyValue("Height") >>= nDocHeight; + + return basegfx::B2ISize( nDocWidth, nDocHeight ); +} + +} // namespace + + +SlideSharedPtr createSlide( const uno::Reference< drawing::XDrawPage >& xDrawPage, + const uno::Reference<drawing::XDrawPagesSupplier>& xDrawPages, + const uno::Reference< animations::XAnimationNode >& xRootNode, + EventQueue& rEventQueue, + EventMultiplexer& rEventMultiplexer, + ScreenUpdater& rScreenUpdater, + ActivitiesQueue& rActivitiesQueue, + UserEventQueue& rUserEventQueue, + CursorManager& rCursorManager, + MediaFileManager& rMediaFileManager, + const UnoViewContainer& rViewContainer, + const uno::Reference< uno::XComponentContext >& xComponentContext, + const ShapeEventListenerMap& rShapeListenerMap, + const ShapeCursorMap& rShapeCursorMap, + PolyPolygonVector&& rPolyPolygonVector, + RGBColor const& rUserPaintColor, + double dUserPaintStrokeWidth, + bool bUserPaintEnabled, + bool bIntrinsicAnimationsAllowed, + bool bDisableAnimationZOrder ) +{ + auto pRet = std::make_shared<SlideImpl>( xDrawPage, xDrawPages, xRootNode, rEventQueue, + rEventMultiplexer, rScreenUpdater, + rActivitiesQueue, rUserEventQueue, + rCursorManager, rMediaFileManager, rViewContainer, + xComponentContext, rShapeListenerMap, + rShapeCursorMap, std::move(rPolyPolygonVector), rUserPaintColor, + dUserPaintStrokeWidth, bUserPaintEnabled, + bIntrinsicAnimationsAllowed, + bDisableAnimationZOrder ); + + rEventMultiplexer.addViewHandler( pRet ); + + return pRet; +} + +} // namespace slideshow + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/targetpropertiescreator.cxx b/slideshow/source/engine/slide/targetpropertiescreator.cxx new file mode 100644 index 000000000..fdc45c3da --- /dev/null +++ b/slideshow/source/engine/slide/targetpropertiescreator.cxx @@ -0,0 +1,370 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <com/sun/star/animations/XIterateContainer.hpp> +#include <com/sun/star/presentation/ParagraphTarget.hpp> +#include <com/sun/star/drawing/XShape.hpp> +#include <com/sun/star/animations/AnimationNodeType.hpp> +#include <com/sun/star/animations/XAnimate.hpp> +#include <comphelper/sequence.hxx> + +#include <unordered_map> +#include <vector> + +#include "targetpropertiescreator.hxx" +#include <tools.hxx> + +namespace slideshow::internal +{ + namespace + { + // Vector containing all properties for a given shape + typedef ::std::vector< beans::NamedValue > VectorOfNamedValues; + + /** The hash map key + + This key contains both XShape reference and a paragraph + index, as we somehow have to handle shape and paragraph + targets with the same data structure. + */ + struct ShapeHashKey + { + /// Shape target + uno::Reference< drawing::XShape > mxRef; + + /** Paragraph index. + + If this is a pure shape target, mnParagraphIndex is + set to -1. + */ + sal_Int16 mnParagraphIndex; + + /// Comparison needed for unordered_map + bool operator==( const ShapeHashKey& rRHS ) const + { + return mxRef == rRHS.mxRef && mnParagraphIndex == rRHS.mnParagraphIndex; + } + }; + + // A hash functor for ShapeHashKey objects + struct ShapeKeyHasher + { + ::std::size_t operator()( const ShapeHashKey& rKey ) const + { + // TODO(P2): Maybe a better hash function would be to + // spread mnParagraphIndex to 32 bit: a0b0c0d0e0... Hakmem + // should have a formula. + + // Yes it has: + // x = (x & 0x0000FF00) << 8) | (x >> 8) & 0x0000FF00 | x & 0xFF0000FF; + // x = (x & 0x00F000F0) << 4) | (x >> 4) & 0x00F000F0 | x & 0xF00FF00F; + // x = (x & 0x0C0C0C0C) << 2) | (x >> 2) & 0x0C0C0C0C | x & 0xC3C3C3C3; + // x = (x & 0x22222222) << 1) | (x >> 1) & 0x22222222 | x & 0x99999999; + + // Costs about 17 cycles on a RISC machine with infinite + // instruction level parallelism (~42 basic + // instructions). Thus I truly doubt this pays off... + return reinterpret_cast< ::std::size_t >(rKey.mxRef.get()) ^ (rKey.mnParagraphIndex << 16); + } + }; + + // A hash map which maps a XShape to the corresponding vector of initial properties + typedef std::unordered_map< ShapeHashKey, VectorOfNamedValues, ShapeKeyHasher > XShapeToNamedValuesMap; + + + class NodeFunctor + { + public: + explicit NodeFunctor( + XShapeToNamedValuesMap& rShapeHash, + bool bInitial ) + : mrShapeHash( rShapeHash ), + mxTargetShape(), + mnParagraphIndex( -1 ), + mbInitial( bInitial) + { + } + + NodeFunctor( XShapeToNamedValuesMap& rShapeHash, + const uno::Reference< drawing::XShape >& rTargetShape, + sal_Int16 nParagraphIndex, + bool bInitial) : + mrShapeHash( rShapeHash ), + mxTargetShape( rTargetShape ), + mnParagraphIndex( nParagraphIndex ), + mbInitial( bInitial ) + { + } + + void operator()( const uno::Reference< animations::XAnimationNode >& xNode ) const + { + if( !xNode.is() ) + { + OSL_FAIL( "AnimCore: NodeFunctor::operator(): invalid XAnimationNode" ); + return; + } + + uno::Reference< drawing::XShape > xTargetShape( mxTargetShape ); + sal_Int16 nParagraphIndex( mnParagraphIndex ); + + switch( xNode->getType() ) + { + case animations::AnimationNodeType::ITERATE: + { + // extract target shape from iterate node + // (will override the target for all children) + + uno::Reference< animations::XIterateContainer > xIterNode( xNode, + uno::UNO_QUERY ); + + // TODO(E1): I'm not too sure what to expect here... + if( !xIterNode->getTarget().hasValue() ) + { + OSL_FAIL( "animcore: NodeFunctor::operator(): no target on ITERATE node" ); + return; + } + + xTargetShape.set( xIterNode->getTarget(), + uno::UNO_QUERY ); + + if( !xTargetShape.is() ) + { + css::presentation::ParagraphTarget aTarget; + + // no shape provided. Maybe a ParagraphTarget? + if( !(xIterNode->getTarget() >>= aTarget) ) + { + OSL_FAIL( "animcore: NodeFunctor::operator(): could not extract any " + "target information" ); + return; + } + + xTargetShape = aTarget.Shape; + nParagraphIndex = aTarget.Paragraph; + + if( !xTargetShape.is() ) + { + OSL_FAIL( "animcore: NodeFunctor::operator(): invalid shape in ParagraphTarget" ); + return; + } + } + [[fallthrough]]; + } + case animations::AnimationNodeType::PAR: + case animations::AnimationNodeType::SEQ: + { + /// forward bInitial + NodeFunctor aFunctor( mrShapeHash, + xTargetShape, + nParagraphIndex, + mbInitial ); + if( !for_each_childNode( xNode, aFunctor ) ) + { + OSL_FAIL( "AnimCore: NodeFunctor::operator(): child node iteration failed, " + "or extraneous container nodes encountered" ); + } + } + break; + + case animations::AnimationNodeType::CUSTOM: + case animations::AnimationNodeType::ANIMATE: + case animations::AnimationNodeType::ANIMATEMOTION: + case animations::AnimationNodeType::ANIMATECOLOR: + case animations::AnimationNodeType::ANIMATETRANSFORM: + case animations::AnimationNodeType::TRANSITIONFILTER: + case animations::AnimationNodeType::AUDIO: + /*default: + // ignore this node, no valuable content for now. + break;*/ + + case animations::AnimationNodeType::SET: + { + // evaluate set node content + uno::Reference< animations::XAnimate > xAnimateNode( xNode, + uno::UNO_QUERY ); + + if( !xAnimateNode.is() ) + break; // invalid node + + // determine target shape (if any) + ShapeHashKey aTarget; + if( xTargetShape.is() ) + { + // override target shape with parent-supplied + aTarget.mxRef = xTargetShape; + aTarget.mnParagraphIndex = nParagraphIndex; + } + else + { + // no parent-supplied target, retrieve + // node target + if( xAnimateNode->getTarget() >>= aTarget.mxRef ) + { + // pure shape target - set paragraph + // index to magic + aTarget.mnParagraphIndex = -1; + } + else + { + // not a pure shape target - maybe a + // ParagraphTarget? + presentation::ParagraphTarget aUnoTarget; + + if( !(xAnimateNode->getTarget() >>= aUnoTarget) ) + { + OSL_FAIL( "AnimCore: NodeFunctor::operator(): unknown target type encountered" ); + break; + } + + aTarget.mxRef = aUnoTarget.Shape; + aTarget.mnParagraphIndex = aUnoTarget.Paragraph; + } + } + + if( !aTarget.mxRef.is() ) + { + OSL_FAIL( "AnimCore: NodeFunctor::operator(): Found target, but XShape is NULL" ); + break; // invalid target XShape + } + + // check whether we already have an entry for + // this target (we only take the first set + // effect for every shape) - but keep going if + // we're requested the final state (which + // eventually gets overwritten in the + // unordered list, see tdf#96083) + if( mbInitial && mrShapeHash.find( aTarget ) != mrShapeHash.end() ) + break; // already an entry in existence for given XShape + + // if this is an appear effect, hide shape + // initially. This is currently the only place + // where a shape effect influences shape + // attributes outside it's effective duration. + bool bVisible( false ); + if( xAnimateNode->getAttributeName().equalsIgnoreAsciiCase("visibility") ) + { + + uno::Any aAny( xAnimateNode->getTo() ); + + // try to extract bool value + if( !(aAny >>= bVisible) ) + { + // try to extract string + OUString aString; + if( aAny >>= aString ) + { + // we also take the strings "true" and "false", + // as well as "on" and "off" here + if( aString.equalsIgnoreAsciiCase("true") || + aString.equalsIgnoreAsciiCase("on") ) + { + bVisible = true; + } + if( aString.equalsIgnoreAsciiCase("false") || + aString.equalsIgnoreAsciiCase("off") ) + { + bVisible = false; + } + } + } + } + + // if initial anim sets shape visible, set it + // to invisible. If we're asked for the final + // state, don't do anything obviously + if(mbInitial) + bVisible = !bVisible; + + // target is set the 'visible' value, + // so we should record the opposite value + mrShapeHash.emplace( + aTarget, + VectorOfNamedValues( + 1, + beans::NamedValue( + //xAnimateNode->getAttributeName(), + "visibility", + uno::Any( bVisible ) ) ) ); + break; + } + } + } + + private: + XShapeToNamedValuesMap& mrShapeHash; + uno::Reference< drawing::XShape > mxTargetShape; + sal_Int16 mnParagraphIndex; + + // get initial or final state + bool mbInitial; + }; + } + + uno::Sequence< animations::TargetProperties > TargetPropertiesCreator::createTargetProperties + ( + const uno::Reference< animations::XAnimationNode >& xRootNode, + bool bInitial + ) //throw (uno::RuntimeException, std::exception) + { + // scan all nodes for visibility changes, and record first + // 'visibility=true' for each shape + XShapeToNamedValuesMap aShapeHash( 101 ); + + NodeFunctor aFunctor( + aShapeHash, + bInitial ); + + // TODO(F1): Maybe limit functor application to main sequence + // alone (CL said something that shape visibility is only + // affected by effects in the main sequence for PPT). + + // OTOH, client code can pass us only the main sequence (which + // it actually does right now, for the slideshow implementation). + aFunctor( xRootNode ); + + // output to result sequence + uno::Sequence< animations::TargetProperties > aRes( aShapeHash.size() ); + auto aResRange = asNonConstRange(aRes); + + ::std::size_t nCurrIndex(0); + for( const auto& rIter : aShapeHash ) + { + animations::TargetProperties& rCurrProps( aResRange[ nCurrIndex++ ] ); + + if( rIter.first.mnParagraphIndex == -1 ) + { + rCurrProps.Target <<= rIter.first.mxRef; + } + else + { + rCurrProps.Target <<= + presentation::ParagraphTarget( + rIter.first.mxRef, + rIter.first.mnParagraphIndex ); + } + + rCurrProps.Properties = ::comphelper::containerToSequence( rIter.second ); + } + + return aRes; + } + +} // namespace slideshow::internal + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/targetpropertiescreator.hxx b/slideshow/source/engine/slide/targetpropertiescreator.hxx new file mode 100644 index 000000000..79c1e49a5 --- /dev/null +++ b/slideshow/source/engine/slide/targetpropertiescreator.hxx @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_TARGETPROPERTIESCREATOR_HXX +#define INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_TARGETPROPERTIESCREATOR_HXX + +#include <com/sun/star/animations/TargetProperties.hpp> +#include <com/sun/star/animations/XAnimationNode.hpp> + +using namespace ::com::sun::star; + +namespace slideshow::internal::TargetPropertiesCreator +{ + /// Generate shape property list - set bInitial to true for initial slide state + uno::Sequence< animations::TargetProperties > createTargetProperties( + const uno::Reference< animations::XAnimationNode >& rootNode, + bool bInitial ); + +} // namespace slideshow::internal::TargetPropertiesCreator + +#endif // INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_TARGETPROPERTIESCREATOR_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/userpaintoverlay.cxx b/slideshow/source/engine/slide/userpaintoverlay.cxx new file mode 100644 index 000000000..d635fc60a --- /dev/null +++ b/slideshow/source/engine/slide/userpaintoverlay.cxx @@ -0,0 +1,485 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <com/sun/star/awt/MouseButton.hpp> +#include <com/sun/star/awt/MouseEvent.hpp> + +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <cppcanvas/basegfxfactory.hxx> +#include <tools/diagnose_ex.h> + +#include <slideshowcontext.hxx> +#include "userpaintoverlay.hxx" +#include <mouseeventhandler.hxx> +#include <eventmultiplexer.hxx> +#include <screenupdater.hxx> +#include <vieweventhandler.hxx> + +#include <slide.hxx> +#include <cursormanager.hxx> + +using namespace ::com::sun::star; + +namespace slideshow::internal +{ + class PaintOverlayHandler : public MouseEventHandler, + public ViewEventHandler, + public UserPaintEventHandler + { + public: + PaintOverlayHandler( const RGBColor& rStrokeColor, + double nStrokeWidth, + ScreenUpdater& rScreenUpdater, + const UnoViewContainer& rViews, + Slide& rSlide, + PolyPolygonVector&& rPolygons, + bool bActive ) : + mrScreenUpdater( rScreenUpdater ), + maViews(), + maPolygons( std::move(rPolygons) ), + maStrokeColor( rStrokeColor ), + mnStrokeWidth( nStrokeWidth ), + maLastPoint(), + maLastMouseDownPos(), + mbIsLastPointValid( false ), + mbIsLastMouseDownPosValid( false ), + //handle the "remove all ink from slide" mode of erasing + mbIsEraseAllModeActivated( false ), + //handle the "remove stroke by stroke" mode of erasing + mbIsEraseModeActivated( false ), + mrSlide(rSlide), + mnSize(100), + mbActive( bActive ) + { + for( const auto& rView : rViews ) + viewAdded( rView ); + + drawPolygons(); + } + + void dispose() + { + maViews.clear(); + } + + // ViewEventHandler methods + virtual void viewAdded( const UnoViewSharedPtr& rView ) override + { + maViews.push_back( rView ); + } + + virtual void viewRemoved( const UnoViewSharedPtr& rView ) override + { + maViews.erase( ::std::remove( maViews.begin(), + maViews.end(), + rView ) ); + } + + virtual void viewChanged( const UnoViewSharedPtr& /*rView*/ ) override + { + // TODO(F2): for persistent drawings, need to store + // polygon and repaint here. + } + + virtual void viewsChanged() override + { + // TODO(F2): for persistent drawings, need to store + // polygon and repaint here. + } + + bool colorChanged( RGBColor const& rUserColor ) override + { + mbIsLastPointValid = false; + mbActive = true; + maStrokeColor = rUserColor; + mbIsEraseModeActivated = false; + return true; + } + + bool widthChanged( double nUserStrokeWidth ) override + { + mnStrokeWidth = nUserStrokeWidth; + mbIsEraseModeActivated = false; + return true; + } + + void repaintWithoutPolygons() + { + // must get access to the instance to erase all polygon + for( const auto& rxView : maViews ) + { + // fully clear view content to background color + //rxView->getCanvas()->clear(); + + //get via SlideImpl instance the bitmap of the slide unmodified to redraw it + SlideBitmapSharedPtr pBitmap( mrSlide.getCurrentSlideBitmap( rxView ) ); + ::cppcanvas::CanvasSharedPtr pCanvas( rxView->getCanvas() ); + + const ::basegfx::B2DHomMatrix aViewTransform( rxView->getTransformation() ); + const ::basegfx::B2DPoint aOutPosPixel( aViewTransform * ::basegfx::B2DPoint() ); + + // setup a canvas with device coordinate space, the slide + // bitmap already has the correct dimension. + ::cppcanvas::CanvasSharedPtr pDevicePixelCanvas( pCanvas->clone() ); + + pDevicePixelCanvas->setTransformation( ::basegfx::B2DHomMatrix() ); + + // render at given output position + pBitmap->move( aOutPosPixel ); + + // clear clip (might have been changed, e.g. from comb + // transition) + pBitmap->clip( ::basegfx::B2DPolyPolygon() ); + pBitmap->draw( pDevicePixelCanvas ); + + mrScreenUpdater.notifyUpdate(rxView,true); + } + } + + bool eraseAllInkChanged( bool bEraseAllInk ) override + { + mbIsEraseAllModeActivated = bEraseAllInk; + // if the erase all mode is activated it will remove all ink from slide, + // therefore destroy all the polygons stored + if(mbIsEraseAllModeActivated) + { + // The Erase Mode should be deactivated + mbIsEraseModeActivated = false; + repaintWithoutPolygons(); + maPolygons.clear(); + } + mbIsEraseAllModeActivated=false; + return true; + } + + bool eraseInkWidthChanged( sal_Int32 rEraseInkSize ) override + { + // Change the size + mnSize=rEraseInkSize; + // Changed to mode Erase + mbIsEraseModeActivated = true; + return true; + } + + bool switchPenMode() override + { + mbIsLastPointValid = false; + mbActive = true; + mbIsEraseModeActivated = false; + return true; + } + + bool switchEraserMode() override + { + mbIsLastPointValid = false; + mbActive = true; + mbIsEraseModeActivated = true; + return true; + } + + bool disable() override + { + mbIsLastPointValid = false; + mbIsLastMouseDownPosValid = false; + mbActive = false; + return true; + } + + //Draw all registered polygons. + void drawPolygons() + { + for( const auto& rxPolygon : maPolygons ) + { + rxPolygon->draw(); + } + // screen update necessary to show painting + mrScreenUpdater.notifyUpdate(); + } + + //Retrieve all registered polygons. + const PolyPolygonVector& getPolygons() const + { + return maPolygons; + } + + // MouseEventHandler methods + virtual bool handleMousePressed( const awt::MouseEvent& e ) override + { + if( !mbActive ) + return false; + + if (e.Buttons == awt::MouseButton::RIGHT) + { + mbIsLastPointValid = false; + return false; + } + + if (e.Buttons != awt::MouseButton::LEFT) + return false; + + maLastMouseDownPos.setX( e.X ); + maLastMouseDownPos.setY( e.Y ); + mbIsLastMouseDownPosValid = true; + + // eat mouse click (though we don't process it + // _directly_, it enables the drag mode + return true; + } + + virtual bool handleMouseReleased( const awt::MouseEvent& e ) override + { + if( !mbActive ) + return false; + + if (e.Buttons == awt::MouseButton::RIGHT) + { + mbIsLastPointValid = false; + return false; + } + + if (e.Buttons != awt::MouseButton::LEFT) + return false; + + // check, whether up- and down press are on exactly + // the same pixel. If that's the case, ignore the + // click, and pass on the event to low-prio + // handlers. This effectively permits effect + // advancements via clicks also when user paint is + // enabled. + if( mbIsLastMouseDownPosValid && + ::basegfx::B2DPoint( e.X, + e.Y ) == maLastMouseDownPos ) + { + mbIsLastMouseDownPosValid = false; + return false; + } + + // invalidate, next downpress will have to start a new + // polygon. + mbIsLastPointValid = false; + + // eat mouse click (though we don't process it + // _directly_, it enables the drag mode + return true; + } + + virtual bool handleMouseDragged( const awt::MouseEvent& e ) override + { + if( !mbActive ) + return false; + + if (e.Buttons == awt::MouseButton::RIGHT) + { + mbIsLastPointValid = false; + return false; + } + + if(mbIsEraseModeActivated) + { + //define the last point as an object + //we suppose that there's no way this point could be valid + ::basegfx::B2DPolygon aPoly; + + maLastPoint.setX( e.X-mnSize ); + maLastPoint.setY( e.Y-mnSize ); + + aPoly.append( maLastPoint ); + + maLastPoint.setX( e.X-mnSize ); + maLastPoint.setY( e.Y+mnSize ); + + aPoly.append( maLastPoint ); + maLastPoint.setX( e.X+mnSize ); + maLastPoint.setY( e.Y+mnSize ); + + aPoly.append( maLastPoint ); + maLastPoint.setX( e.X+mnSize ); + maLastPoint.setY( e.Y-mnSize ); + + aPoly.append( maLastPoint ); + maLastPoint.setX( e.X-mnSize ); + maLastPoint.setY( e.Y-mnSize ); + + aPoly.append( maLastPoint ); + + //now we have defined a Polygon that is closed + + //The point is to redraw the LastPoint the way it was originally on the bitmap, + //of the slide + for (const auto& rxView : maViews) + { + + //get via SlideImpl instance the bitmap of the slide unmodified to redraw it + SlideBitmapSharedPtr pBitmap( mrSlide.getCurrentSlideBitmap( rxView ) ); + ::cppcanvas::CanvasSharedPtr pCanvas( rxView->getCanvas() ); + + ::basegfx::B2DHomMatrix aViewTransform( rxView->getTransformation() ); + const ::basegfx::B2DPoint aOutPosPixel( aViewTransform * ::basegfx::B2DPoint() ); + + // setup a canvas with device coordinate space, the slide + // bitmap already has the correct dimension. + ::cppcanvas::CanvasSharedPtr pDevicePixelCanvas( pCanvas->clone() ); + + pDevicePixelCanvas->setTransformation( ::basegfx::B2DHomMatrix() ); + + // render at given output position + pBitmap->move( aOutPosPixel ); + + ::basegfx::B2DPolyPolygon aPolyPoly(aPoly); + aViewTransform.translate(-aOutPosPixel.getX(), -aOutPosPixel.getY()); + aPolyPoly.transform(aViewTransform); + // set clip so that we just redraw a part of the canvas + pBitmap->clip(aPolyPoly); + pBitmap->draw( pDevicePixelCanvas ); + + mrScreenUpdater.notifyUpdate(rxView,true); + } + + } + else + { + if( !mbIsLastPointValid ) + { + mbIsLastPointValid = true; + maLastPoint.setX( e.X ); + maLastPoint.setY( e.Y ); + } + else + { + ::basegfx::B2DPolygon aPoly; + aPoly.append( maLastPoint ); + + maLastPoint.setX( e.X ); + maLastPoint.setY( e.Y ); + + aPoly.append( maLastPoint ); + + // paint to all views + for (const auto& rxView : maViews) + { + ::cppcanvas::PolyPolygonSharedPtr pPolyPoly( + ::cppcanvas::BaseGfxFactory::createPolyPolygon( rxView->getCanvas(), + aPoly ) ); + + if( pPolyPoly ) + { + pPolyPoly->setStrokeWidth(mnStrokeWidth); + pPolyPoly->setRGBALineColor( maStrokeColor.getIntegerColor() ); + pPolyPoly->draw(); + maPolygons.push_back(pPolyPoly); + } + } + + // screen update necessary to show painting + mrScreenUpdater.notifyUpdate(); + } + } + // mouse events captured + return true; + } + + virtual bool handleMouseMoved( const awt::MouseEvent& /*e*/ ) override + { + // not used here + return false; // did not handle the event + } + + private: + ScreenUpdater& mrScreenUpdater; + UnoViewVector maViews; + PolyPolygonVector maPolygons; + RGBColor maStrokeColor; + double mnStrokeWidth; + basegfx::B2DPoint maLastPoint; + basegfx::B2DPoint maLastMouseDownPos; + bool mbIsLastPointValid; + bool mbIsLastMouseDownPosValid; + // added bool for erasing purpose : + bool mbIsEraseAllModeActivated; + bool mbIsEraseModeActivated; + Slide& mrSlide; + sal_Int32 mnSize; + bool mbActive; + }; + + UserPaintOverlaySharedPtr UserPaintOverlay::create( const RGBColor& rStrokeColor, + double nStrokeWidth, + const SlideShowContext& rContext, + PolyPolygonVector&& rPolygons, + bool bActive ) + { + UserPaintOverlaySharedPtr pRet( new UserPaintOverlay( rStrokeColor, + nStrokeWidth, + rContext, + std::move(rPolygons), + bActive)); + + return pRet; + } + + UserPaintOverlay::UserPaintOverlay( const RGBColor& rStrokeColor, + double nStrokeWidth, + const SlideShowContext& rContext, + PolyPolygonVector&& rPolygons, + bool bActive ) : + mpHandler( std::make_shared<PaintOverlayHandler>( rStrokeColor, + nStrokeWidth, + rContext.mrScreenUpdater, + rContext.mrViewContainer, + //adding a link to Slide + dynamic_cast<Slide&>(rContext.mrCursorManager), + std::move(rPolygons), bActive )), + mrMultiplexer( rContext.mrEventMultiplexer ) + { + mrMultiplexer.addClickHandler( mpHandler, 3.0 ); + mrMultiplexer.addMouseMoveHandler( mpHandler, 3.0 ); + mrMultiplexer.addViewHandler( mpHandler ); + mrMultiplexer.addUserPaintHandler(mpHandler); + } + + PolyPolygonVector const & UserPaintOverlay::getPolygons() const + { + return mpHandler->getPolygons(); + } + + void UserPaintOverlay::drawPolygons() + { + mpHandler->drawPolygons(); + } + + UserPaintOverlay::~UserPaintOverlay() + { + try + { + mrMultiplexer.removeMouseMoveHandler( mpHandler ); + mrMultiplexer.removeClickHandler( mpHandler ); + mrMultiplexer.removeViewHandler( mpHandler ); + mpHandler->dispose(); + } + catch (const uno::Exception&) + { + TOOLS_WARN_EXCEPTION("slideshow", ""); + } + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slide/userpaintoverlay.hxx b/slideshow/source/engine/slide/userpaintoverlay.hxx new file mode 100644 index 000000000..ae443a0fa --- /dev/null +++ b/slideshow/source/engine/slide/userpaintoverlay.hxx @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#ifndef INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_USERPAINTOVERLAY_HXX +#define INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_USERPAINTOVERLAY_HXX + +#include <cppcanvas/canvasgraphic.hxx> + +#include <rgbcolor.hxx> + +#include <memory> +#include <vector> + +/* Definition of UserPaintOverlay class */ + +namespace slideshow::internal + { + class EventMultiplexer; + struct SlideShowContext; + + class PaintOverlayHandler; + typedef ::std::shared_ptr< class UserPaintOverlay > UserPaintOverlaySharedPtr; + typedef ::std::vector< ::cppcanvas::PolyPolygonSharedPtr> PolyPolygonVector; + /** Slide overlay, which can be painted into by the user. + + This class registers itself at the EventMultiplexer, + listening for mouse clicks and moves. When the mouse is + dragged, a hand sketching in the selected color is shown. + */ + class UserPaintOverlay + { + public: + /** Create a UserPaintOverlay + + @param rStrokeColor + Color to use for drawing + + @param nStrokeWidth + Width of the stroked path + */ + static UserPaintOverlaySharedPtr create( const RGBColor& rStrokeColor, + double nStrokeWidth, + const SlideShowContext& rContext, + PolyPolygonVector&& rPolygons, + bool bActive); + ~UserPaintOverlay(); + UserPaintOverlay(const UserPaintOverlay&) = delete; + UserPaintOverlay& operator=(const UserPaintOverlay&) = delete; + PolyPolygonVector const & getPolygons() const; + void drawPolygons(); + + private: + UserPaintOverlay( const RGBColor& rStrokeColor, + double nStrokeWidth, + const SlideShowContext& rContext, + PolyPolygonVector&& rPolygons, + bool bActive ); + + ::std::shared_ptr<PaintOverlayHandler> mpHandler; + EventMultiplexer& mrMultiplexer; + }; + +} + +#endif // INCLUDED_SLIDESHOW_SOURCE_ENGINE_SLIDE_USERPAINTOVERLAY_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slidebitmap.cxx b/slideshow/source/engine/slidebitmap.cxx new file mode 100644 index 000000000..368cb070c --- /dev/null +++ b/slideshow/source/engine/slidebitmap.cxx @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <tools/diagnose_ex.h> +#include <slidebitmap.hxx> + +#include <com/sun/star/rendering/XCanvas.hpp> +#include <com/sun/star/rendering/XBitmap.hpp> + +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> + +#include <canvas/canvastools.hxx> +#include <basegfx/utils/canvastools.hxx> + + +using namespace ::com::sun::star; + +namespace slideshow::internal +{ + + SlideBitmap::SlideBitmap( const ::cppcanvas::BitmapSharedPtr& rBitmap ) : + maOutputPos(), + maClipPoly(), + mxBitmap() + { + if( rBitmap ) + mxBitmap = rBitmap->getUNOBitmap(); + + ENSURE_OR_THROW( mxBitmap.is(), "SlideBitmap::SlideBitmap(): Invalid bitmap" ); + } + + bool SlideBitmap::draw( const ::cppcanvas::CanvasSharedPtr& rCanvas ) const + { + ENSURE_OR_RETURN_FALSE( rCanvas && rCanvas->getUNOCanvas().is(), + "SlideBitmap::draw(): Invalid canvas" ); + + // selectively only copy the transformation from current viewstate, + // don't want no clipping here. + rendering::ViewState aViewState; + aViewState.AffineTransform = rCanvas->getViewState().AffineTransform; + + rendering::RenderState aRenderState; + ::canvas::tools::initRenderState( aRenderState ); + + const basegfx::B2DHomMatrix aTranslation(basegfx::utils::createTranslateB2DHomMatrix(maOutputPos)); + ::canvas::tools::setRenderStateTransform( aRenderState, aTranslation ); + + try + { + if( maClipPoly.count() ) + { + // TODO(P1): Buffer the clip polygon + aRenderState.Clip = + ::basegfx::unotools::xPolyPolygonFromB2DPolyPolygon( + rCanvas->getUNOCanvas()->getDevice(), + maClipPoly ); + } + + rCanvas->getUNOCanvas()->drawBitmap( mxBitmap, + aViewState, + aRenderState ); + } + catch( uno::Exception& ) + { + TOOLS_WARN_EXCEPTION( "slideshow", "" ); + return false; + } + + return true; + } + + ::basegfx::B2ISize SlideBitmap::getSize() const + { + return ::basegfx::unotools::b2ISizeFromIntegerSize2D( mxBitmap->getSize() ); + } + + void SlideBitmap::move( const ::basegfx::B2DPoint& rNewPos ) + { + maOutputPos = rNewPos; + } + + void SlideBitmap::clip( const ::basegfx::B2DPolyPolygon& rClipPoly ) + { + maClipPoly = rClipPoly; + } + + const css::uno::Reference< css::rendering::XBitmap >& SlideBitmap::getXBitmap() const + { + return mxBitmap; + } + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slideshowcontext.cxx b/slideshow/source/engine/slideshowcontext.cxx new file mode 100644 index 000000000..f0433b9d8 --- /dev/null +++ b/slideshow/source/engine/slideshowcontext.cxx @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <com/sun/star/uno/Reference.hxx> +#include <com/sun/star/uno/XComponentContext.hpp> + +#include <slideshowcontext.hxx> +#include <subsettableshapemanager.hxx> +#include <screenupdater.hxx> +#include <eventqueue.hxx> +#include <activitiesqueue.hxx> +#include <usereventqueue.hxx> +#include <eventmultiplexer.hxx> +#include <unoviewcontainer.hxx> + + +using namespace ::com::sun::star; + +namespace slideshow::internal +{ + +SlideShowContext::SlideShowContext( SubsettableShapeManagerSharedPtr& rSubsettableShapeManager, + EventQueue& rEventQueue, + EventMultiplexer& rEventMultiplexer, + ScreenUpdater& rScreenUpdater, + ActivitiesQueue& rActivitiesQueue, + UserEventQueue& rUserEventQueue, + CursorManager& rCursorManager, + MediaFileManager& rMediaFileManager, + const UnoViewContainer& rViewContainer, + const uno::Reference< + uno::XComponentContext>& rComponentContext, + box2d::utils::Box2DWorldSharedPtr& rBox2DWorldPtr ) : + mpSubsettableShapeManager( rSubsettableShapeManager ), + mrEventQueue( rEventQueue ), + mrEventMultiplexer( rEventMultiplexer ), + mrScreenUpdater( rScreenUpdater ), + mrActivitiesQueue( rActivitiesQueue ), + mrUserEventQueue( rUserEventQueue ), + mrCursorManager( rCursorManager ), + mrMediaFileManager( rMediaFileManager ), + mrViewContainer( rViewContainer ), + mxComponentContext( rComponentContext ), + mpBox2DWorld( rBox2DWorldPtr ) + {} + +void SlideShowContext::dispose() +{ + mxComponentContext.clear(); +} + +} // namespace slideshow::internal + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slideshowimpl.cxx b/slideshow/source/engine/slideshowimpl.cxx new file mode 100644 index 000000000..75b15e5c7 --- /dev/null +++ b/slideshow/source/engine/slideshowimpl.cxx @@ -0,0 +1,2439 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + + +#include <config_features.h> +#include <tools/diagnose_ex.h> + +#include <cppuhelper/basemutex.hxx> +#include <cppuhelper/compbase.hxx> +#include <cppuhelper/interfacecontainer.h> +#include <cppuhelper/supportsservice.hxx> + +#include <comphelper/interfacecontainer3.hxx> +#include <comphelper/scopeguard.hxx> +#include <comphelper/storagehelper.hxx> +#include <cppcanvas/polypolygon.hxx> +#include <osl/thread.hxx> + +#include <tools/debug.hxx> + +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/utils/canvastools.hxx> + +#include <sal/log.hxx> + +#include <com/sun/star/beans/XPropertySet.hpp> +#include <com/sun/star/util/XUpdatable.hpp> +#include <com/sun/star/awt/SystemPointer.hpp> +#include <com/sun/star/presentation/XSlideShow.hpp> +#include <com/sun/star/presentation/XSlideShowListener.hpp> +#include <com/sun/star/lang/NoSupportException.hpp> +#include <com/sun/star/lang/XMultiServiceFactory.hpp> +#include <com/sun/star/lang/XServiceInfo.hpp> +#include <com/sun/star/drawing/PointSequenceSequence.hpp> +#include <com/sun/star/drawing/PointSequence.hpp> +#include <com/sun/star/drawing/XLayer.hpp> +#include <com/sun/star/drawing/XLayerSupplier.hpp> +#include <com/sun/star/drawing/XLayerManager.hpp> +#include <com/sun/star/container/XNameAccess.hpp> +#include <com/sun/star/document/XStorageBasedDocument.hpp> + +#include <com/sun/star/uno/Reference.hxx> +#include <com/sun/star/loader/CannotActivateFactoryException.hpp> + +#include <unoviewcontainer.hxx> +#include <transitionfactory.hxx> +#include <eventmultiplexer.hxx> +#include <usereventqueue.hxx> +#include <eventqueue.hxx> +#include <cursormanager.hxx> +#include <mediafilemanager.hxx> +#include <slideshowcontext.hxx> +#include <activitiesqueue.hxx> +#include <activitiesfactory.hxx> +#include <interruptabledelayevent.hxx> +#include <slide.hxx> +#include <shapemaps.hxx> +#include <slideview.hxx> +#include <tools.hxx> +#include <unoview.hxx> +#include "rehearsetimingsactivity.hxx" +#include "waitsymbol.hxx" +#include "effectrewinder.hxx" +#include <framerate.hxx> +#include "pointersymbol.hxx" + +#include <map> +#include <vector> +#include <algorithm> + +using namespace com::sun::star; +using namespace ::slideshow::internal; + +namespace box2d::utils { class box2DWorld; + typedef ::std::shared_ptr< box2DWorld > Box2DWorldSharedPtr; } + +namespace { + +/** During animations the update() method tells its caller to call it as + soon as possible. This gives us more time to render the next frame and + still maintain a steady frame rate. This class is responsible for + synchronizing the display of new frames and thus keeping the frame rate + steady. +*/ +class FrameSynchronization +{ +public: + /** Create new object with a predefined duration between two frames. + @param nFrameDuration + The preferred duration between the display of two frames in + seconds. + */ + explicit FrameSynchronization (const double nFrameDuration); + + /** Set the current time as the time at which the current frame is + displayed. From this the target time of the next frame is derived. + */ + void MarkCurrentFrame(); + + /** When there is time left until the next frame is due then wait. + Otherwise return without delay. + */ + void Synchronize(); + + /** Activate frame synchronization when an animation is active and + frames are to be displayed in a steady rate. While active + Synchronize() will wait until the frame duration time has passed. + */ + void Activate(); + + /** Deactivate frame synchronization when no animation is active and the + time between frames depends on user actions and other external + sources. While deactivated Synchronize() will return without delay. + */ + void Deactivate(); + +private: + /** The timer that is used for synchronization is independent from the + one used by SlideShowImpl: it is not paused or modified by + animations. + */ + canvas::tools::ElapsedTime maTimer; + /** Time between the display of frames. Enforced only when mbIsActive + is <TRUE/>. + */ + const double mnFrameDuration; + /** Time (of maTimer) when the next frame shall be displayed. + Synchronize() will wait until this time. + */ + double mnNextFrameTargetTime; + /** Synchronize() will wait only when this flag is <TRUE/>. Otherwise + it returns immediately. + */ + bool mbIsActive; +}; + +/****************************************************************************** + + SlideShowImpl + + This class encapsulates the slideshow presentation viewer. + + With an instance of this class, it is possible to statically + and dynamically show a presentation, as defined by the + constructor-provided draw model (represented by a sequence + of css::drawing::XDrawPage objects). + + It is possible to show the presentation on multiple views + simultaneously (e.g. for a multi-monitor setup). Since this + class also relies on user interaction, the corresponding + XSlideShowView interface provides means to register some UI + event listeners (mostly borrowed from awt::XWindow interface). + + Since currently (mid 2004), OOo isn't very well suited to + multi-threaded rendering, this class relies on <em>very + frequent</em> external update() calls, which will render the + next frame of animations. This works as follows: after the + displaySlide() has been successfully called (which setup and + starts an actual slide show), the update() method must be + called until it returns false. + Effectively, this puts the burden of providing + concurrency to the clients of this class, which, as noted + above, is currently unavoidable with the current state of + affairs (I've actually tried threading here, but failed + miserably when using the VCL canvas as the render backend - + deadlocked). + + ******************************************************************************/ + +typedef cppu::WeakComponentImplHelper<css::lang::XServiceInfo, presentation::XSlideShow> SlideShowImplBase; + +typedef ::std::vector< ::cppcanvas::PolyPolygonSharedPtr> PolyPolygonVector; + +/// Maps XDrawPage for annotations persistence +typedef ::std::map< css::uno::Reference< + css::drawing::XDrawPage>, + PolyPolygonVector> PolygonMap; + +class SlideShowImpl : private cppu::BaseMutex, + public CursorManager, + public MediaFileManager, + public SlideShowImplBase +{ +public: + explicit SlideShowImpl( + uno::Reference<uno::XComponentContext> const& xContext ); + + /** Notify that the transition phase of the current slide + has ended. + + The life of a slide has three phases: the transition + phase, when the previous slide vanishes, and the + current slide becomes visible, the shape animation + phase, when shape effects are running, and the phase + after the last shape animation has ended, but before + the next slide transition starts. + + This method notifies the end of the first phase. + + @param bPaintSlide + When true, Slide::show() is passed a true as well, denoting + explicit paint of slide content. Pass false here, if e.g. a + slide transition has already rendered the initial slide image. + */ + void notifySlideTransitionEnded( bool bPaintSlide ); + + /** Notify that the shape animation phase of the current slide + has ended. + + The life of a slide has three phases: the transition + phase, when the previous slide vanishes, and the + current slide becomes visible, the shape animation + phase, when shape effects are running, and the phase + after the last shape animation has ended, but before + the next slide transition starts. + + This method notifies the end of the second phase. + */ + void notifySlideAnimationsEnded(); + + /** Notify that the slide has ended. + + The life of a slide has three phases: the transition + phase, when the previous slide vanishes, and the + current slide becomes visible, the shape animation + phase, when shape effects are running, and the phase + after the last shape animation has ended, but before + the next slide transition starts. + + This method notifies the end of the third phase. + */ + void notifySlideEnded (const bool bReverse); + + /** Notification from eventmultiplexer that a hyperlink + has been clicked. + */ + bool notifyHyperLinkClicked( OUString const& hyperLink ); + + /** Notification from eventmultiplexer that an animation event has occurred. + This will be forwarded to all registered XSlideShowListener + */ + bool handleAnimationEvent( const AnimationNodeSharedPtr& rNode ); + + /** Obtain a MediaTempFile for the specified url. */ + virtual std::shared_ptr<avmedia::MediaTempFile> getMediaTempFile(const OUString& aUrl) override; + +private: + // XServiceInfo + virtual OUString SAL_CALL getImplementationName() override; + virtual sal_Bool SAL_CALL supportsService(const OUString& ServiceName) override; + virtual css::uno::Sequence< OUString > SAL_CALL getSupportedServiceNames () override; + + // XSlideShow: + virtual sal_Bool SAL_CALL nextEffect() override; + virtual sal_Bool SAL_CALL previousEffect() override; + virtual sal_Bool SAL_CALL startShapeActivity( + uno::Reference<drawing::XShape> const& xShape ) override; + virtual sal_Bool SAL_CALL stopShapeActivity( + uno::Reference<drawing::XShape> const& xShape ) override; + virtual sal_Bool SAL_CALL pause( sal_Bool bPauseShow ) override; + virtual uno::Reference<drawing::XDrawPage> SAL_CALL getCurrentSlide() override; + virtual void SAL_CALL displaySlide( + uno::Reference<drawing::XDrawPage> const& xSlide, + uno::Reference<drawing::XDrawPagesSupplier> const& xDrawPages, + uno::Reference<animations::XAnimationNode> const& xRootNode, + uno::Sequence<beans::PropertyValue> const& rProperties ) override; + virtual void SAL_CALL registerUserPaintPolygons( const css::uno::Reference< css::lang::XMultiServiceFactory >& xDocFactory ) override; + virtual sal_Bool SAL_CALL setProperty( + beans::PropertyValue const& rProperty ) override; + virtual sal_Bool SAL_CALL addView( + uno::Reference<presentation::XSlideShowView> const& xView ) override; + virtual sal_Bool SAL_CALL removeView( + uno::Reference<presentation::XSlideShowView> const& xView ) override; + virtual sal_Bool SAL_CALL update( double & nNextTimeout ) override; + virtual void SAL_CALL addSlideShowListener( + uno::Reference<presentation::XSlideShowListener> const& xListener ) override; + virtual void SAL_CALL removeSlideShowListener( + uno::Reference<presentation::XSlideShowListener> const& xListener ) override; + virtual void SAL_CALL addShapeEventListener( + uno::Reference<presentation::XShapeEventListener> const& xListener, + uno::Reference<drawing::XShape> const& xShape ) override; + virtual void SAL_CALL removeShapeEventListener( + uno::Reference<presentation::XShapeEventListener> const& xListener, + uno::Reference<drawing::XShape> const& xShape ) override; + virtual void SAL_CALL setShapeCursor( + uno::Reference<drawing::XShape> const& xShape, sal_Int16 nPointerShape ) override; + + // CursorManager + + + virtual bool requestCursor( sal_Int16 nCursorShape ) override; + virtual void resetCursor() override; + + /** This is somewhat similar to displaySlide when called for the current + slide. It has been simplified to take advantage of that no slide + change takes place. Furthermore it does not show the slide + transition. + */ + void redisplayCurrentSlide(); + +protected: + // WeakComponentImplHelperBase + virtual void SAL_CALL disposing() override; + + bool isDisposed() const + { + return (rBHelper.bDisposed || rBHelper.bInDispose); + } + +private: + struct SeparateListenerImpl; friend struct SeparateListenerImpl; + class PrefetchPropertiesFunc; friend class PrefetchPropertiesFunc; + + /// Stop currently running show. + void stopShow(); + + ///Find a polygons vector in maPolygons (map) + PolygonMap::iterator findPolygons( uno::Reference<drawing::XDrawPage> const& xDrawPage); + + /// Creates a new slide. + SlideSharedPtr makeSlide( + uno::Reference<drawing::XDrawPage> const& xDrawPage, + uno::Reference<drawing::XDrawPagesSupplier> const& xDrawPages, + uno::Reference<animations::XAnimationNode> const& xRootNode ); + + /// Checks whether the given slide/animation node matches mpPrefetchSlide + static bool matches( + SlideSharedPtr const& pSlide, + uno::Reference<drawing::XDrawPage> const& xSlide, + uno::Reference<animations::XAnimationNode> const& xNode ) + { + if (pSlide) + return (pSlide->getXDrawPage() == xSlide && + pSlide->getXAnimationNode() == xNode); + else + return (!xSlide.is() && !xNode.is()); + } + + /// Resets the current slide transition sound object with a new one: + SoundPlayerSharedPtr resetSlideTransitionSound( + uno::Any const& url, bool bLoopSound ); + + /// stops the current slide transition sound + void stopSlideTransitionSound(); + + /** Prepare a slide transition + + This method registers all necessary events and + activities for a slide transition. + + @return the slide change activity, or NULL for no transition effect + */ + ActivitySharedPtr createSlideTransition( + const uno::Reference< drawing::XDrawPage >& xDrawPage, + const SlideSharedPtr& rLeavingSlide, + const SlideSharedPtr& rEnteringSlide, + const EventSharedPtr& rTransitionEndEvent ); + + /** Request/release the wait symbol. The wait symbol is displayed when + there are more requests then releases. Locking the wait symbol + helps to avoid intermediate repaints. + + Do not call this method directly. Use WaitSymbolLock instead. + */ + void requestWaitSymbol(); + void releaseWaitSymbol(); + + class WaitSymbolLock {public: + explicit WaitSymbolLock(SlideShowImpl& rSlideShowImpl) : mrSlideShowImpl(rSlideShowImpl) + { mrSlideShowImpl.requestWaitSymbol(); } + ~WaitSymbolLock() + { mrSlideShowImpl.releaseWaitSymbol(); } + private: SlideShowImpl& mrSlideShowImpl; + }; + + /// Filter requested cursor shape against hard slideshow cursors (wait, etc.) + sal_Int16 calcActiveCursor( sal_Int16 nCursorShape ) const; + + /** This method is called asynchronously to finish the rewinding of an + effect to the previous slide that was initiated earlier. + */ + void rewindEffectToPreviousSlide(); + + /// all registered views + UnoViewContainer maViewContainer; + + /// all registered slide show listeners + comphelper::OInterfaceContainerHelper3<presentation::XSlideShowListener> maListenerContainer; + + /// map of vectors, containing all registered listeners for a shape + ShapeEventListenerMap maShapeEventListeners; + /// map of sal_Int16 values, specifying the mouse cursor for every shape + ShapeCursorMap maShapeCursors; + + //map of vector of Polygons, containing polygons drawn on each slide. + PolygonMap maPolygons; + + std::optional<RGBColor> maUserPaintColor; + + double maUserPaintStrokeWidth; + + //changed for the eraser project + std::optional<bool> maEraseAllInk; + std::optional<sal_Int32> maEraseInk; + //end changed + + std::shared_ptr<canvas::tools::ElapsedTime> mpPresTimer; + ScreenUpdater maScreenUpdater; + EventQueue maEventQueue; + EventMultiplexer maEventMultiplexer; + ActivitiesQueue maActivitiesQueue; + UserEventQueue maUserEventQueue; + SubsettableShapeManagerSharedPtr mpDummyPtr; + box2d::utils::Box2DWorldSharedPtr mpBox2DDummyPtr; + + std::shared_ptr<SeparateListenerImpl> mpListener; + + std::shared_ptr<RehearseTimingsActivity> mpRehearseTimingsActivity; + std::shared_ptr<WaitSymbol> mpWaitSymbol; + + std::shared_ptr<PointerSymbol> mpPointerSymbol; + + /// the current slide transition sound object: + SoundPlayerSharedPtr mpCurrentSlideTransitionSound; + + uno::Reference<uno::XComponentContext> mxComponentContext; + uno::Reference< + presentation::XTransitionFactory> mxOptionalTransitionFactory; + + /// the previously running slide + SlideSharedPtr mpPreviousSlide; + /// the currently running slide + SlideSharedPtr mpCurrentSlide; + /// the already prefetched slide: best candidate for upcoming slide + SlideSharedPtr mpPrefetchSlide; + /// slide to be prefetched: best candidate for upcoming slide + uno::Reference<drawing::XDrawPage> mxPrefetchSlide; + /// save the XDrawPagesSupplier to retrieve polygons + uno::Reference<drawing::XDrawPagesSupplier> mxDrawPagesSupplier; + /// Used by MediaFileManager, for media files with package url. + uno::Reference<document::XStorageBasedDocument> mxSBD; + /// slide animation to be prefetched: + uno::Reference<animations::XAnimationNode> mxPrefetchAnimationNode; + + sal_Int16 mnCurrentCursor; + + sal_Int32 mnWaitSymbolRequestCount; + bool mbAutomaticAdvancementMode; + bool mbImageAnimationsAllowed; + bool mbNoSlideTransitions; + bool mbMouseVisible; + bool mbForceManualAdvance; + bool mbShowPaused; + bool mbSlideShowIdle; + bool mbDisableAnimationZOrder; + + EffectRewinder maEffectRewinder; + FrameSynchronization maFrameSynchronization; +}; + +/** Separate event listener for animation, view and hyperlink events. + + This handler is registered for slide animation end, view and + hyperlink events at the global EventMultiplexer, and forwards + notifications to the SlideShowImpl +*/ +struct SlideShowImpl::SeparateListenerImpl : public EventHandler, + public ViewRepaintHandler, + public HyperlinkHandler, + public AnimationEventHandler +{ + SlideShowImpl& mrShow; + ScreenUpdater& mrScreenUpdater; + EventQueue& mrEventQueue; + + SeparateListenerImpl( SlideShowImpl& rShow, + ScreenUpdater& rScreenUpdater, + EventQueue& rEventQueue ) : + mrShow( rShow ), + mrScreenUpdater( rScreenUpdater ), + mrEventQueue( rEventQueue ) + {} + + SeparateListenerImpl( const SeparateListenerImpl& ) = delete; + SeparateListenerImpl& operator=( const SeparateListenerImpl& ) = delete; + + // EventHandler + virtual bool handleEvent() override + { + // DON't call notifySlideAnimationsEnded() + // directly, but queue an event. handleEvent() + // might be called from e.g. + // showNext(), and notifySlideAnimationsEnded() must not be called + // in recursion. Note that the event is scheduled for the next + // frame so that its expensive execution does not come in between + // sprite hiding and shape redraw (at the end of the animation of a + // shape), which would cause a flicker. + mrEventQueue.addEventForNextRound( + makeEvent( [this] () { this->mrShow.notifySlideAnimationsEnded(); }, + "SlideShowImpl::notifySlideAnimationsEnded")); + return true; + } + + // ViewRepaintHandler + virtual void viewClobbered( const UnoViewSharedPtr& rView ) override + { + // given view needs repaint, request update + mrScreenUpdater.notifyUpdate(rView, true); + } + + // HyperlinkHandler + virtual bool handleHyperlink( OUString const& rLink ) override + { + return mrShow.notifyHyperLinkClicked(rLink); + } + + // AnimationEventHandler + virtual bool handleAnimationEvent( const AnimationNodeSharedPtr& rNode ) override + { + return mrShow.handleAnimationEvent(rNode); + } +}; + +SlideShowImpl::SlideShowImpl( + uno::Reference<uno::XComponentContext> const& xContext ) + : SlideShowImplBase(m_aMutex), + maViewContainer(), + maListenerContainer( m_aMutex ), + maShapeEventListeners(), + maShapeCursors(), + maUserPaintColor(), + maUserPaintStrokeWidth(4.0), + mpPresTimer( std::make_shared<canvas::tools::ElapsedTime>() ), + maScreenUpdater(maViewContainer), + maEventQueue( mpPresTimer ), + maEventMultiplexer( maEventQueue, + maViewContainer ), + maActivitiesQueue( mpPresTimer ), + maUserEventQueue( maEventMultiplexer, + maEventQueue, + *this ), + mpDummyPtr(), + mpBox2DDummyPtr(), + mpListener(), + mpRehearseTimingsActivity(), + mpWaitSymbol(), + mpPointerSymbol(), + mpCurrentSlideTransitionSound(), + mxComponentContext( xContext ), + mxOptionalTransitionFactory(), + mpCurrentSlide(), + mpPrefetchSlide(), + mxPrefetchSlide(), + mxDrawPagesSupplier(), + mxSBD(), + mxPrefetchAnimationNode(), + mnCurrentCursor(awt::SystemPointer::ARROW), + mnWaitSymbolRequestCount(0), + mbAutomaticAdvancementMode(false), + mbImageAnimationsAllowed( true ), + mbNoSlideTransitions( false ), + mbMouseVisible( true ), + mbForceManualAdvance( false ), + mbShowPaused( false ), + mbSlideShowIdle( true ), + mbDisableAnimationZOrder( false ), + maEffectRewinder(maEventMultiplexer, maEventQueue, maUserEventQueue), + maFrameSynchronization(1.0 / FrameRate::PreferredFramesPerSecond) + +{ + // keep care not constructing any UNO references to this inside ctor, + // shift that code to create()! + + uno::Reference<lang::XMultiComponentFactory> xFactory( + mxComponentContext->getServiceManager() ); + + if( xFactory.is() ) + { + try + { + // #i82460# try to retrieve special transition factory + mxOptionalTransitionFactory.set( + xFactory->createInstanceWithContext( + "com.sun.star.presentation.TransitionFactory", + mxComponentContext ), + uno::UNO_QUERY ); + } + catch (loader::CannotActivateFactoryException const&) + { + } + } + + mpListener = std::make_shared<SeparateListenerImpl>( + *this, + maScreenUpdater, + maEventQueue ); + maEventMultiplexer.addSlideAnimationsEndHandler( mpListener ); + maEventMultiplexer.addViewRepaintHandler( mpListener ); + maEventMultiplexer.addHyperlinkHandler( mpListener, 0.0 ); + maEventMultiplexer.addAnimationStartHandler( mpListener ); + maEventMultiplexer.addAnimationEndHandler( mpListener ); +} + +// we are about to be disposed (someone call dispose() on us) +void SlideShowImpl::disposing() +{ + osl::MutexGuard const guard( m_aMutex ); + + maEffectRewinder.dispose(); + + // stop slide transition sound, if any: + stopSlideTransitionSound(); + + mxComponentContext.clear(); + + if( mpCurrentSlideTransitionSound ) + { + mpCurrentSlideTransitionSound->dispose(); + mpCurrentSlideTransitionSound.reset(); + } + + mpWaitSymbol.reset(); + mpPointerSymbol.reset(); + + if( mpRehearseTimingsActivity ) + { + mpRehearseTimingsActivity->dispose(); + mpRehearseTimingsActivity.reset(); + } + + if( mpListener ) + { + maEventMultiplexer.removeSlideAnimationsEndHandler(mpListener); + maEventMultiplexer.removeViewRepaintHandler(mpListener); + maEventMultiplexer.removeHyperlinkHandler(mpListener); + maEventMultiplexer.removeAnimationStartHandler( mpListener ); + maEventMultiplexer.removeAnimationEndHandler( mpListener ); + + mpListener.reset(); + } + + maUserEventQueue.clear(); + maActivitiesQueue.clear(); + maEventMultiplexer.clear(); + maEventQueue.clear(); + mpPresTimer.reset(); + maShapeCursors.clear(); + maShapeEventListeners.clear(); + + // send all listeners a disposing() that we are going down: + maListenerContainer.disposeAndClear( + lang::EventObject( static_cast<cppu::OWeakObject *>(this) ) ); + + maViewContainer.dispose(); + + // release slides: + mxPrefetchAnimationNode.clear(); + mxPrefetchSlide.clear(); + mpPrefetchSlide.reset(); + mpCurrentSlide.reset(); + mpPreviousSlide.reset(); +} + +uno::Sequence< OUString > SAL_CALL SlideShowImpl::getSupportedServiceNames() +{ + return { "com.sun.star.presentation.SlideShow" }; +} + +OUString SAL_CALL SlideShowImpl::getImplementationName() +{ + return "com.sun.star.comp.presentation.SlideShow"; +} + +sal_Bool SAL_CALL SlideShowImpl::supportsService(const OUString& aServiceName) +{ + return cppu::supportsService(this, aServiceName); +} + +/// stops the current slide transition sound +void SlideShowImpl::stopSlideTransitionSound() +{ + if (mpCurrentSlideTransitionSound) + { + mpCurrentSlideTransitionSound->stopPlayback(); + mpCurrentSlideTransitionSound->dispose(); + mpCurrentSlideTransitionSound.reset(); + } + } + +SoundPlayerSharedPtr SlideShowImpl::resetSlideTransitionSound( const uno::Any& rSound, bool bLoopSound ) +{ + bool bStopSound = false; + OUString url; + + if( !(rSound >>= bStopSound) ) + bStopSound = false; + rSound >>= url; + + if( !bStopSound && url.isEmpty() ) + return SoundPlayerSharedPtr(); + + stopSlideTransitionSound(); + + if (!url.isEmpty()) + { + try + { + mpCurrentSlideTransitionSound = SoundPlayer::create( + maEventMultiplexer, url, mxComponentContext, *this); + mpCurrentSlideTransitionSound->setPlaybackLoop( bLoopSound ); + } + catch (lang::NoSupportException const&) + { + // catch possible exceptions from SoundPlayer, since + // being not able to playback the sound is not a hard + // error here (still, the slide transition should be + // shown). + } + } + return mpCurrentSlideTransitionSound; +} + +ActivitySharedPtr SlideShowImpl::createSlideTransition( + const uno::Reference< drawing::XDrawPage >& xDrawPage, + const SlideSharedPtr& rLeavingSlide, + const SlideSharedPtr& rEnteringSlide, + const EventSharedPtr& rTransitionEndEvent) +{ + ENSURE_OR_THROW( !maViewContainer.empty(), + "createSlideTransition(): No views" ); + ENSURE_OR_THROW( rEnteringSlide, + "createSlideTransition(): No entering slide" ); + + // return empty transition, if slide transitions + // are disabled. + if (mbNoSlideTransitions) + return ActivitySharedPtr(); + + // retrieve slide change parameters from XDrawPage + uno::Reference< beans::XPropertySet > xPropSet( xDrawPage, + uno::UNO_QUERY ); + + if( !xPropSet.is() ) + { + SAL_INFO("slideshow", "createSlideTransition(): " + "Slide has no PropertySet - assuming no transition" ); + return ActivitySharedPtr(); + } + + sal_Int16 nTransitionType(0); + if( !getPropertyValue( nTransitionType, + xPropSet, + "TransitionType") ) + { + SAL_INFO("slideshow", "createSlideTransition(): " + "Could not extract slide transition type from XDrawPage - assuming no transition" ); + return ActivitySharedPtr(); + } + + sal_Int16 nTransitionSubType(0); + if( !getPropertyValue( nTransitionSubType, + xPropSet, + "TransitionSubtype") ) + { + SAL_INFO("slideshow", "createSlideTransition(): " + "Could not extract slide transition subtype from XDrawPage - assuming no transition" ); + return ActivitySharedPtr(); + } + + bool bTransitionDirection(false); + if( !getPropertyValue( bTransitionDirection, + xPropSet, + "TransitionDirection") ) + { + SAL_INFO("slideshow", "createSlideTransition(): " + "Could not extract slide transition direction from XDrawPage - assuming default direction" ); + } + + sal_Int32 aUnoColor(0); + if( !getPropertyValue( aUnoColor, + xPropSet, + "TransitionFadeColor") ) + { + SAL_INFO("slideshow", "createSlideTransition(): " + "Could not extract slide transition fade color from XDrawPage - assuming black" ); + } + + const RGBColor aTransitionFadeColor( unoColor2RGBColor( aUnoColor )); + + uno::Any aSound; + bool bLoopSound = false; + + if( !getPropertyValue( aSound, xPropSet, "Sound") ) + SAL_INFO("slideshow", "createSlideTransition(): Could not determine transition sound effect URL from XDrawPage - using no sound" ); + + if( !getPropertyValue( bLoopSound, xPropSet, "LoopSound" ) ) + SAL_INFO("slideshow", "createSlideTransition(): Could not get slide property 'LoopSound' - using no sound" ); + + NumberAnimationSharedPtr pTransition( + TransitionFactory::createSlideTransition( + rLeavingSlide, + rEnteringSlide, + maViewContainer, + maScreenUpdater, + maEventMultiplexer, + mxOptionalTransitionFactory, + nTransitionType, + nTransitionSubType, + bTransitionDirection, + aTransitionFadeColor, + resetSlideTransitionSound( aSound, bLoopSound ) )); + + if( !pTransition ) + return ActivitySharedPtr(); // no transition effect has been + // generated. Normally, that means + // that simply no transition is + // set on this slide. + + double nTransitionDuration(0.0); + if( !getPropertyValue( nTransitionDuration, + xPropSet, + "TransitionDuration") ) + { + SAL_INFO("slideshow", "createSlideTransition(): " + "Could not extract slide transition duration from XDrawPage - assuming no transition" ); + return ActivitySharedPtr(); + } + + sal_Int32 nMinFrames(5); + if( !getPropertyValue( nMinFrames, + xPropSet, + "MinimalFrameNumber") ) + { + SAL_INFO("slideshow", "createSlideTransition(): " + "No minimal number of frames given - assuming 5" ); + } + + // prefetch slide transition bitmaps, but postpone it after + // displaySlide() has finished - sometimes, view size has not yet + // reached final size + maEventQueue.addEvent( + makeEvent( [pTransition] () { + pTransition->prefetch(); }, + "Animation::prefetch")); + + return ActivitySharedPtr( + ActivitiesFactory::createSimpleActivity( + ActivitiesFactory::CommonParameters( + rTransitionEndEvent, + maEventQueue, + maActivitiesQueue, + nTransitionDuration, + nMinFrames, + false, + std::optional<double>(1.0), + 0.0, + 0.0, + ShapeSharedPtr(), + basegfx::B2DSize( rEnteringSlide->getSlideSize() ) ), + pTransition, + true )); +} + +PolygonMap::iterator SlideShowImpl::findPolygons( uno::Reference<drawing::XDrawPage> const& xDrawPage) +{ + // TODO(P2): optimize research in the map. + return maPolygons.find(xDrawPage); +} + +SlideSharedPtr SlideShowImpl::makeSlide( + uno::Reference<drawing::XDrawPage> const& xDrawPage, + uno::Reference<drawing::XDrawPagesSupplier> const& xDrawPages, + uno::Reference<animations::XAnimationNode> const& xRootNode ) +{ + if( !xDrawPage.is() ) + return SlideSharedPtr(); + + //Retrieve polygons for the current slide + PolygonMap::iterator aIter = findPolygons(xDrawPage); + + const SlideSharedPtr pSlide( createSlide(xDrawPage, + xDrawPages, + xRootNode, + maEventQueue, + maEventMultiplexer, + maScreenUpdater, + maActivitiesQueue, + maUserEventQueue, + *this, + *this, + maViewContainer, + mxComponentContext, + maShapeEventListeners, + maShapeCursors, + (aIter != maPolygons.end()) ? aIter->second : PolyPolygonVector(), + maUserPaintColor ? *maUserPaintColor : RGBColor(), + maUserPaintStrokeWidth, + !!maUserPaintColor, + mbImageAnimationsAllowed, + mbDisableAnimationZOrder) ); + + // prefetch show content (reducing latency for slide + // bitmap and effect start later on) + pSlide->prefetch(); + + return pSlide; +} + +void SlideShowImpl::requestWaitSymbol() +{ + ++mnWaitSymbolRequestCount; + OSL_ASSERT(mnWaitSymbolRequestCount>0); + + if (mnWaitSymbolRequestCount == 1) + { + if( !mpWaitSymbol ) + { + // fall back to cursor + requestCursor(calcActiveCursor(mnCurrentCursor)); + } + else + mpWaitSymbol->show(); + } +} + +void SlideShowImpl::releaseWaitSymbol() +{ + --mnWaitSymbolRequestCount; + OSL_ASSERT(mnWaitSymbolRequestCount>=0); + + if (mnWaitSymbolRequestCount == 0) + { + if( !mpWaitSymbol ) + { + // fall back to cursor + requestCursor(calcActiveCursor(mnCurrentCursor)); + } + else + mpWaitSymbol->hide(); + } +} + +sal_Int16 SlideShowImpl::calcActiveCursor( sal_Int16 nCursorShape ) const +{ + if( mnWaitSymbolRequestCount>0 && !mpWaitSymbol ) // enforce wait cursor + nCursorShape = awt::SystemPointer::WAIT; + else if( !mbMouseVisible ) // enforce INVISIBLE + nCursorShape = awt::SystemPointer::INVISIBLE; + else if( maUserPaintColor && + nCursorShape == awt::SystemPointer::ARROW ) + nCursorShape = awt::SystemPointer::PEN; + + return nCursorShape; +} + +void SlideShowImpl::stopShow() +{ + // Force-end running animation + // =========================== + if (mpCurrentSlide) + { + mpCurrentSlide->hide(); + //Register polygons in the map + if(findPolygons(mpCurrentSlide->getXDrawPage()) != maPolygons.end()) + maPolygons.erase(mpCurrentSlide->getXDrawPage()); + + maPolygons.insert(make_pair(mpCurrentSlide->getXDrawPage(),mpCurrentSlide->getPolygons())); + } + + // clear all queues + maEventQueue.clear(); + maActivitiesQueue.clear(); + + // Attention: we MUST clear the user event queue here, + // this is because the current slide might have registered + // shape events (click or enter/leave), which might + // otherwise dangle forever in the queue (because of the + // shared ptr nature). If someone needs to change this: + // somehow unregister those shapes at the user event queue + // on notifySlideEnded(). + maUserEventQueue.clear(); + + // re-enable automatic effect advancement + // (maEventQueue.clear() above might have killed + // maEventMultiplexer's tick events) + if (mbAutomaticAdvancementMode) + { + // toggle automatic mode (enabling just again is + // ignored by EventMultiplexer) + maEventMultiplexer.setAutomaticMode( false ); + maEventMultiplexer.setAutomaticMode( true ); + } +} + +class SlideShowImpl::PrefetchPropertiesFunc +{ +public: + PrefetchPropertiesFunc( SlideShowImpl * that_, + bool& rbSkipAllMainSequenceEffects, + bool& rbSkipSlideTransition) + : mpSlideShowImpl(that_), + mrbSkipAllMainSequenceEffects(rbSkipAllMainSequenceEffects), + mrbSkipSlideTransition(rbSkipSlideTransition) + {} + + void operator()( beans::PropertyValue const& rProperty ) const { + if (rProperty.Name == "Prefetch" ) + { + uno::Sequence<uno::Any> seq; + if ((rProperty.Value >>= seq) && seq.getLength() == 2) + { + seq[0] >>= mpSlideShowImpl->mxPrefetchSlide; + seq[1] >>= mpSlideShowImpl->mxPrefetchAnimationNode; + } + } + else if ( rProperty.Name == "SkipAllMainSequenceEffects" ) + { + rProperty.Value >>= mrbSkipAllMainSequenceEffects; + } + else if ( rProperty.Name == "SkipSlideTransition" ) + { + rProperty.Value >>= mrbSkipSlideTransition; + } + else + { + SAL_WARN( "slideshow", rProperty.Name ); + } + } +private: + SlideShowImpl *const mpSlideShowImpl; + bool& mrbSkipAllMainSequenceEffects; + bool& mrbSkipSlideTransition; +}; + +void SlideShowImpl::displaySlide( + uno::Reference<drawing::XDrawPage> const& xSlide, + uno::Reference<drawing::XDrawPagesSupplier> const& xDrawPages, + uno::Reference<animations::XAnimationNode> const& xRootNode, + uno::Sequence<beans::PropertyValue> const& rProperties ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return; + + maEffectRewinder.setRootAnimationNode(xRootNode); + maEffectRewinder.setCurrentSlide(xSlide); + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + mxDrawPagesSupplier = xDrawPages; + mxSBD = uno::Reference<document::XStorageBasedDocument>(mxDrawPagesSupplier, uno::UNO_QUERY); + + stopShow(); // MUST call that: results in + // maUserEventQueue.clear(). What's more, + // stopShow()'s currSlide->hide() call is + // now also required, notifySlideEnded() + // relies on that + // unconditionally. Otherwise, genuine + // shape animations (drawing layer and + // GIF) will not be stopped. + + bool bSkipAllMainSequenceEffects (false); + bool bSkipSlideTransition (false); + std::for_each( rProperties.begin(), + rProperties.end(), + PrefetchPropertiesFunc(this, bSkipAllMainSequenceEffects, bSkipSlideTransition) ); + + OSL_ENSURE( !maViewContainer.empty(), "### no views!" ); + if (maViewContainer.empty()) + return; + + // this here might take some time + { + WaitSymbolLock aLock (*this); + + mpPreviousSlide = mpCurrentSlide; + mpCurrentSlide.reset(); + + if (matches( mpPrefetchSlide, xSlide, xRootNode )) + { + // prefetched slide matches: + mpCurrentSlide = mpPrefetchSlide; + } + else + mpCurrentSlide = makeSlide( xSlide, xDrawPages, xRootNode ); + + OSL_ASSERT( mpCurrentSlide ); + if (mpCurrentSlide) + { + basegfx::B2DSize oldSlideSize; + if( mpPreviousSlide ) + oldSlideSize = basegfx::B2DSize( mpPreviousSlide->getSlideSize() ); + + basegfx::B2DSize const slideSize( mpCurrentSlide->getSlideSize() ); + + // push new transformation to all views, if size changed + if( !mpPreviousSlide || oldSlideSize != slideSize ) + { + for( const auto& pView : maViewContainer ) + pView->setViewSize( slideSize ); + + // explicitly notify view change here, + // because transformation might have changed: + // optimization, this->notifyViewChange() would + // repaint slide which is not necessary. + maEventMultiplexer.notifyViewsChanged(); + } + + // create slide transition, and add proper end event + // (which then starts the slide effects + // via CURRENT_SLIDE.show()) + ActivitySharedPtr pSlideChangeActivity ( + createSlideTransition( + mpCurrentSlide->getXDrawPage(), + mpPreviousSlide, + mpCurrentSlide, + makeEvent( + [this] () { this->notifySlideTransitionEnded(false); }, + "SlideShowImpl::notifySlideTransitionEnded"))); + + if (bSkipSlideTransition) + { + // The transition activity was created for the side effects + // (like sound transitions). Because we want to skip the + // actual transition animation we do not need the activity + // anymore. + pSlideChangeActivity.reset(); + } + + if (pSlideChangeActivity) + { + // factory generated a slide transition - activate it! + maActivitiesQueue.addActivity( pSlideChangeActivity ); + } + else + { + // no transition effect on this slide - schedule slide + // effect start event right away. + maEventQueue.addEvent( + makeEvent( + [this] () { this->notifySlideTransitionEnded(true); }, + "SlideShowImpl::notifySlideTransitionEnded")); + } + } + } // finally + + maListenerContainer.forEach( + [](uno::Reference<presentation::XSlideShowListener> const& xListener) + { + xListener->slideTransitionStarted(); + }); + + // We are currently rewinding an effect. This lead us from the next + // slide to this one. To complete this we have to play back all main + // sequence effects on this slide. + if (bSkipAllMainSequenceEffects) + maEffectRewinder.skipAllMainSequenceEffects(); +} + +void SlideShowImpl::redisplayCurrentSlide() +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return; + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + stopShow(); + + OSL_ENSURE( !maViewContainer.empty(), "### no views!" ); + if (maViewContainer.empty()) + return; + + // No transition effect on this slide - schedule slide + // effect start event right away. + maEventQueue.addEvent( + makeEvent( [this] () { this->notifySlideTransitionEnded(true); }, + "SlideShowImpl::notifySlideTransitionEnded")); + + maListenerContainer.forEach( + [](uno::Reference<presentation::XSlideShowListener> const& xListener) + { + xListener->slideTransitionStarted(); + }); +} + +sal_Bool SlideShowImpl::nextEffect() +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return false; + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + if (mbShowPaused) + return true; + else + return maEventMultiplexer.notifyNextEffect(); +} + +sal_Bool SlideShowImpl::previousEffect() +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return false; + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + if (mbShowPaused) + return true; + else + { + return maEffectRewinder.rewind( + maScreenUpdater.createLock(), + [this]() { return this->redisplayCurrentSlide(); }, + [this]() { return this->rewindEffectToPreviousSlide(); } ); + } +} + +void SlideShowImpl::rewindEffectToPreviousSlide() +{ + // Show the wait symbol now and prevent it from showing temporary slide + // content while effects are played back. + WaitSymbolLock aLock (*this); + + // A previous call to EffectRewinder::Rewind could not rewind the current + // effect because there are no effects on the current slide or none has + // yet been displayed. Go to the previous slide. + notifySlideEnded(true); + + // Process pending events once more in order to have the following + // screen update show the last effect. Not sure whether this should be + // necessary. + maEventQueue.forceEmpty(); + + // We have to call the screen updater before the wait symbol is turned + // off. Otherwise the wait symbol would force the display of an + // intermediate state of the slide (before the effects are replayed.) + maScreenUpdater.commitUpdates(); +} + +sal_Bool SlideShowImpl::startShapeActivity( + uno::Reference<drawing::XShape> const& /*xShape*/ ) +{ + osl::MutexGuard const guard( m_aMutex ); + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + // TODO(F3): NYI + OSL_FAIL( "not yet implemented!" ); + return false; +} + +sal_Bool SlideShowImpl::stopShapeActivity( + uno::Reference<drawing::XShape> const& /*xShape*/ ) +{ + osl::MutexGuard const guard( m_aMutex ); + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + // TODO(F3): NYI + OSL_FAIL( "not yet implemented!" ); + return false; +} + +sal_Bool SlideShowImpl::pause( sal_Bool bPauseShow ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return false; + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + if (bPauseShow) + mpPresTimer->pauseTimer(); + else + mpPresTimer->continueTimer(); + + maEventMultiplexer.notifyPauseMode(bPauseShow); + + mbShowPaused = bPauseShow; + return true; +} + +uno::Reference<drawing::XDrawPage> SlideShowImpl::getCurrentSlide() +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return uno::Reference<drawing::XDrawPage>(); + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + if (mpCurrentSlide) + return mpCurrentSlide->getXDrawPage(); + else + return uno::Reference<drawing::XDrawPage>(); +} + +sal_Bool SlideShowImpl::addView( + uno::Reference<presentation::XSlideShowView> const& xView ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return false; + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + // first of all, check if view has a valid canvas + ENSURE_OR_RETURN_FALSE( xView.is(), "addView(): Invalid view" ); + ENSURE_OR_RETURN_FALSE( xView->getCanvas().is(), + "addView(): View does not provide a valid canvas" ); + + UnoViewSharedPtr const pView( createSlideView( + xView, + maEventQueue, + maEventMultiplexer )); + if (!maViewContainer.addView( pView )) + return false; // view already added + + // initialize view content + // ======================= + + if (mpCurrentSlide) + { + // set view transformation + const basegfx::B2ISize slideSize = mpCurrentSlide->getSlideSize(); + pView->setViewSize( basegfx::B2DSize( slideSize.getX(), + slideSize.getY() ) ); + } + + // clear view area (since it's newly added, + // we need a clean slate) + pView->clearAll(); + + // broadcast newly added view + maEventMultiplexer.notifyViewAdded( pView ); + + // set current mouse ptr + pView->setCursorShape( calcActiveCursor(mnCurrentCursor) ); + + return true; +} + +sal_Bool SlideShowImpl::removeView( + uno::Reference<presentation::XSlideShowView> const& xView ) +{ + osl::MutexGuard const guard( m_aMutex ); + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + ENSURE_OR_RETURN_FALSE( xView.is(), "removeView(): Invalid view" ); + + UnoViewSharedPtr const pView( maViewContainer.removeView( xView ) ); + if( !pView ) + return false; // view was not added in the first place + + // remove view from EventMultiplexer (mouse events etc.) + maEventMultiplexer.notifyViewRemoved( pView ); + + pView->_dispose(); + + return true; +} + +void SlideShowImpl::registerUserPaintPolygons( const uno::Reference< lang::XMultiServiceFactory >& xDocFactory ) +{ + //Retrieve Polygons if user ends presentation by context menu + if (mpCurrentSlide) + { + if(findPolygons(mpCurrentSlide->getXDrawPage()) != maPolygons.end()) + maPolygons.erase(mpCurrentSlide->getXDrawPage()); + + maPolygons.insert(make_pair(mpCurrentSlide->getXDrawPage(),mpCurrentSlide->getPolygons())); + } + + //Creating the layer for shapes drawn during slideshow + // query for the XLayerManager + uno::Reference< drawing::XLayerSupplier > xLayerSupplier(xDocFactory, uno::UNO_QUERY); + uno::Reference< container::XNameAccess > xNameAccess = xLayerSupplier->getLayerManager(); + uno::Reference< drawing::XLayerManager > xLayerManager(xNameAccess, uno::UNO_QUERY); + + // create layer + uno::Reference< drawing::XLayer > xDrawnInSlideshow; + uno::Any aPropLayer; + OUString sLayerName = "DrawnInSlideshow"; + if (xNameAccess->hasByName(sLayerName)) + { + xNameAccess->getByName(sLayerName) >>= xDrawnInSlideshow; + } + else + { + xDrawnInSlideshow = xLayerManager->insertNewByIndex(xLayerManager->getCount()); + aPropLayer <<= sLayerName; + xDrawnInSlideshow->setPropertyValue("Name", aPropLayer); + } + + // ODF defaults from ctor of SdrLayer are not automatically set on the here + // created XLayer. Need to be done explicitly here. + aPropLayer <<= true; + xDrawnInSlideshow->setPropertyValue("IsVisible", aPropLayer); + xDrawnInSlideshow->setPropertyValue("IsPrintable", aPropLayer); + aPropLayer <<= false; + xDrawnInSlideshow->setPropertyValue("IsLocked", aPropLayer); + + //Register polygons for each slide + for( const auto& rPoly : maPolygons ) + { + PolyPolygonVector aPolygons = rPoly.second; + //Get shapes for the slide + css::uno::Reference< css::drawing::XShapes > Shapes = rPoly.first; + //Retrieve polygons for one slide + for( const auto& pPolyPoly : aPolygons ) + { + ::basegfx::B2DPolyPolygon b2DPolyPoly = ::basegfx::unotools::b2DPolyPolygonFromXPolyPolygon2D(pPolyPoly->getUNOPolyPolygon()); + + //Normally there is only one polygon + for(sal_uInt32 i=0; i< b2DPolyPoly.count();i++) + { + const ::basegfx::B2DPolygon& aPoly = b2DPolyPoly.getB2DPolygon(i); + sal_uInt32 nPoints = aPoly.count(); + + if( nPoints > 1) + { + //create the PolyLineShape + uno::Reference< uno::XInterface > polyshape(xDocFactory->createInstance( + "com.sun.star.drawing.PolyLineShape" ) ); + uno::Reference< drawing::XShape > rPolyShape(polyshape, uno::UNO_QUERY); + + //Add the shape to the slide + Shapes->add(rPolyShape); + + //Retrieve shape properties + uno::Reference< beans::XPropertySet > aXPropSet( rPolyShape, uno::UNO_QUERY ); + //Construct a sequence of points sequence + drawing::PointSequenceSequence aRetval; + //Create only one sequence for one polygon + aRetval.realloc( 1 ); + // Retrieve the sequence of points from aRetval + drawing::PointSequence* pOuterSequence = aRetval.getArray(); + // Create 2 points in this sequence + pOuterSequence->realloc(nPoints); + // Get these points which are in an array + awt::Point* pInnerSequence = pOuterSequence->getArray(); + for( sal_uInt32 n = 0; n < nPoints; n++ ) + { + //Create a point from the polygon + *pInnerSequence++ = awt::Point( + basegfx::fround(aPoly.getB2DPoint(n).getX()), + basegfx::fround(aPoly.getB2DPoint(n).getY())); + } + + //Fill the properties + //Give the built PointSequenceSequence. + uno::Any aParam; + aParam <<= aRetval; + aXPropSet->setPropertyValue("PolyPolygon", aParam ); + + //LineStyle : SOLID by default + drawing::LineStyle eLS; + eLS = drawing::LineStyle_SOLID; + aXPropSet->setPropertyValue("LineStyle", uno::Any(eLS) ); + + //LineColor + sal_uInt32 nLineColor; + nLineColor = pPolyPoly->getRGBALineColor(); + //Transform polygon color from RRGGBBAA to AARRGGBB + aXPropSet->setPropertyValue("LineColor", uno::Any(RGBAColor2UnoColor(nLineColor)) ); + + //LineWidth + double fLineWidth; + fLineWidth = pPolyPoly->getStrokeWidth(); + aXPropSet->setPropertyValue("LineWidth", uno::Any(static_cast<sal_Int32>(fLineWidth)) ); + + // make polygons special + xLayerManager->attachShapeToLayer(rPolyShape, xDrawnInSlideshow); + } + } + } + } +} + +sal_Bool SlideShowImpl::setProperty( beans::PropertyValue const& rProperty ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return false; + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + if ( rProperty.Name == "AutomaticAdvancement" ) + { + double nTimeout(0.0); + mbAutomaticAdvancementMode = (rProperty.Value >>= nTimeout); + if (mbAutomaticAdvancementMode) + { + maEventMultiplexer.setAutomaticTimeout( nTimeout ); + } + maEventMultiplexer.setAutomaticMode( mbAutomaticAdvancementMode ); + return true; + } + + if ( rProperty.Name == "UserPaintColor" ) + { + sal_Int32 nColor(0); + if (rProperty.Value >>= nColor) + { + OSL_ENSURE( mbMouseVisible, + "setProperty(): User paint overrides invisible mouse" ); + + // enable user paint + maUserPaintColor = unoColor2RGBColor(nColor); + if( mpCurrentSlide && !mpCurrentSlide->isPaintOverlayActive() ) + mpCurrentSlide->enablePaintOverlay(); + + maEventMultiplexer.notifyUserPaintColor( *maUserPaintColor ); + } + else + { + // disable user paint + maUserPaintColor.reset(); + maEventMultiplexer.notifyUserPaintDisabled(); + } + + resetCursor(); + + return true; + } + + //adding support for erasing features in UserPaintOverlay + if ( rProperty.Name == "EraseAllInk" ) + { + bool bEraseAllInk(false); + if (rProperty.Value >>= bEraseAllInk) + { + OSL_ENSURE( mbMouseVisible, + "setProperty(): User paint overrides invisible mouse" ); + + // enable user paint + maEraseAllInk = bEraseAllInk; + maEventMultiplexer.notifyEraseAllInk( *maEraseAllInk ); + } + + return true; + } + + if ( rProperty.Name == "SwitchPenMode" ) + { + bool bSwitchPenMode(false); + if (rProperty.Value >>= bSwitchPenMode) + { + OSL_ENSURE( mbMouseVisible, + "setProperty(): User paint overrides invisible mouse" ); + + if(bSwitchPenMode){ + // Switch to Pen Mode + maEventMultiplexer.notifySwitchPenMode(); + } + } + return true; + } + + if ( rProperty.Name == "SwitchEraserMode" ) + { + bool bSwitchEraserMode(false); + if (rProperty.Value >>= bSwitchEraserMode) + { + OSL_ENSURE( mbMouseVisible, + "setProperty(): User paint overrides invisible mouse" ); + if(bSwitchEraserMode){ + // switch to Eraser mode + maEventMultiplexer.notifySwitchEraserMode(); + } + } + + return true; + } + + if ( rProperty.Name == "EraseInk" ) + { + sal_Int32 nEraseInk(100); + if (rProperty.Value >>= nEraseInk) + { + OSL_ENSURE( mbMouseVisible, + "setProperty(): User paint overrides invisible mouse" ); + + // enable user paint + maEraseInk = nEraseInk; + maEventMultiplexer.notifyEraseInkWidth( *maEraseInk ); + } + + return true; + } + + // new Property for pen's width + if ( rProperty.Name == "UserPaintStrokeWidth" ) + { + double nWidth(4.0); + if (rProperty.Value >>= nWidth) + { + OSL_ENSURE( mbMouseVisible,"setProperty(): User paint overrides invisible mouse" ); + // enable user paint stroke width + maUserPaintStrokeWidth = nWidth; + maEventMultiplexer.notifyUserPaintStrokeWidth( maUserPaintStrokeWidth ); + } + + return true; + } + + if ( rProperty.Name == "AdvanceOnClick" ) + { + bool bAdvanceOnClick = false; + if (! (rProperty.Value >>= bAdvanceOnClick)) + return false; + maUserEventQueue.setAdvanceOnClick( bAdvanceOnClick ); + return true; + } + + if ( rProperty.Name == "DisableAnimationZOrder" ) + { + bool bDisableAnimationZOrder = false; + if (! (rProperty.Value >>= bDisableAnimationZOrder)) + return false; + mbDisableAnimationZOrder = bDisableAnimationZOrder; + return true; + } + + if ( rProperty.Name == "ImageAnimationsAllowed" ) + { + if (! (rProperty.Value >>= mbImageAnimationsAllowed)) + return false; + + // TODO(F3): Forward to slides! + return true; + } + + if ( rProperty.Name == "MouseVisible" ) + { + if (! (rProperty.Value >>= mbMouseVisible)) + return false; + + requestCursor(mnCurrentCursor); + + return true; + } + + if ( rProperty.Name == "ForceManualAdvance" ) + { + return (rProperty.Value >>= mbForceManualAdvance); + } + + if ( rProperty.Name == "RehearseTimings" ) + { + bool bRehearseTimings = false; + if (! (rProperty.Value >>= bRehearseTimings)) + return false; + + if (bRehearseTimings) + { + // TODO(Q3): Move to slide + mpRehearseTimingsActivity = RehearseTimingsActivity::create( + SlideShowContext( + mpDummyPtr, + maEventQueue, + maEventMultiplexer, + maScreenUpdater, + maActivitiesQueue, + maUserEventQueue, + *this, + *this, + maViewContainer, + mxComponentContext, + mpBox2DDummyPtr ) ); + } + else if (mpRehearseTimingsActivity) + { + // removes timer from all views: + mpRehearseTimingsActivity->dispose(); + mpRehearseTimingsActivity.reset(); + } + return true; + } + + if ( rProperty.Name == "WaitSymbolBitmap" ) + { + uno::Reference<rendering::XBitmap> xBitmap; + if (! (rProperty.Value >>= xBitmap)) + return false; + + mpWaitSymbol = WaitSymbol::create( xBitmap, + maScreenUpdater, + maEventMultiplexer, + maViewContainer ); + + return true; + } + + if ( rProperty.Name == "PointerSymbolBitmap" ) + { + uno::Reference<rendering::XBitmap> xBitmap; + if (! (rProperty.Value >>= xBitmap)) + return false; + + mpPointerSymbol = PointerSymbol::create( xBitmap, + maScreenUpdater, + maEventMultiplexer, + maViewContainer ); + + return true; + } + + if ( rProperty.Name == "PointerVisible" ) + { + bool visible; + if (!(rProperty.Value >>= visible)) + return false; + + mpPointerSymbol->setVisible(visible); + return true; + } + + if ( rProperty.Name == "PointerPosition") + { + css::geometry::RealPoint2D pos; + if (! (rProperty.Value >>= pos)) + return false; + + mpPointerSymbol->viewsChanged(pos); + return true; + } + + if (rProperty.Name == "NoSlideTransitions" ) + { + return (rProperty.Value >>= mbNoSlideTransitions); + } + + if ( rProperty.Name == "IsSoundEnabled" ) + { + uno::Sequence<uno::Any> aValues; + uno::Reference<presentation::XSlideShowView> xView; + bool bValue (false); + if ((rProperty.Value >>= aValues) + && aValues.getLength()==2 + && (aValues[0] >>= xView) + && (aValues[1] >>= bValue)) + { + // Look up the view. + auto iView = std::find_if(maViewContainer.begin(), maViewContainer.end(), + [&xView](const UnoViewSharedPtr& rxView) { return rxView && rxView->getUnoView() == xView; }); + if (iView != maViewContainer.end()) + { + // Store the flag at the view so that media shapes have + // access to it. + (*iView)->setIsSoundEnabled(bValue); + return true; + } + } + } + + return false; +} + +void SlideShowImpl::addSlideShowListener( + uno::Reference<presentation::XSlideShowListener> const& xListener ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return; + + // container syncs with passed mutex ref + maListenerContainer.addInterface(xListener); +} + +void SlideShowImpl::removeSlideShowListener( + uno::Reference<presentation::XSlideShowListener> const& xListener ) +{ + osl::MutexGuard const guard( m_aMutex ); + + // container syncs with passed mutex ref + maListenerContainer.removeInterface(xListener); +} + +void SlideShowImpl::addShapeEventListener( + uno::Reference<presentation::XShapeEventListener> const& xListener, + uno::Reference<drawing::XShape> const& xShape ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return; + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + ShapeEventListenerMap::iterator aIter; + if( (aIter=maShapeEventListeners.find( xShape )) == + maShapeEventListeners.end() ) + { + // no entry for this shape -> create one + aIter = maShapeEventListeners.emplace( + xShape, + std::make_shared<comphelper::OInterfaceContainerHelper3<css::presentation::XShapeEventListener>>( + m_aMutex)).first; + } + + // add new listener to broadcaster + if( aIter->second ) + aIter->second->addInterface( xListener ); + + maEventMultiplexer.notifyShapeListenerAdded(xShape); +} + +void SlideShowImpl::removeShapeEventListener( + uno::Reference<presentation::XShapeEventListener> const& xListener, + uno::Reference<drawing::XShape> const& xShape ) +{ + osl::MutexGuard const guard( m_aMutex ); + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + ShapeEventListenerMap::iterator aIter; + if( (aIter = maShapeEventListeners.find( xShape )) != + maShapeEventListeners.end() ) + { + // entry for this shape found -> remove listener from + // helper object + ENSURE_OR_THROW( + aIter->second, + "SlideShowImpl::removeShapeEventListener(): " + "listener map contains NULL broadcast helper" ); + + aIter->second->removeInterface( xListener ); + } + + maEventMultiplexer.notifyShapeListenerRemoved(xShape); +} + +void SlideShowImpl::setShapeCursor( + uno::Reference<drawing::XShape> const& xShape, sal_Int16 nPointerShape ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return; + + // precondition: must only be called from the main thread! + DBG_TESTSOLARMUTEX(); + + ShapeCursorMap::iterator aIter; + if( (aIter=maShapeCursors.find( xShape )) == maShapeCursors.end() ) + { + // no entry for this shape -> create one + if( nPointerShape != awt::SystemPointer::ARROW ) + { + // add new entry, unless shape shall display + // normal pointer arrow -> no need to handle that + // case + maShapeCursors.emplace(xShape, nPointerShape); + } + } + else if( nPointerShape == awt::SystemPointer::ARROW ) + { + // shape shall display normal cursor -> can disable + // the cursor and clear the entry + maShapeCursors.erase( xShape ); + } + else + { + // existing entry found, update with new cursor ID + aIter->second = nPointerShape; + } +} + +bool SlideShowImpl::requestCursor( sal_Int16 nCursorShape ) +{ + mnCurrentCursor = nCursorShape; + + const sal_Int16 nActualCursor = calcActiveCursor(mnCurrentCursor); + + // change all views to the requested cursor ID + for( const auto& pView : maViewContainer ) + pView->setCursorShape( nActualCursor ); + + return nActualCursor==nCursorShape; +} + +void SlideShowImpl::resetCursor() +{ + mnCurrentCursor = awt::SystemPointer::ARROW; + + const sal_Int16 nActualCursor = calcActiveCursor( mnCurrentCursor ); + // change all views to the default cursor ID + for( const auto& pView : maViewContainer ) + pView->setCursorShape( nActualCursor ); +} + +sal_Bool SlideShowImpl::update( double & nNextTimeout ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (isDisposed()) + return false; + + // precondition: update() must only be called from the + // main thread! + DBG_TESTSOLARMUTEX(); + + if( mbShowPaused ) + { + // commit frame (might be repaints pending) + maScreenUpdater.commitUpdates(); + + return false; + } + else + { + // TODO(F2): re-evaluate whether that timer lagging makes + // sense. + + // hold timer, while processing the queues: + // 1. when there is more than one active activity this ensures the + // same time for all activities and events + // 2. processing of events may lead to creation of further events + // that have zero delay. While the timer is stopped these events + // are processed in the same run. + { + //Get a shared-ptr that outlives the scope-guard which will + //ensure that the pointed-to-item exists in the case of a + //::dispose clearing mpPresTimer + std::shared_ptr<canvas::tools::ElapsedTime> xTimer(mpPresTimer); + comphelper::ScopeGuard scopeGuard( + [&xTimer]() { return xTimer->releaseTimer(); } ); + xTimer->holdTimer(); + + // process queues + maEventQueue.process(); + + // #i118671# the call above may execute a macro bound to an object. In + // that case this macro may have destroyed this local slideshow so that it + // is disposed (see bugdoc at task). In that case, detect this and exit + // gently from this slideshow. Do not forget to disable the scoped + // call to mpPresTimer, this will be deleted if we are disposed. + if (isDisposed()) + { + scopeGuard.dismiss(); + return false; + } + + maActivitiesQueue.process(); + + // commit frame to screen + maFrameSynchronization.Synchronize(); + maScreenUpdater.commitUpdates(); + + // TODO(Q3): remove need to call dequeued() from + // activities. feels like a wart. + + // Rationale for ActivitiesQueue::processDequeued(): when + // an activity ends, it usually pushed the end state to + // the animated shape in question, and ends the animation + // (which, in turn, will usually disable shape sprite + // mode). Disabling shape sprite mode causes shape + // repaint, which, depending on slide content, takes + // considerably more time than sprite updates. Thus, the + // last animation step tends to look delayed. To + // camouflage this, reaching end position and disabling + // sprite mode is split into two (normal Activity::end(), + // and Activity::dequeued()). Now, the reason to call + // commitUpdates() twice here is caused by the unrelated + // fact that during wait cursor display/hide, the screen + // is updated, and shows hidden sprites, but, in case of + // leaving the second commitUpdates() call out and punting + // that to the next round, no updated static slide + // content. In short, the last shape animation of a slide + // tends to blink at its end. + + // process dequeued activities _after_ commit to screen + maActivitiesQueue.processDequeued(); + + // commit frame to screen + maScreenUpdater.commitUpdates(); + } + // Time held until here + + const bool bActivitiesLeft = ! maActivitiesQueue.isEmpty(); + const bool bTimerEventsLeft = ! maEventQueue.isEmpty(); + const bool bRet = (bActivitiesLeft || bTimerEventsLeft); + + if (bRet) + { + // calc nNextTimeout value: + if (bActivitiesLeft) + { + // Activity queue is not empty. Tell caller that we would + // like to render another frame. + + // Return a zero time-out to signal our caller to call us + // back as soon as possible. The actual timing, waiting the + // appropriate amount of time between frames, is then done + // by the maFrameSynchronization object. + nNextTimeout = 0; + maFrameSynchronization.Activate(); + } + else + { + // timer events left: + // difference from current time (nota bene: + // time no longer held here!) to the next event in + // the event queue. + + // #i61190# Retrieve next timeout only _after_ + // processing activity queue + + // ensure positive value: + nNextTimeout = std::max( 0.0, maEventQueue.nextTimeout() ); + + // There is no active animation so the frame rate does not + // need to be synchronized. + maFrameSynchronization.Deactivate(); + } + + mbSlideShowIdle = false; + } + +#if defined(DBG_UTIL) + // when slideshow is idle, issue an XUpdatable::update() call + // exactly once after a previous animation sequence finished - + // this might trigger screen dumps on some canvas + // implementations + if( !mbSlideShowIdle && + (!bRet || + nNextTimeout > 1.0) ) + { + for( const auto& pView : maViewContainer ) + { + try + { + uno::Reference< presentation::XSlideShowView > xView( pView->getUnoView(), + uno::UNO_SET_THROW ); + uno::Reference<util::XUpdatable> const xUpdatable( + xView->getCanvas(), uno::UNO_QUERY); + if (xUpdatable.is()) // not supported in PresenterCanvas + { + xUpdatable->update(); + } + } + catch( uno::RuntimeException& ) + { + throw; + } + catch( uno::Exception& ) + { + TOOLS_WARN_EXCEPTION( "slideshow", "" ); + } + } + + mbSlideShowIdle = true; + } +#endif + + return bRet; + } +} + +void SlideShowImpl::notifySlideTransitionEnded( bool bPaintSlide ) +{ + osl::MutexGuard const guard( m_aMutex ); + + OSL_ENSURE( !isDisposed(), "### already disposed!" ); + OSL_ENSURE( mpCurrentSlide, + "notifySlideTransitionEnded(): Invalid current slide" ); + if (mpCurrentSlide) + { + mpCurrentSlide->update_settings( !!maUserPaintColor, maUserPaintColor ? *maUserPaintColor : RGBColor(), maUserPaintStrokeWidth ); + + // first init show, to give the animations + // the chance to register SlideStartEvents + const bool bBackgroundLayerRendered( !bPaintSlide ); + mpCurrentSlide->show( bBackgroundLayerRendered ); + maEventMultiplexer.notifySlideStartEvent(); + } +} + +void queryAutomaticSlideTransition( uno::Reference<drawing::XDrawPage> const& xDrawPage, + double& nAutomaticNextSlideTimeout, + bool& bHasAutomaticNextSlide ) +{ + // retrieve slide change parameters from XDrawPage + // =============================================== + + uno::Reference< beans::XPropertySet > xPropSet( xDrawPage, + uno::UNO_QUERY ); + + sal_Int32 nChange(0); + if( !xPropSet.is() || + !getPropertyValue( nChange, + xPropSet, + "Change") ) + { + SAL_INFO("slideshow", + "queryAutomaticSlideTransition(): " + "Could not extract slide change mode from XDrawPage - assuming <none>" ); + } + + bHasAutomaticNextSlide = nChange == 1; + + if( !xPropSet.is() || + !getPropertyValue( nAutomaticNextSlideTimeout, + xPropSet, + "HighResDuration") ) + { + SAL_INFO("slideshow", + "queryAutomaticSlideTransition(): " + "Could not extract slide transition timeout from " + "XDrawPage - assuming 1 sec" ); + } +} + +void SlideShowImpl::notifySlideAnimationsEnded() +{ + osl::MutexGuard const guard( m_aMutex ); + + //Draw polygons above animations + mpCurrentSlide->drawPolygons(); + + OSL_ENSURE( !isDisposed(), "### already disposed!" ); + + // This struct will receive the (interruptable) event, + // that triggers the notifySlideEnded() method. + InterruptableEventPair aNotificationEvents; + + if( maEventMultiplexer.getAutomaticMode() ) + { + OSL_ENSURE( ! mpRehearseTimingsActivity, + "unexpected: RehearseTimings mode!" ); + + // schedule a slide end event, with automatic mode's + // delay + aNotificationEvents = makeInterruptableDelay( + [this]() { return this->notifySlideEnded( false ); }, + maEventMultiplexer.getAutomaticTimeout() ); + } + else + { + OSL_ENSURE( mpCurrentSlide, + "notifySlideAnimationsEnded(): Invalid current slide!" ); + + bool bHasAutomaticNextSlide=false; + double nAutomaticNextSlideTimeout=0.0; + queryAutomaticSlideTransition(mpCurrentSlide->getXDrawPage(), + nAutomaticNextSlideTimeout, + bHasAutomaticNextSlide); + + // check whether slide transition should happen + // 'automatically'. If yes, simply schedule the + // specified timeout. + // NOTE: mbForceManualAdvance and mpRehearseTimingsActivity + // override any individual slide setting, to always + // step slides manually. + if( !mbForceManualAdvance && + !mpRehearseTimingsActivity && + bHasAutomaticNextSlide ) + { + aNotificationEvents = makeInterruptableDelay( + [this]() { return this->notifySlideEnded( false ); }, + nAutomaticNextSlideTimeout); + + // TODO(F2): Provide a mechanism to let the user override + // this automatic timeout via next() + } + else + { + if (mpRehearseTimingsActivity) + mpRehearseTimingsActivity->start(); + + // generate click event. Thus, the user must + // trigger the actual end of a slide. No need to + // generate interruptable event here, there's no + // timeout involved. + aNotificationEvents.mpImmediateEvent = + makeEvent( [this] () { this->notifySlideEnded(false); }, + "SlideShowImpl::notifySlideEnded"); + } + } + + // register events on the queues. To make automatic slide + // changes interruptable, register the interruption event + // as a nextEffectEvent target. Note that the timeout + // event is optional (e.g. manual slide changes don't + // generate a timeout) + maUserEventQueue.registerNextEffectEvent( + aNotificationEvents.mpImmediateEvent ); + + if( aNotificationEvents.mpTimeoutEvent ) + maEventQueue.addEvent( aNotificationEvents.mpTimeoutEvent ); + + // current slide's main sequence is over. Now should be + // the time to prefetch the next slide (if any), and + // prepare the initial slide bitmap (speeds up slide + // change setup time a lot). Show the wait cursor, this + // indeed might take some seconds. + { + WaitSymbolLock aLock (*this); + + if (! matches( mpPrefetchSlide, + mxPrefetchSlide, mxPrefetchAnimationNode )) + { + mpPrefetchSlide = makeSlide( mxPrefetchSlide, mxDrawPagesSupplier, + mxPrefetchAnimationNode ); + } + if (mpPrefetchSlide) + { + // ignore return value, this is just to populate + // Slide's internal bitmap buffer, such that the time + // needed to generate the slide bitmap is not spent + // when the slide change is requested. + mpPrefetchSlide->getCurrentSlideBitmap( *maViewContainer.begin() ); + } + } // finally + + maListenerContainer.forEach( + [](uno::Reference<presentation::XSlideShowListener> const& xListener) + { + xListener->slideAnimationsEnded(); + }); +} + +void SlideShowImpl::notifySlideEnded (const bool bReverse) +{ + osl::MutexGuard const guard( m_aMutex ); + + OSL_ENSURE( !isDisposed(), "### already disposed!" ); + + if (mpRehearseTimingsActivity && !bReverse) + { + const double time = mpRehearseTimingsActivity->stop(); + if (mpRehearseTimingsActivity->hasBeenClicked()) + { + // save time at current drawpage: + uno::Reference<beans::XPropertySet> xPropSet( + mpCurrentSlide->getXDrawPage(), uno::UNO_QUERY ); + OSL_ASSERT( xPropSet.is() ); + if (xPropSet.is()) + { + xPropSet->setPropertyValue( + "Change", + uno::Any( static_cast<sal_Int32>(1) ) ); + xPropSet->setPropertyValue( + "Duration", + uno::Any( static_cast<sal_Int32>(time) ) ); + } + } + } + + if (bReverse) + maEventMultiplexer.notifySlideEndEvent(); + + stopShow(); // MUST call that: results in + // maUserEventQueue.clear(). What's more, + // stopShow()'s currSlide->hide() call is + // now also required, notifySlideEnded() + // relies on that + // unconditionally. Otherwise, genuine + // shape animations (drawing layer and + // GIF) will not be stopped. + + maListenerContainer.forEach( + [&bReverse]( const uno::Reference< presentation::XSlideShowListener >& xListener ) + { return xListener->slideEnded( bReverse ); } ); +} + +bool SlideShowImpl::notifyHyperLinkClicked( OUString const& hyperLink ) +{ + osl::MutexGuard const guard( m_aMutex ); + + maListenerContainer.forEach( + [&hyperLink]( const uno::Reference< presentation::XSlideShowListener >& xListener ) + { return xListener->hyperLinkClicked( hyperLink ); } ); + return true; +} + +/** Notification from eventmultiplexer that an animation event has occurred. + This will be forwarded to all registered XSlideShoeListener + */ +bool SlideShowImpl::handleAnimationEvent( const AnimationNodeSharedPtr& rNode ) +{ + osl::MutexGuard const guard( m_aMutex ); + + uno::Reference<animations::XAnimationNode> xNode( rNode->getXAnimationNode() ); + + switch( rNode->getState() ) + { + case AnimationNode::ACTIVE: + maListenerContainer.forEach( + [&xNode]( const uno::Reference< animations::XAnimationListener >& xListener ) + { return xListener->beginEvent( xNode ); } ); + break; + + case AnimationNode::FROZEN: + case AnimationNode::ENDED: + maListenerContainer.forEach( + [&xNode]( const uno::Reference< animations::XAnimationListener >& xListener ) + { return xListener->endEvent( xNode ); } ); + if(mpCurrentSlide->isPaintOverlayActive()) + mpCurrentSlide->drawPolygons(); + break; + default: + break; + } + + return true; +} + +std::shared_ptr<avmedia::MediaTempFile> SlideShowImpl::getMediaTempFile(const OUString& aUrl) +{ + std::shared_ptr<avmedia::MediaTempFile> aRet; + +#if !HAVE_FEATURE_AVMEDIA + (void)aUrl; +#else + if (!mxSBD.is()) + return aRet; + + comphelper::LifecycleProxy aProxy; + uno::Reference<io::XStream> xStream = + comphelper::OStorageHelper::GetStreamAtPackageURL(mxSBD->getDocumentStorage(), aUrl, + css::embed::ElementModes::READ, aProxy); + + uno::Reference<io::XInputStream> xInStream = xStream->getInputStream(); + if (xInStream.is()) + { + sal_Int32 nLastDot = aUrl.lastIndexOf('.'); + sal_Int32 nLastSlash = aUrl.lastIndexOf('/'); + OUString sDesiredExtension; + if (nLastDot > nLastSlash && nLastDot+1 < aUrl.getLength()) + sDesiredExtension = aUrl.copy(nLastDot); + + OUString sTempUrl; + if (::avmedia::CreateMediaTempFile(xInStream, sTempUrl, sDesiredExtension)) + aRet = std::make_shared<avmedia::MediaTempFile>(sTempUrl); + + xInStream->closeInput(); + } +#endif + + return aRet; +} + +//===== FrameSynchronization ================================================== + +FrameSynchronization::FrameSynchronization (const double nFrameDuration) + : maTimer(), + mnFrameDuration(nFrameDuration), + mnNextFrameTargetTime(0), + mbIsActive(false) +{ + MarkCurrentFrame(); +} + +void FrameSynchronization::MarkCurrentFrame() +{ + mnNextFrameTargetTime = maTimer.getElapsedTime() + mnFrameDuration; +} + +void FrameSynchronization::Synchronize() +{ + if (mbIsActive) + { + // Do busy waiting for now. + for(;;) + { + double remainingTime = mnNextFrameTargetTime - maTimer.getElapsedTime(); + if(remainingTime <= 0) + break; + // Try to sleep most of it. + int remainingMilliseconds = remainingTime * 1000; + if(remainingMilliseconds > 2) + osl::Thread::wait(std::chrono::milliseconds(remainingMilliseconds - 2)); + } + } + + MarkCurrentFrame(); +} + +void FrameSynchronization::Activate() +{ + mbIsActive = true; +} + +void FrameSynchronization::Deactivate() +{ + mbIsActive = false; +} + +} // anon namespace + +extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface* +slideshow_SlideShowImpl_get_implementation( + css::uno::XComponentContext* context , css::uno::Sequence<css::uno::Any> const&) +{ + return cppu::acquire(new SlideShowImpl(context)); +} +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/slideshow/source/engine/slideview.cxx b/slideshow/source/engine/slideview.cxx new file mode 100644 index 000000000..013e089a0 --- /dev/null +++ b/slideshow/source/engine/slideview.cxx @@ -0,0 +1,1193 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <tools/diagnose_ex.h> +#include <canvas/canvastools.hxx> + +#include <eventqueue.hxx> +#include <eventmultiplexer.hxx> +#include <slideview.hxx> +#include <delayevent.hxx> +#include <unoview.hxx> + +#include <cppuhelper/basemutex.hxx> +#include <cppuhelper/compbase.hxx> +#include <comphelper/make_shared_from_uno.hxx> + +#include <cppcanvas/spritecanvas.hxx> +#include <cppcanvas/customsprite.hxx> +#include <cppcanvas/vclfactory.hxx> +#include <cppcanvas/basegfxfactory.hxx> + +#include <basegfx/range/b1drange.hxx> +#include <basegfx/range/b2drange.hxx> +#include <basegfx/range/b2irange.hxx> +#include <basegfx/point/b2dpoint.hxx> +#include <basegfx/polygon/b2dpolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/utils/canvastools.hxx> +#include <basegfx/polygon/b2dpolygonclipper.hxx> +#include <basegfx/polygon/b2dpolypolygoncutter.hxx> + +#include <com/sun/star/awt/XPaintListener.hpp> +#include <com/sun/star/presentation/XSlideShowView.hpp> +#include <com/sun/star/rendering/CompositeOperation.hpp> +#include <com/sun/star/util/XModifyListener.hpp> + +#include <memory> +#include <vector> +#include <algorithm> + +using namespace com::sun::star; + +namespace slideshow::internal { + +namespace { + +/** Sprite entry, to store sprite plus priority + + The operator<() defines a strict weak ordering of sprites, sort + key is the sprite priority. + */ +struct SpriteEntry +{ + SpriteEntry( const cppcanvas::CustomSpriteSharedPtr& rSprite, + double nPrio ) : + mpSprite( rSprite ), + mnPriority( nPrio ) + { + } + + bool operator<(const SpriteEntry& rRHS) const + { + return mnPriority < rRHS.mnPriority; + } + + std::weak_ptr< cppcanvas::CustomSprite > mpSprite; + double mnPriority; +}; + +typedef std::vector< SpriteEntry > SpriteVector; + + +/** Create a clip polygon for slide views + + @param rClip + Clip to set (can be empty) + + @param rCanvas + Canvas to create the clip polygon for + + @param rUserSize + The size of the view. Note that the returned clip will + <em>always</em> clip to at least the rect defined herein. + + @return the view clip polygon, in view coordinates, which is + guaranteed to at least clip to the view size. + */ +basegfx::B2DPolyPolygon createClipPolygon( const basegfx::B2DPolyPolygon& rClip, + const cppcanvas::CanvasSharedPtr& /*rCanvas*/, + const basegfx::B2DSize& rUserSize ) +{ + // setup canvas clipping + // ===================== + + // AW: Simplified + const basegfx::B2DRange aClipRange(0, 0, rUserSize.getX(), rUserSize.getY()); + + if(rClip.count()) + { + return basegfx::utils::clipPolyPolygonOnRange(rClip, aClipRange, true, false); + } + else + { + return basegfx::B2DPolyPolygon(basegfx::utils::createPolygonFromRect(aClipRange)); + } +} + +/** Prepare given clip polygon to be stored as the current clip + + Note that this is separate from createClipPolygon(), to allow + SlideView implementations to store this intermediate result + (createClipPolygon() has to be called every time the view size + changes) + */ +basegfx::B2DPolyPolygon prepareClip( const basegfx::B2DPolyPolygon& rClip ) +{ + basegfx::B2DPolyPolygon aClip( rClip ); + + // normalize polygon, preparation for clipping + // in updateCanvas() + aClip = basegfx::utils::correctOrientations(aClip); + aClip = basegfx::utils::solveCrossovers(aClip); + aClip = basegfx::utils::stripNeutralPolygons(aClip); + aClip = basegfx::utils::stripDispensablePolygons(aClip); + + return aClip; +} + + +void clearRect( ::cppcanvas::CanvasSharedPtr const& pCanvas, + basegfx::B2IRange const& rArea ) +{ + // convert clip polygon to device coordinate system + ::basegfx::B2DPolyPolygon const* pClipPoly( pCanvas->getClip() ); + if( pClipPoly ) + { + ::basegfx::B2DPolyPolygon aClipPoly( *pClipPoly ); + aClipPoly.transform( pCanvas->getTransformation() ); + pCanvas->setClip( aClipPoly ); + } + + // set transformation to identity (->device pixel) + pCanvas->setTransformation( ::basegfx::B2DHomMatrix() ); + + // #i42440# Fill the _full_ background in + // black. Since we had to extend the bitmap by one + // pixel, and the bitmap is initialized white, + // depending on the slide content a one pixel wide + // line will show to the bottom and the right. + const ::basegfx::B2DPolygon aPoly( + ::basegfx::utils::createPolygonFromRect( + basegfx::B2DRange(rArea))); + + ::cppcanvas::PolyPolygonSharedPtr pPolyPoly( + ::cppcanvas::BaseGfxFactory::createPolyPolygon( pCanvas, aPoly ) ); + + if( pPolyPoly ) + { + pPolyPoly->setCompositeOp( css::rendering::CompositeOperation::SOURCE ); + pPolyPoly->setRGBAFillColor( 0xFFFFFF00U ); + pPolyPoly->draw(); + } + +#if defined(DBG_UTIL) + ::cppcanvas::CanvasSharedPtr pCliplessCanvas( pCanvas->clone() ); + pCliplessCanvas->setClip(); + + if( pCanvas->getClip() ) + { + ::cppcanvas::PolyPolygonSharedPtr pPolyPoly2( + ::cppcanvas::BaseGfxFactory::createPolyPolygon( pCliplessCanvas, aPoly )); + if( pPolyPoly2 ) + { + pPolyPoly2->setRGBALineColor( 0x008000FFU ); + pPolyPoly2->draw(); + } + } +#endif +} + +/** Get bounds in pixel + + @param rLayerBounds + Bound rect, in user space coordinates + + @param rTransformation + User space to device pixel transformation + + @return the layer bounds in pixel, extended by one pixel to the + right and bottom + */ +basegfx::B2IRange getLayerBoundsPixel( basegfx::B2DRange const& rLayerBounds, + basegfx::B2DHomMatrix const& rTransformation ) +{ + ::basegfx::B2DRange aTmpRect; + ::canvas::tools::calcTransformedRectBounds( aTmpRect, + rLayerBounds, + rTransformation ); + + if( aTmpRect.isEmpty() ) + return ::basegfx::B2IRange(); + + // #i42440# Returned layer size is one pixel too small, as + // rendering happens one pixel to the right and below the + // actual bound rect. + return ::basegfx::B2IRange( ::basegfx::fround(aTmpRect.getMinX()), + ::basegfx::fround(aTmpRect.getMinY()), + ::basegfx::fround(aTmpRect.getMaxX()) + 1, + ::basegfx::fround(aTmpRect.getMaxY()) + 1 ); +} + + +/** Container class for sprites issued by a ViewLayer + + This class handles the sprite prioritization issues, that are + needed for layer sprites (e.g. the need to re-prioritize sprites + when the layer changes prio). + */ +class LayerSpriteContainer +{ + /** Max fill level of maSprites, before we try to prune it from + deceased sprites + */ + enum{ SPRITE_ULLAGE=256 }; + + /** All sprites that have been issued by this container (pruned + from time to time, for invalid references). This vector is + kept sorted with increasing sprite priority. + */ + SpriteVector maSprites; + + /// Priority of this layer, relative to other view layers + basegfx::B1DRange maLayerPrioRange; + + double getSpritePriority( std::size_t nSpriteNum ) const + { + // divide the available layer range equally between all + // sprites, assign upper bound of individual sprite range as + // sprite prio (the layer itself gets assigned the lower bound + // of sprite 0's individual range): + + // | layer 0 | layer 1 | ... + // | sprite 0 | sprite 1 | sprite 0 | sprite 1 | ... + return maLayerPrioRange.getMinimum() + maLayerPrioRange.getRange()*(nSpriteNum+1)/(maSprites.size()+1); + } + + /** Rescan sprite vector, and remove deceased sprites (and reset + sprite prio) + + @param aBegin + Iterator to the first entry to rescan + */ + void updateSprites() + { + SpriteVector aValidSprites; + + // check all sprites for validity and set new priority + for( const auto& rSprite : maSprites ) + { + cppcanvas::CustomSpriteSharedPtr pCurrSprite( rSprite.mpSprite.lock() ); + + if( pCurrSprite ) + { + // only copy still valid sprites over to the refreshed + // sprite vector. + aValidSprites.push_back( rSprite ); + + pCurrSprite->setPriority( + getSpritePriority( aValidSprites.size()-1 )); + } + } + + // replace sprite list with pruned one + maSprites.swap( aValidSprites ); + } + +public: + LayerSpriteContainer() : + maSprites(), + maLayerPrioRange() + { + } + + const basegfx::B1DRange& getLayerPriority() const + { + return maLayerPrioRange; + } + + void setLayerPriority( const basegfx::B1DRange& rRange ) + { + if( rRange != maLayerPrioRange ) + { + maLayerPrioRange = rRange; + + // prune and recalc sprite prios + updateSprites(); + } + } + + void addSprite( const cppcanvas::CustomSpriteSharedPtr& pSprite, + double nPriority ) + { + if( !pSprite ) + return; + + SpriteEntry aEntry( pSprite,nPriority ); + + // insert new sprite, such that vector stays sorted + SpriteVector::iterator aInsertPos( + maSprites.insert( + std::lower_bound( maSprites.begin(), + maSprites.end(), + aEntry ), + aEntry )); + + const std::size_t nNumSprites( maSprites.size() ); + if( nNumSprites > SPRITE_ULLAGE || + maSprites.end() - aInsertPos > 1 ) + { + // updateSprites() also updates all sprite prios + updateSprites(); + } + else + { + // added sprite to the end, and not too many sprites in + // vector - perform optimized update (only need to set + // prio). This basically caters for the common case of + // iterated character animations, which generate lots of + // sprites, all added to the end. + pSprite->setPriority( + getSpritePriority( nNumSprites-1 )); + } + } + + void clear() + { + maSprites.clear(); + } +}; + + +/** This class provides layers for a slide view + + Layers are used to render animations with the correct z order - + because sprites are always in front of the static canvas + background, shapes that must appear <em<before</em> an animation + must also be displayed as a sprite. + + Each layer has a priority assigned to it (valid range [0,1]), which + also affects all sprites created for this specific layer - i.e. if + the layer priority changes, the sprites change z order together + with their parent. + */ +class SlideViewLayer : public ViewLayer +{ + /// Smart container for all sprites issued by this layer + mutable LayerSpriteContainer maSpriteContainer; + + /// Bounds of this layer in user space coordinates + basegfx::B2DRange maLayerBounds; + + /// Bounds of this layer in device pixel + mutable basegfx::B2IRange maLayerBoundsPixel; + + /// Current clip polygon in user coordinates + basegfx::B2DPolyPolygon maClip; + + /// Current size of the view in user coordinates + basegfx::B2DSize maUserSize; + + /// Current overall view transformation + basegfx::B2DHomMatrix maTransformation; + + /// 'parent' canvas, this viewlayer is associated with + const cppcanvas::SpriteCanvasSharedPtr mpSpriteCanvas; + + /** output surface (necessarily a sprite, won't otherwise be able + to display anything <em>before</em> other sprites) + */ + mutable cppcanvas::CustomSpriteSharedPtr mpSprite; + + /// actual output canvas retrieved from a sprite + mutable cppcanvas::CanvasSharedPtr mpOutputCanvas; + + /// ptr back to owning view. needed for isOnView() method + View const* const mpParentView; + +public: + /** Create a new layer + + @param pCanvas + Sprite canvas to create the layer on + + @param rTransform + Initial overall canvas transformation + + @param rLayerBounds + Initial layer bounds, in view coordinate system + */ + SlideViewLayer( const cppcanvas::SpriteCanvasSharedPtr& pCanvas, + const basegfx::B2DHomMatrix& rTransform, + const basegfx::B2DRange& rLayerBounds, + const basegfx::B2DSize& rUserSize, + View const* const pParentView) : + maSpriteContainer(), + maLayerBounds(rLayerBounds), + maLayerBoundsPixel(), + maClip(), + maUserSize(rUserSize), + maTransformation(rTransform), + mpSpriteCanvas(pCanvas), + mpSprite(), + mpOutputCanvas(), + mpParentView(pParentView) + { + } + + SlideViewLayer(const SlideViewLayer&) = delete; + SlideViewLayer& operator=(const SlideViewLayer&) = delete; + + void updateView( const basegfx::B2DHomMatrix& rMatrix, + const basegfx::B2DSize& rUserSize ) + { + maTransformation = rMatrix; + maUserSize = rUserSize; + + // limit layer bounds to visible screen + maLayerBounds.intersect( basegfx::B2DRange(0.0, + 0.0, + maUserSize.getX(), + maUserSize.getY()) ); + + basegfx::B2IRange const& rNewLayerPixel( + getLayerBoundsPixel(maLayerBounds, + maTransformation) ); + if( rNewLayerPixel != maLayerBoundsPixel ) + { + // re-gen sprite with new size + mpOutputCanvas.reset(); + mpSprite.reset(); + } + } + + virtual css::geometry::IntegerSize2D getTranslationOffset() const override + { + basegfx::B2DRectangle aTmpRect; + canvas::tools::calcTransformedRectBounds( aTmpRect, + maLayerBounds, + maTransformation ); + geometry::IntegerSize2D offset(0, 0); + + // Add translation according to the origin of aTmpRect. Ignore the + // translation when aTmpRect was not properly initialized. + if ( ! aTmpRect.isEmpty()) + { + offset.Width = basegfx::fround(aTmpRect.getMinX()); + offset.Height = basegfx::fround(aTmpRect.getMinY()); + } + return offset; + } + +private: + // ViewLayer interface + + + virtual cppcanvas::CustomSpriteSharedPtr createSprite( + const ::basegfx::B2DSize& rSpriteSizePixel, + double nPriority ) const override + { + cppcanvas::CustomSpriteSharedPtr pSprite( + mpSpriteCanvas->createCustomSprite( rSpriteSizePixel ) ); + + maSpriteContainer.addSprite( pSprite, + nPriority ); + + return pSprite; + } + + virtual void setPriority( const basegfx::B1DRange& rRange ) override + { + OSL_ENSURE( !rRange.isEmpty() && + rRange.getMinimum() >= 1.0, + "SlideViewLayer::setPriority(): prio MUST be larger than 1.0 (because " + "the background layer already lies there)" ); + + maSpriteContainer.setLayerPriority( rRange ); + + if( mpSprite ) + mpSprite->setPriority( rRange.getMinimum() ); + } + + virtual basegfx::B2DHomMatrix getTransformation() const override + { + // Offset given transformation by left, top border of given + // range (after transformation through given transformation) + basegfx::B2DRectangle aTmpRect; + canvas::tools::calcTransformedRectBounds( aTmpRect, + maLayerBounds, + maTransformation ); + + basegfx::B2DHomMatrix aMatrix( maTransformation ); + + // Add translation according to the origin of aTmpRect. Ignore the + // translation when aTmpRect was not properly initialized. + if ( ! aTmpRect.isEmpty()) + { + aMatrix.translate( -basegfx::fround(aTmpRect.getMinX()), + -basegfx::fround(aTmpRect.getMinY()) ); + } + + return aMatrix; + } + + virtual basegfx::B2DHomMatrix getSpriteTransformation() const override + { + return maTransformation; + } + + virtual void clear() const override + { + // grab canvas - that also lazy-initializes maLayerBoundsPixel + cppcanvas::CanvasSharedPtr pCanvas=getCanvas()->clone(); + + // clear whole canvas + const basegfx::B2I64Tuple& rSpriteSize(maLayerBoundsPixel.getRange()); + clearRect(pCanvas, + basegfx::B2IRange(0,0,rSpriteSize.getX(),rSpriteSize.getY())); + } + + virtual void clearAll() const override + { + // grab canvas - that also lazy-initializes maLayerBoundsPixel + ::cppcanvas::CanvasSharedPtr pCanvas( getCanvas()->clone() ); + + // clear layer clip, to clear whole area + pCanvas->setClip(); + + // clear whole canvas + const basegfx::B2I64Tuple& rSpriteSize(maLayerBoundsPixel.getRange()); + clearRect(pCanvas, + basegfx::B2IRange(0,0,rSpriteSize.getX(),rSpriteSize.getY())); + } + + virtual bool isOnView(ViewSharedPtr const& rView) const override + { + return rView.get() == mpParentView; + } + + virtual cppcanvas::CanvasSharedPtr getCanvas() const override + { + if( !mpOutputCanvas ) + { + if( !mpSprite ) + { + maLayerBoundsPixel = getLayerBoundsPixel(maLayerBounds, + maTransformation); + + // HACK: ensure at least 1x1 pixel size. clients might + // need an actual canvas (e.g. for bound rect + // calculations) without rendering anything. Better + // solution: introduce something like a reference + // canvas for ViewLayers, which is always available. + if( maLayerBoundsPixel.isEmpty() ) + maLayerBoundsPixel = basegfx::B2IRange(0,0,1,1); + + const basegfx::B2I64Tuple& rSpriteSize(maLayerBoundsPixel.getRange()); + mpSprite = mpSpriteCanvas->createCustomSprite( + basegfx::B2DVector(sal::static_int_cast<sal_Int32>(rSpriteSize.getX()), + sal::static_int_cast<sal_Int32>(rSpriteSize.getY())) ); + + mpSprite->setPriority( + maSpriteContainer.getLayerPriority().getMinimum() ); + +#if defined(DBG_UTIL) + mpSprite->movePixel( + basegfx::B2DPoint(maLayerBoundsPixel.getMinimum()) + + basegfx::B2DPoint(10,10) ); + + mpSprite->setAlpha(0.5); +#else + mpSprite->movePixel( + basegfx::B2DPoint(maLayerBoundsPixel.getMinimum()) ); + + mpSprite->setAlpha(1.0); +#endif + mpSprite->show(); + } + + ENSURE_OR_THROW( mpSprite, + "SlideViewLayer::getCanvas(): no layer sprite" ); + + mpOutputCanvas = mpSprite->getContentCanvas(); + + ENSURE_OR_THROW( mpOutputCanvas, + "SlideViewLayer::getCanvas(): sprite doesn't yield a canvas" ); + + // new canvas retrieved - setup transformation and clip + mpOutputCanvas->setTransformation( getTransformation() ); + mpOutputCanvas->setClip( + createClipPolygon( maClip, + mpOutputCanvas, + maUserSize )); + } + + return mpOutputCanvas; + } + + virtual void setClip( const basegfx::B2DPolyPolygon& rClip ) override + { + basegfx::B2DPolyPolygon aNewClip = prepareClip( rClip ); + + if( aNewClip != maClip ) + { + maClip = aNewClip; + + if(mpOutputCanvas ) + mpOutputCanvas->setClip( + createClipPolygon( maClip, + mpOutputCanvas, + maUserSize )); + } + } + + virtual bool resize( const ::basegfx::B2DRange& rArea ) override + { + const bool bRet( maLayerBounds != rArea ); + maLayerBounds = rArea; + updateView( maTransformation, + maUserSize ); + + return bRet; + } +}; + + +typedef cppu::WeakComponentImplHelper< + css::util::XModifyListener, + css::awt::XPaintListener> SlideViewBase; + +/** SlideView class + + This class implements the View interface, encapsulating + <em>one</em> view a slideshow is displayed on. + */ +class SlideView : private cppu::BaseMutex, + public SlideViewBase, + public UnoView +{ +public: + SlideView( const uno::Reference<presentation::XSlideShowView>& xView, + EventQueue& rEventQueue, + EventMultiplexer& rEventMultiplexer ); + void updateCanvas(); + +private: + // View: + virtual ViewLayerSharedPtr createViewLayer( const basegfx::B2DRange& rLayerBounds ) const override; + virtual bool updateScreen() const override; + virtual bool paintScreen() const override; + virtual void setViewSize( const ::basegfx::B2DSize& ) override; + virtual void setCursorShape( sal_Int16 nPointerShape ) override; + + // ViewLayer interface + virtual bool isOnView(ViewSharedPtr const& rView) const override; + virtual void clear() const override; + virtual void clearAll() const override; + virtual cppcanvas::CanvasSharedPtr getCanvas() const override; + virtual cppcanvas::CustomSpriteSharedPtr createSprite( const ::basegfx::B2DSize& rSpriteSizePixel, + double nPriority ) const override; + virtual void setPriority( const basegfx::B1DRange& rRange ) override; + virtual geometry::IntegerSize2D getTranslationOffset() const override; + virtual ::basegfx::B2DHomMatrix getTransformation() const override; + virtual basegfx::B2DHomMatrix getSpriteTransformation() const override; + virtual void setClip( const ::basegfx::B2DPolyPolygon& rClip ) override; + virtual bool resize( const ::basegfx::B2DRange& rArea ) override; + + // UnoView: + virtual void _dispose() override; + virtual uno::Reference<presentation::XSlideShowView> getUnoView()const override; + virtual void setIsSoundEnabled (const bool bValue) override; + virtual bool isSoundEnabled() const override; + + // XEventListener: + virtual void SAL_CALL disposing( lang::EventObject const& evt ) override; + // XModifyListener: + virtual void SAL_CALL modified( const lang::EventObject& aEvent ) override; + // XPaintListener: + virtual void SAL_CALL windowPaint( const awt::PaintEvent& e ) override; + + // WeakComponentImplHelperBase: + virtual void SAL_CALL disposing() override; + + void updateClip(); + +private: + typedef std::vector< std::weak_ptr<SlideViewLayer> > ViewLayerVector; + + /// Prune viewlayers from deceased ones, optionally update them + void pruneLayers( bool bWithViewLayerUpdate=false ) const; + + /** Max fill level of maViewLayers, before we try to prune it from + deceased layers + */ + enum{ LAYER_ULLAGE=8 }; + + uno::Reference<presentation::XSlideShowView> mxView; + cppcanvas::SpriteCanvasSharedPtr mpCanvas; + + EventMultiplexer& mrEventMultiplexer; + EventQueue& mrEventQueue; + + mutable LayerSpriteContainer maSprites; + mutable ViewLayerVector maViewLayers; + + basegfx::B2DPolyPolygon maClip; + + basegfx::B2DHomMatrix maViewTransform; + basegfx::B2DSize maUserSize; + bool mbIsSoundEnabled; +}; + + +SlideView::SlideView( const uno::Reference<presentation::XSlideShowView>& xView, + EventQueue& rEventQueue, + EventMultiplexer& rEventMultiplexer ) : + SlideViewBase( m_aMutex ), + mxView( xView ), + mpCanvas(), + mrEventMultiplexer( rEventMultiplexer ), + mrEventQueue( rEventQueue ), + maSprites(), + maViewLayers(), + maClip(), + maViewTransform(), + maUserSize( 1.0, 1.0 ), // default size: one-by-one rectangle + mbIsSoundEnabled(true) +{ + // take care not constructing any UNO references to this _inside_ + // ctor, shift that code to createSlideView()! + ENSURE_OR_THROW( mxView.is(), + "SlideView::SlideView(): Invalid view" ); + + mpCanvas = cppcanvas::VCLFactory::createSpriteCanvas( + xView->getCanvas() ); + ENSURE_OR_THROW( mpCanvas, + "Could not create cppcanvas" ); + + geometry::AffineMatrix2D aViewTransform( + xView->getTransformation() ); + + if( basegfx::fTools::equalZero( + basegfx::B2DVector(aViewTransform.m00, + aViewTransform.m10).getLength()) || + basegfx::fTools::equalZero( + basegfx::B2DVector(aViewTransform.m01, + aViewTransform.m11).getLength()) ) + { + OSL_FAIL( "SlideView::SlideView(): Singular matrix!" ); + + canvas::tools::setIdentityAffineMatrix2D(aViewTransform); + } + + basegfx::unotools::homMatrixFromAffineMatrix( + maViewTransform, aViewTransform ); + + // once and forever: set fixed prio to this 'layer' (we're always + // the background layer) + maSprites.setLayerPriority( basegfx::B1DRange(0.0,1.0) ); +} + +void SlideView::disposing() +{ + osl::MutexGuard aGuard( m_aMutex ); + + maViewLayers.clear(); + maSprites.clear(); + mpCanvas.reset(); + + // additionally, also de-register from XSlideShowView + if (mxView.is()) + { + mxView->removeTransformationChangedListener( this ); + mxView->removePaintListener( this ); + mxView.clear(); + } +} + +ViewLayerSharedPtr SlideView::createViewLayer( const basegfx::B2DRange& rLayerBounds ) const +{ + osl::MutexGuard aGuard( m_aMutex ); + + ENSURE_OR_THROW( mpCanvas, + "SlideView::createViewLayer(): Disposed" ); + + const std::size_t nNumLayers( maViewLayers.size() ); + + // avoid filling up layer vector with lots of deceased layer weak + // ptrs + if( nNumLayers > LAYER_ULLAGE ) + pruneLayers(); + + auto xViewLayer = std::make_shared<SlideViewLayer>(mpCanvas, + getTransformation(), + rLayerBounds, + maUserSize, + this); + maViewLayers.push_back(xViewLayer); + + return xViewLayer; +} + +bool SlideView::updateScreen() const +{ + osl::MutexGuard aGuard( m_aMutex ); + + ENSURE_OR_RETURN_FALSE( mpCanvas, + "SlideView::updateScreen(): Disposed" ); + + return mpCanvas->updateScreen( false ); +} + +bool SlideView::paintScreen() const +{ + osl::MutexGuard aGuard( m_aMutex ); + + ENSURE_OR_RETURN_FALSE( mpCanvas, + "SlideView::paintScreen(): Disposed" ); + + return mpCanvas->updateScreen( true ); +} + +void SlideView::clear() const +{ + osl::MutexGuard aGuard( m_aMutex ); + + OSL_ENSURE( mxView.is() && mpCanvas, + "SlideView::clear(): Disposed" ); + if( !mxView.is() || !mpCanvas ) + return; + + // keep layer clip + clearRect(getCanvas()->clone(), + getLayerBoundsPixel( + basegfx::B2DRange(0,0, + maUserSize.getX(), + maUserSize.getY()), + getTransformation())); +} + +void SlideView::clearAll() const +{ + osl::MutexGuard aGuard( m_aMutex ); + + OSL_ENSURE( mxView.is() && mpCanvas, + "SlideView::clear(): Disposed" ); + if( !mxView.is() || !mpCanvas ) + return; + + mpCanvas->clear(); // this is unnecessary, strictly speaking. but + // it makes the SlideView behave exactly like a + // sprite-based SlideViewLayer, because those + // are created from scratch after a resize + + // clear whole view + mxView->clear(); +} + +void SlideView::setViewSize( const basegfx::B2DSize& rSize ) +{ + osl::MutexGuard aGuard( m_aMutex ); + + maUserSize = rSize; + updateCanvas(); +} + +void SlideView::setCursorShape( sal_Int16 nPointerShape ) +{ + osl::MutexGuard const guard( m_aMutex ); + + if (mxView.is()) + mxView->setMouseCursor( nPointerShape ); +} + +bool SlideView::isOnView(ViewSharedPtr const& rView) const +{ + return rView.get() == this; +} + +cppcanvas::CanvasSharedPtr SlideView::getCanvas() const +{ + osl::MutexGuard aGuard( m_aMutex ); + + ENSURE_OR_THROW( mpCanvas, + "SlideView::getCanvas(): Disposed" ); + + return mpCanvas; +} + +cppcanvas::CustomSpriteSharedPtr SlideView::createSprite( + const basegfx::B2DSize& rSpriteSizePixel, + double nPriority ) const +{ + osl::MutexGuard aGuard( m_aMutex ); + + ENSURE_OR_THROW( mpCanvas, "SlideView::createSprite(): Disposed" ); + + cppcanvas::CustomSpriteSharedPtr pSprite( + mpCanvas->createCustomSprite( rSpriteSizePixel ) ); + + maSprites.addSprite( pSprite, + nPriority ); + + return pSprite; +} + +void SlideView::setPriority( const basegfx::B1DRange& /*rRange*/ ) +{ + osl::MutexGuard aGuard( m_aMutex ); + + OSL_FAIL( "SlideView::setPriority() is a NOOP for slide view - " + "content will always be shown in the background" ); +} + +basegfx::B2DHomMatrix SlideView::getTransformation() const +{ + osl::MutexGuard aGuard( m_aMutex ); + + basegfx::B2DHomMatrix aMatrix; + aMatrix.scale( 1.0/maUserSize.getX(), 1.0/maUserSize.getY() ); + + return maViewTransform * aMatrix; +} + +geometry::IntegerSize2D SlideView::getTranslationOffset() const +{ + return mxView->getTranslationOffset(); +} + +basegfx::B2DHomMatrix SlideView::getSpriteTransformation() const +{ + return getTransformation(); +} + +void SlideView::setClip( const basegfx::B2DPolyPolygon& rClip ) +{ + osl::MutexGuard aGuard( m_aMutex ); + + basegfx::B2DPolyPolygon aNewClip = prepareClip( rClip ); + + if( aNewClip != maClip ) + { + maClip = aNewClip; + + updateClip(); + } +} + +bool SlideView::resize( const ::basegfx::B2DRange& /*rArea*/ ) +{ + osl::MutexGuard aGuard( m_aMutex ); + + OSL_FAIL( "SlideView::resize(): ignored for the View, can't change size " + "effectively, anyway" ); + + return false; +} + +uno::Reference<presentation::XSlideShowView> SlideView::getUnoView() const +{ + osl::MutexGuard aGuard( m_aMutex ); + return mxView; +} + +void SlideView::setIsSoundEnabled (const bool bValue) +{ + mbIsSoundEnabled = bValue; +} + +bool SlideView::isSoundEnabled() const +{ + return mbIsSoundEnabled; +} + +void SlideView::_dispose() +{ + dispose(); +} + +// XEventListener +void SlideView::disposing( lang::EventObject const& evt ) +{ + // no deregistration necessary anymore, XView has left: + osl::MutexGuard const guard( m_aMutex ); + + if (mxView.is()) + { + OSL_ASSERT( evt.Source == mxView ); + mxView.clear(); + } + + dispose(); +} + +// silly wrapper to check that event handlers don't touch dead SlideView +struct WeakRefWrapper +{ + SlideView & m_rObj; + uno::WeakReference<uno::XInterface> const m_wObj; + std::function<void (SlideView&)> const m_func; + + WeakRefWrapper(SlideView & rObj, std::function<void (SlideView&)> const& func) + : m_rObj(rObj) + , m_wObj(static_cast<::cppu::OWeakObject*>(&rObj)) + , m_func(func) + { + } + + void operator()() + { + uno::Reference<uno::XInterface> const xObj(m_wObj); + if (xObj.is()) + { + m_func(m_rObj); + } + } +}; + +// XModifyListener +void SlideView::modified( const lang::EventObject& /*aEvent*/ ) +{ + osl::MutexGuard const guard( m_aMutex ); + + OSL_ENSURE( mxView.is(), "SlideView::modified(): " + "Disposed, but event received from XSlideShowView?!"); + + if( !mxView.is() ) + return; + + geometry::AffineMatrix2D aViewTransform( + mxView->getTransformation() ); + + if( basegfx::fTools::equalZero( + basegfx::B2DVector(aViewTransform.m00, + aViewTransform.m10).getLength()) || + basegfx::fTools::equalZero( + basegfx::B2DVector(aViewTransform.m01, + aViewTransform.m11).getLength()) ) + { + OSL_FAIL( "SlideView::modified(): Singular matrix!" ); + + canvas::tools::setIdentityAffineMatrix2D(aViewTransform); + } + + // view transformation really changed? + basegfx::B2DHomMatrix aNewTransform; + basegfx::unotools::homMatrixFromAffineMatrix( + aNewTransform, + aViewTransform ); + + if( aNewTransform == maViewTransform ) + return; // No change, nothing to do + + maViewTransform = aNewTransform; + + updateCanvas(); + + // notify view change. Don't call EventMultiplexer directly, this + // might not be the main thread! + mrEventQueue.addEvent( + makeEvent( WeakRefWrapper(*this, + [] (SlideView & rThis) { rThis.mrEventMultiplexer.notifyViewChanged(rThis.mxView); }), + "EventMultiplexer::notifyViewChanged")); +} + +// XPaintListener +void SlideView::windowPaint( const awt::PaintEvent& /*e*/ ) +{ + osl::MutexGuard aGuard( m_aMutex ); + + OSL_ENSURE( mxView.is() && mpCanvas, "Disposed, but event received?!" ); + + // notify view clobbering. Don't call EventMultiplexer directly, + // this might not be the main thread! + mrEventQueue.addEvent( + makeEvent( WeakRefWrapper(*this, + [] (SlideView & rThis) { rThis.mrEventMultiplexer.notifyViewClobbered(rThis.mxView); }), + "EventMultiplexer::notifyViewClobbered") ); +} + +void SlideView::updateCanvas() +{ + OSL_ENSURE( mpCanvas, + "SlideView::updateCanvasTransform(): Disposed" ); + + if( !mpCanvas || !mxView.is()) + return; + + clearAll(); + mpCanvas->setTransformation( getTransformation() ); + mpCanvas->setClip( + createClipPolygon( maClip, + mpCanvas, + maUserSize )); + + // forward update to viewlayers + pruneLayers( true ); +} + +void SlideView::updateClip() +{ + OSL_ENSURE( mpCanvas, + "SlideView::updateClip(): Disposed" ); + + if( !mpCanvas ) + return; + + mpCanvas->setClip( + createClipPolygon( maClip, + mpCanvas, + maUserSize )); + + pruneLayers(); +} + +void SlideView::pruneLayers( bool bWithViewLayerUpdate ) const +{ + ViewLayerVector aValidLayers; + + const basegfx::B2DHomMatrix& rCurrTransform( + getTransformation() ); + + // check all layers for validity, and retain only the live ones + for( const auto& rView : maViewLayers ) + { + std::shared_ptr< SlideViewLayer > xCurrLayer( rView.lock() ); + + if ( xCurrLayer ) + { + aValidLayers.push_back( xCurrLayer ); + + if( bWithViewLayerUpdate ) + xCurrLayer->updateView( rCurrTransform, + maUserSize ); + } + } + + // replace layer list with pruned one + maViewLayers.swap( aValidLayers ); +} + +} // anonymous namespace + +UnoViewSharedPtr createSlideView( uno::Reference< presentation::XSlideShowView> const& xView, + EventQueue& rEventQueue, + EventMultiplexer& rEventMultiplexer ) +{ + std::shared_ptr<SlideView> const that( + comphelper::make_shared_from_UNO( + new SlideView(xView, + rEventQueue, + rEventMultiplexer))); + + // register listeners with XSlideShowView + xView->addTransformationChangedListener( that.get() ); + xView->addPaintListener( that.get() ); + + // set new transformation + that->updateCanvas(); + + return that; +} + +} // namespace slideshow + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |