/* -*- 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 "menufloatingwindow.hxx"
#include "menuitemlist.hxx"
#include "menubarwindow.hxx"
#include "bufferdevice.hxx"

#include <sal/log.hxx>
#include <salframe.hxx>
#include <svdata.hxx>
#include <vcl/decoview.hxx>
#include <vcl/settings.hxx>
#include <window.h>

MenuFloatingWindow::MenuFloatingWindow(PopupMenu* pMen, vcl::Window* pParent, WinBits nStyle ) :
    FloatingWindow( pParent, nStyle ),
    pMenu(pMen),
    aHighlightChangedTimer("vcl::MenuFloatingWindow aHighlightChangedTimer"),
    aSubmenuCloseTimer( "vcl::MenuFloatingWindow aSubmenuCloseTimer" ),
    aScrollTimer( "vcl::MenuFloatingWindow aScrollTimer" ),
    nHighlightedItem(ITEMPOS_INVALID),
    nMBDownPos(ITEMPOS_INVALID),
    nScrollerHeight(0),
    nFirstEntry(0),
    nPosInParent(ITEMPOS_INVALID),
    bInExecute(false),
    bScrollMenu(false),
    bScrollUp(false),
    bScrollDown(false),
    bIgnoreFirstMove(true),
    bKeyInput(false)
{
    mpWindowImpl->mbMenuFloatingWindow= true;

    ApplySettings(*GetOutDev());

    SetPopupModeEndHdl( LINK( this, MenuFloatingWindow, PopupEnd ) );

    aHighlightChangedTimer.SetInvokeHandler( LINK( this, MenuFloatingWindow, HighlightChanged ) );
    aHighlightChangedTimer.SetTimeout( GetSettings().GetMouseSettings().GetMenuDelay() );

    aSubmenuCloseTimer.SetTimeout( GetSettings().GetMouseSettings().GetMenuDelay() );
    aSubmenuCloseTimer.SetInvokeHandler( LINK( this, MenuFloatingWindow, SubmenuClose ) );

    aScrollTimer.SetInvokeHandler( LINK( this, MenuFloatingWindow, AutoScroll ) );

    AddEventListener( LINK( this, MenuFloatingWindow, ShowHideListener ) );
}

void MenuFloatingWindow::doShutdown()
{
    if( !pMenu )
        return;

    // #105373# notify toolkit that highlight was removed
    // otherwise the entry will not be read when the menu is opened again
    if( nHighlightedItem != ITEMPOS_INVALID )
        pMenu->ImplCallEventListeners( VclEventId::MenuDehighlight, nHighlightedItem );
    if (!bKeyInput && pMenu && pMenu->pStartedFrom && !pMenu->pStartedFrom->IsMenuBar())
    {
        // #102461# remove highlight in parent
        size_t i, nCount = pMenu->pStartedFrom->pItemList->size();
        for(i = 0; i < nCount; i++)
        {
            MenuItemData* pData = pMenu->pStartedFrom->pItemList->GetDataFromPos( i );
            if( pData && ( pData->pSubMenu == pMenu ) )
                break;
        }
        if( i < nCount )
        {
            MenuFloatingWindow* pPWin = static_cast<MenuFloatingWindow*>(pMenu->pStartedFrom->GetWindow());
            if (pPWin)
                pPWin->InvalidateItem(i);
        }
    }

    // free the reference to the accessible component
    SetAccessible({});

    aHighlightChangedTimer.Stop();

    // #95056# invalidate screen area covered by system window
    // so this can be taken into account if the commandhandler performs a scroll operation
    if( GetParent() )
    {
        tools::Rectangle aInvRect( GetWindowExtentsRelative( *GetParent() ) );
        GetParent()->Invalidate( aInvRect );
    }
    pMenu = nullptr;
    RemoveEventListener( LINK( this, MenuFloatingWindow, ShowHideListener ) );

    aScrollTimer.Stop();
    aSubmenuCloseTimer.Stop();
}

MenuFloatingWindow::~MenuFloatingWindow()
{
    disposeOnce();
}

void MenuFloatingWindow::dispose()
{
    doShutdown();
    pMenu.reset();
    pActivePopup.reset();
    xSaveFocusId.reset();

    // unset accessible taken from the PopupMenu (s. CreateAccessible),
    // it is owned and therefore disposed by the PopupMenu
    SetAccessible(nullptr);

    FloatingWindow::dispose();
}

void MenuFloatingWindow::Resize()
{
    InitMenuClipRegion(*GetOutDev()); // FIXME
}

void MenuFloatingWindow::ApplySettings(vcl::RenderContext& rRenderContext)
{
    FloatingWindow::ApplySettings(rRenderContext);

    if (IsNativeControlSupported(ControlType::MenuPopup, ControlPart::MenuItem) &&
        IsNativeControlSupported(ControlType::MenuPopup, ControlPart::Entire))
    {
        AllSettings aSettings(GetSettings());
        ImplGetFrame()->UpdateSettings(aSettings); // Update theme colors.
        StyleSettings aStyle(aSettings.GetStyleSettings());
        Color aHighlightTextColor = ImplGetSVData()->maNWFData.maMenuBarHighlightTextColor;
        if (aHighlightTextColor != COL_TRANSPARENT)
        {
            aStyle.SetMenuHighlightTextColor(aHighlightTextColor);
        }
        aSettings.SetStyleSettings(aStyle);
        GetOutDev()->SetSettings(aSettings);
    }

    const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings();
    SetPointFont(rRenderContext, rStyleSettings.GetMenuFont());

    if (rRenderContext.IsNativeControlSupported(ControlType::MenuPopup, ControlPart::Entire))
    {
        rRenderContext.SetBackground(); // background will be drawn by NWF
    }
    else
        rRenderContext.SetBackground(Wallpaper(rStyleSettings.GetMenuColor()));

    rRenderContext.SetTextColor(rStyleSettings.GetMenuTextColor());
    rRenderContext.SetTextFillColor();
    rRenderContext.SetLineColor();
}

/// Get a negative pixel offset for an offset menu
tools::Long MenuFloatingWindow::ImplGetStartY() const
{
    tools::Long nY = 0;
    if( pMenu )
    {
        // avoid crash if somehow menu got disposed, and MenuItemList is empty (workaround for tdf#104686)
        if ( nFirstEntry > 0 && !pMenu->GetItemList()->GetDataFromPos(nFirstEntry - 1) )
        {
            return 0;
        }

        for ( sal_uInt16 n = 0; n < nFirstEntry; n++ )
            nY += pMenu->GetItemList()->GetDataFromPos( n )->aSz.Height();
        nY -= pMenu->GetTitleHeight();
    }
    return -nY;
}

vcl::Region MenuFloatingWindow::ImplCalcClipRegion() const
{
    Size aOutSz = GetOutputSizePixel();
    tools::Rectangle aRect( Point(), aOutSz );
    aRect.AdjustTop(nScrollerHeight );
    aRect.AdjustBottom( -nScrollerHeight );

    vcl::Region aRegion(aRect);

    return aRegion;
}

void MenuFloatingWindow::InitMenuClipRegion(vcl::RenderContext& rRenderContext)
{
    if (IsScrollMenu())
    {
        rRenderContext.SetClipRegion(ImplCalcClipRegion());
    }
    else
    {
        rRenderContext.SetClipRegion();
    }
}

void MenuFloatingWindow::ImplHighlightItem( const MouseEvent& rMEvt, bool bMBDown )
{
    if( ! pMenu )
        return;

    tools::Long nY = GetInitialItemY();
    tools::Long nMouseY = rMEvt.GetPosPixel().Y();
    Size aOutSz = GetOutputSizePixel();
    if ( ( nMouseY >= nY ) && ( nMouseY < aOutSz.Height() ) )
    {
        bool bHighlighted = false;
        size_t nCount = pMenu->pItemList->size();
        for ( size_t n = 0; !bHighlighted && ( n < nCount ); n++ )
        {
            if ( pMenu->ImplIsVisible( n ) )
            {
                MenuItemData* pItemData = pMenu->pItemList->GetDataFromPos( n );
                tools::Long nOldY = nY;
                nY += pItemData->aSz.Height();
                if ( ( nOldY <= nMouseY ) && ( nY > nMouseY ) && pMenu->ImplIsSelectable( n ) )
                {
                    bool bPopupArea = true;
                    if ( pItemData->nBits & MenuItemBits::POPUPSELECT )
                    {
                        // only when clicked over the arrow...
                        Size aSz = GetOutputSizePixel();
                        tools::Long nFontHeight = GetTextHeight();
                        bPopupArea = ( rMEvt.GetPosPixel().X() >= ( aSz.Width() - nFontHeight - nFontHeight/4 ) );
                    }

                    if ( bMBDown )
                    {
                        if ( n != nHighlightedItem )
                        {
                            ChangeHighlightItem( static_cast<sal_uInt16>(n), false );
                        }

                        bool bAllowNewPopup = true;
                        if ( pActivePopup )
                        {
                            MenuItemData* pData = pMenu->pItemList->GetDataFromPos( n );
                            bAllowNewPopup = pData && ( pData->pSubMenu != pActivePopup );
                            if ( bAllowNewPopup )
                                KillActivePopup();
                        }

                        if ( bPopupArea && bAllowNewPopup )
                        {
                            HighlightChanged( nullptr );
                        }
                    }
                    else
                    {
                        if ( n != nHighlightedItem )
                        {
                            ChangeHighlightItem( static_cast<sal_uInt16>(n), true );
                        }
                        else if ( pItemData->nBits & MenuItemBits::POPUPSELECT )
                        {
                            if ( bPopupArea && ( pActivePopup != pItemData->pSubMenu ) )
                                HighlightChanged( nullptr );
                        }
                    }
                    bHighlighted = true;
                }
            }
        }
        if ( !bHighlighted )
            ChangeHighlightItem( ITEMPOS_INVALID, true );
    }
    else
    {
        ImplScroll( rMEvt.GetPosPixel() );
        ChangeHighlightItem( ITEMPOS_INVALID, true );
    }
}

IMPL_LINK_NOARG(MenuFloatingWindow, PopupEnd, FloatingWindow*, void)
{
    // "this" will be deleted before the end of this method!
    Menu* pM = pMenu;
    if ( bInExecute )
    {
        End();
        if ( pActivePopup )
        {
            KillActivePopup(); // should be ok to just remove it
            //pActivePopup->bCanceled = true;
        }
        pMenu->bInCallback = true;
        pMenu->Deactivate();
        pMenu->bInCallback = false;
    }
    else
    {
        if (pMenu && pMenu->pStartedFrom)
            pMenu->pStartedFrom->ClosePopup(pMenu);
    }

    if ( pM )
        pM->pStartedFrom = nullptr;
}

IMPL_LINK_NOARG(MenuFloatingWindow, AutoScroll, Timer *, void)
{
    ImplScroll( GetPointerPosPixel() );
}

IMPL_LINK( MenuFloatingWindow, HighlightChanged, Timer*, pTimer, void )
{
    if( ! pMenu )
        return;

    MenuItemData* pItemData = pMenu->pItemList->GetDataFromPos( nHighlightedItem );
    if ( !pItemData )
        return;

    if ( pActivePopup && ( pActivePopup != pItemData->pSubMenu ) )
    {
        FloatWinPopupFlags nOldFlags = GetPopupModeFlags();
        SetPopupModeFlags( GetPopupModeFlags() | FloatWinPopupFlags::NoAppFocusClose );
        KillActivePopup();
        SetPopupModeFlags( nOldFlags );
    }
    if ( !(pItemData->bEnabled && pItemData->pSubMenu && pItemData->pSubMenu->GetItemCount() && ( pItemData->pSubMenu != pActivePopup )) )
        return;

    pActivePopup = pItemData->pSubMenu.get();
    tools::Long nY = nScrollerHeight+ImplGetStartY();
    MenuItemData* pData = nullptr;
    for ( sal_uLong n = 0; n < nHighlightedItem; n++ )
    {
        pData = pMenu->pItemList->GetDataFromPos( n );
        nY += pData->aSz.Height();
    }
    pData = pMenu->pItemList->GetDataFromPos( nHighlightedItem );
    Size MySize = GetOutputSizePixel();
    Point aItemTopLeft( 0, nY );
    Point aItemBottomRight( aItemTopLeft );
    aItemBottomRight.AdjustX(MySize.Width() );
    aItemBottomRight.AdjustY(pData->aSz.Height() );

    // shift the popups a little
    aItemTopLeft.AdjustX(2 );
    aItemBottomRight.AdjustX( -2 );
    if ( nHighlightedItem )
        aItemTopLeft.AdjustY( -2 );
    else
    {
        sal_Int32 nL, nT, nR, nB;
        GetBorder( nL, nT, nR, nB );
        aItemTopLeft.AdjustY( -nT );
    }

    // pTest: crash due to Reschedule() in call of Activate()
    // Also it is prevented that submenus are displayed which
    // were for long in Activate Rescheduled and which should not be
    // displayed now.
    Menu* pTest = pActivePopup;
    FloatWinPopupFlags nOldFlags = GetPopupModeFlags();
    SetPopupModeFlags( GetPopupModeFlags() | FloatWinPopupFlags::NoAppFocusClose );
    sal_uInt16 nRet = pActivePopup->ImplExecute( this, tools::Rectangle( aItemTopLeft, aItemBottomRight ), FloatWinPopupFlags::Right, pMenu, pTimer == nullptr );
    SetPopupModeFlags( nOldFlags );

    // nRet != 0, if it was stopped during Activate()...
    if ( !nRet && ( pActivePopup == pTest ) && pActivePopup->GetWindow() )
        pActivePopup->ImplGetFloatingWindow()->AddPopupModeWindow( this );
}

IMPL_LINK_NOARG(MenuFloatingWindow, SubmenuClose, Timer *, void)
{
    if( pMenu && pMenu->pStartedFrom )
    {
        MenuFloatingWindow* pWin = static_cast<MenuFloatingWindow*>(pMenu->pStartedFrom->GetWindow());
        if( pWin )
            pWin->KillActivePopup();
    }
}

IMPL_LINK( MenuFloatingWindow, ShowHideListener, VclWindowEvent&, rEvent, void )
{
    if( ! pMenu )
        return;

    if( rEvent.GetId() == VclEventId::WindowShow )
        pMenu->ImplCallEventListeners( VclEventId::MenuShow, ITEMPOS_INVALID );
    else if( rEvent.GetId() == VclEventId::WindowHide )
        pMenu->ImplCallEventListeners( VclEventId::MenuHide, ITEMPOS_INVALID );
}

void MenuFloatingWindow::EnableScrollMenu( bool b )
{
    bScrollMenu = b;
    nScrollerHeight = b ? static_cast<sal_uInt16>(GetSettings().GetStyleSettings().GetScrollBarSize()) /2 : 0;
    bScrollDown = true;
    InitMenuClipRegion(*GetOutDev());
}

void MenuFloatingWindow::Start()
{
    if (bInExecute)
        return;
    bInExecute = true;
    if (GetParent())
        GetParent()->IncModalCount();
}

bool MenuFloatingWindow::MenuInHierarchyHasFocus() const
{
    if (HasChildPathFocus())
        return true;
    PopupMenu* pSub = GetActivePopup();
    if (!pSub)
        return false;
    return pSub->ImplGetFloatingWindow()->HasChildPathFocus();
}

void MenuFloatingWindow::End()
{
    if (!bInExecute)
        return;

    if (GetParent() && !GetParent()->isDisposed())
        GetParent()->DecModalCount();

    // restore focus to previous window if we still have the focus
    VclPtr<vcl::Window> xFocusId(xSaveFocusId);
    xSaveFocusId = nullptr;
    if (xFocusId != nullptr && MenuInHierarchyHasFocus())
    {
        ImplGetSVData()->mpWinData->mbNoDeactivate = false;
        Window::EndSaveFocus(xFocusId);
    }

    bInExecute = false;
}

void MenuFloatingWindow::Execute()
{
    ImplSVData* pSVData = ImplGetSVData();

    pSVData->maAppData.mpActivePopupMenu = pMenu.get();

    Start();

    while (bInExecute && !Application::IsQuit())
        Application::Yield();

    pSVData->maAppData.mpActivePopupMenu = nullptr;
}

void MenuFloatingWindow::StopExecute()
{
    End();

    ImplEndPopupMode(FloatWinPopupEndFlags::NONE, xSaveFocusId);

    aHighlightChangedTimer.Stop();
    if (pActivePopup)
    {
        KillActivePopup();
    }
    // notify parent, needed for accessibility
    if( pMenu && pMenu->pStartedFrom )
        pMenu->pStartedFrom->ImplCallEventListeners( VclEventId::MenuSubmenuDeactivate, nPosInParent );
}

void MenuFloatingWindow::KillActivePopup()
{
    if (!pActivePopup)
        return;

    if (MenuFloatingWindow* pFloatWin = pActivePopup->ImplGetFloatingWindow())
    {
        if (pFloatWin->IsInCleanUp())
            return; // kill it later
    }
    if ( pActivePopup->bInCallback )
        pActivePopup->bCanceled = true;

    // For all actions pActivePopup = 0, if e.g.
    // PopupModeEndHdl the popups to destroy were called synchronous
    PopupMenu* pPopup = pActivePopup;
    pActivePopup = nullptr;
    pPopup->bInCallback = true;
    pPopup->Deactivate();
    pPopup->bInCallback = false;
    if (pPopup->GetWindow())
    {
        pPopup->ImplGetFloatingWindow()->StopExecute();
        pPopup->ImplGetFloatingWindow()->doShutdown();
        pPopup->m_pWindow.disposeAndClear();

        PaintImmediately();
    }
}

void MenuFloatingWindow::EndExecute()
{
    Menu* pStart = pMenu ? pMenu->ImplGetStartMenu() : nullptr;

    // if started elsewhere, cleanup there as well
    MenuFloatingWindow* pCleanUpFrom = this;
    MenuFloatingWindow* pWin = this;
    while (pWin && !pWin->bInExecute &&
        pWin->pMenu->pStartedFrom && !pWin->pMenu->pStartedFrom->IsMenuBar())
    {
        pWin = static_cast<PopupMenu*>(pWin->pMenu->pStartedFrom.get())->ImplGetFloatingWindow();
    }
    if ( pWin )
        pCleanUpFrom = pWin;

    // this window will be destroyed => store date locally...
    Menu* pM = pMenu;
    sal_uInt16 nItem = nHighlightedItem;

    pCleanUpFrom->StopExecute();

    if ( !(nItem != ITEMPOS_INVALID && pM) )
        return;

    MenuItemData* pItemData = pM->GetItemList()->GetDataFromPos( nItem );
    if ( pItemData && !pItemData->bIsTemporary )
    {
        pM->nSelectedId = pItemData->nId;
        pM->sSelectedIdent = pItemData->sIdent;
        if (pStart)
        {
            pStart->nSelectedId = pItemData->nId;
            pStart->sSelectedIdent = pItemData->sIdent;
        }

        pM->ImplSelect();
    }
}

void MenuFloatingWindow::EndExecute( sal_uInt16 nId )
{
    size_t nPos;
    if ( pMenu && pMenu->GetItemList()->GetData( nId, nPos ) )
        nHighlightedItem = nPos;
    else
        nHighlightedItem = ITEMPOS_INVALID;

    EndExecute();
}

void MenuFloatingWindow::MouseButtonDown( const MouseEvent& rMEvt )
{
    // TH creates a ToTop on this window, but the active popup
    // should stay on top...
    // due to focus change this would close all menus -> don't do it (#94123)
    //if ( pActivePopup && pActivePopup->ImplGetWindow() && !pActivePopup->ImplGetFloatingWindow()->pActivePopup )
    //    pActivePopup->ImplGetFloatingWindow()->ToTop( ToTopFlags::NoGrabFocus );

    ImplHighlightItem( rMEvt, true );

    nMBDownPos = nHighlightedItem;
}

void MenuFloatingWindow::MouseButtonUp( const MouseEvent& rMEvt )
{
    MenuItemData* pData = pMenu ? pMenu->GetItemList()->GetDataFromPos( nHighlightedItem ) : nullptr;
    // nMBDownPos store in local variable and reset immediately,
    // as it will be too late after EndExecute
    sal_uInt16 _nMBDownPos = nMBDownPos;
    nMBDownPos = ITEMPOS_INVALID;
    if ( !(pData && pData->bEnabled && ( pData->eType != MenuItemType::SEPARATOR )) )
        return;

    if ( !pData->pSubMenu )
    {
        EndExecute();
    }
    else if ( ( pData->nBits & MenuItemBits::POPUPSELECT ) && ( nHighlightedItem == _nMBDownPos ) && ( rMEvt.GetClicks() == 2 ) )
    {
        // not when clicked over the arrow...
        Size aSz = GetOutputSizePixel();
        tools::Long nFontHeight = GetTextHeight();
        if ( rMEvt.GetPosPixel().X() < ( aSz.Width() - nFontHeight - nFontHeight/4 ) )
            EndExecute();
    }

}

void MenuFloatingWindow::MouseMove( const MouseEvent& rMEvt )
{
    if ( !IsVisible() || rMEvt.IsSynthetic() || rMEvt.IsEnterWindow() )
        return;

    if ( rMEvt.IsLeaveWindow() )
    {
        // #102461# do not remove highlight if a popup menu is open at this position
        MenuItemData* pData = pMenu ? pMenu->pItemList->GetDataFromPos( nHighlightedItem ) : nullptr;
        // close popup with some delayed if we leave somewhere else
        if( pActivePopup && pData && pData->pSubMenu != pActivePopup )
            pActivePopup->ImplGetFloatingWindow()->aSubmenuCloseTimer.Start();

        if( !pActivePopup || (pData && pData->pSubMenu != pActivePopup ) )
            ChangeHighlightItem( ITEMPOS_INVALID, false );

        if ( IsScrollMenu() )
            ImplScroll( rMEvt.GetPosPixel() );
    }
    else
    {
        aSubmenuCloseTimer.Stop();
        if( bIgnoreFirstMove )
            bIgnoreFirstMove = false;
        else
            ImplHighlightItem( rMEvt, false );
    }
}

void MenuFloatingWindow::ImplScroll( bool bUp )
{
    KillActivePopup();
    PaintImmediately();

    if (!pMenu)
        return;

    Invalidate();

    pMenu->ImplKillLayoutData();

    if ( bScrollUp && bUp )
    {
        nFirstEntry = pMenu->ImplGetPrevVisible( nFirstEntry );
        SAL_WARN_IF( nFirstEntry == ITEMPOS_INVALID, "vcl", "Scroll?!" );

        // avoid crash if somehow menu got disposed, and MenuItemList is empty (workaround for tdf#104686)
        const auto pItemData = pMenu->GetItemList()->GetDataFromPos( nFirstEntry );
        if ( pItemData )
        {
            tools::Long nScrollEntryHeight = pItemData->aSz.Height();

            if ( !bScrollDown )
            {
                bScrollDown = true;
                Invalidate();
            }

            if ( pMenu->ImplGetPrevVisible( nFirstEntry ) == ITEMPOS_INVALID )
            {
                bScrollUp = false;
                Invalidate();
            }

            Scroll( 0, nScrollEntryHeight, ImplCalcClipRegion().GetBoundRect(), ScrollFlags::Clip );
        }
    }
    else if ( bScrollDown && !bUp )
    {
        // avoid crash if somehow menu got disposed, and MenuItemList is empty (workaround for tdf#104686)
        const auto pItemData = pMenu->GetItemList()->GetDataFromPos( nFirstEntry );
        if ( pItemData )
        {
            tools::Long nScrollEntryHeight = pItemData->aSz.Height();

            nFirstEntry = pMenu->ImplGetNextVisible( nFirstEntry );
            SAL_WARN_IF( nFirstEntry == ITEMPOS_INVALID, "vcl", "Scroll?!" );

            if ( !bScrollUp )
            {
                bScrollUp = true;
                Invalidate();
            }

            tools::Long nHeight = GetOutputSizePixel().Height();
            sal_uInt16 nLastVisible;
            pMenu->ImplCalcVisEntries(nHeight, nFirstEntry, &nLastVisible);
            if ( pMenu->ImplGetNextVisible( nLastVisible ) == ITEMPOS_INVALID )
            {
                bScrollDown = false;
                Invalidate();
            }

            Scroll( 0, -nScrollEntryHeight, ImplCalcClipRegion().GetBoundRect(), ScrollFlags::Clip );
        }
    }

    Invalidate();
}

void MenuFloatingWindow::ImplScroll( const Point& rMousePos )
{
    Size aOutSz = GetOutputSizePixel();

    tools::Long nY = nScrollerHeight;
    tools::Long nMouseY = rMousePos.Y();
    tools::Long nDelta = 0;

    if ( bScrollUp && ( nMouseY < nY ) )
    {
        ImplScroll( true );
        nDelta = nY - nMouseY;
    }
    else if ( bScrollDown && ( nMouseY > ( aOutSz.Height() - nY ) ) )
    {
        ImplScroll( false );
        nDelta = nMouseY - ( aOutSz.Height() - nY );
    }

    if ( !nDelta )
        return;

    aScrollTimer.Stop();    // if scrolled through MouseMove.
    tools::Long nTimeout;
    if ( nDelta < 3 )
        nTimeout = 200;
    else if ( nDelta < 5 )
        nTimeout = 100;
    else if ( nDelta < 8 )
        nTimeout = 70;
    else if ( nDelta < 12 )
        nTimeout = 40;
    else
        nTimeout = 20;
    aScrollTimer.SetTimeout( nTimeout );
    aScrollTimer.Start();
}
void MenuFloatingWindow::ChangeHighlightItem( sal_uInt16 n, bool bStartPopupTimer )
{
    // #57934# if necessary, immediately close the active, as TH's backgroundstorage works.
    // #65750# we prefer to refrain from the background storage of small lines.
    //         otherwise the menus are difficult to operate.
    //  MenuItemData* pNextData = pMenu->pItemList->GetDataFromPos( n );
    //  if ( pActivePopup && pNextData && ( pActivePopup != pNextData->pSubMenu ) )
    //      KillActivePopup();

    aSubmenuCloseTimer.Stop();
    if( ! pMenu )
        return;

    if ( nHighlightedItem != ITEMPOS_INVALID )
    {
        InvalidateItem(nHighlightedItem);
        pMenu->ImplCallEventListeners( VclEventId::MenuDehighlight, nHighlightedItem );
    }

    nHighlightedItem = n;
    SAL_WARN_IF( !pMenu->ImplIsVisible( nHighlightedItem ) && nHighlightedItem != ITEMPOS_INVALID, "vcl", "ChangeHighlightItem: Not visible!" );
    if( nHighlightedItem != ITEMPOS_INVALID )
    {
        if (pMenu->pStartedFrom && !pMenu->pStartedFrom->IsMenuBar())
        {
            // #102461# make sure parent entry is highlighted as well
            size_t i, nCount = pMenu->pStartedFrom->pItemList->size();
            for(i = 0; i < nCount; i++)
            {
                MenuItemData* pData = pMenu->pStartedFrom->pItemList->GetDataFromPos( i );
                if( pData && ( pData->pSubMenu == pMenu ) )
                    break;
            }
            if( i < nCount )
            {
                MenuFloatingWindow* pPWin = static_cast<MenuFloatingWindow*>(pMenu->pStartedFrom->GetWindow());
                if( pPWin && pPWin->nHighlightedItem != i )
                {
                    pPWin->InvalidateItem(i);
                    pPWin->nHighlightedItem = i;
                }
            }
        }
        InvalidateItem(nHighlightedItem);
        pMenu->ImplCallHighlight( nHighlightedItem );
    }
    else
    {
        pMenu->nSelectedId = 0;
        pMenu->sSelectedIdent.clear();
    }

    if ( bStartPopupTimer )
    {
        // #102438# Menu items are not selectable
        // If a menu item is selected by an AT-tool via the XAccessibleAction, XAccessibleValue
        // or XAccessibleSelection interface, and the parent popup menus are not executed yet,
        // the parent popup menus must be executed SYNCHRONOUSLY, before the menu item is selected.
        if ( GetSettings().GetMouseSettings().GetMenuDelay() )
            aHighlightChangedTimer.Start();
        else
            HighlightChanged( &aHighlightChangedTimer );
    }
}

/// Calculate the initial vertical pixel offset of the first item.
/// May be negative for scrolled windows.
tools::Long MenuFloatingWindow::GetInitialItemY(tools::Long *pStartY) const
{
    tools::Long nStartY = ImplGetStartY();
    if (pStartY)
        *pStartY = nStartY;
    return nScrollerHeight + nStartY +
        ImplGetSVData()->maNWFData.mnMenuFormatBorderY;
}

/// Emit an Invalidate just for this item's area
void MenuFloatingWindow::InvalidateItem(sal_uInt16 nPos)
{
    if (!pMenu)
        return;

    tools::Long nY = GetInitialItemY();
    size_t nCount = pMenu->pItemList->size();
    for (size_t n = 0; n < nCount; n++)
    {
        MenuItemData* pData = pMenu->pItemList->GetDataFromPos( n );
        tools::Long nHeight = pData->aSz.Height();
        if (n == nPos)
        {
            Size aWidth( GetSizePixel() );
            tools::Rectangle aRect(Point(0, nY), Size(aWidth.Width(), nHeight));
            Invalidate( aRect );
        }
        nY += nHeight;
    }
}

void MenuFloatingWindow::RenderHighlightItem(vcl::RenderContext& rRenderContext, sal_uInt16 nPos)
{
    if (!pMenu)
        return;

    Size aSz(GetOutputSizePixel());

    tools::Long nX = 0;
    tools::Long nStartY;
    tools::Long nY = GetInitialItemY(&nStartY);

    int nOuterSpaceX = ImplGetSVData()->maNWFData.mnMenuFormatBorderX;

    size_t nCount = pMenu->pItemList->size();
    for (size_t n = 0; n < nCount; n++)
    {
        MenuItemData* pData = pMenu->pItemList->GetDataFromPos( n );
        if (n == nPos)
        {
            SAL_WARN_IF(!pMenu->ImplIsVisible(n), "vcl", "Highlight: Item not visible!");
            if (pData->eType != MenuItemType::SEPARATOR)
            {
                bool bRestoreLineColor = false;
                Color oldLineColor;
                bool bDrawItemRect = true;

                tools::Rectangle aItemRect(Point(nX + nOuterSpaceX, nY), Size(aSz.Width() - 2 * nOuterSpaceX, pData->aSz.Height()));
                if (pData->nBits & MenuItemBits::POPUPSELECT)
                {
                    tools::Long nFontHeight = GetTextHeight();
                    aItemRect.AdjustRight( -(nFontHeight + nFontHeight / 4) );
                }

                if (rRenderContext.IsNativeControlSupported(ControlType::MenuPopup, ControlPart::Entire))
                {
                    Size aPxSize(GetOutputSizePixel());
                    auto popIt = rRenderContext.ScopedPush(vcl::PushFlags::CLIPREGION);
                    rRenderContext.IntersectClipRegion(tools::Rectangle(Point(nX, nY), Size(aSz.Width(), pData->aSz.Height())));
                    tools::Rectangle aCtrlRect(Point(nX, 0), Size(aPxSize.Width()-nX, aPxSize.Height()));
                    MenupopupValue aVal(pMenu->nTextPos-GUTTERBORDER, aItemRect);
                    rRenderContext.DrawNativeControl(ControlType::MenuPopup, ControlPart::Entire,
                                                     aCtrlRect, ControlState::ENABLED, aVal, OUString());
                    if (rRenderContext.IsNativeControlSupported(ControlType::MenuPopup, ControlPart::MenuItem))
                    {
                        bDrawItemRect = false;
                        if (!rRenderContext.DrawNativeControl(ControlType::MenuPopup, ControlPart::MenuItem, aItemRect,
                                                              ControlState::SELECTED | (pData->bEnabled
                                                                                            ? ControlState::ENABLED
                                                                                            : ControlState::NONE),
                                                              aVal, OUString()))
                        {
                            bDrawItemRect = true;
                        }
                    }
                    else
                        bDrawItemRect = true;
                }
                if (bDrawItemRect)
                {
                    if (pData->bEnabled)
                        rRenderContext.SetFillColor(rRenderContext.GetSettings().GetStyleSettings().GetMenuHighlightColor());
                    else
                    {
                        rRenderContext.SetFillColor();
                        oldLineColor = rRenderContext.GetLineColor();
                        rRenderContext.SetLineColor(rRenderContext.GetSettings().GetStyleSettings().GetMenuHighlightColor());
                        bRestoreLineColor = true;
                    }

                    rRenderContext.DrawRect(aItemRect);
                }
                pMenu->ImplPaint(rRenderContext, GetOutputSizePixel(), nScrollerHeight, nStartY, pData, true/*bHighlight*/);
                if (bRestoreLineColor)
                    rRenderContext.SetLineColor(oldLineColor);
            }
            return;
        }

        nY += pData->aSz.Height();
    }
}

tools::Rectangle MenuFloatingWindow::ImplGetItemRect( sal_uInt16 nPos ) const
{
    if( ! pMenu )
        return tools::Rectangle();

    tools::Rectangle aRect;
    Size    aSz = GetOutputSizePixel();
    tools::Long    nStartY = ImplGetStartY();
    tools::Long    nY = nScrollerHeight+nStartY;

    size_t nCount = pMenu->pItemList->size();
    for ( size_t n = 0; n < nCount; n++ )
    {
        MenuItemData* pData = pMenu->pItemList->GetDataFromPos( n );
        if ( n == nPos )
        {
            SAL_WARN_IF( !pMenu->ImplIsVisible( n ), "vcl", "ImplGetItemRect: Item not visible!" );
            if ( pData->eType != MenuItemType::SEPARATOR )
            {
                aRect = tools::Rectangle( Point( 0, nY ), Size( aSz.Width(), pData->aSz.Height() ) );
                if ( pData->nBits & MenuItemBits::POPUPSELECT )
                {
                    tools::Long nFontHeight = GetTextHeight();
                    aRect.AdjustRight( -(nFontHeight + nFontHeight/4) );
                }
            }
            break;
        }
        nY += pData->aSz.Height();
    }
    return aRect;
}

void MenuFloatingWindow::ImplCursorUpDown( bool bUp, bool bHomeEnd )
{
    if( ! pMenu )
        return;

    const StyleSettings& rSettings = GetSettings().GetStyleSettings();

    sal_uInt16 n = nHighlightedItem;
    if ( n == ITEMPOS_INVALID )
    {
        if ( bUp )
            n = 0;
        else
            n = pMenu->GetItemCount()-1;
    }

    sal_uInt16 nLoop = n;

    if( bHomeEnd )
    {
        // absolute positioning
        if( bUp )
        {
            n = pMenu->GetItemCount();
            nLoop = n-1;
        }
        else
        {
            n = ITEMPOS_INVALID;
            nLoop = 0;
        }
    }

    do
    {
        if ( bUp )
        {
            if ( n )
                n--;
            else
                if ( !IsScrollMenu() || ( nHighlightedItem == ITEMPOS_INVALID ) )
                    n = pMenu->GetItemCount()-1;
                else
                    break;
        }
        else
        {
            n = (n == ITEMPOS_INVALID) ? 0 : n + 1;
            if ( n >= pMenu->GetItemCount() )
            {
                if ( !IsScrollMenu() || ( nHighlightedItem == ITEMPOS_INVALID ) )
                    n = 0;
                else
                    break;
            }
        }

        MenuItemData* pData = pMenu->GetItemList()->GetDataFromPos( n );
        if ( ( pData->bEnabled || !rSettings.GetSkipDisabledInMenus() )
              && ( pData->eType != MenuItemType::SEPARATOR ) && pMenu->ImplIsVisible( n ) && pMenu->ImplIsSelectable( n ) )
        {
            // Is selection in visible area?
            if ( IsScrollMenu() )
            {
                ChangeHighlightItem( ITEMPOS_INVALID, false );

                while ( n < nFirstEntry )
                    ImplScroll( true );

                Size aOutSz = GetOutputSizePixel();
                sal_uInt16 nLastVisible;
                pMenu->ImplCalcVisEntries(aOutSz.Height(), nFirstEntry, &nLastVisible);
                while ( n > nLastVisible )
                {
                    ImplScroll( false );
                    pMenu->ImplCalcVisEntries(aOutSz.Height(), nFirstEntry, &nLastVisible);
                }
            }
            ChangeHighlightItem( n, false );
            break;
        }
    } while ( n != nLoop );
}

void MenuFloatingWindow::KeyInput( const KeyEvent& rKEvent )
{
    VclPtr<vcl::Window> xWindow = this;

    bool autoacc = ImplGetSVData()->maNWFData.mbAutoAccel;
    sal_uInt16 nCode = rKEvent.GetKeyCode().GetCode();
    bKeyInput = true;
    switch ( nCode )
    {
        case KEY_UP:
        case KEY_DOWN:
        {
            ImplCursorUpDown( nCode == KEY_UP );
        }
        break;
        case KEY_END:
        case KEY_HOME:
        {
            ImplCursorUpDown( nCode == KEY_END, true );
        }
        break;
        case KEY_F6:
        case KEY_ESCAPE:
        {
            // Ctrl-F6 acts like ESC here, the menu bar however will then put the focus in the document
            if( nCode == KEY_F6 && !rKEvent.GetKeyCode().IsMod1() )
                break;
            if( pMenu )
            {
                if ( !pMenu->pStartedFrom )
                {
                    StopExecute();
                    KillActivePopup();
                }
                else if (pMenu->pStartedFrom->IsMenuBar())
                {
                    pMenu->pStartedFrom->MenuBarKeyInput(rKEvent);
                }
                else
                {
                    StopExecute();
                    PopupMenu* pPopupMenu = static_cast<PopupMenu*>(pMenu->pStartedFrom.get());
                    MenuFloatingWindow* pFloat = pPopupMenu->ImplGetFloatingWindow();
                    pFloat->GrabFocus();
                    pFloat->KillActivePopup();
                    pPopupMenu->ImplCallHighlight(pFloat->nHighlightedItem);
                }
            }
        }
        break;
        case KEY_LEFT:
        {
            if ( pMenu && pMenu->pStartedFrom )
            {
                StopExecute();
                if (pMenu->pStartedFrom->IsMenuBar())
                {
                    pMenu->pStartedFrom->MenuBarKeyInput(rKEvent);
                }
                else
                {
                    MenuFloatingWindow* pFloat = static_cast<PopupMenu*>(pMenu->pStartedFrom.get())->ImplGetFloatingWindow();
                    pFloat->GrabFocus();
                    pFloat->KillActivePopup();
                    sal_uInt16 highlightItem = pFloat->GetHighlightedItem();
                    pFloat->ChangeHighlightItem(highlightItem, false);
                }
            }
        }
        break;
        case KEY_RIGHT:
        {
            if( pMenu )
            {
                bool bDone = false;
                if ( nHighlightedItem != ITEMPOS_INVALID )
                {
                    MenuItemData* pData = pMenu->GetItemList()->GetDataFromPos( nHighlightedItem );
                    if ( pData && pData->pSubMenu )
                    {
                        HighlightChanged( nullptr );
                        bDone = true;
                    }
                }
                if ( !bDone )
                {
                    Menu* pStart = pMenu->ImplGetStartMenu();
                    if (pStart && pStart->IsMenuBar())
                    {
                        // Forward...
                        pStart->GetWindow()->KeyInput(rKEvent);
                    }
                }
            }
        }
        break;
        case KEY_RETURN:
        {
            if( pMenu )
            {
                MenuItemData* pData = pMenu->GetItemList()->GetDataFromPos( nHighlightedItem );
                if ( pData && pData->bEnabled )
                {
                    if ( pData->pSubMenu )
                        HighlightChanged( nullptr );
                    else
                        EndExecute();
                }
                else
                    StopExecute();
            }
        }
        break;
        case KEY_MENU:
        {
            if( pMenu )
            {
                Menu* pStart = pMenu->ImplGetStartMenu();
                if (pStart && pStart->IsMenuBar())
                {
                    // Forward...
                    pStart->GetWindow()->KeyInput(rKEvent);
                }
            }
        }
        break;
        default:
        {
            sal_Unicode nCharCode = rKEvent.GetCharCode();
            size_t nPos = 0;
            size_t nDuplicates = 0;
            MenuItemData* pData = (nCharCode && pMenu) ?
                pMenu->GetItemList()->SearchItem(nCharCode, rKEvent.GetKeyCode(), nPos, nDuplicates, nHighlightedItem) : nullptr;
            if (pData)
            {
                if ( pData->pSubMenu || nDuplicates > 1 )
                {
                    ChangeHighlightItem( nPos, false );
                    HighlightChanged( nullptr );
                }
                else
                {
                    nHighlightedItem = nPos;
                    EndExecute();
                }
            }
            else
                FloatingWindow::KeyInput( rKEvent );
        }
    }

    if (pMenu && pMenu->pStartedFrom && pMenu->pStartedFrom->IsMenuBar())
    {
        MenuBar *pMenuBar = static_cast<MenuBar*>(pMenu->pStartedFrom.get());
        const bool bShowAccels = !autoacc || nCode != KEY_ESCAPE;
        if (pMenuBar->getMenuBarWindow()->GetMBWMenuKey() != bShowAccels)
        {
            pMenuBar->getMenuBarWindow()->SetMBWMenuKey(bShowAccels);
            pMenuBar->getMenuBarWindow()->SetMBWHideAccel(!bShowAccels);
            if (autoacc)
                Invalidate(InvalidateFlags::Update);
        }
    }

    // #105474# check if menu window was not destroyed
    if ( !xWindow->isDisposed() )
    {
        bKeyInput = false;
    }
}

void MenuFloatingWindow::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle &rPaintRect)
{
    if (!pMenu)
        return;

    // Set the clip before the buffering starts: rPaintRect may be larger than the current clip,
    // this way the buffer -> render context copy happens with this clip.
    auto popIt = rRenderContext.ScopedPush(vcl::PushFlags::CLIPREGION);
    rRenderContext.SetClipRegion(vcl::Region(rPaintRect));

    // Make sure that all actual rendering happens in one go to avoid flicker.
    vcl::BufferDevice pBuffer(this, rRenderContext);

    if (rRenderContext.IsNativeControlSupported(ControlType::MenuPopup, ControlPart::Entire))
    {
        pBuffer->SetClipRegion();
        tools::Long nX = 0;
        Size aPxSize(GetOutputSizePixel());
        aPxSize.AdjustWidth( -nX );
        ImplControlValue aVal(pMenu->nTextPos - GUTTERBORDER);
        pBuffer->DrawNativeControl(ControlType::MenuPopup, ControlPart::Entire,
                                   tools::Rectangle(Point(nX, 0), aPxSize), ControlState::ENABLED,
                                   aVal, OUString());
        InitMenuClipRegion(*pBuffer);
    }
    if (IsScrollMenu())
    {
        ImplDrawScroller(*pBuffer, true);
        ImplDrawScroller(*pBuffer, false);
    }
    pBuffer->SetFillColor(rRenderContext.GetSettings().GetStyleSettings().GetMenuColor());
    pMenu->ImplPaint(*pBuffer, GetOutputSizePixel(), nScrollerHeight, ImplGetStartY());
    if (nHighlightedItem != ITEMPOS_INVALID)
        RenderHighlightItem(*pBuffer, nHighlightedItem);

    pBuffer.Dispose();
}

void MenuFloatingWindow::ImplDrawScroller(vcl::RenderContext& rRenderContext, bool bUp)
{
    if (!pMenu)
        return;

    rRenderContext.SetClipRegion();

    Size aOutSz(GetOutputSizePixel());
    tools::Long nY = bUp ? 0 : (aOutSz.Height() - nScrollerHeight);
    tools::Long nX = 0;
    tools::Rectangle aRect(Point(nX, nY), Size(aOutSz.Width() - nX, nScrollerHeight));

    DecorationView aDecoView(&rRenderContext);
    SymbolType eSymbol = bUp ? SymbolType::SPIN_UP : SymbolType::SPIN_DOWN;

    DrawSymbolFlags nStyle = DrawSymbolFlags::NONE;
    if ((bUp && !bScrollUp) || (!bUp && !bScrollDown))
        nStyle |= DrawSymbolFlags::Disable;

    aDecoView.DrawSymbol(aRect, eSymbol, rRenderContext.GetSettings().GetStyleSettings().GetButtonTextColor(), nStyle);

    InitMenuClipRegion(rRenderContext);
}

void MenuFloatingWindow::RequestHelp( const HelpEvent& rHEvt )
{
    sal_uInt16 nId = nHighlightedItem;
    Menu* pM = pMenu;
    vcl::Window* pW = this;

    // #102618# Get item rect before destroying the window in EndExecute() call
    tools::Rectangle aHighlightRect( ImplGetItemRect( nHighlightedItem ) );

    if ( rHEvt.GetMode() & HelpEventMode::CONTEXT )
    {
        nHighlightedItem = ITEMPOS_INVALID;
        EndExecute();
        pW = nullptr;
    }

    if( !ImplHandleHelpEvent( pW, pM, nId, rHEvt, aHighlightRect ) )
        Window::RequestHelp( rHEvt );
}

void MenuFloatingWindow::StateChanged( StateChangedType nType )
{
    FloatingWindow::StateChanged( nType );

    if ( ( nType == StateChangedType::ControlForeground ) || ( nType == StateChangedType::ControlBackground ) )
    {
        ApplySettings(*GetOutDev());
        Invalidate();
    }
}

void MenuFloatingWindow::DataChanged( const DataChangedEvent& rDCEvt )
{
    FloatingWindow::DataChanged( rDCEvt );

    if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) ||
         (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) ||
         ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) &&
          (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) )
    {
        ApplySettings(*GetOutDev());
        Invalidate();
    }
}

void MenuFloatingWindow::Command( const CommandEvent& rCEvt )
{
    if ( rCEvt.GetCommand() == CommandEventId::Wheel )
    {
        const CommandWheelData* pData = rCEvt.GetWheelData();
        if( !pData->GetModifier() && ( pData->GetMode() == CommandWheelMode::SCROLL ) )
        {
            ImplScroll( pData->GetDelta() > 0 );
            MouseMove( MouseEvent( GetPointerPosPixel(), 0 ) );
        }
    }
}

rtl::Reference<comphelper::OAccessible> MenuFloatingWindow::CreateAccessible()
{
    if (pMenu && !pMenu->pStartedFrom)
        return pMenu->GetAccessible();

    return {};
}

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