root/src/apps/webpositive/tabview/TabContainerView.cpp
/*
 * Copyright (C) 2010 Rene Gollent <rene@gollent.com>
 * Copyright (C) 2010 Stephan Aßmus <superstippi@gmx.de>
 *
 * All rights reserved. Distributed under the terms of the MIT License.
 */

#include "TabContainerView.h"

#include <stdio.h>

#include <Application.h>
#include <AbstractLayoutItem.h>
#include <Bitmap.h>
#include <Button.h>
#include <CardLayout.h>
#include <Catalog.h>
#include <ControlLook.h>
#include <GroupView.h>
#include <SpaceLayoutItem.h>
#include <Window.h>

#include "TabView.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Tab Manager"


static const float kLeftTabInset = 4;


TabContainerView::TabContainerView(Controller* controller)
        :
        BGroupView(B_HORIZONTAL, 0.0),
        fLastMouseEventTab(NULL),
        fMouseDown(false),
        fClickCount(0),
        fSelectedTab(NULL),
        fController(controller),
        fFirstVisibleTabIndex(0)
{
        SetFlags(Flags() | B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE);
        SetViewColor(B_TRANSPARENT_COLOR);
        GroupLayout()->SetInsets(kLeftTabInset, 0, 0, 1);
        GroupLayout()->AddItem(BSpaceLayoutItem::CreateGlue(), 0.0f);
}


TabContainerView::~TabContainerView()
{
}


BSize
TabContainerView::MinSize()
{
        // Eventually, we want to be scrolling if the tabs don't fit.
        BSize size(BGroupView::MinSize());
        size.width = 300;
        return size;
}


void
TabContainerView::MessageReceived(BMessage* message)
{
        switch (message->what) {
                default:
                        BGroupView::MessageReceived(message);
        }
}


void
TabContainerView::Draw(BRect updateRect)
{
        // draw tab frame
        BRect rect(Bounds());
        rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR);
        uint32 borders = BControlLook::B_TOP_BORDER
                | BControlLook::B_BOTTOM_BORDER;
        be_control_look->DrawTabFrame(this, rect, updateRect, base, 0,
                borders, B_NO_BORDER);

        // draw tabs on top of frame
        BGroupLayout* layout = GroupLayout();
        int32 count = layout->CountItems() - 1;
        for (int32 i = 0; i < count; i++) {
                TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(layout->ItemAt(i));
                if (item == NULL || !item->IsVisible())
                        continue;
                item->Parent()->Draw(item->Frame());
        }
}


void
TabContainerView::MouseDown(BPoint where)
{
        if (Window() == NULL)
                return;

        BMessage* currentMessage = Window()->CurrentMessage();
        if (currentMessage == NULL)
                return;

        uint32 buttons;
        if (currentMessage->FindInt32("buttons", (int32*)&buttons) != B_OK)
                buttons = B_PRIMARY_MOUSE_BUTTON;

        uint32 clicks;
        if (currentMessage->FindInt32("clicks", (int32*)&clicks) != B_OK)
                clicks = 1;

        fMouseDown = true;
        SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);

        if (fLastMouseEventTab != NULL)
                fLastMouseEventTab->MouseDown(where, buttons);
        else {
                if ((buttons & B_TERTIARY_MOUSE_BUTTON) != 0) {
                        // Middle click outside tabs should always open a new tab.
                        fController->DoubleClickOutsideTabs();
                } else if (clicks > 1)
                        fClickCount++;
                else
                        fClickCount = 1;
        }
}


void
TabContainerView::MouseUp(BPoint where)
{
        fMouseDown = false;
        if (fLastMouseEventTab) {
                fLastMouseEventTab->MouseUp(where);
                fClickCount = 0;
        } else if (fClickCount > 1) {
                // NOTE: fClickCount is >= 1 only if the first click was outside
                // any tab. So even if fLastMouseEventTab has been reset to NULL
                // because this tab was removed during mouse down, we wouldn't
                // run the "outside tabs" code below.
                fController->DoubleClickOutsideTabs();
                fClickCount = 0;
        }
        // Always check the tab under the mouse again, since we don't update
        // it with fMouseDown == true.
        _SendFakeMouseMoved();
}


void
TabContainerView::MouseMoved(BPoint where, uint32 transit,
        const BMessage* dragMessage)
{
        _MouseMoved(where, transit, dragMessage);
}


void
TabContainerView::DoLayout()
{
        BGroupView::DoLayout();

        _ValidateTabVisibility();
        _SendFakeMouseMoved();
}

void
TabContainerView::AddTab(const char* label, int32 index)
{
        TabView* tab;
        if (fController != NULL)
                tab = fController->CreateTabView();
        else
                tab = new TabView();

        tab->SetLabel(label);
        AddTab(tab, index);
}


void
TabContainerView::AddTab(TabView* tab, int32 index)
{
        tab->SetContainerView(this);

        if (index == -1)
                index = GroupLayout()->CountItems() - 1;

        tab->Update();

        GroupLayout()->AddItem(index, tab->LayoutItem());

        if (fSelectedTab == NULL)
                SelectTab(tab);

        bool isLast = index == GroupLayout()->CountItems() - 1;
        if (isLast) {
                TabLayoutItem* item
                        = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index - 1));
                if (item != NULL)
                        item->Parent()->Update();
        }


        SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex());
        _ValidateTabVisibility();
}


TabView*
TabContainerView::RemoveTab(int32 index)
{
        TabLayoutItem* item
                = dynamic_cast<TabLayoutItem*>(GroupLayout()->RemoveItem(index));
        if (item == NULL)
                return NULL;

        BRect dirty(Bounds());
        dirty.left = item->Frame().left;
        TabView* removedTab = item->Parent();
        removedTab->SetContainerView(NULL);

        if (removedTab == fLastMouseEventTab)
                fLastMouseEventTab = NULL;

        // Update tabs after or before the removed tab.
        item = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index));
        if (item != NULL) {
                // This tab is behind the removed tab.
                TabView* tab = item->Parent();
                tab->Update();
                if (removedTab == fSelectedTab) {
                        fSelectedTab = NULL;
                        SelectTab(tab);
                } else if (fController != NULL && tab == fSelectedTab)
                        fController->UpdateSelection(index);
        } else {
                // The removed tab was the last tab.
                item = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index - 1));
                if (item != NULL) {
                        TabView* tab = item->Parent();
                        tab->Update();
                        if (removedTab == fSelectedTab) {
                                fSelectedTab = NULL;
                                SelectTab(tab);
                        }
                }
        }

        Invalidate(dirty);
        _ValidateTabVisibility();

        return removedTab;
}


TabView*
TabContainerView::TabAt(int32 index) const
{
        TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
                GroupLayout()->ItemAt(index));
        if (item != NULL)
                return item->Parent();

        return NULL;
}


int32
TabContainerView::IndexOf(TabView* tab) const
{
        if (tab == NULL || GroupLayout() == NULL)
                return -1;

        return GroupLayout()->IndexOfItem(tab->LayoutItem());
}


void
TabContainerView::SelectTab(int32 index)
{
        TabView* tab = NULL;
        TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
                GroupLayout()->ItemAt(index));
        if (item != NULL)
                tab = item->Parent();

        SelectTab(tab);
}


void
TabContainerView::SelectTab(TabView* tab)
{
        if (tab == fSelectedTab)
                return;

        // update old selected tab
        if (fSelectedTab != NULL)
                fSelectedTab->Update();

        fSelectedTab = tab;

        // update new selected tab
        if (fSelectedTab != NULL)
                fSelectedTab->Update();

        int32 index = -1;
        if (fSelectedTab != NULL)
                index = GroupLayout()->IndexOfItem(tab->LayoutItem());

        if (!tab->LayoutItem()->IsVisible())
                SetFirstVisibleTabIndex(index);

        if (fController != NULL)
                fController->UpdateSelection(index);
}


void
TabContainerView::SetTabLabel(int32 index, const char* label)
{
        TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
                GroupLayout()->ItemAt(index));
        if (item == NULL)
                return;

        item->Parent()->SetLabel(label);
}


void
TabContainerView::SetFirstVisibleTabIndex(int32 index)
{
        if (index < 0)
                index = 0;
        if (index > MaxFirstVisibleTabIndex())
                index = MaxFirstVisibleTabIndex();
        if (fFirstVisibleTabIndex == index)
                return;

        fFirstVisibleTabIndex = index;

        _UpdateTabVisibility();
}


int32
TabContainerView::FirstVisibleTabIndex() const
{
        return fFirstVisibleTabIndex;
}


int32
TabContainerView::MaxFirstVisibleTabIndex() const
{
        float availableWidth = _AvailableWidthForTabs();
        if (availableWidth < 0)
                return 0;
        float visibleTabsWidth = 0;

        BGroupLayout* layout = GroupLayout();
        int32 i = layout->CountItems() - 2;
        for (; i >= 0; i--) {
                TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
                        layout->ItemAt(i));
                if (item == NULL)
                        continue;

                float itemWidth = item->MinSize().width;
                if (availableWidth >= visibleTabsWidth + itemWidth)
                        visibleTabsWidth += itemWidth;
                else {
                        // The tab before this tab is the last one that can be visible.
                        return i + 1;
                }
        }

        return 0;
}


bool
TabContainerView::CanScrollLeft() const
{
        return fFirstVisibleTabIndex < MaxFirstVisibleTabIndex();
}


bool
TabContainerView::CanScrollRight() const
{
        BGroupLayout* layout = GroupLayout();
        int32 count = layout->CountItems() - 1;
        if (count > 0) {
                TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
                        layout->ItemAt(count - 1));
                return !item->IsVisible();
        }
        return false;
}


// #pragma mark -


TabView*
TabContainerView::_TabAt(const BPoint& where) const
{
        BGroupLayout* layout = GroupLayout();
        int32 count = layout->CountItems() - 1;
        for (int32 i = 0; i < count; i++) {
                TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(layout->ItemAt(i));
                if (item == NULL || !item->IsVisible())
                        continue;
                // Account for the fact that the tab frame does not contain the
                // visible bottom border.
                BRect frame = item->Frame();
                frame.bottom++;
                if (frame.Contains(where))
                        return item->Parent();
        }
        return NULL;
}


void
TabContainerView::_MouseMoved(BPoint where, uint32 _transit,
        const BMessage* dragMessage)
{
        TabView* tab = _TabAt(where);
        if (fMouseDown) {
                uint32 transit = tab == fLastMouseEventTab
                        ? B_INSIDE_VIEW : B_OUTSIDE_VIEW;
                if (fLastMouseEventTab)
                        fLastMouseEventTab->MouseMoved(where, transit, dragMessage);
                return;
        }

        if (fLastMouseEventTab != NULL && fLastMouseEventTab == tab)
                fLastMouseEventTab->MouseMoved(where, B_INSIDE_VIEW, dragMessage);
        else {
                if (fLastMouseEventTab)
                        fLastMouseEventTab->MouseMoved(where, B_EXITED_VIEW, dragMessage);
                fLastMouseEventTab = tab;
                if (fLastMouseEventTab)
                        fLastMouseEventTab->MouseMoved(where, B_ENTERED_VIEW, dragMessage);
                else {
                        fController->SetToolTip(
                                B_TRANSLATE("Double-click or middle-click to open new tab."));
                }
        }
}


void
TabContainerView::_ValidateTabVisibility()
{
        if (fFirstVisibleTabIndex > MaxFirstVisibleTabIndex())
                SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex());
        else
                _UpdateTabVisibility();
}


void
TabContainerView::_UpdateTabVisibility()
{
        float availableWidth = _AvailableWidthForTabs();
        if (availableWidth < 0)
                return;
        float visibleTabsWidth = 0;

        bool canScrollTabsLeft = fFirstVisibleTabIndex > 0;
        bool canScrollTabsRight = false;

        BGroupLayout* layout = GroupLayout();
        int32 count = layout->CountItems() - 1;
        for (int32 i = 0; i < count; i++) {
                TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(
                        layout->ItemAt(i));
                if (i < fFirstVisibleTabIndex)
                        item->SetVisible(false);
                else {
                        float itemWidth = item->MinSize().width;
                        bool visible = availableWidth >= visibleTabsWidth + itemWidth;
                        item->SetVisible(visible && !canScrollTabsRight);
                        visibleTabsWidth += itemWidth;
                        if (!visible)
                                canScrollTabsRight = true;
                }
        }
        fController->UpdateTabScrollability(canScrollTabsLeft, canScrollTabsRight);
}


float
TabContainerView::_AvailableWidthForTabs() const
{
        float width = Bounds().Width() - 10;
                // TODO: Don't really know why -10 is needed above.

        float left;
        float right;
        GroupLayout()->GetInsets(&left, NULL, &right, NULL);
        width -= left + right;

        return width;
}


void
TabContainerView::_SendFakeMouseMoved()
{
        BPoint where;
        uint32 buttons;
        GetMouse(&where, &buttons, false);
        if (Bounds().Contains(where))
                _MouseMoved(where, B_INSIDE_VIEW, NULL);
}