root/src/kits/interface/MenuBar.cpp
/*
 * Copyright 2001-2015, Haiku Inc. All rights reserved.
 * Distributed under the terms of the MIT license.
 *
 * Authors:
 *              Marc Flerackers (mflerackers@androme.be)
 *              Stefano Ceccherini (burton666@libero.it)
 *              Stephan Aßmus <superstippi@gmx.de>
 */


#include <MenuBar.h>

#include <math.h>

#include <Application.h>
#include <Autolock.h>
#include <ControlLook.h>
#include <LayoutUtils.h>
#include <MenuItem.h>
#include <Window.h>

#include <AppMisc.h>
#include <binary_compatibility/Interface.h>
#include <MenuPrivate.h>
#include <TokenSpace.h>
#include <InterfaceDefs.h>

#include "BMCPrivate.h"


using BPrivate::gDefaultTokens;


struct menubar_data {
        BMenuBar*       menuBar;
        int32           menuIndex;

        bool            sticky;
        bool            showMenu;

        bool            useRect;
        BRect           rect;
};


BMenuBar::BMenuBar(BRect frame, const char* name, uint32 resizingMode,
                menu_layout layout, bool resizeToFit)
        :
        BMenu(frame, name, resizingMode, B_WILL_DRAW | B_FRAME_EVENTS
                | B_FULL_UPDATE_ON_RESIZE, layout, resizeToFit),
        fBorder(B_BORDER_FRAME),
        fTrackingPID(-1),
        fPrevFocusToken(-1),
        fMenuSem(-1),
        fLastBounds(NULL),
        fTracking(false)
{
        _InitData(layout);
}


BMenuBar::BMenuBar(const char* name, menu_layout layout, uint32 flags)
        :
        BMenu(BRect(), name, B_FOLLOW_NONE,
                flags | B_WILL_DRAW | B_FRAME_EVENTS | B_SUPPORTS_LAYOUT,
                layout, false),
        fBorder(B_BORDER_FRAME),
        fTrackingPID(-1),
        fPrevFocusToken(-1),
        fMenuSem(-1),
        fLastBounds(NULL),
        fTracking(false)
{
        _InitData(layout);
}


BMenuBar::BMenuBar(BMessage* archive)
        :
        BMenu(archive),
        fBorder(B_BORDER_FRAME),
        fTrackingPID(-1),
        fPrevFocusToken(-1),
        fMenuSem(-1),
        fLastBounds(NULL),
        fTracking(false)
{
        int32 border;

        if (archive->FindInt32("_border", &border) == B_OK)
                SetBorder((menu_bar_border)border);

        menu_layout layout = B_ITEMS_IN_COLUMN;
        archive->FindInt32("_layout", (int32*)&layout);

        _InitData(layout);
}


BMenuBar::~BMenuBar()
{
        if (fTracking) {
                status_t dummy;
                wait_for_thread(fTrackingPID, &dummy);
        }

        delete fLastBounds;
}


BArchivable*
BMenuBar::Instantiate(BMessage* data)
{
        if (validate_instantiation(data, "BMenuBar"))
                return new BMenuBar(data);

        return NULL;
}


status_t
BMenuBar::Archive(BMessage* data, bool deep) const
{
        status_t err = BMenu::Archive(data, deep);

        if (err < B_OK)
                return err;

        if (Border() != B_BORDER_FRAME)
                err = data->AddInt32("_border", Border());

        return err;
}


// #pragma mark -


void
BMenuBar::AttachedToWindow()
{
        Window()->SetKeyMenuBar(this);

        BMenu::AttachedToWindow();

        *fLastBounds = Bounds();
}


void
BMenuBar::DetachedFromWindow()
{
        BMenu::DetachedFromWindow();
}


void
BMenuBar::AllAttached()
{
        BMenu::AllAttached();
}


void
BMenuBar::AllDetached()
{
        BMenu::AllDetached();
}


void
BMenuBar::WindowActivated(bool state)
{
        BView::WindowActivated(state);
}


void
BMenuBar::MakeFocus(bool state)
{
        BMenu::MakeFocus(state);
}


// #pragma mark -


void
BMenuBar::ResizeToPreferred()
{
        BMenu::ResizeToPreferred();
}


void
BMenuBar::GetPreferredSize(float* width, float* height)
{
        BMenu::GetPreferredSize(width, height);
}


BSize
BMenuBar::MinSize()
{
        return BMenu::MinSize();
}


BSize
BMenuBar::MaxSize()
{
        BSize size = BMenu::MaxSize();
        return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
                BSize(B_SIZE_UNLIMITED, size.height));
}


BSize
BMenuBar::PreferredSize()
{
        return BMenu::PreferredSize();
}


void
BMenuBar::FrameMoved(BPoint newPosition)
{
        BMenu::FrameMoved(newPosition);
}


void
BMenuBar::FrameResized(float newWidth, float newHeight)
{
        // invalidate right border
        if (newWidth != fLastBounds->Width()) {
                BRect rect(min_c(fLastBounds->right, newWidth), 0,
                        max_c(fLastBounds->right, newWidth), newHeight);
                Invalidate(rect);
        }

        // invalidate bottom border
        if (newHeight != fLastBounds->Height()) {
                BRect rect(0, min_c(fLastBounds->bottom, newHeight) - 1,
                        newWidth, max_c(fLastBounds->bottom, newHeight));
                Invalidate(rect);
        }

        fLastBounds->Set(0, 0, newWidth, newHeight);

        BMenu::FrameResized(newWidth, newHeight);
}


// #pragma mark -


void
BMenuBar::Show()
{
        BView::Show();
}


void
BMenuBar::Hide()
{
        BView::Hide();
}


void
BMenuBar::Draw(BRect updateRect)
{
        if (_RelayoutIfNeeded()) {
                Invalidate();
                return;
        }

        BRect rect(Bounds());
        rgb_color base = LowColor();
        uint32 flags = 0;

        be_control_look->DrawBorder(this, rect, updateRect, base,
                B_PLAIN_BORDER, flags, BControlLook::B_BOTTOM_BORDER);

        be_control_look->DrawMenuBarBackground(this, rect, updateRect, base,
                0, fBorders);

        DrawItems(updateRect);
}


// #pragma mark -


void
BMenuBar::MessageReceived(BMessage* message)
{
        BMenu::MessageReceived(message);
}


void
BMenuBar::MouseDown(BPoint where)
{
        if (fTracking)
                return;

        uint32 buttons;
        GetMouse(&where, &buttons);

        BWindow* window = Window();
        if (!window->IsActive() || !window->IsFront()) {
                if ((mouse_mode() == B_FOCUS_FOLLOWS_MOUSE)
                        || ((mouse_mode() == B_CLICK_TO_FOCUS_MOUSE)
                                && ((buttons & B_SECONDARY_MOUSE_BUTTON) != 0))) {
                        // right-click to bring-to-front and send-to-back
                        // (might cause some regressions in FFM)
                        window->Activate();
                        window->UpdateIfNeeded();
                }
        }

        StartMenuBar(-1, false, false);
}


void
BMenuBar::MouseUp(BPoint where)
{
        BView::MouseUp(where);
}


// #pragma mark -


BHandler*
BMenuBar::ResolveSpecifier(BMessage* msg, int32 index, BMessage* specifier,
        int32 form, const char* property)
{
        return BMenu::ResolveSpecifier(msg, index, specifier, form, property);
}


status_t
BMenuBar::GetSupportedSuites(BMessage* data)
{
        return BMenu::GetSupportedSuites(data);
}


// #pragma mark -


void
BMenuBar::SetBorder(menu_bar_border border)
{
        fBorder = border;
}


menu_bar_border
BMenuBar::Border() const
{
        return fBorder;
}


void
BMenuBar::SetBorders(uint32 borders)
{
        fBorders = borders;
}


uint32
BMenuBar::Borders() const
{
        return fBorders;
}


// #pragma mark -


status_t
BMenuBar::Perform(perform_code code, void* _data)
{
        switch (code) {
                case PERFORM_CODE_MIN_SIZE:
                        ((perform_data_min_size*)_data)->return_value
                                = BMenuBar::MinSize();
                        return B_OK;

                case PERFORM_CODE_MAX_SIZE:
                        ((perform_data_max_size*)_data)->return_value
                                = BMenuBar::MaxSize();
                        return B_OK;

                case PERFORM_CODE_PREFERRED_SIZE:
                        ((perform_data_preferred_size*)_data)->return_value
                                = BMenuBar::PreferredSize();
                        return B_OK;

                case PERFORM_CODE_LAYOUT_ALIGNMENT:
                        ((perform_data_layout_alignment*)_data)->return_value
                                = BMenuBar::LayoutAlignment();
                        return B_OK;

                case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
                        ((perform_data_has_height_for_width*)_data)->return_value
                                = BMenuBar::HasHeightForWidth();
                        return B_OK;

                case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
                {
                        perform_data_get_height_for_width* data
                                = (perform_data_get_height_for_width*)_data;
                        BMenuBar::GetHeightForWidth(data->width, &data->min, &data->max,
                                &data->preferred);
                        return B_OK;
                }

                case PERFORM_CODE_SET_LAYOUT:
                {
                        perform_data_set_layout* data = (perform_data_set_layout*)_data;
                        BMenuBar::SetLayout(data->layout);
                        return B_OK;
                }

                case PERFORM_CODE_LAYOUT_INVALIDATED:
                {
                        perform_data_layout_invalidated* data
                                = (perform_data_layout_invalidated*)_data;
                        BMenuBar::LayoutInvalidated(data->descendants);
                        return B_OK;
                }

                case PERFORM_CODE_DO_LAYOUT:
                {
                        BMenuBar::DoLayout();
                        return B_OK;
                }
        }

        return BMenu::Perform(code, _data);
}


// #pragma mark -


void BMenuBar::_ReservedMenuBar1() {}
void BMenuBar::_ReservedMenuBar2() {}
void BMenuBar::_ReservedMenuBar3() {}
void BMenuBar::_ReservedMenuBar4() {}


BMenuBar&
BMenuBar::operator=(const BMenuBar &)
{
        return *this;
}


// #pragma mark -


void
BMenuBar::StartMenuBar(int32 menuIndex, bool sticky, bool showMenu,
        BRect* specialRect)
{
        if (fTracking)
                return;

        BWindow* window = Window();
        if (window == NULL)
                debugger("MenuBar must be added to a window before it can be used.");

        BAutolock lock(window);
        if (!lock.IsLocked())
                return;

        fPrevFocusToken = -1;
        fTracking = true;

        // We are called from the window's thread,
        // so let's call MenusBeginning() directly
        window->MenusBeginning();

        fMenuSem = create_sem(0, "window close sem");
        _set_menu_sem_(window, fMenuSem);

        fTrackingPID = spawn_thread(_TrackTask, "menu_tracking", B_DISPLAY_PRIORITY, NULL);
        if (fTrackingPID >= 0) {
                menubar_data data;
                data.menuBar = this;
                data.menuIndex = menuIndex;
                data.sticky = sticky;
                data.showMenu = showMenu;
                data.useRect = specialRect != NULL;
                if (data.useRect)
                        data.rect = *specialRect;

                resume_thread(fTrackingPID);
                send_data(fTrackingPID, 0, &data, sizeof(data));
        } else {
                fTracking = false;
                _set_menu_sem_(window, B_NO_MORE_SEMS);
                delete_sem(fMenuSem);
        }
}


/*static*/ int32
BMenuBar::_TrackTask(void* arg)
{
        menubar_data data;
        thread_id id;
        receive_data(&id, &data, sizeof(data));

        BMenuBar* menuBar = data.menuBar;
        if (data.useRect)
                menuBar->fExtraRect = &data.rect;
        menuBar->_SetStickyMode(data.sticky);

        int32 action;
        menuBar->_Track(&action, data.menuIndex, data.showMenu);

        menuBar->fTracking = false;
        menuBar->fExtraRect = NULL;

        // We aren't the BWindow thread, so don't call MenusEnded() directly
        BWindow* window = menuBar->Window();
        window->PostMessage(_MENUS_DONE_);

        _set_menu_sem_(window, B_BAD_SEM_ID);
        delete_sem(menuBar->fMenuSem);
        menuBar->fMenuSem = B_BAD_SEM_ID;

        return 0;
}


BMenuItem*
BMenuBar::_Track(int32* action, int32 startIndex, bool showMenu)
{
        // TODO: Cleanup, merge some "if" blocks if possible
        BMenuItem* item = NULL;
        fState = MENU_STATE_TRACKING;
        fChosenItem = NULL;
                // we will use this for keyboard selection

        BPoint where;
        uint32 buttons;
        if (LockLooper()) {
                if (startIndex != -1) {
                        be_app->ObscureCursor();
                        _SelectItem(ItemAt(startIndex), true, false);
                }
                GetMouse(&where, &buttons);
                UnlockLooper();
        }

        while (fState != MENU_STATE_CLOSED) {
                bigtime_t snoozeAmount = 40000;
                if (!LockLooper())
                        break;

                item = dynamic_cast<_BMCMenuBar_*>(this) != NULL ? ItemAt(0)
                        : _HitTestItems(where, B_ORIGIN);

                if (_OverSubmenu(fSelected, ConvertToScreen(where))
                        || fState == MENU_STATE_KEY_TO_SUBMENU) {
                        // call _Track() from the selected sub-menu when the mouse cursor
                        // is over its window
                        BMenu* submenu = fSelected->Submenu();
                        UnlockLooper();
                        snoozeAmount = 30000;
                        submenu->_SetStickyMode(_IsStickyMode());
                        int localAction;
                        fChosenItem = submenu->_Track(&localAction);

                        // The mouse could have meen moved since the last time we
                        // checked its position, or buttons might have been pressed.
                        // Unfortunately our child menus don't tell
                        // us the new position.
                        // TODO: Maybe have a shared struct between all menus
                        // where to store the current mouse position ?
                        // (Or just use the BView mouse hooks)
                        BPoint newWhere;
                        if (LockLooper()) {
                                GetMouse(&newWhere, &buttons);
                                UnlockLooper();
                        }

                        // Needed to make BMenuField child menus "sticky"
                        // (see ticket #953)
                        if (localAction == MENU_STATE_CLOSED) {
                                if (fExtraRect != NULL && fExtraRect->Contains(where)
                                        && point_distance(newWhere, where) < 9) {
                                        // 9 = 3 pixels ^ 2 (since point_distance() returns the
                                        // square of the distance)
                                        _SetStickyMode(true);
                                        fExtraRect = NULL;
                                } else
                                        fState = MENU_STATE_CLOSED;
                        }
                        if (!LockLooper())
                                break;
                } else if (item != NULL) {
                        if (item->Submenu() != NULL && item != fSelected) {
                                if (item->Submenu()->Window() == NULL) {
                                        // open the menu if it's not opened yet
                                        _SelectItem(item);
                                } else {
                                        // Menu was already opened, close it and bail
                                        _SelectItem(NULL);
                                        fState = MENU_STATE_CLOSED;
                                        fChosenItem = NULL;
                                }
                        } else {
                                // No submenu, just select the item
                                _SelectItem(item);
                        }
                } else if (item == NULL && fSelected != NULL
                        && !_IsStickyMode() && Bounds().Contains(where)) {
                        _SelectItem(NULL);
                        fState = MENU_STATE_TRACKING;
                }

                UnlockLooper();

                if (fState != MENU_STATE_CLOSED) {
                        BPoint newWhere = where;
                        uint32 newButtons = buttons;

                        do {
                                // If user doesn't move the mouse or change buttons loop
                                // here so that we don't interfere with keyboard menu
                                // navigation
                                snooze(snoozeAmount);
                                if (!LockLooper())
                                        break;

                                GetMouse(&newWhere, &newButtons);
                                UnlockLooper();
                        } while (newWhere == where && newButtons == buttons
                                && fState == MENU_STATE_TRACKING);

                        if (newButtons != 0 && _IsStickyMode()) {
                                if (item == NULL || (item->Submenu() != NULL
                                                && item->Submenu()->Window() != NULL)) {
                                        // clicked outside the menu bar or on item with already
                                        // open sub menu
                                        fState = MENU_STATE_CLOSED;
                                } else
                                        _SetStickyMode(false);
                        } else if (newButtons == 0 && !_IsStickyMode()) {
                                if ((fSelected != NULL && fSelected->Submenu() == NULL)
                                        || item == NULL) {
                                        // clicked on an item without a submenu or clicked and
                                        // released the mouse button outside the menu bar
                                        fChosenItem = fSelected;
                                        fState = MENU_STATE_CLOSED;
                                } else
                                        _SetStickyMode(true);
                        }
                        where = newWhere;
                        buttons = newButtons;
                }
        }

        if (LockLooper()) {
                if (fSelected != NULL)
                        _SelectItem(NULL);

                if (fChosenItem != NULL)
                        fChosenItem->Invoke();

                _RestoreFocus();
                UnlockLooper();
        }

        if (_IsStickyMode())
                _SetStickyMode(false);

        _DeleteMenuWindow();

        if (action != NULL)
                *action = fState;

        return fChosenItem;
}


void
BMenuBar::_StealFocus()
{
        // We already stole the focus, don't do anything
        if (fPrevFocusToken != -1)
                return;

        BWindow* window = Window();
        if (window != NULL && window->Lock()) {
                BView* focusView = window->CurrentFocus();
                if (focusView != NULL && focusView != this)
                        fPrevFocusToken = _get_object_token_(focusView);
                MakeFocus();
                window->Unlock();
        }
}


void
BMenuBar::_RestoreFocus()
{
        BWindow* window = Window();
        if (window != NULL && window->Lock()) {
                BHandler* handler = NULL;
                if (fPrevFocusToken != -1
                        && gDefaultTokens.GetToken(fPrevFocusToken, B_HANDLER_TOKEN,
                                (void**)&handler) == B_OK) {
                        BView* view = dynamic_cast<BView*>(handler);
                        if (view != NULL && view->Window() == window)
                                view->MakeFocus();
                } else if (IsFocus())
                        MakeFocus(false);

                fPrevFocusToken = -1;
                window->Unlock();
        }
}


void
BMenuBar::_InitData(menu_layout layout)
{
        const float fontSize = be_plain_font->Size();
        float lr = fontSize * 2.0f / 3.0f, tb = fontSize / 6.0f;
        SetItemMargins(lr, tb, lr, tb);

        fBorders = BControlLook::B_ALL_BORDERS;
        fLastBounds = new BRect(Bounds());
        _SetIgnoreHidden(true);
        SetLowUIColor(B_MENU_BACKGROUND_COLOR);
        SetViewColor(B_TRANSPARENT_COLOR);
}