/* -*- 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/presentation/EffectPresetClass.hpp>
#include <com/sun/star/animations/XAnimationNodeSupplier.hpp>
#include <com/sun/star/animations/AnimationNodeType.hpp>
#include <com/sun/star/animations/ParallelTimeContainer.hpp>
#include <com/sun/star/view/XSelectionSupplier.hpp>
#include <com/sun/star/document/XActionLockable.hpp>
#include <com/sun/star/drawing/XDrawView.hpp>
#include <com/sun/star/drawing/XShape.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/presentation/EffectNodeType.hpp>
#include <com/sun/star/presentation/EffectCommands.hpp>
#include <com/sun/star/animations/AnimationTransformType.hpp>
#include <com/sun/star/text/XTextRangeCompare.hpp>
#include <com/sun/star/container/XEnumerationAccess.hpp>
#include <com/sun/star/container/XIndexAccess.hpp>
#include <com/sun/star/presentation/ParagraphTarget.hpp>
#include <com/sun/star/text/XText.hpp>
#include <com/sun/star/drawing/LineStyle.hpp>
#include <com/sun/star/drawing/FillStyle.hpp>
#include <comphelper/processfactory.hxx>
#include <comphelper/scopeguard.hxx>
#include <sfx2/dispatch.hxx>
#include <sfx2/viewfrm.hxx>
#include <tools/debug.hxx>
#include "STLPropertySet.hxx"
#include <CustomAnimationPane.hxx>
#include "CustomAnimationDialog.hxx"
#include <CustomAnimationList.hxx>
#include "motionpathtag.hxx"
#include <CustomAnimationPreset.hxx>

#include <comphelper/lok.hxx>
#include <comphelper/sequence.hxx>
#include <sfx2/frame.hxx>
#include <comphelper/diagnose_ex.hxx>

#include <svx/svxids.hrc>
#include <DrawDocShell.hxx>
#include <ViewShellBase.hxx>
#include <DrawViewShell.hxx>
#include <DrawController.hxx>
#include <sdresid.hxx>
#include <drawview.hxx>
#include <slideshow.hxx>
#include <undoanim.hxx>
#include <optsitem.hxx>
#include <sdmod.hxx>
#include <framework/FrameworkHelper.hxx>

#include <EventMultiplexer.hxx>

#include <strings.hrc>
#include <sdpage.hxx>
#include <app.hrc>

#include <svx/strings.hrc>
#include <svx/dialmgr.hxx>

#include <algorithm>
#include <memory>

using namespace ::com::sun::star;
using namespace ::com::sun::star::animations;
using namespace ::com::sun::star::presentation;
using namespace ::com::sun::star::text;

using namespace ::com::sun::star::uno;
using namespace ::com::sun::star::drawing;
using ::com::sun::star::view::XSelectionSupplier;
using ::com::sun::star::beans::XPropertySet;
using ::com::sun::star::container::XIndexAccess;
using ::com::sun::star::container::XEnumerationAccess;
using ::com::sun::star::container::XEnumeration;
using ::com::sun::star::text::XText;
using ::sd::framework::FrameworkHelper;
using ::com::sun::star::uno::UNO_QUERY;
using ::com::sun::star::uno::UNO_QUERY_THROW;
using ::com::sun::star::uno::Any;
using ::com::sun::star::uno::Reference;
using ::com::sun::star::uno::Exception;

namespace sd {

void fillRepeatComboBox(weld::ComboBox& rBox)
{
    OUString aNone( SdResId( STR_CUSTOMANIMATION_REPEAT_NONE ) );
    rBox.append_text(aNone);
    rBox.append_text(OUString::number(2));
    rBox.append_text(OUString::number(3));
    rBox.append_text(OUString::number(4));
    rBox.append_text(OUString::number(5));
    rBox.append_text(OUString::number(10));

    OUString aUntilClick( SdResId( STR_CUSTOMANIMATION_REPEAT_UNTIL_NEXT_CLICK ) );
    rBox.append_text(aUntilClick);

    OUString aEndOfSlide( SdResId( STR_CUSTOMANIMATION_REPEAT_UNTIL_END_OF_SLIDE ) );
    rBox.append_text(aEndOfSlide);
}

CustomAnimationPane::CustomAnimationPane( weld::Widget* pParent, ViewShellBase& rBase )
    : PanelLayout(pParent, u"CustomAnimationsPanel"_ustr, u"modules/simpress/ui/customanimationspanel.ui"_ustr)
    , mrBase(rBase)
    // load resources
    , mxFTAnimation(m_xBuilder->weld_label(u"effectlabel"_ustr))
    , mxCustomAnimationList(new CustomAnimationList(m_xBuilder->weld_tree_view(u"custom_animation_list"_ustr),
                                                    m_xBuilder->weld_label(u"custom_animation_label"_ustr),
                                                    m_xBuilder->weld_widget(u"custom_animation_label_parent"_ustr)))
    , mxPBAddEffect(m_xBuilder->weld_button(u"add_effect"_ustr))
    , mxPBRemoveEffect(m_xBuilder->weld_button(u"remove_effect"_ustr))
    , mxPBMoveUp(m_xBuilder->weld_button(u"move_up"_ustr))
    , mxPBMoveDown(m_xBuilder->weld_button(u"move_down"_ustr))
    , mxFTCategory(m_xBuilder->weld_label(u"categorylabel"_ustr))
    , mxLBCategory(m_xBuilder->weld_combo_box(u"categorylb"_ustr))
    , mxFTEffect(m_xBuilder->weld_label(u"effect_label"_ustr))
    , mxLBAnimation(m_xBuilder->weld_tree_view(u"effect_list"_ustr))
    , mxFTStart(m_xBuilder->weld_label(u"start_effect"_ustr))
    , mxLBStart(m_xBuilder->weld_combo_box(u"start_effect_list"_ustr))
    , mxFTProperty(m_xBuilder->weld_label(u"effect_property"_ustr))
    , mxPlaceholderBox(m_xBuilder->weld_container(u"placeholder"_ustr))
    , mxPBPropertyMore(m_xBuilder->weld_button(u"more_properties"_ustr))
    , mxFTDuration(m_xBuilder->weld_label(u"effect_duration"_ustr))
    , mxCBXDuration(m_xBuilder->weld_metric_spin_button(u"anim_duration"_ustr, FieldUnit::SECOND))
    , mxFTStartDelay(m_xBuilder->weld_label(u"delay_label"_ustr))
    , mxMFStartDelay(m_xBuilder->weld_metric_spin_button(u"delay_value"_ustr, FieldUnit::SECOND))
    , mxCBAutoPreview(m_xBuilder->weld_check_button(u"auto_preview"_ustr))
    , mxPBPlay(m_xBuilder->weld_button(u"play"_ustr))
    , maIdle("sd idle treeview select")
    , mnLastSelectedAnimation(-1)
    , mnPropertyType(nPropertyTypeNone)
    , mnCurvePathPos(-1)
    , mnPolygonPathPos(-1)
    , mnFreeformPathPos(-1)
    , maLateInitTimer("sd CustomAnimationPane maLateInitTimer")
{
    initialize();
}

css::ui::LayoutSize CustomAnimationPane::GetHeightForWidth(const sal_Int32 /*nWidth*/)
{
    sal_Int32 nMinimumHeight = get_preferred_size().Height();
    return css::ui::LayoutSize(nMinimumHeight, -1, nMinimumHeight);
}

void CustomAnimationPane::initialize()
{
    mxLBAnimation->connect_selection_changed(LINK(this, CustomAnimationPane, AnimationSelectHdl));
    mxCustomAnimationList->setController(this);
    mxCustomAnimationList->set_size_request(mxCustomAnimationList->get_approximate_digit_width() * 15,
                                            mxCustomAnimationList->get_height_rows(4));

    mxLBAnimation->set_size_request(mxLBAnimation->get_approximate_digit_width() * 15,
                                    mxLBAnimation->get_height_rows(4));

    maStrProperty = mxFTProperty->get_label();

    mxPBAddEffect->connect_clicked( LINK( this, CustomAnimationPane, implClickHdl ) );
    mxPBRemoveEffect->connect_clicked( LINK( this, CustomAnimationPane, implClickHdl ) );
    mxLBStart->connect_changed( LINK( this, CustomAnimationPane, implControlListBoxHdl ) );
    mxCBXDuration->connect_value_changed(LINK( this, CustomAnimationPane, DurationModifiedHdl));
    mxPBPropertyMore->connect_clicked( LINK( this, CustomAnimationPane, implClickHdl ) );
    mxPBMoveUp->connect_clicked( LINK( this, CustomAnimationPane, implClickHdl ) );
    mxPBMoveDown->connect_clicked( LINK( this, CustomAnimationPane, implClickHdl ) );
    mxPBPlay->connect_clicked( LINK( this, CustomAnimationPane, implClickHdl ) );
    mxCBAutoPreview->connect_toggled( LINK( this, CustomAnimationPane, implToggleHdl ) );
    mxLBCategory->connect_changed( LINK(this, CustomAnimationPane, UpdateAnimationLB) );
    mxMFStartDelay->connect_value_changed( LINK(this, CustomAnimationPane, DelayModifiedHdl) );
    mxMFStartDelay->connect_focus_out(LINK( this, CustomAnimationPane, DelayLoseFocusHdl));

    maIdle.SetPriority(TaskPriority::DEFAULT);
    maIdle.SetInvokeHandler(LINK(this, CustomAnimationPane, SelectionHandler));

    maStrModify = mxFTEffect->get_label();

    // get current controller and initialize listeners
    try
    {
        mxView = mrBase.GetDrawController();
        addListener();
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationPane::CustomAnimationPane()" );
    }

    // tdf#137637 keep user selection during initialization
    ScopeLockGuard aGuard(maSelectionLock);
    // get current page and update custom animation list
    onChangeCurrentPage();

    // Wait a short time before the presets list is created.  This gives the
    // system time to paint the control.
    maLateInitTimer.SetTimeout(100);
    maLateInitTimer.SetInvokeHandler(LINK(this, CustomAnimationPane, lateInitCallback));
    maLateInitTimer.Start();
}

CustomAnimationPane::~CustomAnimationPane()
{
    maLateInitTimer.Stop();

    removeListener();

    MotionPathTagVector aTags;
    aTags.swap( maMotionPathTags );
    for (auto const& tag : aTags)
        tag->Dispose();

    mxPBAddEffect.reset();
    mxPBRemoveEffect.reset();
    mxFTEffect.reset();
    mxFTStart.reset();
    mxLBStart.reset();
    mxLBSubControl.reset();
    mxFTProperty.reset();
    mxPlaceholderBox.reset();
    mxPBPropertyMore.reset();
    mxFTDuration.reset();
    mxCBXDuration.reset();
    mxFTStartDelay.reset();
    mxMFStartDelay.reset();
    mxCustomAnimationList.reset();
    mxPBMoveUp.reset();
    mxPBMoveDown.reset();
    mxPBPlay.reset();
    mxCBAutoPreview.reset();
    mxFTCategory.reset();
    mxLBCategory.reset();
    mxFTAnimation.reset();
    mxLBAnimation.reset();
}

void CustomAnimationPane::addUndo()
{
    SfxUndoManager* pManager = mrBase.GetDocShell()->GetUndoManager();
    if( pManager )
    {
        SdPage* pPage = SdPage::getImplementation( mxCurrentPage );
        if( pPage )
            pManager->AddUndoAction( std::make_unique<UndoAnimation>( mrBase.GetDocShell()->GetDoc(), pPage ) );
    }
}

void CustomAnimationPane::addListener()
{
    Link<sdtools::EventMultiplexerEvent&,void> aLink( LINK(this,CustomAnimationPane,EventMultiplexerListener) );
    mrBase.GetEventMultiplexer()->AddEventListener(aLink);
}

void CustomAnimationPane::removeListener()
{
    Link<sdtools::EventMultiplexerEvent&,void> aLink( LINK(this,CustomAnimationPane,EventMultiplexerListener) );
    mrBase.GetEventMultiplexer()->RemoveEventListener( aLink );
}

IMPL_LINK(CustomAnimationPane,EventMultiplexerListener,
    sdtools::EventMultiplexerEvent&, rEvent, void)
{
    switch (rEvent.meEventId)
    {
        case EventMultiplexerEventId::EditViewSelection:
            onSelectionChanged();
            break;

        case EventMultiplexerEventId::CurrentPageChanged:
            onChangeCurrentPage();
            break;

        case EventMultiplexerEventId::MainViewAdded:
            // At this moment the controller may not yet been set at model
            // or ViewShellBase.  Take it from the view shell passed with
            // the event.
            if (auto pMainViewShell = mrBase.GetMainViewShell().get())
            {
                if( pMainViewShell->GetShellType() == ViewShell::ST_IMPRESS )
                {
                    mxView = mrBase.GetDrawController();
                    onSelectionChanged();
                    onChangeCurrentPage();
                    break;
                }
            }
            [[fallthrough]];
        case EventMultiplexerEventId::MainViewRemoved:
            mxView = nullptr;
            mxCurrentPage = nullptr;
            updateControls();
            break;

        case EventMultiplexerEventId::Disposing:
            mxView.clear();
            onSelectionChanged();
            onChangeCurrentPage();
            break;
        case EventMultiplexerEventId::EndTextEdit:
            if (mpMainSequence && rEvent.mpUserData)
                mxCustomAnimationList->update( mpMainSequence );
            break;
        default: break;
    }
}

static sal_Int32 getPropertyType( std::u16string_view rProperty )
{
    if ( rProperty == u"Direction" )
        return nPropertyTypeDirection;

    if ( rProperty == u"Spokes" )
        return nPropertyTypeSpokes;

    if ( rProperty == u"Zoom" )
        return nPropertyTypeZoom;

    if ( rProperty == u"Accelerate" )
        return nPropertyTypeAccelerate;

    if ( rProperty == u"Decelerate" )
        return nPropertyTypeDecelerate;

    if ( rProperty == u"Color1" )
        return nPropertyTypeFirstColor;

    if ( rProperty == u"Color2" )
        return nPropertyTypeSecondColor;

    if ( rProperty == u"FillColor" )
        return nPropertyTypeFillColor;

    if ( rProperty == u"ColorStyle" )
        return nPropertyTypeColorStyle;

    if ( rProperty == u"AutoReverse" )
        return nPropertyTypeAutoReverse;

    if ( rProperty == u"FontStyle" )
        return nPropertyTypeFont;

    if ( rProperty == u"CharColor" )
        return nPropertyTypeCharColor;

    if ( rProperty == u"CharHeight" )
        return nPropertyTypeCharHeight;

    if ( rProperty == u"CharDecoration" )
        return nPropertyTypeCharDecoration;

    if ( rProperty == u"LineColor" )
        return nPropertyTypeLineColor;

    if ( rProperty == u"Rotate" )
        return nPropertyTypeRotate;

    if ( rProperty == u"Transparency" )
        return nPropertyTypeTransparency;

    if ( rProperty == u"Color" )
        return nPropertyTypeColor;

    if ( rProperty == u"Scale" )
        return nPropertyTypeScale;

    return nPropertyTypeNone;
}

OUString getPropertyName( sal_Int32 nPropertyType )
{
    switch( nPropertyType )
    {
    case nPropertyTypeDirection:
        return SdResId(STR_CUSTOMANIMATION_DIRECTION_PROPERTY);

    case nPropertyTypeSpokes:
        return SdResId(STR_CUSTOMANIMATION_SPOKES_PROPERTY);

    case nPropertyTypeFirstColor:
        return SdResId(STR_CUSTOMANIMATION_FIRST_COLOR_PROPERTY);

    case nPropertyTypeSecondColor:
        return SdResId(STR_CUSTOMANIMATION_SECOND_COLOR_PROPERTY);

    case nPropertyTypeZoom:
        return SdResId(STR_CUSTOMANIMATION_ZOOM_PROPERTY);

    case nPropertyTypeFillColor:
        return SdResId(STR_CUSTOMANIMATION_FILL_COLOR_PROPERTY);

    case nPropertyTypeColorStyle:
        return SdResId(STR_CUSTOMANIMATION_STYLE_PROPERTY);

    case nPropertyTypeFont:
        return SdResId(STR_CUSTOMANIMATION_FONT_PROPERTY);

    case nPropertyTypeCharHeight:
        return SdResId(STR_CUSTOMANIMATION_SIZE_PROPERTY);

    case nPropertyTypeCharColor:
        return SdResId(STR_CUSTOMANIMATION_FONT_COLOR_PROPERTY);

    case nPropertyTypeCharHeightStyle:
        return SdResId(STR_CUSTOMANIMATION_FONT_SIZE_STYLE_PROPERTY);

    case nPropertyTypeCharDecoration:
        return SdResId(STR_CUSTOMANIMATION_FONT_STYLE_PROPERTY);

    case nPropertyTypeLineColor:
        return SdResId(STR_CUSTOMANIMATION_LINE_COLOR_PROPERTY);

    case nPropertyTypeRotate:
        return SdResId(STR_CUSTOMANIMATION_AMOUNT_PROPERTY);

    case nPropertyTypeColor:
        return SdResId(STR_CUSTOMANIMATION_COLOR_PROPERTY);

    case nPropertyTypeTransparency:
        return SdResId(STR_CUSTOMANIMATION_AMOUNT_PROPERTY);

    case nPropertyTypeScale:
        return SdResId(STR_CUSTOMANIMATION_SCALE_PROPERTY);
    }

    return OUString();
}

void CustomAnimationPane::updateControls()
{
    mxFTDuration->set_sensitive(mxView.is());
    mxCBXDuration->set_sensitive(mxView.is());
    mxCustomAnimationList->set_sensitive(mxView.is());
    if (comphelper::LibreOfficeKit::isActive())
    {
        mxPBPlay->hide();
        mxCBAutoPreview->set_active(false);
        mxCBAutoPreview->hide();
    }
    else
    {
        mxPBPlay->set_sensitive(mxView.is());
        mxCBAutoPreview->set_sensitive(mxView.is());
    }

    if (!mxView.is())
    {
        mxPBAddEffect->set_sensitive(false);
        mxPBRemoveEffect->set_sensitive(false);
        mxFTStart->set_sensitive(false);
        mxLBStart->set_sensitive(false);
        mxPBPropertyMore->set_sensitive(false);
        mxPlaceholderBox->set_sensitive(false);
        mxFTProperty->set_sensitive(false);
        mxFTCategory->set_sensitive(false);
        mxLBCategory->set_sensitive(false);
        mxFTAnimation->set_sensitive(false);
        mxLBAnimation->set_sensitive(false);
        mxFTStartDelay->set_sensitive(false);
        mxMFStartDelay->set_sensitive(false);
        mxLBAnimation->clear();
        mnLastSelectedAnimation = -1;
        mxCustomAnimationList->clear();
        return;
    }

    const int nSelectionCount = maListSelection.size();

    mxPBAddEffect->set_sensitive( maViewSelection.hasValue() );
    mxPBRemoveEffect->set_sensitive(nSelectionCount != 0);
    bool bIsSelected = (nSelectionCount > 0);

    if(bIsSelected)
    {
        mxFTAnimation->set_sensitive(true);
        mxLBAnimation->set_sensitive(true);
    }
    else
    {
        mxFTAnimation->set_sensitive(false);
        mxLBAnimation->set_sensitive(false);
        mxLBAnimation->clear();
        mnLastSelectedAnimation = -1;
    }

    mxLBCategory->set_sensitive(bIsSelected);
    mxFTCategory->set_sensitive(bIsSelected);

    mxFTStart->set_sensitive(nSelectionCount > 0);
    mxLBStart->set_sensitive(nSelectionCount > 0);
    mxPlaceholderBox->set_sensitive(nSelectionCount > 0);
    mxPBPropertyMore->set_sensitive(nSelectionCount > 0);
    mxFTStartDelay->set_sensitive(nSelectionCount > 0);
    mxMFStartDelay->set_sensitive(nSelectionCount > 0);

    mxFTProperty->set_label(maStrProperty);

    sal_Int32 nOldPropertyType = mnPropertyType;

    mnPropertyType = nPropertyTypeNone;

    if(bIsSelected)
    {
        CustomAnimationEffectPtr pEffect = maListSelection.front();

        OUString aUIName( CustomAnimationPresets::getCustomAnimationPresets().getUINameForPresetId( pEffect->getPresetId() ) );

        OUString aTemp( maStrModify );

        if( !aUIName.isEmpty() )
        {
            aTemp += " " + aUIName;
            mxFTEffect->set_label( aTemp );
        }

        Any aValue;
        CustomAnimationPresetPtr pDescriptor = CustomAnimationPresets::getCustomAnimationPresets().getEffectDescriptor( pEffect->getPresetId() );
        if (pDescriptor)
        {
            std::vector<OUString> aProperties( pDescriptor->getProperties() );
            if( !aProperties.empty() )
            {
                mnPropertyType = getPropertyType( aProperties.front() );

                mxFTProperty->set_label( getPropertyName( mnPropertyType )  );

                aValue = getProperty1Value( mnPropertyType, pEffect );
            }
        }

        sal_Int32 nNewPropertyType = mnPropertyType;
        // if there is no value, then the control will be disabled, just show a disabled Direction box in that
        // case to have something to fill the space
        if (!aValue.hasValue())
            nNewPropertyType = nPropertyTypeDirection;

        if (!mxLBSubControl || nOldPropertyType != nNewPropertyType)
        {
            // for LOK destroy old widgets first
            mxLBSubControl.reset(nullptr);
            // then create new control, to keep correct pointers for actions
            mxLBSubControl = SdPropertySubControl::create(nNewPropertyType, mxFTProperty.get(), mxPlaceholderBox.get(), GetFrameWeld(), aValue, pEffect->getPresetId(), LINK(this, CustomAnimationPane, implPropertyHdl));
        }
        else
        {
            mxLBSubControl->setValue(aValue, pEffect->getPresetId());
        }

        bool bEnable = aValue.hasValue();
        mxPlaceholderBox->set_sensitive( bEnable );
        mxFTProperty->set_sensitive( bEnable );

        if (!pDescriptor)
        {
            mxPBPropertyMore->set_sensitive( false );
            mxFTStartDelay->set_sensitive( false );
            mxMFStartDelay->set_sensitive( false );
        }
        sal_Int32 nCategoryPos = -1;
        switch(pEffect->getPresetClass())
        {
            case EffectPresetClass::ENTRANCE: nCategoryPos = 0; break;
            case EffectPresetClass::EMPHASIS: nCategoryPos = 1; break;
            case EffectPresetClass::EXIT: nCategoryPos = 2; break;
            case EffectPresetClass::MOTIONPATH: nCategoryPos = 3; break;
            default:
                break;
        }
        switch(pEffect->getCommand())
        {
            case EffectCommands::TOGGLEPAUSE:
            case EffectCommands::STOP:
            case EffectCommands::PLAY:
                nCategoryPos = 4; break;
            default:
            break;
        }
        mxLBCategory->set_active(nCategoryPos);

        fillAnimationLB( pEffect->hasText() );

        OUString rsPresetId = pEffect->getPresetId();
        sal_Int32 nAnimationPos = mxLBAnimation->n_children();
        while( nAnimationPos-- )
        {
            auto pEntryData = weld::fromId<CustomAnimationPresetPtr*>(mxLBAnimation->get_id(nAnimationPos));
            if (pEntryData)
            {
                CustomAnimationPresetPtr& pPtr = *pEntryData;
                if( pPtr && pPtr->getPresetId() == rsPresetId )
                {
                    mxLBAnimation->select( nAnimationPos );
                    mnLastSelectedAnimation = nAnimationPos;
                    break;
                }
            }
        }

        // If preset id is missing and category is motion path.
        if (nAnimationPos < 0 && nCategoryPos == 3)
        {
            if (rsPresetId == "libo-motionpath-curve")
            {
                mxLBAnimation->select(mnCurvePathPos);
                mnLastSelectedAnimation = mnCurvePathPos;
            }
            else if (rsPresetId == "libo-motionpath-polygon")
            {
                mxLBAnimation->select(mnPolygonPathPos);
                mnLastSelectedAnimation = mnPolygonPathPos;
            }
            else if (rsPresetId == "libo-motionpath-freeform-line")
            {
                mxLBAnimation->select(mnFreeformPathPos);
                mnLastSelectedAnimation = mnFreeformPathPos;
            }
        }

        sal_uInt16 nPos = 0xffff;

        sal_Int16 nNodeType = pEffect->getNodeType();
        switch( nNodeType )
        {
        case EffectNodeType::ON_CLICK:          nPos = 0; break;
        case EffectNodeType::WITH_PREVIOUS:     nPos = 1; break;
        case EffectNodeType::AFTER_PREVIOUS:    nPos = 2; break;
        }

        mxLBStart->set_active( nPos );

        double fDuration = pEffect->getDuration();
        const bool bHasSpeed = fDuration > 0.001;

        mxFTDuration->set_sensitive(bHasSpeed);
        mxCBXDuration->set_sensitive(bHasSpeed);

        if( bHasSpeed )
        {
            mxCBXDuration->set_value(fDuration*100.0, FieldUnit::NONE);
        }

        mxPBPropertyMore->set_sensitive(true);

        mxFTStartDelay->set_sensitive(true);
        mxMFStartDelay->set_sensitive(true);
        double fBegin = pEffect->getBegin();
        mxMFStartDelay->set_value(fBegin*10.0, FieldUnit::NONE);
    }
    else
    {
        // use an empty direction box to fill the space
        if (!mxLBSubControl || (nOldPropertyType != nPropertyTypeDirection && nOldPropertyType != nPropertyTypeNone))
        {
            // for LOK destroy old widgets first
            mxLBSubControl.reset(nullptr);
            // then create new control, to keep correct pointers for actions
            mxLBSubControl = SdPropertySubControl::create(nPropertyTypeDirection, mxFTProperty.get(), mxPlaceholderBox.get(), GetFrameWeld(), uno::Any(), OUString(), LINK(this, CustomAnimationPane, implPropertyHdl));
        }
        else
            mxLBSubControl->setValue(uno::Any(), OUString());

        mxPlaceholderBox->set_sensitive(false);
        mxFTProperty->set_sensitive(false);
        mxFTStartDelay->set_sensitive(false);
        mxMFStartDelay->set_sensitive(false);
        mxPBPropertyMore->set_sensitive(false);
        mxFTDuration->set_sensitive(false);
        mxCBXDuration->set_sensitive(false);
        mxCBXDuration->set_text(OUString());
        mxFTEffect->set_label(maStrModify);
    }

    bool bEnableUp = true;
    bool bEnableDown = true;
    if( nSelectionCount == 0 )
    {
        bEnableUp = false;
        bEnableDown = false;
    }
    else
    {
        if( mpMainSequence->find( maListSelection.front() ) == mpMainSequence->getBegin() )
            bEnableUp = false;

        EffectSequence::iterator aIter( mpMainSequence->find( maListSelection.back() ) );
        if( aIter == mpMainSequence->getEnd() )
        {
            bEnableDown = false;
        }
        else
        {
            do
            {
                ++aIter;
            }
            while( (aIter != mpMainSequence->getEnd()) && !(mxCustomAnimationList->isExpanded(*aIter) ) );

            if( aIter == mpMainSequence->getEnd() )
                bEnableDown = false;
        }

        if( bEnableUp || bEnableDown )
        {
            MainSequenceRebuildGuard aGuard( mpMainSequence );

            EffectSequenceHelper* pSequence = nullptr;
            for( const CustomAnimationEffectPtr& pEffect : maListSelection )
            {
                if( pEffect )
                {
                    if( pSequence == nullptr )
                    {
                        pSequence = pEffect->getEffectSequence();
                    }
                    else
                    {
                        if( pSequence != pEffect->getEffectSequence() )
                        {
                            bEnableUp = false;
                            bEnableDown = false;
                            break;
                        }
                    }
                }
            }
        }
    }

    mxPBMoveUp->set_sensitive(mxView.is() &&  bEnableUp);
    mxPBMoveDown->set_sensitive(mxView.is() && bEnableDown);

    SdOptions* pOptions = SdModule::get()->GetSdOptions(DocumentType::Impress);
    mxCBAutoPreview->set_active(pOptions->IsPreviewChangedEffects());

    updateMotionPathTags();
}

static bool updateMotionPathImpl( CustomAnimationPane& rPane, ::sd::View& rView,  EffectSequence::iterator aIter, const EffectSequence::iterator& aEnd, MotionPathTagVector& rOldTags, MotionPathTagVector& rNewTags )
{
    bool bChanges = false;
    while( aIter != aEnd )
    {
        CustomAnimationEffectPtr pEffect( *aIter++ );
        if( pEffect && pEffect->getPresetClass() == css::presentation::EffectPresetClass::MOTIONPATH )
        {
            rtl::Reference< MotionPathTag > xMotionPathTag;
            // first try to find if there is already a tag for this
            auto aMIter = std::find_if(rOldTags.begin(), rOldTags.end(),
                [&pEffect](const rtl::Reference<MotionPathTag>& xTag) { return xTag->getEffect() == pEffect; });
            if (aMIter != rOldTags.end())
            {
                rtl::Reference< MotionPathTag > xTag( *aMIter );
                if( !xTag->isDisposed() )
                {
                    xMotionPathTag = std::move(xTag);
                    rOldTags.erase( aMIter );
                }
            }

            // if not found, create new one
            if( !xMotionPathTag.is() )
            {
                xMotionPathTag.set( new MotionPathTag( rPane, rView, pEffect ) );
                bChanges = true;
            }

            if( xMotionPathTag.is() )
                rNewTags.push_back( xMotionPathTag );
        }
    }

    return bChanges;
}

void CustomAnimationPane::updateMotionPathTags()
{
    bool bChanges = false;

    MotionPathTagVector aTags;
    aTags.swap( maMotionPathTags );

    ::sd::View* pView = nullptr;

    if( mxView.is() )
    {
        std::shared_ptr<ViewShell> xViewShell( mrBase.GetMainViewShell() );
        if( xViewShell )
            pView = xViewShell->GetView();
    }

    if (mpMainSequence && pView)
    {
        bChanges = updateMotionPathImpl( *this, *pView, mpMainSequence->getBegin(), mpMainSequence->getEnd(), aTags, maMotionPathTags );

        auto rInteractiveSequenceVector = mpMainSequence->getInteractiveSequenceVector();
        for (InteractiveSequencePtr const& pIS : rInteractiveSequenceVector)
        {
            bChanges |= updateMotionPathImpl( *this, *pView, pIS->getBegin(), pIS->getEnd(), aTags, maMotionPathTags );
        }
    }

    if( !aTags.empty() )
    {
        bChanges = true;
        for( rtl::Reference< MotionPathTag >& xTag : aTags )
        {
            xTag->Dispose();
        }
    }

    if( bChanges && pView )
        pView->updateHandles();
}

void CustomAnimationPane::onSelectionChanged()
{
    if( maSelectionLock.isLocked() )
        return;

    ScopeLockGuard aGuard( maSelectionLock );

    if( mxView.is() ) try
    {
        maViewSelection = mxView->getSelection();
        mxCustomAnimationList->onSelectionChanged( maViewSelection );
        updateControls();
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationPane::onSelectionChanged()" );
    }
}

void CustomAnimationPane::onDoubleClick()
{
    showOptions();
}

void CustomAnimationPane::onContextMenu(const OUString &rIdent)
{
    if (rIdent == "onclick")
        onChangeStart( EffectNodeType::ON_CLICK );
    else if (rIdent == "withprev")
        onChangeStart( EffectNodeType::WITH_PREVIOUS  );
    else if (rIdent == "afterprev")
        onChangeStart( EffectNodeType::AFTER_PREVIOUS );
    else if (rIdent == "options")
        showOptions();
    else if (rIdent == "timing")
        showOptions(u"timing"_ustr);
    else if (rIdent == "remove")
        onRemove();
    else if (rIdent == "create" && maViewSelection.hasValue())
        onAdd();
    updateControls();
}

static void addValue( const std::unique_ptr<STLPropertySet>& pSet, sal_Int32 nHandle, const Any& rValue )
{
    switch( pSet->getPropertyState( nHandle ) )
    {
    case STLPropertyState::Ambiguous:
        // value is already ambiguous, do nothing
        break;
    case STLPropertyState::Direct:
        // set to ambiguous if existing value is different
        if( rValue != pSet->getPropertyValue( nHandle ) )
            pSet->setPropertyState( nHandle, STLPropertyState::Ambiguous );
        break;
    case STLPropertyState::Default:
        // just set new value
        pSet->setPropertyValue( nHandle, rValue );
        break;
    }
}

static sal_Int32 calcMaxParaDepth( const Reference< XShape >& xTargetShape )
{
    sal_Int32 nMaxParaDepth = -1;

    if( xTargetShape.is() )
    {
        Reference< XEnumerationAccess > xText( xTargetShape, UNO_QUERY );
        if( xText.is() )
        {
            Reference< XPropertySet > xParaSet;

            Reference< XEnumeration > xEnumeration( xText->createEnumeration(), UNO_SET_THROW );
            while( xEnumeration->hasMoreElements() )
            {
                xEnumeration->nextElement() >>= xParaSet;
                if( xParaSet.is() )
                {
                    sal_Int32 nParaDepth = 0;
                    xParaSet->getPropertyValue( u"NumberingLevel"_ustr ) >>= nParaDepth;

                    if( nParaDepth > nMaxParaDepth )
                        nMaxParaDepth = nParaDepth;
                }
            }
        }
    }

    return nMaxParaDepth + 1;
}

Any CustomAnimationPane::getProperty1Value( sal_Int32 nType, const CustomAnimationEffectPtr& pEffect )
{
    switch( nType )
    {
    case nPropertyTypeDirection:
    case nPropertyTypeSpokes:
    case nPropertyTypeZoom:
        return Any( pEffect->getPresetSubType() );

    case nPropertyTypeColor:
    case nPropertyTypeFillColor:
    case nPropertyTypeFirstColor:
    case nPropertyTypeSecondColor:
    case nPropertyTypeCharColor:
    case nPropertyTypeLineColor:
        {
            const sal_Int32 nIndex = (nPropertyTypeFirstColor == nType) ? 0 : 1;
            return pEffect->getColor( nIndex );
        }

    case nPropertyTypeFont:
        return pEffect->getProperty( AnimationNodeType::SET, u"CharFontName" , EValue::To );

    case nPropertyTypeCharHeight:
        {
            static constexpr OUString aAttributeName( u"CharHeight"_ustr );
            Any aValue( pEffect->getProperty( AnimationNodeType::SET, aAttributeName, EValue::To ) );
            if( !aValue.hasValue() )
                aValue = pEffect->getProperty( AnimationNodeType::ANIMATE, aAttributeName, EValue::To );
            return aValue;
        }

    case nPropertyTypeRotate:
        return pEffect->getTransformationProperty( AnimationTransformType::ROTATE, EValue::By);

    case nPropertyTypeTransparency:
        return pEffect->getProperty( AnimationNodeType::SET, u"Opacity" , EValue::To );

    case nPropertyTypeScale:
        return pEffect->getTransformationProperty( AnimationTransformType::SCALE, EValue::By );

    case nPropertyTypeCharDecoration:
        {
            Sequence< Any > aValues{
                pEffect->getProperty( AnimationNodeType::SET, u"CharWeight" , EValue::To ),
                pEffect->getProperty( AnimationNodeType::SET, u"CharPosture" , EValue::To ),
                pEffect->getProperty( AnimationNodeType::SET, u"CharUnderline" , EValue::To )
            };
            return Any( aValues );
        }
    }

    Any aAny;
    return aAny;
}

bool CustomAnimationPane::setProperty1Value( sal_Int32 nType, const CustomAnimationEffectPtr& pEffect, const Any& rValue )
{
    bool bEffectChanged = false;
    switch( nType )
    {
    case nPropertyTypeDirection:
    case nPropertyTypeSpokes:
    case nPropertyTypeZoom:
        {
            OUString aPresetSubType;
            rValue >>= aPresetSubType;
            if( aPresetSubType != pEffect->getPresetSubType() )
            {
                CustomAnimationPresets::getCustomAnimationPresets().changePresetSubType( pEffect, aPresetSubType );
                bEffectChanged = true;
            }
        }
        break;

    case nPropertyTypeFillColor:
    case nPropertyTypeColor:
    case nPropertyTypeFirstColor:
    case nPropertyTypeSecondColor:
    case nPropertyTypeCharColor:
    case nPropertyTypeLineColor:
        {
            const sal_Int32 nIndex = (nPropertyTypeFirstColor == nType) ? 0 : 1;
            Any aOldColor( pEffect->getColor( nIndex ) );
            if( aOldColor != rValue )
            {
                pEffect->setColor( nIndex, rValue );
                bEffectChanged = true;
            }
        }
        break;

    case nPropertyTypeFont:
        bEffectChanged = pEffect->setProperty( AnimationNodeType::SET, u"CharFontName" , EValue::To, rValue );
        break;

    case nPropertyTypeCharHeight:
        {
            static constexpr OUString aAttributeName( u"CharHeight"_ustr );
            bEffectChanged = pEffect->setProperty( AnimationNodeType::SET, aAttributeName, EValue::To, rValue );
            if( !bEffectChanged )
                bEffectChanged = pEffect->setProperty( AnimationNodeType::ANIMATE, aAttributeName, EValue::To, rValue );
        }
        break;
    case nPropertyTypeRotate:
        bEffectChanged = pEffect->setTransformationProperty( AnimationTransformType::ROTATE, EValue::By , rValue );
        break;

    case nPropertyTypeTransparency:
        bEffectChanged = pEffect->setProperty( AnimationNodeType::SET, u"Opacity" , EValue::To, rValue );
        break;

    case nPropertyTypeScale:
        bEffectChanged = pEffect->setTransformationProperty( AnimationTransformType::SCALE, EValue::By, rValue );
        break;

    case nPropertyTypeCharDecoration:
        {
            Sequence< Any > aValues(3);
            rValue >>= aValues;
            bEffectChanged = pEffect->setProperty( AnimationNodeType::SET, u"CharWeight" , EValue::To, aValues[0] );
            bEffectChanged |= pEffect->setProperty( AnimationNodeType::SET, u"CharPosture" , EValue::To, aValues[1] );
            bEffectChanged |= pEffect->setProperty( AnimationNodeType::SET, u"CharUnderline" , EValue::To, aValues[2] );
        }
        break;

    }

    return bEffectChanged;
}

static bool hasVisibleShape( const Reference< XShape >& xShape )
{
    try
    {
        const OUString sShapeType( xShape->getShapeType() );

        if( sShapeType == "com.sun.star.presentation.TitleTextShape" || sShapeType == "com.sun.star.presentation.OutlinerShape" ||
            sShapeType == "com.sun.star.presentation.SubtitleShape" || sShapeType == "com.sun.star.drawing.TextShape" )
        {
            Reference< XPropertySet > xSet( xShape, UNO_QUERY_THROW );

            FillStyle eFillStyle;
            xSet->getPropertyValue( u"FillStyle"_ustr ) >>= eFillStyle;

            css::drawing::LineStyle eLineStyle;
            xSet->getPropertyValue( u"LineStyle"_ustr ) >>= eLineStyle;

            return eFillStyle != FillStyle_NONE || eLineStyle != css::drawing::LineStyle_NONE;
        }
    }
    catch( Exception& )
    {
    }
    return true;
}

std::unique_ptr<STLPropertySet> CustomAnimationPane::createSelectionSet()
{
    std::unique_ptr<STLPropertySet> pSet = CustomAnimationDialog::createDefaultSet();

    pSet->setPropertyValue( nHandleCurrentPage, Any( mxCurrentPage ) );

    sal_Int32 nMaxParaDepth = 0;

    // get options from selected effects
    const CustomAnimationPresets& rPresets (CustomAnimationPresets::getCustomAnimationPresets());
    for( CustomAnimationEffectPtr& pEffect : maListSelection )
    {
        EffectSequenceHelper* pEffectSequence = pEffect->getEffectSequence();
        if( !pEffectSequence )
            pEffectSequence = mpMainSequence.get();

        if( pEffect->hasText() )
        {
            sal_Int32 n = calcMaxParaDepth(pEffect->getTargetShape());
            if( n > nMaxParaDepth )
                nMaxParaDepth = n;
        }

        addValue( pSet, nHandleHasAfterEffect, Any( pEffect->hasAfterEffect() ) );
        addValue( pSet, nHandleAfterEffectOnNextEffect, Any( pEffect->IsAfterEffectOnNext() ) );
        addValue( pSet, nHandleDimColor, pEffect->getDimColor() );
        addValue( pSet, nHandleIterateType, Any( pEffect->getIterateType() ) );

        // convert absolute time to percentage value
        // This calculation is done in float to avoid some rounding artifacts.
        float fIterateInterval = static_cast<float>(pEffect->getIterateInterval());
        if( pEffect->getDuration() )
            fIterateInterval = static_cast<float>(fIterateInterval / pEffect->getDuration() );
        fIterateInterval *= 100.0;
        addValue( pSet, nHandleIterateInterval, Any( static_cast<double>(fIterateInterval) ) );

        addValue( pSet, nHandleBegin, Any( pEffect->getBegin() ) );
        addValue( pSet, nHandleDuration, Any( pEffect->getDuration() ) );
        addValue( pSet, nHandleStart, Any( pEffect->getNodeType() ) );
        addValue( pSet, nHandleRepeat, pEffect->getRepeatCount() );
        addValue( pSet, nHandleEnd, pEffect->getEnd() );
        addValue( pSet, nHandleRewind, Any( pEffect->getFill() ) );

        addValue( pSet, nHandlePresetId, Any( pEffect->getPresetId() ) );

        addValue( pSet, nHandleHasText, Any( pEffect->hasText() ) );

        addValue( pSet, nHandleHasVisibleShape, Any( hasVisibleShape( pEffect->getTargetShape() ) ) );

        Any aSoundSource;
        if( pEffect->getAudio().is() )
        {
            aSoundSource = pEffect->getAudio()->getSource();
            addValue( pSet, nHandleSoundVolume, Any( pEffect->getAudio()->getVolume() ) );
// todo     addValue( pSet, nHandleSoundEndAfterSlide, makeAny( pEffect->getAudio()->getEndAfterSlide() ) );
// this is now stored at the XCommand parameter sequence
        }
        else if( pEffect->getCommand() == EffectCommands::STOPAUDIO )
        {
            aSoundSource <<= true;
        }
        addValue( pSet, nHandleSoundURL, aSoundSource );

        sal_Int32 nGroupId = pEffect->getGroupId();
        CustomAnimationTextGroupPtr pTextGroup;
        if( nGroupId != -1 )
            pTextGroup = pEffectSequence->findGroup( nGroupId );

        addValue( pSet, nHandleTextGrouping, Any( pTextGroup ? pTextGroup->getTextGrouping() : sal_Int32(-1) ) );
        addValue( pSet, nHandleAnimateForm, Any( !pTextGroup || pTextGroup->getAnimateForm() ) );
        addValue( pSet, nHandleTextGroupingAuto, Any( pTextGroup ? pTextGroup->getTextGroupingAuto() : -1.0 ) );
        addValue( pSet, nHandleTextReverse, Any( pTextGroup && pTextGroup->getTextReverse() ) );

        if( pEffectSequence->getSequenceType() == EffectNodeType::INTERACTIVE_SEQUENCE  )
        {
            InteractiveSequence* pIS = static_cast< InteractiveSequence* >( pEffectSequence );
            addValue( pSet, nHandleTrigger, Any( pIS->getTriggerShape() ) );
        }

        CustomAnimationPresetPtr pDescriptor = rPresets.getEffectDescriptor( pEffect->getPresetId() );
        if( pDescriptor )
        {
            sal_Int32 nType = nPropertyTypeNone;

            std::vector<OUString> aProperties( pDescriptor->getProperties() );
            if( !aProperties.empty() )
                nType = getPropertyType( aProperties.front() );

            if( nType != nPropertyTypeNone )
            {
                addValue( pSet, nHandleProperty1Type, Any( nType ) );
                addValue( pSet, nHandleProperty1Value, getProperty1Value( nType, pEffect ) );
            }

            if( pDescriptor->hasProperty( u"Accelerate" ) )
            {
                addValue( pSet, nHandleAccelerate, Any( pEffect->getAcceleration() ) );
            }

            if( pDescriptor->hasProperty( u"Decelerate" ) )
            {
                addValue( pSet, nHandleDecelerate, Any( pEffect->getDecelerate() ) );
            }

            if( pDescriptor->hasProperty( u"AutoReverse" ) )
            {
                addValue( pSet, nHandleAutoReverse, Any( pEffect->getAutoReverse() ) );
            }
        }
    }

    addValue( pSet, nHandleMaxParaDepth, Any( nMaxParaDepth ) );

    return pSet;
}

void CustomAnimationPane::changeSelection( STLPropertySet const * pResultSet, STLPropertySet const * pOldSet )
{
    // change selected effect
    bool bChanged = false;

    MainSequenceRebuildGuard aGuard( mpMainSequence );

    for( CustomAnimationEffectPtr& pEffect : maListSelection )
    {
        DBG_ASSERT( pEffect->getEffectSequence(), "sd::CustomAnimationPane::changeSelection(), dead effect in selection!" );
        if( !pEffect->getEffectSequence() )
            continue;

        double fDuration = 0.0; // we might need this for iterate-interval
        if( pResultSet->getPropertyState( nHandleDuration ) == STLPropertyState::Direct )
        {
            pResultSet->getPropertyValue( nHandleDuration ) >>= fDuration;
        }
        else
        {
            fDuration = pEffect->getDuration();
        }

        if( pResultSet->getPropertyState( nHandleIterateType ) == STLPropertyState::Direct )
        {
            sal_Int16 nIterateType = 0;
            pResultSet->getPropertyValue( nHandleIterateType ) >>= nIterateType;
            if( pEffect->getIterateType() != nIterateType )
            {
                pEffect->setIterateType( nIterateType );
                bChanged = true;
            }
        }

        if( pEffect->getIterateType() )
        {
            if( pResultSet->getPropertyState( nHandleIterateInterval ) == STLPropertyState::Direct )
            {
                double fIterateInterval = 0.0;
                pResultSet->getPropertyValue( nHandleIterateInterval ) >>= fIterateInterval;
                if( pEffect->getIterateInterval() != fIterateInterval )
                {
                    const double f = fIterateInterval * pEffect->getDuration() / 100;
                    pEffect->setIterateInterval( f );
                    bChanged = true;
                }
            }
        }

        double fBegin = 0.0;

        if( pResultSet->getPropertyState( nHandleBegin ) == STLPropertyState::Direct )
            pResultSet->getPropertyValue( nHandleBegin ) >>= fBegin;
        else
            fBegin = pEffect->getBegin();

        if( pEffect->getBegin() != fBegin && pResultSet->getPropertyState( nHandleBegin ) == STLPropertyState::Direct)
        {
            pEffect->setBegin( fBegin );
            bChanged = true;
        }

        if( pResultSet->getPropertyState( nHandleDuration ) == STLPropertyState::Direct )
        {
            if( pEffect->getDuration() != fDuration )
            {
                pEffect->setDuration( fDuration );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleStart ) == STLPropertyState::Direct )
        {
            sal_Int16 nNodeType = 0;
            pResultSet->getPropertyValue( nHandleStart ) >>= nNodeType;
            if( pEffect->getNodeType() != nNodeType )
            {
                pEffect->setNodeType( nNodeType );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleRepeat ) == STLPropertyState::Direct )
        {
            Any aRepeatCount( pResultSet->getPropertyValue( nHandleRepeat ) );
            if( aRepeatCount != pEffect->getRepeatCount() )
            {
                pEffect->setRepeatCount( aRepeatCount );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleEnd ) == STLPropertyState::Direct )
        {
            Any aEndValue( pResultSet->getPropertyValue( nHandleEnd ) );
            if( pEffect->getEnd() != aEndValue )
            {
                pEffect->setEnd( aEndValue );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleRewind ) == STLPropertyState::Direct )
        {
            sal_Int16 nFill = 0;
            pResultSet->getPropertyValue( nHandleRewind ) >>= nFill;
            if( pEffect->getFill() != nFill )
            {
                pEffect->setFill( nFill );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleHasAfterEffect ) == STLPropertyState::Direct )
        {
            bool bHasAfterEffect = false;
            if( pResultSet->getPropertyValue( nHandleHasAfterEffect )  >>= bHasAfterEffect )
            {
                if( pEffect->hasAfterEffect() != bHasAfterEffect )
                {
                    pEffect->setHasAfterEffect( bHasAfterEffect );
                    bChanged = true;
                }
            }
        }

        if( pResultSet->getPropertyState( nHandleAfterEffectOnNextEffect ) == STLPropertyState::Direct )
        {
            bool bAfterEffectOnNextEffect = false;
            if(   (pResultSet->getPropertyValue( nHandleAfterEffectOnNextEffect ) >>= bAfterEffectOnNextEffect)
               && (pEffect->IsAfterEffectOnNext() != bAfterEffectOnNextEffect) )
            {
                pEffect->setAfterEffectOnNext( bAfterEffectOnNextEffect );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleDimColor ) == STLPropertyState::Direct )
        {
            Any aDimColor( pResultSet->getPropertyValue( nHandleDimColor ) );
            if( pEffect->getDimColor() != aDimColor )
            {
                pEffect->setDimColor( aDimColor );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleAccelerate ) == STLPropertyState::Direct )
        {
            double fAccelerate = 0.0;
            pResultSet->getPropertyValue( nHandleAccelerate ) >>= fAccelerate;
            if( pEffect->getAcceleration() != fAccelerate )
            {
                pEffect->setAcceleration( fAccelerate );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleDecelerate ) == STLPropertyState::Direct )
        {
            double fDecelerate = 0.0;
            pResultSet->getPropertyValue( nHandleDecelerate ) >>= fDecelerate;
            if( pEffect->getDecelerate() != fDecelerate )
            {
                pEffect->setDecelerate( fDecelerate );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleAutoReverse ) == STLPropertyState::Direct )
        {
            bool bAutoReverse = false;
            pResultSet->getPropertyValue( nHandleAutoReverse ) >>= bAutoReverse;
            if( pEffect->getAutoReverse() != bAutoReverse )
            {
                pEffect->setAutoReverse( bAutoReverse );
                bChanged = true;
            }
        }

        if( pResultSet->getPropertyState( nHandleProperty1Value ) == STLPropertyState::Direct )
        {
            sal_Int32 nType = 0;
            pOldSet->getPropertyValue( nHandleProperty1Type ) >>= nType;

            bChanged |= setProperty1Value( nType, pEffect, pResultSet->getPropertyValue( nHandleProperty1Value ) );
        }

        if( pResultSet->getPropertyState( nHandleSoundURL ) == STLPropertyState::Direct )
        {
            const Any aSoundSource( pResultSet->getPropertyValue( nHandleSoundURL ) );

            if( aSoundSource.getValueType() == ::cppu::UnoType<sal_Bool>::get() )
            {
                pEffect->setStopAudio();
                bChanged = true;
            }
            else
            {
                OUString aSoundURL;
                aSoundSource >>= aSoundURL;

                if( !aSoundURL.isEmpty() )
                {
                    if( !pEffect->getAudio().is() )
                    {
                        pEffect->createAudio( aSoundSource );
                        bChanged = true;
                    }
                    else
                    {
                        if( pEffect->getAudio()->getSource() != aSoundSource )
                        {
                            pEffect->getAudio()->setSource( aSoundSource );
                            bChanged = true;
                        }
                    }
                }
                else
                {
                    if( pEffect->getAudio().is() || pEffect->getStopAudio() )
                    {
                        pEffect->removeAudio();
                        bChanged = true;
                    }
                }
            }
        }

        if( pResultSet->getPropertyState( nHandleTrigger ) == STLPropertyState::Direct )
        {
            Reference< XShape > xTriggerShape;
            pResultSet->getPropertyValue( nHandleTrigger ) >>= xTriggerShape;
            bChanged |= mpMainSequence->setTrigger( pEffect, xTriggerShape );
        }
    }

    const bool bHasTextGrouping = pResultSet->getPropertyState( nHandleTextGrouping ) == STLPropertyState::Direct;
    const bool bHasAnimateForm = pResultSet->getPropertyState( nHandleAnimateForm ) == STLPropertyState::Direct;
    const bool bHasTextGroupingAuto = pResultSet->getPropertyState( nHandleTextGroupingAuto ) == STLPropertyState::Direct;
    const bool bHasTextReverse = pResultSet->getPropertyState( nHandleTextReverse ) == STLPropertyState::Direct;

    if( bHasTextGrouping || bHasAnimateForm || bHasTextGroupingAuto || bHasTextReverse )
    {
        // we need to do a second pass for text grouping options
        // since changing them can cause effects to be removed
        // or replaced, we do this after we applied all other options
        // above

        sal_Int32 nTextGrouping = 0;
        bool bAnimateForm = true, bTextReverse = false;
        double fTextGroupingAuto = -1.0;

        if( bHasTextGrouping )
            pResultSet->getPropertyValue(nHandleTextGrouping) >>= nTextGrouping;
        else
            pOldSet->getPropertyValue(nHandleTextGrouping) >>= nTextGrouping;

        if( bHasAnimateForm )
            pResultSet->getPropertyValue(nHandleAnimateForm) >>= bAnimateForm;
        else
            pOldSet->getPropertyValue(nHandleAnimateForm) >>= bAnimateForm;

        if( bHasTextGroupingAuto )
            pResultSet->getPropertyValue(nHandleTextGroupingAuto) >>= fTextGroupingAuto;
        else
            pOldSet->getPropertyValue(nHandleTextGroupingAuto) >>= fTextGroupingAuto;

        if( bHasTextReverse )
            pResultSet->getPropertyValue(nHandleTextReverse) >>= bTextReverse;
        else
            pOldSet->getPropertyValue(nHandleTextReverse) >>= bTextReverse;

        EffectSequence const aSelectedEffects( maListSelection );
        for( CustomAnimationEffectPtr const& pEffect : aSelectedEffects )
        {
            EffectSequenceHelper* pEffectSequence = pEffect->getEffectSequence();
            if( !pEffectSequence )
                pEffectSequence = mpMainSequence.get();

            sal_Int32 nGroupId = pEffect->getGroupId();
            CustomAnimationTextGroupPtr pTextGroup;
            if( nGroupId != -1 )
            {
                // use existing group
                pTextGroup = pEffectSequence->findGroup( nGroupId );
            }
            else
            {
                // somethings changed so we need a group now
                pTextGroup = pEffectSequence->createTextGroup( pEffect, nTextGrouping, fTextGroupingAuto, bAnimateForm, bTextReverse );
                bChanged = true;
            }

            //#i119988#
            /************************************************************************/
            /*
            Note, the setAnimateForm means set the animation from TextGroup to Object's Shape
            And on the UI in means "Animate attached shape" in "Effect Option" dialog
            The setTextGrouping means set animation to Object's Text,
            the nTextGrouping is Text Animation Type
            nTextGrouping = -1 is "As one Object", means no text animation.

            The previous call order first do the setTextGrouping and then do the setAnimateForm,
            that will cause such defect: in the setTextGrouping, the effect has been removed,
            but in setAnimateForm still need this effect, then a NULL pointer of that effect will
            be gotten, and cause crash.

            []bHasAnimateForm means the UI has changed, bAnimateForm is it value

            So if create a new textgroup animation, the following animation will never be run!
            Since the \A1\B0Animate attached shape\A1\B1 is default checked.
            And the bHasAnimateForm default is false, and if user uncheck it the value bAnimateForm will be false,
            it same as the TextGroup\A1\AFs default value, also could not be run setAnimateForm.
            if( bHasAnimateForm )
            {
            if( pTextGroup->getAnimateForm() != bAnimateForm )
            {
            pEffectSequence->setAnimateForm( pTextGroup, bAnimateForm );
            bChanged = true;
            }
            }

            In setTextGrouping, there are three case:
            1.  Create new text effects for empty TextGroup
            2.  Remove all text effects of TextGroup (nTextGrouping == -1)
            3.  Change all the text effects\A1\AF start type

            So here is the right logic:
            If set the animation from text to shape and remove text animation,
            should do setAnimateForm first, then do setTextGrouping.
            Other case,do setTextGrouping first, then do setAnimateForm.

            */
            /************************************************************************/

            bool    bDoSetAnimateFormFirst = false;
            bool    bNeedDoSetAnimateForm = false;

            if( bHasAnimateForm )
            {
                if( pTextGroup && pTextGroup->getAnimateForm() != bAnimateForm )
                {
                    if( (pTextGroup->getTextGrouping() >= 0) && (nTextGrouping == -1 ) )
                    {
                        bDoSetAnimateFormFirst = true;
                    }
                    bNeedDoSetAnimateForm = true;
                }
            }

            if (bDoSetAnimateFormFirst)
            {
                pEffectSequence->setAnimateForm( pTextGroup, bAnimateForm );
                bChanged = true;
            }

            if( bHasTextGrouping )
            {
                if( pTextGroup && pTextGroup->getTextGrouping() != nTextGrouping )
                {
                    pEffectSequence->setTextGrouping( pTextGroup, nTextGrouping );

                    // All the effects of the outline object is removed so we need to
                    // put it back. OTOH, the shape object that still has effects
                    // in the text group is fine.
                    if (nTextGrouping == -1 && pTextGroup->getEffects().empty())
                    {
                        pEffect->setTarget(Any(pEffect->getTargetShape()));
                        pEffect->setGroupId(-1);
                        mpMainSequence->append(pEffect);
                    }

                    bChanged = true;
                }
            }

            if (!bDoSetAnimateFormFirst && bNeedDoSetAnimateForm)
            {
                if( pTextGroup )
                {
                    pEffectSequence->setAnimateForm( pTextGroup, bAnimateForm );
                    bChanged = true;
                }
            }

            if( bHasTextGroupingAuto )
            {
                if( pTextGroup && pTextGroup->getTextGroupingAuto() != fTextGroupingAuto )
                {
                    pEffectSequence->setTextGroupingAuto( pTextGroup, fTextGroupingAuto );
                    bChanged = true;
                }
            }

            if( bHasTextReverse )
            {
                if( pTextGroup && pTextGroup->getTextReverse() != bTextReverse )
                {
                    pEffectSequence->setTextReverse( pTextGroup, bTextReverse );
                    bChanged = true;
                }
            }
        }
    }

    if( bChanged )
    {
        mpMainSequence->rebuild();
        updateControls();
        mrBase.GetDocShell()->SetModified();
    }
}

void CustomAnimationPane::showOptions(const OUString& rPage)
{
    std::unique_ptr<STLPropertySet> xSet = createSelectionSet();

    auto xDlg = std::make_shared<CustomAnimationDialog>(GetFrameWeld(), std::move(xSet), rPage);

    weld::DialogController::runAsync(xDlg, [xDlg, this](sal_Int32 nResult){
        if (nResult )
        {
            addUndo();
            changeSelection(xDlg->getResultSet(), xDlg->getPropertySet());
            updateControls();
        }
    });
}

void CustomAnimationPane::onChangeCurrentPage()
{
    if( !mxView.is() )
        return;

    try
    {
        Reference< XDrawPage > xNewPage( mxView->getCurrentPage() );
        if( xNewPage != mxCurrentPage )
        {
            mxCurrentPage = std::move(xNewPage);
            SdPage* pPage = SdPage::getImplementation( mxCurrentPage );
            if( pPage )
            {
                mpMainSequence = pPage->getMainSequence();
                mxCustomAnimationList->update( mpMainSequence );
            }
            updateControls();
        }
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationPane::onChangeCurrentPage()" );
    }
}

static bool getTextSelection(const Reference< XTextRange >& xSelectedText, Reference< XShape >& xShape, std::vector< sal_Int16 >& rParaList )
{
    if( xSelectedText.is() ) try
    {
        xShape.set( xSelectedText->getText(), UNO_QUERY_THROW );

        css::uno::Reference<css::document::XActionLockable> xLockable(xShape, css::uno::UNO_QUERY);
        if (xLockable.is())
            xLockable->addActionLock();
        comphelper::ScopeGuard aGuard([&xLockable]()
        {
            if (xLockable.is())
                xLockable->removeActionLock();
        });

        Reference< XTextRangeCompare > xTextRangeCompare( xShape, UNO_QUERY_THROW );
        Reference< XEnumerationAccess > xParaEnumAccess( xShape, UNO_QUERY_THROW );
        Reference< XEnumeration > xParaEnum( xParaEnumAccess->createEnumeration(), UNO_SET_THROW );
        Reference< XTextRange > xRange;
        Reference< XTextRange > xStart( xSelectedText->getStart() );
        Reference< XTextRange > xEnd( xSelectedText->getEnd() );

        if (xTextRangeCompare->compareRegionEnds(xStart, xEnd) < 0)
            std::swap(xStart, xEnd);

        sal_Int16 nPara = 0;
        while( xParaEnum->hasMoreElements() )
        {
            xParaEnum->nextElement() >>= xRange;

            // break if start of selection is prior to end of current paragraph
            if( xRange.is() && (xTextRangeCompare->compareRegionEnds( xStart, xRange ) >= 0 ) )
                break;

            nPara++;
        }

        while( xRange.is() )
        {
            if( xRange.is() && !xRange->getString().isEmpty() )
                rParaList.push_back( nPara );

            // break if end of selection is before or at end of current paragraph
            if( xRange.is() && xTextRangeCompare->compareRegionEnds( xEnd, xRange ) >= 0 )
                break;

            nPara++;

            if( xParaEnum->hasMoreElements() )
                xParaEnum->nextElement() >>= xRange;
            else
                xRange.clear();
        }

        return true;
    }
    catch( Exception& )
    {
        TOOLS_WARN_EXCEPTION( "sd", "sd::CustomAnimationPane::getTextSelection()" );
    }

    return false;
}

namespace
{
    Reference<XShape> getTargetShape(const Any& rTarget)
    {
        Reference<XShape> xShape;
        rTarget >>= xShape;
        if( !xShape.is() )
        {
            ParagraphTarget aParaTarget;
            if (rTarget >>= aParaTarget)
                xShape = aParaTarget.Shape;
        }
        return xShape;
    }
}

void CustomAnimationPane::onAdd()
{
    bool bHasText = true;

    // first create vector of targets for dialog preview
    std::vector< Any > aTargets;

    // gather shapes from the selection
    maViewSelection = mxView->getSelection();

    if (Reference<XIndexAccess> xShapes; maViewSelection >>= xShapes)
    {
        sal_Int32 nCount = xShapes->getCount();
        aTargets.reserve( nCount );
        for( sal_Int32 nIndex = 0; nIndex < nCount; nIndex++ )
        {
            Any aTarget( xShapes->getByIndex( nIndex ) );
            aTargets.push_back( aTarget );
            if( bHasText )
            {
                Reference< XText > xText;
                aTarget >>= xText;
                if( !xText.is() || xText->getString().isEmpty() )
                    bHasText = false;
            }
        }
    }
    else if (Reference<XText> xText; maViewSelection >>= xText)
    {
        aTargets.push_back( maViewSelection );
        if( !xText.is() || xText->getString().isEmpty() )
            bHasText = false;
    }
    else if (Reference<XTextCursor> xCursor; maViewSelection >>= xCursor)
    {
        Reference< XShape > xShape;
        std::vector< sal_Int16 > aParaList;
        if (getTextSelection(xCursor, xShape, aParaList))
        {
            ParagraphTarget aParaTarget;
            aParaTarget.Shape = std::move(xShape);

            for( const auto& rPara : aParaList )
            {
                aParaTarget.Paragraph = rPara;
                aTargets.push_back( Any( aParaTarget ) );
            }
        }
    }
    else
    {
        OSL_FAIL("sd::CustomAnimationPane::onAdd(), unknown view selection!" );
        return;
    }

    CustomAnimationPresetPtr pDescriptor;
    mxFTCategory->set_sensitive(true);
    mxFTAnimation->set_sensitive(true);

    bool bCategoryReset = false;

    if (!mxLBCategory->get_sensitive() || mxLBCategory->get_active() == -1)
    {
        mxLBCategory->set_sensitive(true);
        mxLBCategory->set_active(0);
        bCategoryReset = true;
    }

    if (bCategoryReset || !mxLBAnimation->get_sensitive() ||
        mxLBAnimation->get_selected_index() == -1)
    {
        mxLBAnimation->set_sensitive(true);

        sal_Int32 nFirstEffect = fillAnimationLB(bHasText);
        if (nFirstEffect == -1)
            return;

        mxLBAnimation->select(nFirstEffect);
        mnLastSelectedAnimation = nFirstEffect;
    }

    auto pEntryData = weld::fromId<CustomAnimationPresetPtr*>(mxLBAnimation->get_selected_id());
    if (pEntryData)
        pDescriptor = *pEntryData;

    if( pDescriptor )
    {
        const double fDuration = pDescriptor->getDuration();
        mxCBXDuration->set_value(fDuration*100.0, FieldUnit::NONE);
        bool bHasSpeed = pDescriptor->getDuration() > 0.001;
        mxCBXDuration->set_sensitive( bHasSpeed );
        mxFTDuration->set_sensitive( bHasSpeed );

        mxCustomAnimationList->unselect_all();

        // gather shapes from the selection
        bool bFirst = true;
        for( const auto& rTarget : aTargets )
        {
            css::uno::Reference<css::document::XActionLockable> xLockable(getTargetShape(rTarget), css::uno::UNO_QUERY);
            if (xLockable.is())
                xLockable->addActionLock();
            comphelper::ScopeGuard aGuard([&xLockable]()
            {
                if (xLockable.is())
                    xLockable->removeActionLock();
            });

            CustomAnimationEffectPtr pCreated = mpMainSequence->append( pDescriptor, rTarget, fDuration );

            // if only one shape with text and no fill or outline is selected, animate only by first level paragraphs
            if( bHasText && (aTargets.size() == 1) )
            {
                Reference< XShape > xShape( rTarget, UNO_QUERY );
                if( xShape.is() && !hasVisibleShape( xShape ) )
                {
                    mpMainSequence->createTextGroup( pCreated, 1, -1.0, false, false );
                }
            }

            if( bFirst )
                bFirst = false;
            else
                pCreated->setNodeType( EffectNodeType::WITH_PREVIOUS );

            if( pCreated )
                mxCustomAnimationList->select( pCreated );
        }
    }

    PathKind ePathKind = getCreatePathKind();

    if (ePathKind != PathKind::NONE)
    {
        createPath( ePathKind, aTargets, 0.0 );
        updateMotionPathTags();
    }

    addUndo();
    mrBase.GetDocShell()->SetModified();

    updateControls();

    if (!SlideShow::IsInteractiveSlideshow(mrBase)) // IASS
        SlideShow::Stop( mrBase );
}

void CustomAnimationPane::onRemove()
{
    if( maListSelection.empty() )
        return;

    addUndo();

    MainSequenceRebuildGuard aGuard( mpMainSequence );

    EffectSequence aList( maListSelection );

    for( CustomAnimationEffectPtr& pEffect : aList )
    {
        if( pEffect->getEffectSequence() )
            pEffect->getEffectSequence()->remove( pEffect );
    }

    maListSelection.clear();
    mrBase.GetDocShell()->SetModified();
}

void CustomAnimationPane::remove( CustomAnimationEffectPtr const & pEffect )
{
    if( pEffect->getEffectSequence() )
    {
        addUndo();
        pEffect->getEffectSequence()->remove( pEffect );
        mrBase.GetDocShell()->SetModified();
    }
}

void CustomAnimationPane::onChangeStart()
{
    sal_Int16 nNodeType;
    switch( mxLBStart->get_active() )
    {
    case 0: nNodeType = EffectNodeType::ON_CLICK; break;
    case 1: nNodeType = EffectNodeType::WITH_PREVIOUS; break;
    case 2: nNodeType = EffectNodeType::AFTER_PREVIOUS; break;
    default:
        return;
    }

    onChangeStart( nNodeType );
}

void CustomAnimationPane::onChangeStart( sal_Int16 nNodeType )
{
    addUndo();

    MainSequenceRebuildGuard aGuard( mpMainSequence );

    bool bNeedRebuild = false;

    for( CustomAnimationEffectPtr& pEffect : maListSelection )
    {
        if( pEffect->getNodeType() != nNodeType )
        {
            pEffect->setNodeType( nNodeType );
            bNeedRebuild = true;
        }
    }

    if( bNeedRebuild )
    {
        mpMainSequence->rebuild();
        updateControls();
        mrBase.GetDocShell()->SetModified();
    }
}

void CustomAnimationPane::onChangeSpeed()
{
    double fDuration = getDuration();

    if(fDuration < 0)
        return;
    else
    {
        addUndo();

        MainSequenceRebuildGuard aGuard( mpMainSequence );

        // change selected effect
        for( CustomAnimationEffectPtr& pEffect : maListSelection )
        {
            pEffect->setDuration( fDuration );
        }

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

double CustomAnimationPane::getDuration() const
{
    double fDuration = 0;

    if (!mxCBXDuration->get_text().isEmpty())
        fDuration = mxCBXDuration->get_value(FieldUnit::NONE) / 100.0;

    return fDuration;
}

PathKind CustomAnimationPane::getCreatePathKind() const
{
    PathKind eKind = PathKind::NONE;

    if (mxLBAnimation->count_selected_rows() == 1 &&
        mxLBCategory->get_active() == gnMotionPathPos)
    {
        const sal_Int32 nPos = mxLBAnimation->get_selected_index();
        if( nPos == mnCurvePathPos )
        {
            eKind = PathKind::CURVE;
        }
        else if( nPos == mnPolygonPathPos )
        {
            eKind = PathKind::POLYGON;
        }
        else if( nPos == mnFreeformPathPos )
        {
            eKind = PathKind::FREEFORM;
        }
    }

    return eKind;
}

void CustomAnimationPane::createPath( PathKind eKind, std::vector< Any >& rTargets, double fDuration)
{
    sal_uInt16 nSID = 0;

    switch( eKind )
    {
    case PathKind::CURVE:     nSID = SID_DRAW_BEZIER_NOFILL; break;
    case PathKind::POLYGON:   nSID = SID_DRAW_POLYGON_NOFILL; break;
    case PathKind::FREEFORM:  nSID = SID_DRAW_FREELINE_NOFILL; break;
    default: break;
    }

    if( !nSID )
        return;

    DrawViewShell* pViewShell = dynamic_cast< DrawViewShell* >(
        FrameworkHelper::Instance(mrBase)->GetViewShell(FrameworkHelper::msCenterPaneURL).get());

    if( pViewShell )
    {
        DrawView* pView = pViewShell->GetDrawView();
        if( pView )
            pView->UnmarkAllObj();

        std::vector< Any > aTargets( 1, Any( fDuration ) );
        aTargets.insert( aTargets.end(), rTargets.begin(), rTargets.end() );
        Sequence< Any > aTargetSequence( comphelper::containerToSequence( aTargets ) );
        const SfxUnoAnyItem aItem( SID_ADD_MOTION_PATH, Any( aTargetSequence ) );
        pViewShell->GetViewFrame()->GetDispatcher()->ExecuteList( nSID, SfxCallMode::ASYNCHRON, {&aItem} );
    }
}


/// this link is called when the property box is modified by the user
IMPL_LINK_NOARG(CustomAnimationPane, implPropertyHdl, LinkParamNone*, void)
{
    if (!mxLBSubControl)
        return;

    addUndo();

    MainSequenceRebuildGuard aGuard( mpMainSequence );

    const Any aValue(mxLBSubControl->getValue());

    bool bNeedUpdate = false;

    // change selected effect
    for( const CustomAnimationEffectPtr& pEffect : maListSelection )
    {
        if( setProperty1Value( mnPropertyType, pEffect, aValue ) )
            bNeedUpdate = true;
    }

    if( bNeedUpdate )
    {
        mpMainSequence->rebuild();
        updateControls();
        mrBase.GetDocShell()->SetModified();
    }

    onPreview( false );
}

IMPL_LINK_NOARG(CustomAnimationPane, DelayModifiedHdl, weld::MetricSpinButton&, void)
{
    addUndo();
}

IMPL_LINK_NOARG(CustomAnimationPane, DelayLoseFocusHdl, weld::Widget&, void)
{
    double fBegin = mxMFStartDelay->get_value(FieldUnit::NONE);

    //sequence rebuild only when the control loses focus
    MainSequenceRebuildGuard aGuard( mpMainSequence );

    // change selected effect
    for( CustomAnimationEffectPtr& pEffect : maListSelection )
    {
        pEffect->setBegin( fBegin/10.0 );
    }

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

IMPL_LINK_NOARG(CustomAnimationPane, AnimationSelectHdl, weld::TreeView&, void)
{
    maIdle.Start();
}

IMPL_LINK_NOARG(CustomAnimationPane, SelectionHandler, Timer*, void)
{
    if (mxLBAnimation->has_mouse_grab()) // tdf#136474 try again later
    {
        maIdle.Start();
        return;
    }

    int nSelected = mxLBAnimation->get_selected_index();
    if (nSelected == -1)
        return;

    // tdf#99137, the selected entry may also be a subcategory title, so not an effect
    // just skip it and move to the next one in this case
    if (mxLBAnimation->get_text_emphasis(nSelected, 0))
    {
        if (nSelected == 0 || nSelected > mnLastSelectedAnimation)
            mxLBAnimation->select(++nSelected);
        else
            mxLBAnimation->select(--nSelected);
    }

    mnLastSelectedAnimation = nSelected;

    CustomAnimationPresetPtr* pPreset = weld::fromId<CustomAnimationPresetPtr*>(mxLBAnimation->get_id(nSelected));
    PathKind ePathKind = getCreatePathKind();

    if ( ePathKind != PathKind::NONE )
    {
        std::vector< Any > aTargets;
        MainSequenceRebuildGuard aGuard( mpMainSequence );

        for( const CustomAnimationEffectPtr& pEffect : maListSelection )
        {
            aTargets.push_back( pEffect->getTarget() );

            EffectSequenceHelper* pEffectSequence = pEffect->getEffectSequence();
            if( !pEffectSequence )
                pEffectSequence = mpMainSequence.get();

            // delete the old animation, new one will be appended
            // by createPath and SID_ADD_MOTION_PATH therein
            pEffectSequence->remove( pEffect );
        }

        createPath( ePathKind, aTargets, 0.0 );
        updateMotionPathTags();
        return;
    }

    CustomAnimationPresetPtr pDescriptor(*pPreset);
    const double fDuration = (*pPreset)->getDuration();
    MainSequenceRebuildGuard aGuard( mpMainSequence );

    // get selected effect
    for( const CustomAnimationEffectPtr& pEffect : maListSelection )
    {
        // Dispose the deprecated motion path tag. It will be rebuilt later.
        if (pEffect->getPresetClass() == css::presentation::EffectPresetClass::MOTIONPATH)
        {
            for (auto const& xTag: maMotionPathTags)
            {
                if(xTag->getEffect() == pEffect && !xTag->isDisposed())
                    xTag->Dispose();
            }
        }

        EffectSequenceHelper* pEffectSequence = pEffect->getEffectSequence();
        if( !pEffectSequence )
            pEffectSequence = mpMainSequence.get();

        pEffectSequence->replace( pEffect, pDescriptor, fDuration );
    }

    addUndo();
    onPreview(false);
}

IMPL_LINK_NOARG(CustomAnimationPane, UpdateAnimationLB, weld::ComboBox&, void)
{
    //FIXME: first effect only? what if there is more?
    bool bHasText = false;
    if (!maListSelection.empty())
    {
        CustomAnimationEffectPtr pEffect = maListSelection.front();
        bHasText = pEffect && pEffect->hasText();
    }
    fillAnimationLB(bHasText);
}

IMPL_LINK_NOARG(CustomAnimationPane, DurationModifiedHdl, weld::MetricSpinButton&, void)
{
    if (!mxCBXDuration->get_text().isEmpty())
    {
        double duration_value = static_cast<double>(mxCBXDuration->get_value(FieldUnit::NONE));
        if(duration_value <= 0.0)
        {
            mxCBXDuration->set_value(1, FieldUnit::NONE);
        }
        onChangeSpeed();
    }
}

namespace
{
    void InsertCategory(weld::TreeView& rLBAnimation, const OUString& rMotionPathLabel)
    {
        int nRow = rLBAnimation.n_children();
        rLBAnimation.append_text(rMotionPathLabel);
        rLBAnimation.set_text_emphasis(nRow, true, 0);
        rLBAnimation.set_text_align(nRow, 0.5, 0);
    }
}

sal_Int32 CustomAnimationPane::fillAnimationLB( bool bHasText )
{
    PresetCategoryList rCategoryList;
    sal_uInt16 nPosition = mxLBCategory->get_active();
    const CustomAnimationPresets& rPresets (CustomAnimationPresets::getCustomAnimationPresets());
    switch(nPosition)
    {
        case 0:rCategoryList = rPresets.getEntrancePresets();break;
        case 1:rCategoryList = rPresets.getEmphasisPresets();break;
        case 2:rCategoryList = rPresets.getExitPresets();break;
        case 3:rCategoryList = rPresets.getMotionPathsPresets();break;
        case 4:rCategoryList = rPresets.getMiscPresets();break;
    }

    sal_Int32 nFirstEffect = -1;

    int nOldEntryCount = mxLBAnimation->n_children();
    int nOldScrollPos = mxLBAnimation->vadjustment_get_value();

    mxLBAnimation->freeze();
    mxLBAnimation->clear();
    mnLastSelectedAnimation = -1;

    if (nPosition == gnMotionPathPos)
    {
        OUString sMotionPathLabel( SdResId( STR_CUSTOMANIMATION_USERPATH ) );
        InsertCategory(*mxLBAnimation, sMotionPathLabel);
        mnCurvePathPos = mxLBAnimation->n_children();
        mxLBAnimation->append_text( SvxResId(STR_ObjNameSingulCOMBLINE) );
        mxLBAnimation->set_text_emphasis(mnCurvePathPos, false, 0);
        mnPolygonPathPos = mnCurvePathPos + 1;
        mxLBAnimation->append_text( SvxResId(STR_ObjNameSingulPOLY) );
        mxLBAnimation->set_text_emphasis(mnPolygonPathPos, false, 0);
        mnFreeformPathPos = mnPolygonPathPos + 1;
        mxLBAnimation->append_text( SvxResId(STR_ObjNameSingulFREELINE) );
        mxLBAnimation->set_text_emphasis(mnFreeformPathPos, false, 0);
    }

    for (const PresetCategoryPtr& pCategory : rCategoryList)
    {
        if( pCategory )
        {
            InsertCategory(*mxLBAnimation, pCategory->maLabel);

            int nPos = mxLBAnimation->n_children();

            std::vector< CustomAnimationPresetPtr > aSortedVector =
                pCategory->maEffects;

            for( const CustomAnimationPresetPtr& pDescriptor : aSortedVector )
            {
                // ( !isTextOnly || ( isTextOnly && bHasText ) ) <=> !isTextOnly || bHasText
                if( pDescriptor && ( !pDescriptor->isTextOnly() || bHasText ) )
                {
                    auto pCustomPtr = new CustomAnimationPresetPtr(pDescriptor);
                    OUString sId = weld::toId(pCustomPtr);
                    mxLBAnimation->append(sId, pDescriptor->getLabel());
                    mxLBAnimation->set_text_emphasis(nPos, false, 0);

                    if (nFirstEffect == -1)
                        nFirstEffect = nPos;

                    ++nPos;
                }
            }
        }
    }

    mxLBAnimation->thaw();

    if (mxLBAnimation->n_children() == nOldEntryCount)
        mxLBAnimation->vadjustment_set_value(nOldScrollPos);

    return nFirstEffect;
}

IMPL_LINK(CustomAnimationPane, implToggleHdl, weld::Toggleable&, rBtn, void)
{
    implControlHdl(&rBtn);
}

IMPL_LINK(CustomAnimationPane, implClickHdl, weld::Button&, rBtn, void)
{
    implControlHdl(&rBtn);
}

IMPL_LINK( CustomAnimationPane, implControlListBoxHdl, weld::ComboBox&, rListBox, void )
{
    implControlHdl(&rListBox);
}

/// this link is called when one of the controls is modified
void CustomAnimationPane::implControlHdl(const weld::Widget* pControl)
{
    if (pControl == mxPBAddEffect.get())
        onAdd();
    else if (pControl == mxPBRemoveEffect.get())
        onRemove();
    else if (pControl == mxLBStart.get())
        onChangeStart();
    else if (pControl == mxPBPropertyMore.get())
        showOptions();
    else if (pControl == mxPBMoveUp.get())
        moveSelection( true );
    else if (pControl == mxPBMoveDown.get())
        moveSelection( false );
    else if (pControl == mxPBPlay.get())
        onPreview( true );
    else if (pControl == mxCBAutoPreview.get())
    {
        SdOptions* pOptions = SdModule::get()->GetSdOptions(DocumentType::Impress);
        pOptions->SetPreviewChangedEffects(mxCBAutoPreview->get_active());
    }
}

IMPL_LINK_NOARG(CustomAnimationPane, lateInitCallback, Timer *, void)
{
    // Call getPresets() to initiate the (expensive) construction of the
    // presets list.
    CustomAnimationPresets::getCustomAnimationPresets();

    // update selection and control states
    onSelectionChanged();
}

void CustomAnimationPane::moveSelection( bool bUp )
{
    if( maListSelection.empty() )
        return;

    EffectSequenceHelper* pSequence = maListSelection.front()->getEffectSequence();
    if( pSequence == nullptr )
        return;

    addUndo();

    bool bChanged = false;

    MainSequenceRebuildGuard aGuard( mpMainSequence );
    EffectSequence& rEffectSequence = pSequence->getSequence();

    if( bUp )
    {
        for( const CustomAnimationEffectPtr& pEffect : maListSelection )
        {
            EffectSequence::iterator aUpEffectPos( pSequence->find( pEffect ) );
            // coverity[copy_paste_error : FALSE] - this is correct, checking if it exists
            if( aUpEffectPos != rEffectSequence.end() )
            {
                EffectSequence::iterator aInsertPos( rEffectSequence.erase( aUpEffectPos ) );

                if( aInsertPos != rEffectSequence.begin() )
                {
                    --aInsertPos;
                    while( (aInsertPos != rEffectSequence.begin()) && !mxCustomAnimationList->isExpanded(*aInsertPos))
                        --aInsertPos;
                    rEffectSequence.insert( aInsertPos, pEffect );
                }
                else
                {
                    rEffectSequence.push_front( pEffect );
                }
                bChanged = true;
            }
        }
    }
    else
    {
        EffectSequence::reverse_iterator aIter( maListSelection.rbegin() );
        const EffectSequence::reverse_iterator aEnd( maListSelection.rend() );

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

            EffectSequence::iterator aDownEffectPos( pSequence->find( pEffect ) );
            // coverity[copy_paste_error : FALSE] - this is correct, checking if it exists
            if( aDownEffectPos != rEffectSequence.end() )
            {
                EffectSequence::iterator aInsertPos( rEffectSequence.erase( aDownEffectPos ) );

                if( aInsertPos != rEffectSequence.end() )
                {
                    ++aInsertPos;
                    // Advance over rolled-up (un-expanded) items, unless we just moved it there.
                    while( (aInsertPos != rEffectSequence.end())
                        && !mxCustomAnimationList->isExpanded(*aInsertPos)
                        && (std::find(maListSelection.begin(), maListSelection.end(), *aInsertPos)
                                == maListSelection.end())
                    )
                        ++aInsertPos;
                    rEffectSequence.insert( aInsertPos, pEffect );
                }
                else
                {
                    rEffectSequence.push_back(std::move(pEffect));
                }
                bChanged = true;
            }
        }
    }

    if( bChanged )
    {
        mpMainSequence->rebuild();
        updateControls();
        mrBase.GetDocShell()->SetModified();
    }
}

void CustomAnimationPane::onPreview( bool bForcePreview )
{
    if (!bForcePreview && !mxCBAutoPreview->get_active())
        return;

    // No preview in LOK.
    if (comphelper::LibreOfficeKit::isActive())
        return;

    if( maListSelection.empty() )
    {
        rtl::Reference< MotionPathTag > xMotionPathTag;
        auto aIter = std::find_if(maMotionPathTags.begin(), maMotionPathTags.end(),
            [](const MotionPathTagVector::value_type& rxMotionPathTag) { return rxMotionPathTag->isSelected(); });
        if (aIter != maMotionPathTags.end())
            xMotionPathTag = *aIter;

        if( xMotionPathTag.is() )
        {
            MainSequencePtr pSequence = std::make_shared<MainSequence>();
            pSequence->append( xMotionPathTag->getEffect()->clone() );
            preview( pSequence->getRootNode() );
        }
        else
        {
            Reference< XAnimationNodeSupplier > xNodeSupplier( mxCurrentPage, UNO_QUERY );
            if( !xNodeSupplier.is() )
                return;

            preview( xNodeSupplier->getAnimationNode() );
        }
    }
    else
    {
        MainSequencePtr pSequence = std::make_shared<MainSequence>();

        for( const CustomAnimationEffectPtr& pEffect : maListSelection )
        {
            pSequence->append( pEffect->clone() );
        }

        preview( pSequence->getRootNode() );
    }
}

void CustomAnimationPane::preview( const Reference< XAnimationNode >& xAnimationNode )
{
    Reference< XParallelTimeContainer > xRoot = ParallelTimeContainer::create( ::comphelper::getProcessComponentContext() );
    Sequence< css::beans::NamedValue > aUserData
        { { u"node-type"_ustr, css::uno::Any(css::presentation::EffectNodeType::TIMING_ROOT) } };
    xRoot->setUserData( aUserData );
    xRoot->appendChild( xAnimationNode );

    SlideShow::StartPreview( mrBase, mxCurrentPage, xRoot );
}

// ICustomAnimationListController
void CustomAnimationPane::onSelect()
{
    maListSelection = mxCustomAnimationList->getSelection();
    updateControls();

    // mark shapes from selected effects
    if( maSelectionLock.isLocked() )
        return;

    // tdf#145030 if nothing is selected in the effects list, leave the selection of
    // objects in the slide untouched
    if (maListSelection.empty())
        return;

    ScopeLockGuard aGuard( maSelectionLock );
    DrawViewShell* pViewShell = dynamic_cast< DrawViewShell* >(
        FrameworkHelper::Instance(mrBase)->GetViewShell(FrameworkHelper::msCenterPaneURL).get());
    DrawView* pView = pViewShell ? pViewShell->GetDrawView() : nullptr;

    if( pView )
    {
        pView->UnmarkAllObj();
        for( const CustomAnimationEffectPtr& pEffect : maListSelection )
        {
            Reference< XShape > xShape( pEffect->getTargetShape() );
            SdrObject* pObj = SdrObject::getSdrObjectFromXShape(xShape);
            if( pObj )
                pView->MarkObj(pObj, pView->GetSdrPageView());
        }
    }
}

// ICustomAnimationListController
// pEffectInsertBefore may be null if moving to end of list.
void CustomAnimationPane::onDragNDropComplete(std::vector< CustomAnimationEffectPtr > pEffectsDragged, CustomAnimationEffectPtr pEffectInsertBefore)
{
    if ( !mpMainSequence )
        return;

    addUndo();

    MainSequenceRebuildGuard aGuard( mpMainSequence );

    // Move all selected effects
    for( auto const& pEffectDragged : pEffectsDragged )
    {
        // Move this 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 (aIter != aEnd && mxCustomAnimationList->isVisible(*aIter))
                break;
        }
    }

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

void CustomAnimationPane::updatePathFromMotionPathTag( const rtl::Reference< MotionPathTag >& xTag )
{
    MainSequenceRebuildGuard aGuard( mpMainSequence );
    if( !xTag.is() )
        return;

    SdrPathObj* pPathObj = xTag->getPathObj();
    CustomAnimationEffectPtr pEffect = xTag->getEffect();
    if( (pPathObj != nullptr) && pEffect )
    {
        SfxUndoManager* pManager = mrBase.GetDocShell()->GetUndoManager();
        if( pManager )
        {
            SdPage* pPage = SdPage::getImplementation( mxCurrentPage );
            if( pPage )
                pManager->AddUndoAction( std::make_unique<UndoAnimationPath>( mrBase.GetDocShell()->GetDoc(), pPage, pEffect->getNode() ) );
        }

        pEffect->updatePathFromSdrPathObj( *pPathObj );
    }
}

}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
