/* This file is part of the KDE libraries
  Copyright (C) 2003 Stephan Binner <binner@kde.org>
  Copyright (C) 2003 Zack Rusin <zack@kde.org>
  Copyright (C) 2009 Urs Wolfer <uwolfer @ kde.org>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Library General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Library General Public License for more details.

  You should have received a copy of the GNU Library General Public License
  along with this library; see the file COPYING.LIB.  If not, write to
  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  Boston, MA 02110-1301, USA.
*/

#include "ktabwidget.h"

#include <QApplication>
#include <QDragMoveEvent>
#include <QDropEvent>
#include <QMouseEvent>
#include <QStyle>
#include <QStyleOption>
#include <QTextDocument>
#include <QWheelEvent>
#include <QtCore/QList>

#include <ksharedconfig.h>
#include <kiconloader.h>
#include <kstringhandler.h>
#include <kdebug.h>

#include <ktabbar.h>

#include <kconfiggroup.h>

class KTabWidget::Private
{
public:
    enum {
        ResizeEnabled = 0,
        ResizeDisabled,
        ResizeLater
    } m_resizeSuspend;

    Private(KTabWidget *parent)
        : m_resizeSuspend(ResizeEnabled),
          m_parent(parent),
          m_automaticResizeTabs(false),
          m_tabBarHidden(false)
    {

        KConfigGroup cg(KSharedConfig::openConfig(), "General");
        m_maxLength = cg.readEntry("MaximumTabLength", 30);
        m_minLength = cg.readEntry("MinimumTabLength", 3);
        Q_ASSERT(m_maxLength >= m_minLength);
        m_currentTabLength = m_minLength;
    }

    KTabWidget *m_parent;
    bool m_automaticResizeTabs;
    bool m_tabBarHidden;
    int m_maxLength;
    int m_minLength;
    int m_currentTabLength;

    //holds the full names of the tab, otherwise all we
    //know about is the shortened name
    QStringList m_tabNames;

    bool isEmptyTabbarSpace(const QPoint &)  const;
    void resizeTabs(int changedTabIndex = -1);
    void updateTab(int index);
    void removeTab(int index);

    void slotTabMoved(int from, int to);
};

bool KTabWidget::Private::isEmptyTabbarSpace(const QPoint &point) const
{
    if (m_parent->count() == 0) {
        return true;
    }
    if (m_parent->tabBar()->isHidden()) {
        return false;
    }
    QSize size(m_parent->tabBar()->sizeHint());
    if ((m_parent->tabPosition() == QTabWidget::North && point.y() < size.height()) ||
            (m_parent->tabPosition() == QTabWidget::South && point.y() > (m_parent->height() - size.height()))) {

        QWidget *rightcorner = m_parent->cornerWidget(Qt::TopRightCorner);
        if (rightcorner && rightcorner->isVisible()) {
            if (point.x() >= m_parent->width() - rightcorner->width()) {
                return false;
            }
        }

        QWidget *leftcorner = m_parent->cornerWidget(Qt::TopLeftCorner);
        if (leftcorner && leftcorner->isVisible()) {
            if (point.x() <= leftcorner->width()) {
                return false;
            }
        }

        for (int i = 0; i < m_parent->count(); ++i)
            if (m_parent->tabBar()->tabRect(i).contains(m_parent->tabBar()->mapFromParent(point))) {
                return false;
            }

        return true;
    }

    return false;
}

void KTabWidget::Private::removeTab(int index)
{
    // prevent cascading resize slowness, not to mention crashes due to tab count()
    // and m_tabNames.count() being out of sync!
    m_resizeSuspend = ResizeDisabled;

    // Need to do this here, rather than in tabRemoved().  Calling
    // QTabWidget::removeTab() below may cause a relayout of the tab bar, which
    // will call resizeTabs() immediately.  If m_automaticResizeTabs is true,
    // that will use the m_tabNames[] list before it has been updated to reflect
    // the new tab arrangement.  See bug 190528.
    m_tabNames.removeAt(index);

    m_parent->QTabWidget::removeTab(index);

    const bool doResize = (m_resizeSuspend == ResizeLater) || m_automaticResizeTabs;
    m_resizeSuspend = ResizeEnabled;
    if (doResize) {
        resizeTabs();
    }

}

void KTabWidget::Private::resizeTabs(int changeTabIndex)
{
    if (m_resizeSuspend != ResizeEnabled) {
        m_resizeSuspend = ResizeLater;
        return;
    }

    int newTabLength = m_maxLength;

    if (m_automaticResizeTabs) {
        // Calculate new max length
        int lcw = 0, rcw = 0;

        const int tabBarHeight = m_parent->tabBar()->sizeHint().height();
        if (m_parent->cornerWidget(Qt::TopLeftCorner) &&
                m_parent->cornerWidget(Qt::TopLeftCorner)->isVisible()) {
            lcw = qMax(m_parent->cornerWidget(Qt::TopLeftCorner)->width(), tabBarHeight);
        }
        if (m_parent->cornerWidget(Qt::TopRightCorner) &&
                m_parent->cornerWidget(Qt::TopRightCorner)->isVisible()) {
            rcw = qMax(m_parent->cornerWidget(Qt::TopRightCorner)->width(), tabBarHeight);
        }

        const int maxTabBarWidth = m_parent->width() - lcw - rcw;

        // binary search for the best fitting tab title length; some wiggling was
        // required to make this behave in the face of rounding.
        int newTabLengthHi = m_maxLength + 1;
        int newTabLengthLo = m_minLength;
        int prevTabLengthMid = -1;
        while (true) {
            int newTabLengthMid = (newTabLengthHi + newTabLengthLo) / 2;
            if (prevTabLengthMid == newTabLengthMid) {
                // no change, we're stuck due to rounding.
                break;
            }
            prevTabLengthMid = newTabLengthMid;

            if (m_parent->tabBarWidthForMaxChars(newTabLengthMid) > maxTabBarWidth) {
                newTabLengthHi = newTabLengthMid;
            } else {
                newTabLengthLo = newTabLengthMid;
            }
        }
        newTabLength = qMin(newTabLengthLo, m_maxLength);
    }

    // Update hinted or all tabs
    if (m_currentTabLength != newTabLength) {
        m_currentTabLength = newTabLength;
        for (int i = 0; i < m_parent->count(); i++) {
            updateTab(i);
        }
    } else if (changeTabIndex != -1) {
        updateTab(changeTabIndex);
    }
}

void KTabWidget::Private::updateTab(int index)
{
    QString title = m_automaticResizeTabs ? m_tabNames[ index ] : m_parent->QTabWidget::tabText(index);
    m_parent->setTabToolTip(index, QString());

    if (title.length() > m_currentTabLength) {
        QString toolTipText = title;
        // Remove '&'s, which are indicators for keyboard shortcuts in tab titles. "&&" is replaced by '&'.
        for (int i = toolTipText.indexOf('&'); i >= 0 && i < toolTipText.length(); i = toolTipText.indexOf('&', i + 1)) {
            toolTipText.remove(i, 1);
        }

        if (Qt::mightBeRichText(toolTipText)) {
            m_parent->setTabToolTip(index, toolTipText.toHtmlEscaped());
        } else {
            m_parent->setTabToolTip(index, toolTipText);
        }
    }

    title = KStringHandler::rsqueeze(title, m_currentTabLength).leftJustified(m_minLength, ' ');

    if (m_parent->QTabWidget::tabText(index) != title) {
        m_parent->QTabWidget::setTabText(index, title);
    }
}

void KTabWidget::Private::slotTabMoved(int from, int to)
{
    /* called from Qt slot when Qt has moved the tab, so we only
       need to adjust the m_tabNames list */
    if (m_automaticResizeTabs) {
        QString movedName = m_tabNames.takeAt(from);
        m_tabNames.insert(to, movedName);
    }
}

KTabWidget::KTabWidget(QWidget *parent, Qt::WindowFlags flags)
    : QTabWidget(parent),
      d(new Private(this))
{
    setWindowFlags(flags);
    setTabBar(new KTabBar(this));
    setObjectName("tabbar");
    setAcceptDrops(true);

    connect(tabBar(), SIGNAL(contextMenu(int,QPoint)), SLOT(contextMenu(int,QPoint)));
    connect(tabBar(), SIGNAL(tabDoubleClicked(int)), SLOT(mouseDoubleClick(int)));
    connect(tabBar(), SIGNAL(newTabRequest()), this, SIGNAL(mouseDoubleClick())); // #185487
    connect(tabBar(), SIGNAL(mouseMiddleClick(int)), SLOT(mouseMiddleClick(int)));
    connect(tabBar(), SIGNAL(initiateDrag(int)), SLOT(initiateDrag(int)));
    connect(tabBar(), SIGNAL(testCanDecode(const QDragMoveEvent*,bool&)), SIGNAL(testCanDecode(const QDragMoveEvent*,bool&)));
    connect(tabBar(), SIGNAL(receivedDropEvent(int,QDropEvent*)), SLOT(receivedDropEvent(int,QDropEvent*)));
    connect(tabBar(), SIGNAL(moveTab(int,int)), SLOT(moveTab(int,int)));
    connect(tabBar(), SIGNAL(tabMoved(int,int)), SLOT(slotTabMoved(int,int)));
    connect(tabBar(), SIGNAL(tabCloseRequested(int)), SLOT(closeRequest(int)));
}

KTabWidget::~KTabWidget()
{
    delete d;
}

/*void KTabWidget::insertTab( QWidget *child, const QString &label, int index )
{
  QTabWidget::insertTab( child, label, index );
}

void KTabWidget::insertTab( QWidget *child, const QIcon& iconset, const QString &label, int index )
{
  QTabWidget::insertTab( child, iconset, label, index );
}

void KTabWidget::insertTab( QWidget *child, QTab *tab, int index )
{
  QTabWidget::insertTab( child, tab, index);
  if ( d->m_automaticResizeTabs ) {
    if ( index < 0 || index >= count() ) {
      d->m_tabNames.append( tab->text() );
      d->resizeTabs( d->m_tabNames.count()-1 );
    }
    else {
      d->m_tabNames.insert( d->m_tabNames.at( index ), tab->text() );
      d->resizeTabs( index );
    }
  }
}*/

void KTabWidget::setTabBarHidden(bool hide)
{
    if (hide == isTabBarHidden()) {
        return;
    }

    QWidget *rightcorner = cornerWidget(Qt::TopRightCorner);
    QWidget *leftcorner = cornerWidget(Qt::TopLeftCorner);

    d->m_tabBarHidden = hide;
    if (hide) {
        if (leftcorner) {
            leftcorner->hide();
        }
        if (rightcorner) {
            rightcorner->hide();
        }
        tabBar()->hide();
    } else {
        tabBar()->show();
        if (leftcorner) {
            leftcorner->show();
        }
        if (rightcorner) {
            rightcorner->show();
        }
    }
}

bool KTabWidget::isTabBarHidden() const
{
    return d->m_tabBarHidden;
}

void KTabWidget::setTabTextColor(int index, const QColor &color)
{
    tabBar()->setTabTextColor(index, color);
}

QColor KTabWidget::tabTextColor(int index) const
{
    return tabBar()->tabTextColor(index);
}

#ifndef KDE_NO_DEPRECATED
void KTabWidget::setTabReorderingEnabled(bool on)
{
    static_cast<KTabBar *>(tabBar())->setTabReorderingEnabled(on);
}
#endif

#ifndef KDE_NO_DEPRECATED
bool KTabWidget::isTabReorderingEnabled() const
{
    return static_cast<KTabBar *>(tabBar())->isTabReorderingEnabled();
}
#endif

#ifndef KDE_NO_DEPRECATED
void KTabWidget::setTabCloseActivatePrevious(bool previous)
{
    static_cast<KTabBar *>(tabBar())->setTabCloseActivatePrevious(previous);
}
#endif

#ifndef KDE_NO_DEPRECATED
bool KTabWidget::tabCloseActivatePrevious() const
{
    return static_cast<KTabBar *>(tabBar())->tabCloseActivatePrevious();
}
#endif

int KTabWidget::tabBarWidthForMaxChars(int maxLength)
{
    const int hframe  = tabBar()->style()->pixelMetric(QStyle::PM_TabBarTabHSpace, 0L, tabBar());

    const QFontMetrics fm = tabBar()->fontMetrics();
    int x = 0;
    for (int i = 0; i < count(); ++i) {
        QString newTitle = d->m_tabNames.value(i);
        newTitle = KStringHandler::rsqueeze(newTitle, maxLength).leftJustified(d->m_minLength, ' ');

        int lw = fm.width(newTitle);
        int iw = 0;
        if (!tabBar()->tabIcon(i).isNull()) {
            iw = tabBar()->tabIcon(i).pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize), QIcon::Normal).width() + 4;
        }
#ifndef KDE_NO_DEPRECATED
        if (isCloseButtonEnabled()) {
            // FIXME: how to get the size of the close button directly from the tabBar()?
            iw += KIconLoader::SizeSmall * 3 / 2;
        }
#endif
        x += (tabBar()->style()->sizeFromContents(QStyle::CT_TabBarTab, 0L,
                QSize(qMax(lw + hframe + iw, QApplication::globalStrut().width()), 0),
                this)).width();
    }

    return x;
}

QString KTabWidget::tabText(int index) const
{
    if (d->m_automaticResizeTabs) {
        if (index >= 0 && index < count()) {
            if (index >= d->m_tabNames.count()) {
                // Ooops, the tab exists, but tabInserted wasn't called yet.
                // This can happen when inserting the first tab,
                // and calling tabText from slotCurrentChanged,
                // see KTabWidget_UnitTest.
                const_cast<KTabWidget *>(this)->tabInserted(index);
            }
            return d->m_tabNames[ index ];
        } else {
            return QString();
        }
    } else {
        return QTabWidget::tabText(index);
    }
}

void KTabWidget::setTabText(int index, const QString &text)
{
    if (text == tabText(index)) {
        return;
    }

    if (d->m_automaticResizeTabs) {

        tabBar()->setUpdatesEnabled(false); //no flicker

        QTabWidget::setTabText(index, text);

        if (index != -1) {
            if (index >= d->m_tabNames.count()) {
                kWarning(240) << "setTabText(" << index << ") called but d->m_tabNames has only" << d->m_tabNames.count() << "entries";
                while (index >= d->m_tabNames.count()) {
                    d->m_tabNames.append(QString());
                }
            }
            d->m_tabNames[ index ] = text;
            d->resizeTabs(index);
        }

        tabBar()->setUpdatesEnabled(true);

    } else {
        QTabWidget::setTabText(index, text);
    }
}

void KTabWidget::dragEnterEvent(QDragEnterEvent *event)
{
    if (d->isEmptyTabbarSpace(event->pos())) {
        bool accept = false;
        // The receivers of the testCanDecode() signal has to adjust
        // 'accept' accordingly.
        emit testCanDecode(event, accept);

        event->setAccepted(accept);
        return;
    }

    QTabWidget::dragEnterEvent(event);
}

void KTabWidget::dragMoveEvent(QDragMoveEvent *event)
{
    if (d->isEmptyTabbarSpace(event->pos())) {
        bool accept = false;
        // The receivers of the testCanDecode() signal has to adjust
        // 'accept' accordingly.
        emit testCanDecode(event, accept);

        event->setAccepted(accept);
        return;
    }

    QTabWidget::dragMoveEvent(event);
}

void KTabWidget::dropEvent(QDropEvent *event)
{
    if (d->isEmptyTabbarSpace(event->pos())) {
        emit(receivedDropEvent(event));
        return;
    }

    QTabWidget::dropEvent(event);
}

#ifndef QT_NO_WHEELEVENT
void KTabWidget::wheelEvent(QWheelEvent *event)
{
    if (d->isEmptyTabbarSpace(event->pos())) {
        QCoreApplication::sendEvent(tabBar(), event);
    } else {
        QTabWidget::wheelEvent(event);
    }
}

void KTabWidget::wheelDelta(int delta)
{
    if (count() < 2) {
        return;
    }

    int page = currentIndex();
    if (delta < 0) {
        page = (page + 1) % count();
    } else {
        page--;
        if (page < 0) {
            page = count() - 1;
        }
    }
    setCurrentIndex(page);
}
#endif

void KTabWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
    if (event->button() != Qt::LeftButton) {
        return;
    }

    if (d->isEmptyTabbarSpace(event->pos())) {
        emit(mouseDoubleClick());
        return;
    }

    QTabWidget::mouseDoubleClickEvent(event);
}

void KTabWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::RightButton) {
        if (d->isEmptyTabbarSpace(event->pos())) {
            emit(contextMenu(mapToGlobal(event->pos())));
            return;
        }
    }

    QTabWidget::mousePressEvent(event);
}

void KTabWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::MidButton) {
        if (d->isEmptyTabbarSpace(event->pos())) {
            emit(mouseMiddleClick());
            return;
        }
    }

    QTabWidget::mouseReleaseEvent(event);
}

void KTabWidget::receivedDropEvent(int index, QDropEvent *event)
{
    emit(receivedDropEvent(widget(index), event));
}

void KTabWidget::initiateDrag(int index)
{
    emit(initiateDrag(widget(index)));
}

void KTabWidget::contextMenu(int index, const QPoint &point)
{
    emit(contextMenu(widget(index), point));
}

void KTabWidget::mouseDoubleClick(int index)
{
    emit(mouseDoubleClick(widget(index)));
}

void KTabWidget::mouseMiddleClick(int index)
{
    emit(mouseMiddleClick(widget(index)));
}

void KTabWidget::moveTab(int from, int to)
{
    setUpdatesEnabled(false);

    const QString tablabel = tabText(from);
    QWidget *w = widget(from);
    const QColor color = tabTextColor(from);
    const QIcon tabiconset = tabIcon(from);
    const QString tabtooltip = tabToolTip(from);
    const bool current = (from == currentIndex());
    const bool enabled = isTabEnabled(from);

    const bool blocked = blockSignals(true);

    QWidget *fw = QApplication::focusWidget();

    removeTab(from);
    insertTab(to, w, tablabel);

    // Don't lose focus due to moving the tab (#159295)
    // (removeTab hides the widget, which gives focus to the "next in chain", could be anything)
    if (w->isAncestorOf(fw)) {
        fw->setFocus();
    }

    setTabIcon(to, tabiconset);
    setTabText(to, tablabel);
    setTabToolTip(to, tabtooltip);
    setTabTextColor(to, color);
    if (current) {
        setCurrentIndex(to);
    }
    setTabEnabled(to, enabled);
    if (d->m_automaticResizeTabs) {
        d->resizeTabs(to);
    }
    blockSignals(blocked);

    setUpdatesEnabled(true);

#ifndef KDE_NO_DEPRECATED
    emit(movedTab(from, to));
#endif
}

void KTabWidget::removePage(QWidget *widget)
{
    // not just calling removeTab() because that one is also virtual.
    const int index = indexOf(widget);
    if (d->m_automaticResizeTabs) {
        setUpdatesEnabled(false);
        d->removeTab(index);
        setUpdatesEnabled(true);
    } else {
        d->removeTab(index);
    }
}

void KTabWidget::removeTab(int index)
{
    if (d->m_automaticResizeTabs) {
        const bool wasUpdatesEnabled = updatesEnabled();
        setUpdatesEnabled(false);
        d->removeTab(index);
        setUpdatesEnabled(wasUpdatesEnabled);
    } else {
        d->removeTab(index);
    }
}

#ifndef KDE_NO_DEPRECATED
void KTabWidget::setHoverCloseButton(bool button)
{
    // deprecated
    setTabsClosable(button);
}
#endif

#ifndef KDE_NO_DEPRECATED
bool KTabWidget::hoverCloseButton() const
{
    // deprecated
    return false;
}
#endif

#ifndef KDE_NO_DEPRECATED
void KTabWidget::setHoverCloseButtonDelayed(bool delayed)
{
    // deprecated
    Q_UNUSED(delayed);
}
#endif

#ifndef KDE_NO_DEPRECATED
bool KTabWidget::hoverCloseButtonDelayed() const
{
    // deprecated
    return tabsClosable();
}
#endif

#ifndef KDE_NO_DEPRECATED
void KTabWidget::setCloseButtonEnabled(bool enable)
{
    static_cast<KTabBar *>(tabBar())->setTabsClosable(enable);
}
#endif

#ifndef KDE_NO_DEPRECATED
bool KTabWidget::isCloseButtonEnabled() const
{
    return static_cast<KTabBar *>(tabBar())->tabsClosable();
}
#endif

void KTabWidget::setAutomaticResizeTabs(bool enabled)
{
    if (d->m_automaticResizeTabs == enabled) {
        return;
    }

    setUpdatesEnabled(false);

    d->m_automaticResizeTabs = enabled;
    if (enabled) {
        d->m_tabNames.clear();
        for (int i = 0; i < count(); ++i) {
            d->m_tabNames.append(tabBar()->tabText(i));
        }
    } else
        for (int i = 0; i < count(); ++i) {
            tabBar()->setTabText(i, d->m_tabNames[ i ]);
        }

    d->resizeTabs();

    setUpdatesEnabled(true);
}

bool KTabWidget::automaticResizeTabs() const
{
    return d->m_automaticResizeTabs;
}

void KTabWidget::closeRequest(int index)
{
    emit(closeRequest(widget(index)));
}

void KTabWidget::resizeEvent(QResizeEvent *event)
{
    QTabWidget::resizeEvent(event);
    d->resizeTabs();
}

void KTabWidget::tabInserted(int idx)
{
    d->m_tabNames.insert(idx, tabBar()->tabText(idx));
}

void KTabWidget::tabRemoved(int idx)
{
    Q_UNUSED(idx)
// d->m_tabNames is now updated in KTabWidget::Private::removeTab()
}

/* This function is kept only for BC reasons, it is not useful anymore */
#ifndef KDE_NO_DEPRECATED
void KTabWidget::currentChanged(int)
{
}
#endif

#include "moc_ktabwidget.cpp"
