/* -*- 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 <sal/config.h>

#include <vcl/image.hxx>
#include <vcl/taskpanelist.hxx>
#include <vcl/settings.hxx>
#include <vcl/syswin.hxx>
#include <osl/diagnose.h>

#include <olinewin.hxx>
#include <olinetab.hxx>
#include <document.hxx>
#include <dbfunc.hxx>
#include <bitmaps.hlst>

const tools::Long SC_OL_BITMAPSIZE                 = 12;
const tools::Long SC_OL_POSOFFSET                  = 2;

const size_t SC_OL_NOLEVEL                  = static_cast< size_t >( -1 );
const size_t SC_OL_HEADERENTRY              = static_cast< size_t >( -1 );

ScOutlineWindow::ScOutlineWindow( vcl::Window* pParent, ScOutlineMode eMode, ScViewData& rViewData, ScSplitPos eWhich ) :
    Window( pParent ),
    mrViewData( rViewData ),
    meWhich( eWhich ),
    mbHoriz( eMode == SC_OUTLINE_HOR ),
    mbMirrorEntries( false ),           // updated in SetHeaderSize
    mbMirrorLevels( false ),            // updated in SetHeaderSize
    maLineColor( COL_BLACK ),
    mnHeaderSize( 0 ),
    mnHeaderPos( 0 ),
    mnMainFirstPos( 0 ),
    mnMainLastPos( 0 ),
    mbMTActive( false ),
    mbMTPressed( false ),
    mnFocusLevel( 0 ),
    mnFocusEntry( SC_OL_HEADERENTRY ),
    mbDontDrawFocus( false )
{
    EnableRTL( false );                 // mirroring is done manually

    InitSettings();
    maFocusRect.SetEmpty();
    SetHeaderSize( 0 );

    // insert the window into task pane list for "F6 cycling"
    if( SystemWindow* pSysWin = GetSystemWindow() )
        if( TaskPaneList* pTaskPaneList = pSysWin->GetTaskPaneList() )
            pTaskPaneList->AddWindow( this );
}

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

void ScOutlineWindow::dispose()
{
    // remove the window from task pane list
    if( SystemWindow* pSysWin = GetSystemWindow() )
        if( TaskPaneList* pTaskPaneList = pSysWin->GetTaskPaneList() )
            pTaskPaneList->RemoveWindow( this );
    vcl::Window::dispose();
}

void ScOutlineWindow::SetHeaderSize( tools::Long nNewSize )
{
    bool bLayoutRTL = GetDoc().IsLayoutRTL( GetTab() );
    mbMirrorEntries = bLayoutRTL && mbHoriz;
    mbMirrorLevels = bLayoutRTL && !mbHoriz;

    bool bNew = (nNewSize != mnHeaderSize);
    mnHeaderSize = nNewSize;
    mnHeaderPos = mbMirrorEntries ? (GetOutputSizeEntry() - mnHeaderSize) : 0;
    mnMainFirstPos = mbMirrorEntries ? 0 : mnHeaderSize;
    mnMainLastPos = GetOutputSizeEntry() - (mbMirrorEntries ? mnHeaderSize : 0) - 1;
    if ( bNew )
        Invalidate();
}

tools::Long ScOutlineWindow::GetDepthSize() const
{
    tools::Long nSize = GetLevelCount() * SC_OL_BITMAPSIZE;
    if ( nSize > 0 )
        nSize += 2 * SC_OL_POSOFFSET + 1;
    return nSize;
}

void ScOutlineWindow::ScrollPixel( tools::Long nDiff )
{
    HideFocus();
    mbDontDrawFocus = true;

    tools::Long nStart = mnMainFirstPos;
    tools::Long nEnd = mnMainLastPos;

    tools::Long nInvStart, nInvEnd;
    if (nDiff < 0)
    {
        nStart -= nDiff;
        nInvStart = nEnd + nDiff;
        nInvEnd = nEnd;
    }
    else
    {
        nEnd -= nDiff;
        nInvStart = nStart;
        nInvEnd = nStart + nDiff;
    }

    ScrollRel( nDiff, nStart, nEnd );
    Invalidate( GetRectangle( 0, nInvStart, GetOutputSizeLevel() - 1, nInvEnd ) );

    // if focus becomes invisible, move it to next visible button
    ImplMoveFocusToVisible( nDiff < 0 );

    mbDontDrawFocus = false;
    ShowFocus();
}

void ScOutlineWindow::ScrollRel( tools::Long nEntryDiff, tools::Long nEntryStart, tools::Long nEntryEnd )
{
    tools::Rectangle aRect( GetRectangle( 0, nEntryStart, GetOutputSizeLevel() - 1, nEntryEnd ) );
    if ( mbHoriz )
        Scroll( nEntryDiff, 0, aRect );
    else
        Scroll( 0, nEntryDiff, aRect );
}

// internal -------------------------------------------------------------------

void ScOutlineWindow::InitSettings()
{
    const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings();
    SetBackground( rStyleSettings.GetFaceColor() );
    maLineColor = rStyleSettings.GetButtonTextColor();
    Invalidate();
}

const ScOutlineArray* ScOutlineWindow::GetOutlineArray() const
{
    const ScOutlineTable* pTable = GetDoc().GetOutlineTable( GetTab() );
    if ( !pTable ) return nullptr;
    return mbHoriz ? &pTable->GetColArray() : &pTable->GetRowArray();
}

const ScOutlineEntry* ScOutlineWindow::GetOutlineEntry( size_t nLevel, size_t nEntry ) const
{
    const ScOutlineArray* pArray = GetOutlineArray();
    return pArray ? pArray->GetEntry( sal::static_int_cast<sal_uInt16>(nLevel), sal::static_int_cast<sal_uInt16>(nEntry) ) : nullptr;
}

bool ScOutlineWindow::IsHidden( SCCOLROW nColRowIndex ) const
{
    return mbHoriz ?
        GetDoc().ColHidden(static_cast<SCCOL>(nColRowIndex), GetTab()) :
        GetDoc().RowHidden(static_cast<SCROW>(nColRowIndex), GetTab());
}

bool ScOutlineWindow::IsFiltered( SCCOLROW nColRowIndex ) const
{
    // columns cannot be filtered
    return !mbHoriz && GetDoc().RowFiltered( static_cast<SCROW>(nColRowIndex), GetTab() );
}

bool ScOutlineWindow::IsFirstVisible( SCCOLROW nColRowIndex ) const
{
    bool bAllHidden = true;
    for ( SCCOLROW nPos = 0; (nPos < nColRowIndex) && bAllHidden; ++nPos )
        bAllHidden = IsHidden( nPos );
    return bAllHidden;
}

void ScOutlineWindow::GetVisibleRange( SCCOLROW& rnColRowStart, SCCOLROW& rnColRowEnd ) const
{
    if ( mbHoriz )
    {
        rnColRowStart = mrViewData.GetPosX( WhichH( meWhich ) );
        rnColRowEnd = rnColRowStart + mrViewData.VisibleCellsX( WhichH( meWhich ) );
    }
    else
    {
        rnColRowStart = mrViewData.GetPosY( WhichV( meWhich ) );
        rnColRowEnd = rnColRowStart + mrViewData.VisibleCellsY( WhichV( meWhich ) );
    }

    // include collapsed columns/rows in front of visible range
    while ( (rnColRowStart > 0) && IsHidden( rnColRowStart - 1 ) )
        --rnColRowStart;
}

Point ScOutlineWindow::GetPoint( tools::Long nLevelPos, tools::Long nEntryPos ) const
{
    return mbHoriz ? Point( nEntryPos, nLevelPos ) : Point( nLevelPos, nEntryPos );
}

tools::Rectangle ScOutlineWindow::GetRectangle(
        tools::Long nLevelStart, tools::Long nEntryStart, tools::Long nLevelEnd, tools::Long nEntryEnd ) const
{
    return tools::Rectangle( GetPoint( nLevelStart, nEntryStart ), GetPoint( nLevelEnd, nEntryEnd ) );
}

tools::Long ScOutlineWindow::GetOutputSizeLevel() const
{
    Size aSize( GetOutputSizePixel() );
    return mbHoriz ? aSize.Height() : aSize.Width();
}

tools::Long ScOutlineWindow::GetOutputSizeEntry() const
{
    Size aSize( GetOutputSizePixel() );
    return mbHoriz ? aSize.Width() : aSize.Height();
}

size_t ScOutlineWindow::GetLevelCount() const
{
    const ScOutlineArray* pArray = GetOutlineArray();
    size_t nLevelCount = pArray ? pArray->GetDepth() : 0;
    return nLevelCount ? (nLevelCount + 1) : 0;
}

tools::Long ScOutlineWindow::GetLevelPos( size_t nLevel ) const
{
    // #i51970# must always return the *left* edge of the area used by a level
    tools::Long nPos = static_cast< tools::Long >( SC_OL_POSOFFSET + nLevel * SC_OL_BITMAPSIZE );
    return mbMirrorLevels ? (GetOutputSizeLevel() - nPos - SC_OL_BITMAPSIZE) : nPos;
}

size_t ScOutlineWindow::GetLevelFromPos( tools::Long nLevelPos ) const
{
    if( mbMirrorLevels ) nLevelPos = GetOutputSizeLevel() - nLevelPos - 1;
    tools::Long nStart = SC_OL_POSOFFSET;
    if ( nLevelPos < nStart ) return SC_OL_NOLEVEL;
    size_t nLevel = static_cast< size_t >( (nLevelPos - nStart) / SC_OL_BITMAPSIZE );
    return (nLevel < GetLevelCount()) ? nLevel : SC_OL_NOLEVEL;
}

tools::Long ScOutlineWindow::GetColRowPos( SCCOLROW nColRowIndex ) const
{
    tools::Long nDocPos = mbHoriz ?
        mrViewData.GetScrPos( static_cast<SCCOL>(nColRowIndex), 0, meWhich, true ).X() :
        mrViewData.GetScrPos( 0, static_cast<SCROW>(nColRowIndex), meWhich, true ).Y();
    return mnMainFirstPos + nDocPos;
}

tools::Long ScOutlineWindow::GetHeaderEntryPos() const
{
    return mnHeaderPos + (mnHeaderSize - SC_OL_BITMAPSIZE) / 2;
}

bool ScOutlineWindow::GetEntryPos(
        size_t nLevel, size_t nEntry,
        tools::Long& rnStartPos, tools::Long& rnEndPos, tools::Long& rnImagePos ) const
{
    const ScOutlineEntry* pEntry = GetOutlineEntry( nLevel, nEntry );
    if ( !pEntry || !pEntry->IsVisible() )
        return false;

    SCCOLROW nStart = pEntry->GetStart();
    SCCOLROW nEnd = pEntry->GetEnd();

    tools::Long nEntriesSign = mbMirrorEntries ? -1 : 1;

    // --- common calculation ---

    rnStartPos = GetColRowPos( nStart );
    rnEndPos = GetColRowPos( nEnd + 1 );

    bool bHidden = IsHidden( nStart );
    rnImagePos = bHidden ?
                (rnStartPos - ( SC_OL_BITMAPSIZE / 2 ) * nEntriesSign) :
                rnStartPos + nEntriesSign;
    tools::Long nCenter = (rnStartPos + rnEndPos - SC_OL_BITMAPSIZE * nEntriesSign +
                        ( mbMirrorEntries ? 1 : 0 )) / 2;
    rnImagePos = mbMirrorEntries ? std::max( rnImagePos, nCenter ) : std::min( rnImagePos, nCenter );

    // --- refinements ---

    // do not cut leftmost/topmost image
    if ( bHidden && IsFirstVisible( nStart ) )
        rnImagePos = rnStartPos;

    // do not cover previous collapsed image
    bool bDoNoCover = !bHidden && nEntry;
    const ScOutlineEntry* pPrevEntry = bDoNoCover ? GetOutlineEntry(nLevel, nEntry - 1) : nullptr;
    if (pPrevEntry)
    {
        SCCOLROW nPrevEnd = pPrevEntry->GetEnd();
        if ( (nPrevEnd + 1 == nStart) && IsHidden( nPrevEnd ) )
        {
            if ( IsFirstVisible( pPrevEntry->GetStart() ) )
                rnStartPos += SC_OL_BITMAPSIZE * nEntriesSign;
            else
                rnStartPos += ( SC_OL_BITMAPSIZE / 2 ) * nEntriesSign;
            rnImagePos = rnStartPos;
        }
    }

    // restrict rnStartPos...rnEndPos to valid area
    rnStartPos = std::max( rnStartPos, mnMainFirstPos );
    rnEndPos = std::max( rnEndPos, mnMainFirstPos );

    if ( mbMirrorEntries )
        rnImagePos -= SC_OL_BITMAPSIZE - 1;     // start pos aligns with right edge of bitmap

    // --- all rows filtered? ---

    bool bVisible = true;
    if ( !mbHoriz )
    {
        bVisible = false;
        for ( SCCOLROW nRow = nStart; (nRow <= nEnd) && !bVisible; ++nRow )
            bVisible = !IsFiltered( nRow );
    }
    return bVisible;
}

bool ScOutlineWindow::GetImagePos( size_t nLevel, size_t nEntry, Point& rPos ) const
{
    bool bRet = nLevel < GetLevelCount();
    if ( bRet )
    {
        tools::Long nLevelPos = GetLevelPos( nLevel );
        if ( nEntry == SC_OL_HEADERENTRY )
            rPos = GetPoint( nLevelPos, GetHeaderEntryPos() );
        else
        {
            tools::Long nStartPos, nEndPos, nImagePos;
            bRet = GetEntryPos( nLevel, nEntry, nStartPos, nEndPos, nImagePos );
            rPos = GetPoint( nLevelPos, nImagePos );
        }
    }
    return bRet;
}

bool ScOutlineWindow::IsButtonVisible( size_t nLevel, size_t nEntry ) const
{
    bool bRet = false;
    if ( nEntry == SC_OL_HEADERENTRY )
        bRet = (mnHeaderSize > 0) && (nLevel < GetLevelCount());
    else
    {
        const ScOutlineEntry* pEntry = GetOutlineEntry( nLevel, nEntry );
        if ( pEntry && pEntry->IsVisible() )
        {
            SCCOLROW nStart, nEnd;
            GetVisibleRange( nStart, nEnd );
            bRet = (nStart <= pEntry->GetStart()) && (pEntry->GetStart() <= nEnd);
        }
    }
    return bRet;
}

bool ScOutlineWindow::ItemHit( const Point& rPos, size_t& rnLevel, size_t& rnEntry, bool& rbButton ) const
{
    const ScOutlineArray* pArray = GetOutlineArray();
    if ( !pArray ) return false;

    SCCOLROW nStartIndex, nEndIndex;
    GetVisibleRange( nStartIndex, nEndIndex );

    size_t nLevel = GetLevelFromPos( mbHoriz ? rPos.Y() : rPos.X() );
    if ( nLevel == SC_OL_NOLEVEL )
        return false;

    tools::Long nEntryMousePos = mbHoriz ? rPos.X() : rPos.Y();

    // --- level buttons ---

    if ( mnHeaderSize > 0 )
    {
        tools::Long nImagePos = GetHeaderEntryPos();
        if ( (nImagePos <= nEntryMousePos) && (nEntryMousePos < nImagePos + SC_OL_BITMAPSIZE) )
        {
            rnLevel = nLevel;
            rnEntry = SC_OL_HEADERENTRY;
            rbButton = true;
            return true;
        }
    }

    // --- expand/collapse buttons and expanded lines ---

    // search outline entries backwards
    size_t nEntry = pArray->GetCount( sal::static_int_cast<sal_uInt16>(nLevel) );
    while ( nEntry )
    {
        --nEntry;

        const ScOutlineEntry* pEntry = pArray->GetEntry( sal::static_int_cast<sal_uInt16>(nLevel),
                                                         sal::static_int_cast<sal_uInt16>(nEntry) );
        SCCOLROW nStart = pEntry->GetStart();
        SCCOLROW nEnd = pEntry->GetEnd();

        if ( (nEnd >= nStartIndex) && (nStart <= nEndIndex) )
        {
            tools::Long nStartPos, nEndPos, nImagePos;
            if ( GetEntryPos( nLevel, nEntry, nStartPos, nEndPos, nImagePos ) )
            {
                rnLevel = nLevel;
                rnEntry = nEntry;

                // button?
                if ( (nStart >= nStartIndex) && (nImagePos <= nEntryMousePos) && (nEntryMousePos < nImagePos + SC_OL_BITMAPSIZE) )
                {
                    rbButton = true;
                    return true;
                }

                // line?
                if ( mbMirrorEntries )
                    ::std::swap( nStartPos, nEndPos );      // in RTL mode, nStartPos is the larger value
                if ( (nStartPos <= nEntryMousePos) && (nEntryMousePos <= nEndPos) )
                {
                    rbButton = false;
                    return true;
                }
            }
        }
    }

    return false;
}

bool ScOutlineWindow::ButtonHit( const Point& rPos, size_t& rnLevel, size_t& rnEntry ) const
{
    bool bButton;
    bool bRet = ItemHit( rPos, rnLevel, rnEntry, bButton );
    return bRet && bButton;
}

bool ScOutlineWindow::LineHit( const Point& rPos, size_t& rnLevel, size_t& rnEntry ) const
{
    bool bButton;
    bool bRet = ItemHit( rPos, rnLevel, rnEntry, bButton );
    return bRet && !bButton;
}

void ScOutlineWindow::DoFunction( size_t nLevel, size_t nEntry ) const
{
    ScDBFunc& rFunc = *mrViewData.GetView();
    if ( nEntry == SC_OL_HEADERENTRY )
        rFunc.SelectLevel( mbHoriz, sal::static_int_cast<sal_uInt16>(nLevel) );
    else
    {
        const ScOutlineEntry* pEntry = GetOutlineEntry( nLevel, nEntry );
        if ( pEntry )
        {
            if ( pEntry->IsHidden() )
                rFunc.ShowOutline( mbHoriz, sal::static_int_cast<sal_uInt16>(nLevel), sal::static_int_cast<sal_uInt16>(nEntry) );
            else
                rFunc.HideOutline( mbHoriz, sal::static_int_cast<sal_uInt16>(nLevel), sal::static_int_cast<sal_uInt16>(nEntry) );
        }
    }
}

void ScOutlineWindow::DoExpand( size_t nLevel, size_t nEntry ) const
{
    const ScOutlineEntry* pEntry = GetOutlineEntry( nLevel, nEntry );
    if ( pEntry && pEntry->IsHidden() )
        DoFunction( nLevel, nEntry );
}

void ScOutlineWindow::DoCollapse( size_t nLevel, size_t nEntry ) const
{
    const ScOutlineEntry* pEntry = GetOutlineEntry( nLevel, nEntry );
    if ( pEntry && !pEntry->IsHidden() )
        DoFunction( nLevel, nEntry );
}

void ScOutlineWindow::Resize()
{
    Window::Resize();
    SetHeaderSize( mnHeaderSize );  // recalculates header/group positions
    if ( !IsFocusButtonVisible() )
    {
        HideFocus();
        ShowFocus();    // calculates valid position
    }
}

void ScOutlineWindow::DataChanged( const DataChangedEvent& rDCEvt )
{
    if ( (rDCEvt.GetType() == DataChangedEventType::SETTINGS) &&
         (rDCEvt.GetFlags() & AllSettingsFlags::STYLE) )
    {
        InitSettings();
        Invalidate();
    }
    Window::DataChanged( rDCEvt );
}

// drawing --------------------------------------------------------------------

void ScOutlineWindow::SetEntryAreaClipRegion()
{
    GetOutDev()->SetClipRegion( vcl::Region(tools::Rectangle(
        GetPoint( 0, mnMainFirstPos ),
        GetPoint( GetOutputSizeLevel() - 1, mnMainLastPos ))));
}

void ScOutlineWindow::DrawLineRel(
        tools::Long nLevelStart, tools::Long nEntryStart, tools::Long nLevelEnd, tools::Long nEntryEnd )
{
    GetOutDev()->DrawLine( GetPoint( nLevelStart, nEntryStart ), GetPoint( nLevelEnd, nEntryEnd ) );
}

void ScOutlineWindow::DrawRectRel(
        tools::Long nLevelStart, tools::Long nEntryStart, tools::Long nLevelEnd, tools::Long nEntryEnd )
{
    GetOutDev()->DrawRect( GetRectangle( nLevelStart, nEntryStart, nLevelEnd, nEntryEnd ) );
}

namespace
{
    Image GetImage(const OUString& rId)
    {
        return Image(StockImage::Yes, rId);
    }
}

void ScOutlineWindow::DrawImageRel(tools::Long nLevelPos, tools::Long nEntryPos, const OUString& rId)
{
    const Image aImage = GetImage(rId);
    GetOutDev()->SetLineColor();
    GetOutDev()->SetFillColor( GetBackground().GetColor() );
    Point aPos( GetPoint( nLevelPos, nEntryPos ) );
    GetOutDev()->DrawRect( tools::Rectangle( aPos, aImage.GetSizePixel() ) );
    GetOutDev()->DrawImage( aPos, aImage );
}

void ScOutlineWindow::DrawBorderRel( size_t nLevel, size_t nEntry, bool bPressed )
{
    Point aPos;
    if ( GetImagePos( nLevel, nEntry, aPos ) )
    {
        OUString sId = bPressed ? RID_BMP_PRESSED : RID_BMP_NOTPRESSED;
        bool bClip = (nEntry != SC_OL_HEADERENTRY);
        if ( bClip )
            SetEntryAreaClipRegion();
        GetOutDev()->DrawImage(aPos, GetImage(sId));
        if ( bClip )
            GetOutDev()->SetClipRegion();
    }
    mbMTPressed = bPressed;
}

void ScOutlineWindow::ShowFocus()
{
    if ( !HasFocus() )
        return;

    // first move to a visible position
    ImplMoveFocusToVisible( true );

    if ( !IsFocusButtonVisible() )
        return;

    Point aPos;
    if ( GetImagePos( mnFocusLevel, mnFocusEntry, aPos ) )
    {
        aPos += Point( 1, 1 );
        maFocusRect = tools::Rectangle( aPos, Size( SC_OL_BITMAPSIZE - 2, SC_OL_BITMAPSIZE - 2 ) );
        bool bClip = (mnFocusEntry != SC_OL_HEADERENTRY);
        if ( bClip )
            SetEntryAreaClipRegion();
        InvertTracking( maFocusRect, ShowTrackFlags::Small | ShowTrackFlags::TrackWindow );
        if ( bClip )
            GetOutDev()->SetClipRegion();
    }
}

void ScOutlineWindow::HideFocus()
{
    if ( !maFocusRect.IsEmpty() )
    {
        bool bClip = (mnFocusEntry != SC_OL_HEADERENTRY);
        if ( bClip )
            SetEntryAreaClipRegion();
        InvertTracking( maFocusRect, ShowTrackFlags::Small | ShowTrackFlags::TrackWindow );
        if ( bClip )
            GetOutDev()->SetClipRegion();
        maFocusRect.SetEmpty();
    }
}

constexpr OUString aLevelBmps[]=
{
    RID_BMP_LEVEL1,
    RID_BMP_LEVEL2,
    RID_BMP_LEVEL3,
    RID_BMP_LEVEL4,
    RID_BMP_LEVEL5,
    RID_BMP_LEVEL6,
    RID_BMP_LEVEL7,
    RID_BMP_LEVEL8
};

void ScOutlineWindow::Paint( vcl::RenderContext& /*rRenderContext*/, const tools::Rectangle& /* rRect */ )
{
    tools::Long nEntriesSign = mbMirrorEntries ? -1 : 1;
    tools::Long nLevelsSign  = mbMirrorLevels  ? -1 : 1;

    Size aSize = GetOutputSizePixel();
    tools::Long nLevelEnd = (mbHoriz ? aSize.Height() : aSize.Width()) - 1;
    tools::Long nEntryEnd = (mbHoriz ? aSize.Width() : aSize.Height()) - 1;

    GetOutDev()->SetLineColor( maLineColor );
    tools::Long nBorderPos = mbMirrorLevels ? 0 : nLevelEnd;
    DrawLineRel( nBorderPos, 0, nBorderPos, nEntryEnd );

    const ScOutlineArray* pArray = GetOutlineArray();
    if ( !pArray ) return;

    size_t nLevelCount = GetLevelCount();

    // --- draw header images ---

    if ( mnHeaderSize > 0 )
    {
        tools::Long nEntryPos = GetHeaderEntryPos();
        for ( size_t nLevel = 0; nLevel < nLevelCount; ++nLevel )
            DrawImageRel(GetLevelPos(nLevel), nEntryPos, aLevelBmps[nLevel]);

        GetOutDev()->SetLineColor( maLineColor );
        tools::Long nLinePos = mnHeaderPos + (mbMirrorEntries ? 0 : (mnHeaderSize - 1));
        DrawLineRel( 0, nLinePos, nLevelEnd, nLinePos );
    }

    // --- draw lines & collapse/expand images ---

    SetEntryAreaClipRegion();

    SCCOLROW nStartIndex, nEndIndex;
    GetVisibleRange( nStartIndex, nEndIndex );

    for ( size_t nLevel = 0; nLevel + 1 < nLevelCount; ++nLevel )
    {
        tools::Long nLevelPos = GetLevelPos( nLevel );
        tools::Long nEntryPos1 = 0, nEntryPos2 = 0, nImagePos = 0;

        size_t nEntryCount = pArray->GetCount( sal::static_int_cast<sal_uInt16>(nLevel) );
        size_t nEntry;

        // first draw all lines in the current level
        GetOutDev()->SetLineColor();
        GetOutDev()->SetFillColor( maLineColor );
        for ( nEntry = 0; nEntry < nEntryCount; ++nEntry )
        {
            const ScOutlineEntry* pEntry = pArray->GetEntry( sal::static_int_cast<sal_uInt16>(nLevel),
                                                             sal::static_int_cast<sal_uInt16>(nEntry) );
            SCCOLROW nStart = pEntry->GetStart();
            SCCOLROW nEnd = pEntry->GetEnd();

            // visible range?
            bool bDraw = (nEnd >= nStartIndex) && (nStart <= nEndIndex);
            // find output coordinates
            if ( bDraw )
                bDraw = GetEntryPos( nLevel, nEntry, nEntryPos1, nEntryPos2, nImagePos );
            // draw, if not collapsed
            if ( bDraw && !pEntry->IsHidden() )
            {
                if ( nStart >= nStartIndex )
                    nEntryPos1 += nEntriesSign;
                nEntryPos2 -= 2 * nEntriesSign;
                tools::Long nLinePos = nLevelPos;
                if ( mbMirrorLevels )
                    nLinePos += SC_OL_BITMAPSIZE - 1;   // align with right edge of bitmap
                DrawRectRel( nLinePos, nEntryPos1, nLinePos + nLevelsSign, nEntryPos2 );

                if ( nEnd <= nEndIndex )
                    DrawRectRel( nLinePos, nEntryPos2 - nEntriesSign,
                                 nLinePos + ( SC_OL_BITMAPSIZE / 3 ) * nLevelsSign, nEntryPos2 );
            }
        }

        // draw all images in the level from last to first
        nEntry = nEntryCount;
        while ( nEntry )
        {
            --nEntry;

            const ScOutlineEntry* pEntry = pArray->GetEntry( sal::static_int_cast<sal_uInt16>(nLevel),
                                                             sal::static_int_cast<sal_uInt16>(nEntry) );
            SCCOLROW nStart = pEntry->GetStart();

            // visible range?
            bool bDraw = (nStartIndex <= nStart) && (nStart <= nEndIndex + 1);
            // find output coordinates
            if ( bDraw )
                bDraw = GetEntryPos( nLevel, nEntry, nEntryPos1, nEntryPos2, nImagePos );
            // draw, if not hidden by higher levels
            if ( bDraw )
            {
                OUString sImageId = pEntry->IsHidden() ? RID_BMP_PLUS : RID_BMP_MINUS;
                DrawImageRel(nLevelPos, nImagePos, sImageId);
            }
        }
    }

    GetOutDev()->SetClipRegion();

    if ( !mbDontDrawFocus )
        ShowFocus();
}

// focus ----------------------------------------------------------------------

/** Increments or decrements a value and wraps at the specified limits.
    @return  true = value wrapped. */
static bool lcl_RotateValue( size_t& rnValue, size_t nMin, size_t nMax, bool bForward )
{
    OSL_ENSURE( nMin <= nMax, "lcl_RotateValue - invalid range" );
    OSL_ENSURE( nMax < static_cast< size_t >( -1 ), "lcl_RotateValue - range overflow" );
    bool bWrap = false;
    if ( bForward )
    {
        if ( rnValue < nMax )
            ++rnValue;
        else
        {
            rnValue = nMin;
            bWrap = true;
        }
    }
    else
    {
        if ( rnValue > nMin )
            --rnValue;
        else
        {
            rnValue = nMax;
            bWrap = true;
        }
    }
    return bWrap;
}

bool ScOutlineWindow::IsFocusButtonVisible() const
{
    return IsButtonVisible( mnFocusLevel, mnFocusEntry );
}

bool ScOutlineWindow::ImplMoveFocusByEntry( bool bForward, bool bFindVisible )
{
    const ScOutlineArray* pArray = GetOutlineArray();
    if ( !pArray )
        return false;

    bool bWrapped = false;
    size_t nEntryCount = pArray->GetCount( sal::static_int_cast<sal_uInt16>(mnFocusLevel) );
    // #i29530# entry count may be decreased after changing active sheet
    if( mnFocusEntry >= nEntryCount )
        mnFocusEntry = SC_OL_HEADERENTRY;
    size_t nOldEntry = mnFocusEntry;

    do
    {
        if ( mnFocusEntry == SC_OL_HEADERENTRY )
        {
            // move from header to first or last entry
            if ( nEntryCount > 0 )
                mnFocusEntry = bForward ? 0 : (nEntryCount - 1);
            /*  wrapped, if forward from right header to first entry,
                or if backward from left header to last entry */
            // Header and entries are now always in consistent order,
            // so there's no need to check for mirroring here.
            if ( !nEntryCount || !bForward )
                bWrapped = true;
        }
        else if ( lcl_RotateValue( mnFocusEntry, 0, nEntryCount - 1, bForward ) )
        {
            // lcl_RotateValue returns true -> wrapped the entry range -> move to header
            mnFocusEntry = SC_OL_HEADERENTRY;
            /*  wrapped, if forward from last entry to left header,
                or if backward from first entry to right header */
            if ( bForward )
                bWrapped = true;
        }
    }
    while ( bFindVisible && !IsFocusButtonVisible() && (nOldEntry != mnFocusEntry) );

    return bWrapped;
}

bool ScOutlineWindow::ImplMoveFocusByLevel( bool bForward )
{
    const ScOutlineArray* pArray = GetOutlineArray();
    if ( !pArray )
        return false;

    bool bWrapped = false;
    size_t nLevelCount = GetLevelCount();

    if ( mnFocusEntry == SC_OL_HEADERENTRY )
    {
        if ( nLevelCount > 0 )
            bWrapped = lcl_RotateValue( mnFocusLevel, 0, nLevelCount - 1, bForward );
    }
    else
    {
        const ScOutlineEntry* pEntry = pArray->GetEntry(
            mnFocusLevel, mnFocusEntry);

        if ( pEntry )
        {
            SCCOLROW nStart = pEntry->GetStart();
            SCCOLROW nEnd = pEntry->GetEnd();
            size_t nNewLevel = mnFocusLevel;
            size_t nNewEntry = 0;

            bool bFound = false;
            if ( bForward && (mnFocusLevel + 2 < nLevelCount) )
            {
                // next level -> find first child entry
                nNewLevel = mnFocusLevel + 1;
                bFound = pArray->GetEntryIndexInRange(nNewLevel, nStart, nEnd, nNewEntry);
            }
            else if ( !bForward && (mnFocusLevel > 0) )
            {
                // previous level -> find parent entry
                nNewLevel = mnFocusLevel - 1;
                bFound = pArray->GetEntryIndex(nNewLevel, nStart, nNewEntry);
            }

            if ( bFound && IsButtonVisible( nNewLevel, nNewEntry ) )
            {
                mnFocusLevel = nNewLevel;
                mnFocusEntry = nNewEntry;
            }
        }
    }

    return bWrapped;
}

bool ScOutlineWindow::ImplMoveFocusByTabOrder( bool bForward )
{
    bool bRet = false;
    size_t nOldLevel = mnFocusLevel;
    size_t nOldEntry = mnFocusEntry;

    do
    {
        /*  one level up, if backward from left header,
            or one level down, if forward from right header */
        if ( (!bForward) && (mnFocusEntry == SC_OL_HEADERENTRY) )
            bRet |= ImplMoveFocusByLevel( bForward );
        // move to next/previous entry
        bool bWrapInLevel = ImplMoveFocusByEntry( bForward, false );
        bRet |= bWrapInLevel;
        /*  one level up, if wrapped backward to right header,
            or one level down, if wrapped forward to right header */
        if ( bForward && bWrapInLevel )
            bRet |= ImplMoveFocusByLevel( bForward );
    }
    while ( !IsFocusButtonVisible() && ((nOldLevel != mnFocusLevel) || (nOldEntry != mnFocusEntry)) );

    return bRet;
}

void ScOutlineWindow::ImplMoveFocusToVisible( bool bForward )
{
    // first try to find an entry in the same level
    if ( !IsFocusButtonVisible() )
        ImplMoveFocusByEntry( bForward, true );
    // then try to find any other entry
    if ( !IsFocusButtonVisible() )
        ImplMoveFocusByTabOrder( bForward );
}

void ScOutlineWindow::MoveFocusByEntry( bool bForward )
{
    HideFocus();
    ImplMoveFocusByEntry( bForward, true );
    ShowFocus();
}

void ScOutlineWindow::MoveFocusByLevel( bool bForward )
{
    HideFocus();
    ImplMoveFocusByLevel( bForward );
    ShowFocus();
}

void ScOutlineWindow::MoveFocusByTabOrder( bool bForward )
{
    HideFocus();
    ImplMoveFocusByTabOrder( bForward );
    ShowFocus();
}

void ScOutlineWindow::GetFocus()
{
    Window::GetFocus();
    ShowFocus();
}

void ScOutlineWindow::LoseFocus()
{
    HideFocus();
    Window::LoseFocus();
}

// mouse ----------------------------------------------------------------------

void ScOutlineWindow::StartMouseTracking( size_t nLevel, size_t nEntry )
{
    mbMTActive = true;
    mnMTLevel = nLevel;
    mnMTEntry = nEntry;
    DrawBorderRel( nLevel, nEntry, true );
}

void ScOutlineWindow::EndMouseTracking()
{
    if ( mbMTPressed )
        DrawBorderRel( mnMTLevel, mnMTEntry, false );
    mbMTActive = false;
}

void ScOutlineWindow::MouseMove( const MouseEvent& rMEvt )
{
    if ( IsMouseTracking() )
    {
        size_t nLevel, nEntry;
        bool bHit = false;

        if ( ButtonHit( rMEvt.GetPosPixel(), nLevel, nEntry ) )
            bHit = (nLevel == mnMTLevel) && (nEntry == mnMTEntry);

        if ( bHit != mbMTPressed )
            DrawBorderRel( mnMTLevel, mnMTEntry, bHit );
    }
}

void ScOutlineWindow::MouseButtonUp( const MouseEvent& rMEvt )
{
    if ( IsMouseTracking() )
    {
        EndMouseTracking();

        size_t nLevel, nEntry;
        if ( ButtonHit( rMEvt.GetPosPixel(), nLevel, nEntry ) )
            if ( (nLevel == mnMTLevel) && (nEntry == mnMTEntry) )
                DoFunction( nLevel, nEntry );
    }
}

void ScOutlineWindow::MouseButtonDown( const MouseEvent& rMEvt )
{
    size_t nLevel, nEntry;
    bool bHit = ButtonHit( rMEvt.GetPosPixel(), nLevel, nEntry );
    if ( bHit )
        StartMouseTracking( nLevel, nEntry );
    else if ( rMEvt.GetClicks() == 2 )
    {
        bHit = LineHit( rMEvt.GetPosPixel(), nLevel, nEntry );
        if ( bHit )
            DoFunction( nLevel, nEntry );
    }

    // if an item has been hit and window is focused, move focus to this item
    if ( bHit && HasFocus() )
    {
        HideFocus();
        mnFocusLevel = nLevel;
        mnFocusEntry = nEntry;
        ShowFocus();
    }
}

// keyboard -------------------------------------------------------------------

void ScOutlineWindow::KeyInput( const KeyEvent& rKEvt )
{
    const vcl::KeyCode& rKCode = rKEvt.GetKeyCode();
    bool bNoMod = !rKCode.GetModifier();
    bool bShift = (rKCode.GetModifier() == KEY_SHIFT);
    bool bCtrl = (rKCode.GetModifier() == KEY_MOD1);

    sal_uInt16 nCode = rKCode.GetCode();
    bool bUpDownKey = (nCode == KEY_UP) || (nCode == KEY_DOWN);
    bool bLeftRightKey = (nCode == KEY_LEFT) || (nCode == KEY_RIGHT);

    // TAB key
    if ( (nCode == KEY_TAB) && (bNoMod || bShift) )
        // move forward without SHIFT key
        MoveFocusByTabOrder( bNoMod );      // TAB uses logical order, regardless of mirroring

    // LEFT/RIGHT/UP/DOWN keys
    else if ( bNoMod && (bUpDownKey || bLeftRightKey) )
    {
        bool bForward = (nCode == KEY_DOWN) || (nCode == KEY_RIGHT);
        if ( mbHoriz == bLeftRightKey )
            // move inside level with LEFT/RIGHT in horizontal and with UP/DOWN in vertical
            MoveFocusByEntry( bForward != mbMirrorEntries );
        else
            // move to next/prev level with LEFT/RIGHT in vertical and with UP/DOWN in horizontal
            MoveFocusByLevel( bForward != mbMirrorLevels );
    }

    // CTRL + number
    else if ( bCtrl && (nCode >= KEY_1) && (nCode <= KEY_9) )
    {
        size_t nLevel = static_cast< size_t >( nCode - KEY_1 );
        if ( nLevel < GetLevelCount() )
            DoFunction( nLevel, SC_OL_HEADERENTRY );
    }

    // other key codes
    else switch ( rKCode.GetFullCode() )
    {
        case KEY_ADD:       DoExpand( mnFocusLevel, mnFocusEntry );     break;
        case KEY_SUBTRACT:  DoCollapse( mnFocusLevel, mnFocusEntry );   break;
        case KEY_SPACE:
        case KEY_RETURN:    DoFunction( mnFocusLevel, mnFocusEntry );   break;
        default:            Window::KeyInput( rKEvt );
    }
}

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