root/src/kits/interface/PopUpMenu.cpp
/*
 * Copyright 2001-2025, Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Marc Flerackers (mflerackers@androme.be)
 *              Stefano Ceccherini (burton666@libero.it)
 *              John Scipione (jscipione@gmail.com)
 */


#include <PopUpMenu.h>

#include <new>

#include <Application.h>
#include <Looper.h>
#include <MenuItem.h>
#include <Window.h>

#include <binary_compatibility/Interface.h>


struct popup_menu_data {
        BPopUpMenu* object;
        BWindow* window;
        BMenuItem* selected;

        BPoint where;
        BRect rect;

        bool async;
        bool autoInvoke;
        bool startOpened;
        bool useRect;

        sem_id lock;
};


BPopUpMenu::BPopUpMenu(const char* name, bool radioMode, bool labelFromMarked,
        menu_layout layout)
        :
        BMenu(name, layout),
        fUseWhere(false),
        fAutoDestruct(false),
        fTrackThread(-1)
{
        if (radioMode)
                SetRadioMode(true);

        if (labelFromMarked)
                SetLabelFromMarked(true);
}


BPopUpMenu::BPopUpMenu(BMessage* archive)
        :
        BMenu(archive),
        fUseWhere(false),
        fAutoDestruct(false),
        fTrackThread(-1)
{
}


BPopUpMenu::~BPopUpMenu()
{
        if (fTrackThread >= 0) {
                status_t status;
                while (wait_for_thread(fTrackThread, &status) == B_INTERRUPTED)
                        ;
        }
}


status_t
BPopUpMenu::Archive(BMessage* data, bool deep) const
{
        return BMenu::Archive(data, deep);
}


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

        return NULL;
}


BMenuItem*
BPopUpMenu::Go(BPoint where, bool deliversMessage, bool openAnyway, bool async)
{
        return _Go(where, deliversMessage, openAnyway, NULL, async);
}


BMenuItem*
BPopUpMenu::Go(BPoint where, bool deliversMessage, bool openAnyway,
        BRect clickToOpen, bool async)
{
        return _Go(where, deliversMessage, openAnyway, &clickToOpen, async);
}


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


void
BPopUpMenu::MouseDown(BPoint point)
{
        BView::MouseDown(point);
}


void
BPopUpMenu::MouseUp(BPoint point)
{
        BView::MouseUp(point);
}


void
BPopUpMenu::MouseMoved(BPoint point, uint32 code, const BMessage* message)
{
        BView::MouseMoved(point, code, message);
}


void
BPopUpMenu::AttachedToWindow()
{
        BMenu::AttachedToWindow();
}


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


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


void
BPopUpMenu::FrameResized(float newWidth, float newHeight)
{
        BMenu::FrameResized(newWidth, newHeight);
}


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


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


status_t
BPopUpMenu::Perform(perform_code code, void* _data)
{
        switch (code) {
                case PERFORM_CODE_MIN_SIZE:
                        ((perform_data_min_size*)_data)->return_value
                                = BPopUpMenu::MinSize();
                        return B_OK;
                case PERFORM_CODE_MAX_SIZE:
                        ((perform_data_max_size*)_data)->return_value
                                = BPopUpMenu::MaxSize();
                        return B_OK;
                case PERFORM_CODE_PREFERRED_SIZE:
                        ((perform_data_preferred_size*)_data)->return_value
                                = BPopUpMenu::PreferredSize();
                        return B_OK;
                case PERFORM_CODE_LAYOUT_ALIGNMENT:
                        ((perform_data_layout_alignment*)_data)->return_value
                                = BPopUpMenu::LayoutAlignment();
                        return B_OK;
                case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
                        ((perform_data_has_height_for_width*)_data)->return_value
                                = BPopUpMenu::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;
                        BPopUpMenu::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;
                        BPopUpMenu::SetLayout(data->layout);
                        return B_OK;
                }
                case PERFORM_CODE_LAYOUT_INVALIDATED:
                {
                        perform_data_layout_invalidated* data
                                = (perform_data_layout_invalidated*)_data;
                        BPopUpMenu::LayoutInvalidated(data->descendants);
                        return B_OK;
                }
                case PERFORM_CODE_DO_LAYOUT:
                {
                        BPopUpMenu::DoLayout();
                        return B_OK;
                }
        }

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


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


void
BPopUpMenu::GetPreferredSize(float* _width, float* _height)
{
        BMenu::GetPreferredSize(_width, _height);
}


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


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


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


void
BPopUpMenu::SetAsyncAutoDestruct(bool on)
{
        fAutoDestruct = on;
}


bool
BPopUpMenu::AsyncAutoDestruct() const
{
        return fAutoDestruct;
}


BPoint
BPopUpMenu::ScreenLocation()
{
        // This case is when the BPopUpMenu is standalone
        if (fUseWhere)
                return fWhere;

        // This case is when the BPopUpMenu is inside a BMenuField
        BMenuItem* superItem = Superitem();
        BMenu* superMenu = Supermenu();
        BMenuItem* selectedItem = FindItem(superItem->Label());
        BPoint point = superItem->Frame().LeftTop();

        superMenu->ConvertToScreen(&point);

        if (selectedItem != NULL) {
                while (selectedItem->Menu() != this
                        && selectedItem->Menu()->Superitem() != NULL) {
                        selectedItem = selectedItem->Menu()->Superitem();
                }
                point.y -= selectedItem->Frame().top;
        }

        return point;
}


//      #pragma mark - private methods


void BPopUpMenu::_ReservedPopUpMenu1() {}
void BPopUpMenu::_ReservedPopUpMenu2() {}
void BPopUpMenu::_ReservedPopUpMenu3() {}


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


BMenuItem*
BPopUpMenu::_Go(BPoint where, bool autoInvoke, bool startOpened,
        BRect* _specialRect, bool async)
{
        if (fTrackThread >= B_OK) {
                // we already have an active menu, wait for it to go away before
                // spawning another
                status_t unused;
                while (wait_for_thread(fTrackThread, &unused) == B_INTERRUPTED)
                        ;
        }

        popup_menu_data* data = new (std::nothrow) popup_menu_data;
        if (data == NULL)
                return NULL;

        sem_id sem = create_sem(0, "window close lock");
        if (sem < B_OK) {
                delete data;
                return NULL;
        }

        // Get a pointer to the window from which Go() was called
        BWindow* window = dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL)));
        data->window = window;

        // Asynchronous menu: we set the BWindow menu's semaphore
        // and let BWindow block when needed
        if (async && window != NULL)
                _set_menu_sem_(window, sem);

        data->object = this;
        data->autoInvoke = autoInvoke;
        data->useRect = _specialRect != NULL;
        if (_specialRect != NULL)
                data->rect = *_specialRect;
        data->async = async;
        data->where = where;
        data->startOpened = startOpened;
        data->selected = NULL;
        data->lock = sem;

        // Spawn the tracking thread
        fTrackThread = spawn_thread(_thread_entry, "popup", B_DISPLAY_PRIORITY, data);
        if (fTrackThread < B_OK) {
                // Something went wrong. Cleanup and return NULL
                delete_sem(sem);
                if (async && window != NULL)
                        _set_menu_sem_(window, B_BAD_SEM_ID);
                delete data;
                return NULL;
        }

        resume_thread(fTrackThread);

        if (!async)
                return _WaitMenu(data);

        return 0;
}


/* static */
int32
BPopUpMenu::_thread_entry(void* menuData)
{
        popup_menu_data* data = static_cast<popup_menu_data*>(menuData);
        BPopUpMenu* menu = data->object;
        BRect* rect = NULL;

        if (data->useRect)
                rect = &data->rect;

        data->selected = menu->_StartTrack(data->where, data->autoInvoke,
                data->startOpened, rect);

        // Reset the window menu semaphore
        if (data->async && data->window)
                _set_menu_sem_(data->window, B_BAD_SEM_ID);

        delete_sem(data->lock);

        // Commit suicide if needed
        if (data->async && menu->fAutoDestruct) {
                menu->fTrackThread = -1;
                delete menu;
        }

        if (data->async)
                delete data;

        return 0;
}


BMenuItem*
BPopUpMenu::_StartTrack(BPoint where, bool autoInvoke, bool startOpened,
        BRect* _specialRect)
{
        // I know, this doesn't look senseful, but don't be fooled,
        // fUseWhere is used in ScreenLocation(), which is a virtual
        // called by BMenu::Track()
        fWhere = where;
        fUseWhere = true;

        // Determine when mouse-down-up will be taken as a 'press',
        // rather than a 'click'
        bigtime_t clickMaxTime = 0;
        get_click_speed(&clickMaxTime);
        clickMaxTime += system_time();

        // Show the menu's window
        Show();
        snooze(50000);
        BMenuItem* result = Track(startOpened, _specialRect);

        // If it was a click, keep the menu open and tracking
        if (system_time() <= clickMaxTime)
                result = Track(true, _specialRect);
        if (result != NULL && autoInvoke)
                result->Invoke();

        fUseWhere = false;

        Hide();
        be_app->ShowCursor();

        return result;
}


BMenuItem*
BPopUpMenu::_WaitMenu(void* _data)
{
        popup_menu_data* data = static_cast<popup_menu_data*>(_data);
        BWindow* window = data->window;
        sem_id sem = data->lock;
        if (window != NULL) {
                while (acquire_sem_etc(sem, 1, B_RELATIVE_TIMEOUT, 50000) != B_BAD_SEM_ID)
                        window->UpdateIfNeeded();
        }

        status_t unused;
        while (wait_for_thread(fTrackThread, &unused) == B_INTERRUPTED)
                ;

        fTrackThread = -1;

        BMenuItem* selected = data->selected;
                // data->selected is filled by the tracking thread

        delete data;

        return selected;
}