tdf#37483 Add drag'n'drop to reorder custom Impress animations

- Support drag'n'drop of single items. If the animation is collapsed,
  it will move its collapsed sub-animations as well.
- Sub-lists remain expanded when its parent animation is moved, or
  a sub animation becomes the new parent of the text group (due to moving).
- Maintain the cursor between custom animation list redraws.
- Don't change selection on collapse after custom animation list
  rebuild.

Change-Id: I92d96e9a01c6884ef739612e456cc541218b6ebb
Reviewed-on: https://gerrit.libreoffice.org/62342
Reviewed-by: Thorsten Behrens <Thorsten.Behrens@CIB.de>
Tested-by: Thorsten Behrens <Thorsten.Behrens@CIB.de>
diff --git a/sd/inc/CustomAnimationEffect.hxx b/sd/inc/CustomAnimationEffect.hxx
index b70bbdd..0319e9e 100644
--- a/sd/inc/CustomAnimationEffect.hxx
+++ b/sd/inc/CustomAnimationEffect.hxx
@@ -277,6 +277,7 @@ public:
    SAL_DLLPRIVATE void replace( const CustomAnimationEffectPtr& pEffect, const CustomAnimationPresetPtr& pDescriptor, double fDuration );
    SAL_DLLPRIVATE void replace( const CustomAnimationEffectPtr& pEffect, const CustomAnimationPresetPtr& pDescriptor, const OUString& rPresetSubType, double fDuration );
    SAL_DLLPRIVATE void remove( const CustomAnimationEffectPtr& pEffect );
    SAL_DLLPRIVATE void moveToBeforeEffect( const CustomAnimationEffectPtr& pEffect,  const CustomAnimationEffectPtr& pInsertBefore);

    SAL_DLLPRIVATE void create( const css::uno::Reference< css::animations::XAnimationNode >& xNode );
    SAL_DLLPRIVATE void createEffectsequence( const css::uno::Reference< css::animations::XAnimationNode >& xNode );
diff --git a/sd/source/core/CustomAnimationEffect.cxx b/sd/source/core/CustomAnimationEffect.cxx
index d695ef8..230f281 100644
--- a/sd/source/core/CustomAnimationEffect.cxx
+++ b/sd/source/core/CustomAnimationEffect.cxx
@@ -1790,6 +1790,20 @@ void EffectSequenceHelper::remove( const CustomAnimationEffectPtr& pEffect )
    rebuild();
}

void EffectSequenceHelper::moveToBeforeEffect( const CustomAnimationEffectPtr& pEffect, const CustomAnimationEffectPtr& pInsertBefore)
{
    if ( pEffect.get() )
    {
        maEffects.remove( pEffect );
        EffectSequence::iterator aInsertIter( find( pInsertBefore ) );

        // aInsertIter being end() is OK: pInsertBefore could be null, so put at end.
        maEffects.insert( aInsertIter, pEffect );

        rebuild();
    }
}

void EffectSequenceHelper::rebuild()
{
    implRebuild();
diff --git a/sd/source/ui/animations/CustomAnimationList.cxx b/sd/source/ui/animations/CustomAnimationList.cxx
index 909ea4d..0bc6f9f 100644
--- a/sd/source/ui/animations/CustomAnimationList.cxx
+++ b/sd/source/ui/animations/CustomAnimationList.cxx
@@ -440,11 +440,180 @@ CustomAnimationList::CustomAnimationList( vcl::Window* pParent )
    , mpController(nullptr)
    , mnLastGroupId(0)
    , mpLastParentEntry(nullptr)
    , mpDndEffectDragging(nullptr)
    , mpDndEffectInsertBefore(nullptr)
{
    EnableContextMenuHandling();
    SetSelectionMode( SelectionMode::Multiple );
    SetOptimalImageIndent();
    SetNodeDefaultImages();

    SetDragDropMode(DragDropMode::CTRL_MOVE);
}

// D'n'D #1: Prepare selected element for moving.
DragDropMode CustomAnimationList::NotifyStartDrag( TransferDataContainer& /*rData*/, SvTreeListEntry* pEntry )
{
    mpDndEffectDragging = pEntry;
    mpDndEffectInsertBefore = pEntry;

    return DragDropMode::CTRL_MOVE;
}

// D'n'D #2: Called each time mouse moves during drag
sal_Int8 CustomAnimationList::AcceptDrop( const AcceptDropEvent& rEvt )
{
    /*
        Don't call SvTreeListBox::AcceptDrop because it puts an unnecessary
        highlight via ImplShowTargetEmphasis()
    */

    sal_Int8 ret = DND_ACTION_NONE;
    const bool bIsMove = ( DND_ACTION_MOVE == rEvt.mnAction );

    if( mpDndEffectDragging && !rEvt.mbLeaving && bIsMove )
    {
        SvTreeListEntry* pEntry = GetDropTarget( rEvt.maPosPixel );

        const bool bNotOverSelf = ( pEntry != mpDndEffectDragging );
        if( pEntry && bNotOverSelf )
        {
            /*
                If dragged effect has visible children then we must re-parent the children
                first so that they are not dragged with the parent. Re-parenting (only in the UI!)
                dragged effect's first child to the root, and the remaining children to 1st child.
            */
            if( GetVisibleChildCount( mpDndEffectDragging ) > 0 )
            {
                SvTreeListEntry* pFirstChild = FirstChild( mpDndEffectDragging );
                SvTreeListEntry* pEntryParent = GetParent( mpDndEffectDragging );
                sal_uLong nInsertAfterPos = SvTreeList::GetRelPos( mpDndEffectDragging ) + 1;

                // Re-parent 1st child to root, below all the other children.
                pModel->Move( pFirstChild, pEntryParent, nInsertAfterPos );

                // Re-parent children after 1st child to the first child
                sal_uLong nInsertNextChildPos = 0;
                while( FirstChild( mpDndEffectDragging ) )
                {
                    SvTreeListEntry* pNextChild = FirstChild( mpDndEffectDragging );
                    ++nInsertNextChildPos;
                    pModel->Move( pNextChild, pFirstChild, nInsertNextChildPos );
                }

                // Expand all children (they were previously visible)
                Expand( pFirstChild );
            }

            ReorderEffectsInUiDuringDragOver( pEntry );
        }

        // Return DND_ACTION_MOVE on internal drag'n'drops so that ExecuteDrop() is called.
        // Return MOVE even if we are over othe dragged effect because dragged effect moves.
        ret = DND_ACTION_MOVE;
    }

    return ret;
}

// D'n'D: Update UI to show where dragged event will appear if dropped now.
void CustomAnimationList::ReorderEffectsInUiDuringDragOver( SvTreeListEntry* pOverEntry )
{
    /*
        Update the order of effects on *just the UI* as the user drags.
        The model (MainSequence) will only be changed after the user drops
        the effect because this triggers a rebuild of the list which removes
        and recreates all effects (on a background timer). Hence, this would
        invalidate the pointer for the entry currently being dragged.
        Plus, reordering the model during drag would have to reverse any model changes
        if the drag were canceled, and ensure only one Undo record created per successful drag.
    */

    // Compute new location in *UI*
    SvTreeListEntry* pNewParent = nullptr;
    sal_uLong nInsertAfterPos = 0;

    Point aPosOverEffect( GetEntryPosition(pOverEntry) );
    Point aPosDraggedEffect( GetEntryPosition(mpDndEffectDragging) );
    const bool bDraggingUp = (aPosDraggedEffect.Y() - aPosOverEffect.Y()) > 0;

    if( bDraggingUp )
    {
        // Drag up   --> place above the element we are over
        pNewParent = GetParent( pOverEntry );
        nInsertAfterPos = SvTreeList::GetRelPos( pOverEntry );
        mpDndEffectInsertBefore = pOverEntry;
    }
    else
    {
        // Drag down -->  place below the element we are over
        SvTreeListEntry* pNextVisBelowTarget = NextVisible( pOverEntry );
        if( pNextVisBelowTarget )
        {
            // Match parent of NEXT visible effect (works for sub-items too)
            pNewParent = GetParent( pNextVisBelowTarget );
            nInsertAfterPos = SvTreeList::GetRelPos( pNextVisBelowTarget );
            mpDndEffectInsertBefore = pNextVisBelowTarget;
        }
        else
        {
            // Over the last element: no next to work with
            pNewParent = GetParent( pOverEntry );
            nInsertAfterPos = SvTreeList::GetRelPos( pOverEntry ) + 1;
            mpDndEffectInsertBefore = nullptr;
        }
    }

    // Update *just* the UI to show where dragged element would currently be if dropped.
    pModel->Move( mpDndEffectDragging, pNewParent, nInsertAfterPos );

    // Restore selection
    Select( mpDndEffectDragging );
}

// D'n'D #4: Tell model to update effect order.
sal_Int8 CustomAnimationList::ExecuteDrop( const ExecuteDropEvent& /*rEvt*/ )
{
    // NOTE: We cannot just override NotifyMoving() because it's not called
    //       since we dynamically reorder effects during drag.

    sal_Int8 ret = DND_ACTION_NONE;

    const bool bMovingEffect = ( mpDndEffectDragging != nullptr );
    const bool bMoveNotSelf  = ( mpDndEffectInsertBefore != mpDndEffectDragging );
    const bool bHaveSequence = ( mpMainSequence.get() != nullptr );

    if( bMovingEffect && bMoveNotSelf && bHaveSequence )
    {
        CustomAnimationListEntry*  pEntryMoved = static_cast< CustomAnimationListEntry* >( mpDndEffectDragging );
        CustomAnimationListEntry*  pTarget = static_cast< CustomAnimationListEntry* >( mpDndEffectInsertBefore );

        // Callback to observer to have it update the model.
        // If pTarget is null, pass nullptr to indicate end of list.
        mpController->onDragNDropComplete(
            pEntryMoved->getEffect(),
            pTarget ? pTarget->getEffect() : nullptr );

        // Reset selection
        Select( mpDndEffectDragging );

        ret = DND_ACTION_MOVE;
    }

    return ret;
}

// D'n'D #5: Cleanup (regardless of if we were target of drop or not)
void CustomAnimationList::DragFinished( sal_Int8 nDropAction )
{
    mpDndEffectDragging = nullptr;
    mpDndEffectInsertBefore = nullptr;

    // Rebuild because we may have re-parented the dragged effect's first child.
    // Can hit this without running ExecuteDrop(...) when drag canceled.
    mpMainSequence->rebuild();

    SvTreeListBox::DragFinished( nDropAction );
}

VCL_BUILDER_FACTORY(CustomAnimationList)
@@ -553,8 +722,9 @@ void CustomAnimationList::update()

    CustomAnimationListEntry* pEntry = nullptr;

    std::vector< CustomAnimationEffectPtr > aExpanded;
    std::vector< CustomAnimationEffectPtr > aVisible;
    std::vector< CustomAnimationEffectPtr > aSelected;
    CustomAnimationEffectPtr aCurrent;

    CustomAnimationEffectPtr pFirstSelEffect;
    CustomAnimationEffectPtr pLastSelEffect;
@@ -588,7 +758,7 @@ void CustomAnimationList::update()
            nLastSelOld = GetAbsPos( pEntry );
        }

        // save selection and expand states
        // save selection, current, and expand (visible) states
        pEntry = static_cast<CustomAnimationListEntry*>(First());

        while( pEntry )
@@ -596,8 +766,8 @@ void CustomAnimationList::update()
            CustomAnimationEffectPtr pEffect( pEntry->getEffect() );
            if( pEffect.get() )
            {
                if( IsExpanded( pEntry ) )
                    aExpanded.push_back( pEffect );
                if( IsEntryVisible( pEntry ) )
                    aVisible.push_back( pEffect );

                if( IsSelected( pEntry ) )
                    aSelected.push_back( pEffect );
@@ -605,6 +775,10 @@ void CustomAnimationList::update()

            pEntry = static_cast<CustomAnimationListEntry*>(Next( pEntry ));
        }

        pEntry = static_cast<CustomAnimationListEntry*>(GetCurEntry());
        if( pEntry )
            aCurrent = pEntry->getEffect();
    }

    // rebuild list
@@ -639,7 +813,7 @@ void CustomAnimationList::update()
            }
        }

        // restore selection and expand states
        // restore selection state, expand state, and current-entry (under cursor)
        pEntry = static_cast<CustomAnimationListEntry*>(First());

        while( pEntry )
@@ -647,12 +821,21 @@ void CustomAnimationList::update()
            CustomAnimationEffectPtr pEffect( pEntry->getEffect() );
            if( pEffect.get() )
            {
                if( std::find( aExpanded.begin(), aExpanded.end(), pEffect ) != aExpanded.end() )
                    Expand( pEntry );
                // Any effects that were visible should still be visible, so expand their parents.
                // (a previously expanded parent may have moved leaving a child to now be the new parent to expand)
                if( std::find( aVisible.begin(), aVisible.end(), pEffect ) != aVisible.end() )
                {
                    if( GetParent(pEntry) )
                        Expand( GetParent(pEntry) );
                }

                if( std::find( aSelected.begin(), aSelected.end(), pEffect ) != aSelected.end() )
                    Select( pEntry );

                // Restore the cursor; don't use SetCurEntry() as it may deselect other effects
                if( pEffect == aCurrent )
                    SetCursor( pEntry );

                if( pEffect == pFirstSelEffect )
                    nFirstSelNew = GetAbsPos( pEntry );

@@ -835,6 +1018,21 @@ bool CustomAnimationList::isExpanded( const CustomAnimationEffectPtr& pEffect ) 
    return (pEntry == nullptr) || IsExpanded( pEntry );
}

bool CustomAnimationList::isVisible( const CustomAnimationEffectPtr& pEffect ) const
{
    CustomAnimationListEntry* pEntry = static_cast<CustomAnimationListEntry*>(First());

    while( pEntry )
    {
        if( pEntry->getEffect() == pEffect )
            break;

        pEntry = static_cast<CustomAnimationListEntry*>(Next( pEntry ));
    }

    return (pEntry == nullptr) || IsEntryVisible( pEntry );
}

EffectSequence CustomAnimationList::getSelection() const
{
    EffectSequence aSelection;
diff --git a/sd/source/ui/animations/CustomAnimationList.hxx b/sd/source/ui/animations/CustomAnimationList.hxx
index 2040e7f..7252bea 100644
--- a/sd/source/ui/animations/CustomAnimationList.hxx
+++ b/sd/source/ui/animations/CustomAnimationList.hxx
@@ -40,6 +40,7 @@ public:
    virtual void onSelect() = 0;
    virtual void onDoubleClick() = 0;
    virtual void onContextMenu(const OString &rIdent) = 0;
    virtual void onDragNDropComplete( CustomAnimationEffectPtr pEffectDragged, CustomAnimationEffectPtr pEffectInsertBefore ) = 0;
    virtual ~ICustomAnimationListController() {}
};

@@ -83,8 +84,9 @@ public:
    virtual void notify_change() override;

    bool isExpanded( const CustomAnimationEffectPtr& pEffect ) const;
    bool isVisible( const CustomAnimationEffectPtr& pEffect ) const;

    /// clears all entries from the listbox
    // clears all entries from the listbox
    void clear();

    void setController( ICustomAnimationListController* pController )
@@ -92,6 +94,15 @@ public:
        mpController = pController;
    };


protected:
    // drag & drop
    virtual DragDropMode NotifyStartDrag( TransferDataContainer& rData, SvTreeListEntry* pEntry ) override;
    virtual sal_Int8     AcceptDrop( const AcceptDropEvent& rEvt ) override;
    virtual void         ReorderEffectsInUiDuringDragOver( SvTreeListEntry* pOverEntry);
    virtual sal_Int8     ExecuteDrop( const ExecuteDropEvent& rEvt ) override;
    virtual void         DragFinished( sal_Int8 nDropAction ) override;

private:
    std::unique_ptr<VclBuilder> mxBuilder;
    VclPtr<PopupMenu> mxMenu;
@@ -109,6 +120,9 @@ private:
    sal_Int32 mnLastGroupId;
    SvTreeListEntry* mpLastParentEntry;

    // drag & drop
    SvTreeListEntry* mpDndEffectDragging;
    SvTreeListEntry* mpDndEffectInsertBefore;
};

OUString getPropertyName( sal_Int32 nPropertyType );
diff --git a/sd/source/ui/animations/CustomAnimationPane.cxx b/sd/source/ui/animations/CustomAnimationPane.cxx
index 1e04f66..ab69507 100644
--- a/sd/source/ui/animations/CustomAnimationPane.cxx
+++ b/sd/source/ui/animations/CustomAnimationPane.cxx
@@ -2509,6 +2509,39 @@ void CustomAnimationPane::onSelect()
    }
}

// ICustomAnimationListController
// pEffectInsertBefore may be null if moving to end of list.
void CustomAnimationPane::onDragNDropComplete(CustomAnimationEffectPtr pEffectDragged, CustomAnimationEffectPtr pEffectInsertBefore)
{
    if ( mpMainSequence.get() )
    {
        addUndo();

        MainSequenceRebuildGuard aGuard( mpMainSequence );

        // Move the dragged effect and any hidden sub-effects
        EffectSequence::iterator aIter = mpMainSequence->find( pEffectDragged );
        const EffectSequence::iterator aEnd( mpMainSequence->getEnd() );

        while( aIter != aEnd )
        {
            CustomAnimationEffectPtr pEffect = (*aIter++);

            // Update model with new location (function triggers a rebuild)
            // target may be null, which will insert at the end.
            mpMainSequence->moveToBeforeEffect( pEffect, pEffectInsertBefore );

            // Done moving effect and its hidden sub-effects when *next* effect is visible.
            if ( mpCustomAnimationList->isVisible( *aIter ) )
                break;
        }

        updateControls();
        mrBase.GetDocShell()->SetModified();
    }
}


void CustomAnimationPane::updatePathFromMotionPathTag( const rtl::Reference< MotionPathTag >& xTag )
{
    MainSequenceRebuildGuard aGuard( mpMainSequence );
diff --git a/sd/source/ui/animations/CustomAnimationPane.hxx b/sd/source/ui/animations/CustomAnimationPane.hxx
index f13b0cb..596d2b5 100644
--- a/sd/source/ui/animations/CustomAnimationPane.hxx
+++ b/sd/source/ui/animations/CustomAnimationPane.hxx
@@ -88,6 +88,7 @@ public:
    virtual void onSelect() override;
    virtual void onDoubleClick() override;
    virtual void onContextMenu(const OString& rIdent) override;
    virtual void onDragNDropComplete( CustomAnimationEffectPtr pEffectDragged, CustomAnimationEffectPtr pEffectInsertBefore ) override;

    // Window
    virtual void DataChanged (const DataChangedEvent& rEvent) override;