root/src/kits/interface/Alert.cpp
/*
 * Copyright 2001-2015 Haiku, Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Axel Dörfler, axeld@pinc-software.de
 *              Erik Jaesler, erik@cgsoftware.com
 *              John Scipione, jscipione@gmail.com
 *              Ron Ben Aroya, sed4906birdie@gmail.com
 */


//!     BAlert displays a modal alert window.


#include <Alert.h>

#include <new>

#include <stdio.h>

#include <Beep.h>
#include <Bitmap.h>
#include <Button.h>
#include <ControlLook.h>
#include <Debug.h>
#include <FindDirectory.h>
#include <IconUtils.h>
#include <LayoutBuilder.h>
#include <MediaSounds.h>
#include <MenuField.h>
#include <MessageFilter.h>
#include <Path.h>
#include <Resources.h>
#include <Screen.h>
#include <String.h>
#include <Window.h>

#include <binary_compatibility/Interface.h>


//#define DEBUG_ALERT
#ifdef DEBUG_ALERT
#       define FTRACE(x) fprintf(x)
#else
#       define FTRACE(x) ;
#endif


class TAlertView : public BView {
public:
                                                                TAlertView();
                                                                TAlertView(BMessage* archive);
                                                                ~TAlertView();

        static  TAlertView*                     Instantiate(BMessage* archive);
        virtual status_t                        Archive(BMessage* archive,
                                                                        bool deep = true) const;

        virtual void                            GetPreferredSize(float* _width, float* _height);
        virtual BSize                           MaxSize();
        virtual void                            Draw(BRect updateRect);

                        void                            SetBitmap(BBitmap* icon);
                        BBitmap*                        Bitmap()
                                                                        { return fIconBitmap; }

private:
                        BBitmap*                        fIconBitmap;
};


class _BAlertFilter_ : public BMessageFilter {
public:
                                                                _BAlertFilter_(BAlert* Alert);
                                                                ~_BAlertFilter_();

        virtual filter_result           Filter(BMessage* msg, BHandler** target);

private:
                        BAlert*                         fAlert;
};


static const unsigned int kAlertButtonMsg = 'ALTB';
static const int kSemTimeOut = 50000;

static const int kButtonOffsetSpacing = 62;
static const int kButtonUsualWidth = 55;
static const int kIconStripeWidthFactor = 5;

static const int kWindowMinWidth = 310;
static const int kWindowOffsetMinWidth = 335;


// #pragma mark -


BAlert::BAlert()
        :
        BWindow(BRect(0, 0, 100, 100), "", B_MODAL_WINDOW,
                B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS)
{
        _Init(NULL, NULL, NULL, NULL, B_WIDTH_FROM_WIDEST, B_EVEN_SPACING,
                B_INFO_ALERT);
}


BAlert::BAlert(const char *title, const char *text, const char *button1,
                const char *button2, const char *button3, button_width width,
                alert_type type)
        :
        BWindow(BRect(0, 0, 100, 100), title, B_MODAL_WINDOW,
                B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS)
{
        _Init(text, button1, button2, button3, width, B_EVEN_SPACING, type);
}


BAlert::BAlert(const char *title, const char *text, const char *button1,
                const char *button2, const char *button3, button_width width,
                button_spacing spacing, alert_type type)
        :
        BWindow(BRect(0, 0, 100, 100), title, B_MODAL_WINDOW,
                B_NOT_CLOSABLE | B_NOT_RESIZABLE | B_ASYNCHRONOUS_CONTROLS)
{
        _Init(text, button1, button2, button3, width, spacing, type);
}


BAlert::BAlert(BMessage* data)
        :
        BWindow(data)
{
        fInvoker = NULL;
        fAlertSem = -1;
        fAlertValue = -1;

        fTextView = (BTextView*)FindView("_tv_");

        // TODO: window loses default button on dearchive!
        // TODO: ButtonAt() doesn't work afterwards (also affects shortcuts)

        TAlertView* view = (TAlertView*)FindView("_master_");
        if (view)
                view->SetBitmap(_CreateTypeIcon());

        // Get keys
        char key;
        for (int32 i = 0; i < 3; ++i) {
                if (data->FindInt8("_but_key", i, (int8*)&key) == B_OK)
                        fKeys[i] = key;
        }

        AddCommonFilter(new(std::nothrow) _BAlertFilter_(this));
}


BAlert::~BAlert()
{
        // Probably not necessary, but it makes me feel better.
        if (fAlertSem >= B_OK)
                delete_sem(fAlertSem);
}


BArchivable*
BAlert::Instantiate(BMessage* data)
{
        if (!validate_instantiation(data, "BAlert"))
                return NULL;

        return new(std::nothrow) BAlert(data);
}


status_t
BAlert::Archive(BMessage* data, bool deep) const
{
        status_t ret = BWindow::Archive(data, deep);

        // Stow the text
        if (ret == B_OK)
                ret = data->AddString("_text", fTextView->Text());

        // Stow the alert type
        if (ret == B_OK)
                ret = data->AddInt32("_atype", fType);

        // Stow the button width
        if (ret == B_OK)
                ret = data->AddInt32("_but_width", fButtonWidth);

        // Stow the shortcut keys
        if (fKeys[0] || fKeys[1] || fKeys[2]) {
                // If we have any to save, we must save something for everyone so it
                // doesn't get confusing on the unarchive.
                if (ret == B_OK)
                        ret = data->AddInt8("_but_key", fKeys[0]);
                if (ret == B_OK)
                        ret = data->AddInt8("_but_key", fKeys[1]);
                if (ret == B_OK)
                        ret = data->AddInt8("_but_key", fKeys[2]);
        }

        return ret;
}


alert_type
BAlert::Type() const
{
        return (alert_type)fType;
}


void
BAlert::SetType(alert_type type)
{
        fType = type;
}


void
BAlert::SetText(const char* text)
{
        TextView()->SetText(text);
}


void
BAlert::SetIcon(BBitmap* bitmap)
{
        fIconView->SetBitmap(bitmap);
}


void
BAlert::SetButtonSpacing(button_spacing spacing)
{
        fButtonSpacing = spacing;
}


void
BAlert::SetButtonWidth(button_width width)
{
        fButtonWidth = width;
}


void
BAlert::SetShortcut(int32 index, char key)
{
        if (index >= 0 && (size_t)index < fKeys.size())
                fKeys[index] = key;
}


char
BAlert::Shortcut(int32 index) const
{
        if (index >= 0 && (size_t)index < fKeys.size())
                return fKeys[index];

        return 0;
}


int32
BAlert::Go()
{
        fAlertSem = create_sem(0, "AlertSem");
        if (fAlertSem < 0) {
                Quit();
                return -1;
        }

        // Get the originating window, if it exists
        BWindow* window = dynamic_cast<BWindow*>(
                BLooper::LooperForThread(find_thread(NULL)));

        _Prepare();
        Show();
        _PlaySound();

        if (window != NULL) {
                status_t status;
                for (;;) {
                        do {
                                status = acquire_sem_etc(fAlertSem, 1, B_RELATIVE_TIMEOUT,
                                        kSemTimeOut);
                                // We've (probably) had our time slice taken away from us
                        } while (status == B_INTERRUPTED);

                        if (status == B_BAD_SEM_ID) {
                                // Semaphore was finally nuked in MessageReceived
                                break;
                        }
                        window->UpdateIfNeeded();
                }
        } else {
                // No window to update, so just hang out until we're done.
                while (acquire_sem(fAlertSem) == B_INTERRUPTED) {
                }
        }

        // Have to cache the value since we delete on Quit()
        int32 value = fAlertValue;
        if (Lock())
                Quit();

        return value;
}


status_t
BAlert::Go(BInvoker* invoker)
{
        fInvoker = invoker;
        _Prepare();
        Show();
        _PlaySound();
        return B_OK;
}


void
BAlert::MessageReceived(BMessage* msg)
{
        if (msg->what != kAlertButtonMsg)
                return BWindow::MessageReceived(msg);

        int32 which;
        if (msg->FindInt32("which", &which) == B_OK) {
                if (fAlertSem < 0) {
                        // Semaphore hasn't been created; we're running asynchronous
                        if (fInvoker != NULL) {
                                BMessage* out = fInvoker->Message();
                                if (out && (out->ReplaceInt32("which", which) == B_OK
                                                        || out->AddInt32("which", which) == B_OK))
                                        fInvoker->Invoke();
                        }
                        PostMessage(B_QUIT_REQUESTED);
                } else {
                        // Created semaphore means were running synchronously
                        fAlertValue = which;

                        // TextAlertVar does release_sem() below, and then sets the
                        // member var.  That doesn't make much sense to me, since we
                        // want to be able to clean up at some point.  Better to just
                        // nuke the semaphore now; we don't need it any more and this
                        // lets synchronous Go() continue just as well.
                        delete_sem(fAlertSem);
                        fAlertSem = -1;
                }
        }
}


void
BAlert::FrameResized(float newWidth, float newHeight)
{
        BWindow::FrameResized(newWidth, newHeight);
}


void
BAlert::AddButton(const char* label, char key)
{
        if (label == NULL || label[0] == '\0')
                return;

        BButton* button = _CreateButton(fButtons.size(), label);
        fButtons.push_back(button);
        fKeys.push_back(key);

        SetDefaultButton(button);
        fButtonLayout->AddView(button);
}


int32
BAlert::CountButtons() const
{
        return (int32)fButtons.size();
}


BButton*
BAlert::ButtonAt(int32 index) const
{
        if (index >= 0 && (size_t)index < fButtons.size())
                return fButtons[index];

        return NULL;
}


BTextView*
BAlert::TextView() const
{
        return fTextView;
}


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


status_t
BAlert::GetSupportedSuites(BMessage* data)
{
        return BWindow::GetSupportedSuites(data);
}


void
BAlert::DispatchMessage(BMessage* msg, BHandler* handler)
{
        BWindow::DispatchMessage(msg, handler);
}


void
BAlert::Quit()
{
        BWindow::Quit();
}


bool
BAlert::QuitRequested()
{
        return BWindow::QuitRequested();
}


//! This method is deprecated, do not use - use BWindow::CenterIn() instead.
BPoint
BAlert::AlertPosition(float width, float height)
{
        BPoint result(100, 100);

        BWindow* window =
                dynamic_cast<BWindow*>(BLooper::LooperForThread(find_thread(NULL)));

        BScreen screen(window);
        BRect screenFrame(0, 0, 640, 480);
        if (screen.IsValid())
                screenFrame = screen.Frame();

        // Horizontally, we're smack in the middle
        result.x = screenFrame.left + (screenFrame.Width() / 2.0) - (width / 2.0);

        // This is probably sooo wrong, but it looks right on 1024 x 768
        result.y = screenFrame.top + (screenFrame.Height() / 4.0) - ceil(height / 3.0);

        return result;
}


status_t
BAlert::Perform(perform_code code, void* _data)
{
        switch (code) {
                case PERFORM_CODE_SET_LAYOUT:
                        perform_data_set_layout* data = (perform_data_set_layout*)_data;
                        BAlert::SetLayout(data->layout);
                        return B_OK;
        }

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


void BAlert::_ReservedAlert1() {}
void BAlert::_ReservedAlert2() {}
void BAlert::_ReservedAlert3() {}


void
BAlert::_Init(const char* text, const char* button1, const char* button2,
        const char* button3, button_width buttonWidth, button_spacing spacing,
        alert_type type)
{
        fInvoker = NULL;
        fAlertSem = -1;
        fAlertValue = -1;

        fIconView = new TAlertView();

        fTextView = new BTextView("_tv_");
        fTextView->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
        rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR);
        fTextView->SetFontAndColor(be_plain_font, B_FONT_ALL, &textColor);
        fTextView->MakeEditable(false);
        fTextView->MakeSelectable(false);
        fTextView->SetWordWrap(true);
        fTextView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));

        fButtonLayout = new BGroupLayout(B_HORIZONTAL, B_USE_HALF_ITEM_SPACING);

        SetType(type);
        SetButtonWidth(buttonWidth);
        SetButtonSpacing(spacing);
        SetText(text);

        BLayoutBuilder::Group<>(this, B_HORIZONTAL, 0)
                .Add(fIconView)
                .AddGroup(B_VERTICAL, B_USE_HALF_ITEM_SPACING)
                        .SetInsets(B_USE_HALF_ITEM_INSETS)
                        .Add(fTextView)
                        .AddGroup(B_HORIZONTAL, 0)
                                .AddGlue()
                                .Add(fButtonLayout);

        AddButton(button1);
        AddButton(button2);
        AddButton(button3);

        AddCommonFilter(new(std::nothrow) _BAlertFilter_(this));
}


BBitmap*
BAlert::_CreateTypeIcon()
{
        if (Type() == B_EMPTY_ALERT)
                return NULL;

        // The icons are in the app_server resources
        BBitmap* icon = NULL;

        // Which icon are we trying to load?
        const char* iconName;
        switch (fType) {
                case B_INFO_ALERT:
                        iconName = "dialog-information";
                        break;
                case B_IDEA_ALERT:
                        iconName = "dialog-idea";
                        break;
                case B_WARNING_ALERT:
                        iconName = "dialog-warning";
                        break;
                case B_STOP_ALERT:
                        iconName = "dialog-error";
                        break;

                default:
                        // Alert type is either invalid or B_EMPTY_ALERT;
                        // either way, we're not going to load an icon
                        return NULL;
        }

        // Allocate the icon bitmap
        icon = new(std::nothrow) BBitmap(BRect(BPoint(0, 0), be_control_look->ComposeIconSize(32)),
                0, B_RGBA32);
        if (icon == NULL || icon->InitCheck() < B_OK) {
                FTRACE((stderr, "BAlert::_CreateTypeIcon() - No memory for bitmap\n"));
                delete icon;
                return NULL;
        }

        // Load the raw icon data
        BIconUtils::GetSystemIcon(iconName, icon);

        return icon;
}


BButton*
BAlert::_CreateButton(int32 which, const char* label)
{
        BMessage* message = new BMessage(kAlertButtonMsg);
        if (message == NULL)
                return NULL;

        message->AddInt32("which", which);

        char name[32];
        snprintf(name, sizeof(name), "_b%" B_PRId32 "_", which);

        return new(std::nothrow) BButton(name, label, message);
}


/*!     Tweaks the layout according to the configuration.
*/
void
BAlert::_Prepare()
{
        // Must have at least one button
        if (CountButtons() == 0)
                debugger("BAlerts must have at least one button.");

        float fontFactor = be_plain_font->Size() / 11.0f;

        if (fIconView->Bitmap() == NULL)
                fIconView->SetBitmap(_CreateTypeIcon());

        if (fButtonWidth == B_WIDTH_AS_USUAL) {
                float usualWidth = kButtonUsualWidth * fontFactor;

                for (int32 index = 0; index < CountButtons(); index++) {
                        BButton* button = ButtonAt(index);
                        if (button->MinSize().width < usualWidth)
                                button->SetExplicitSize(BSize(usualWidth, B_SIZE_UNSET));
                }
        } else if (fButtonWidth == B_WIDTH_FROM_WIDEST) {
                // Get width of widest label
                float maxWidth = 0;
                for (int32 index = 0; index < CountButtons(); index++) {
                        BButton* button = ButtonAt(index);
                        float width;
                        button->GetPreferredSize(&width, NULL);

                        if (width > maxWidth)
                                maxWidth = width;
                }
                for (int32 index = 0; index < CountButtons(); index++) {
                        BButton* button = ButtonAt(index);
                        button->SetExplicitSize(BSize(maxWidth, B_SIZE_UNSET));
                }
        }

        if (fButtonSpacing == B_OFFSET_SPACING && CountButtons() > 1) {
                // Insert some strut
                fButtonLayout->AddItem(1, BSpaceLayoutItem::CreateHorizontalStrut(
                        kButtonOffsetSpacing * fontFactor));
        }

        // Position the alert so that it is centered vertically but offset a bit
        // horizontally in the parent window's frame or, if unavailable, the
        // screen frame.
        float minWindowWidth = (fButtonSpacing == B_OFFSET_SPACING
                ? kWindowOffsetMinWidth : kWindowMinWidth) * fontFactor;
        GetLayout()->SetExplicitMinSize(BSize(minWindowWidth, B_SIZE_UNSET));

        ResizeToPreferred();

        // Return early if we've already been moved...
        if (Frame().left != 0 && Frame().right != 0)
                return;

        // otherwise center ourselves on-top of parent window/screen
        BWindow* parent = dynamic_cast<BWindow*>(BLooper::LooperForThread(
                find_thread(NULL)));
        const BRect frame = parent != NULL ? parent->Frame()
                : BScreen(this).Frame();

        MoveTo(static_cast<BWindow*>(this)->AlertPosition(frame));
                // Hidden by BAlert::AlertPosition()
}


void
BAlert::_PlaySound()
{
        switch (Type()) {
                case B_INFO_ALERT:
                        system_beep(MEDIA_SOUNDS_INFORMATION_ALERT);
                        break;
                case B_WARNING_ALERT:
                        system_beep(MEDIA_SOUNDS_IMPORTANT_ALERT);
                        break;
                case B_STOP_ALERT:
                        system_beep(MEDIA_SOUNDS_ERROR_ALERT);
                        break;

                default:
                        break;
        }
}


//      #pragma mark - TAlertView


TAlertView::TAlertView()
        :
        BView("TAlertView", B_WILL_DRAW),
        fIconBitmap(NULL)
{
        SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
}


TAlertView::TAlertView(BMessage* archive)
        :
        BView(archive),
        fIconBitmap(NULL)
{
}


TAlertView::~TAlertView()
{
        delete fIconBitmap;
}


TAlertView*
TAlertView::Instantiate(BMessage* archive)
{
        if (!validate_instantiation(archive, "TAlertView"))
                return NULL;

        return new(std::nothrow) TAlertView(archive);
}


status_t
TAlertView::Archive(BMessage* archive, bool deep) const
{
        return BView::Archive(archive, deep);
}


void
TAlertView::SetBitmap(BBitmap* icon)
{
        if (icon == NULL && fIconBitmap == NULL)
                return;

        ASSERT(icon != fIconBitmap);

        BBitmap* oldBitmap = fIconBitmap;
        fIconBitmap = icon;
        Invalidate();

        if (oldBitmap == NULL || icon == NULL || oldBitmap->Bounds() != icon->Bounds())
                InvalidateLayout();

        delete oldBitmap;
}


void
TAlertView::GetPreferredSize(float* _width, float* _height)
{
        if (_width != NULL) {
                *_width = be_control_look->DefaultLabelSpacing() * 3;
                if (fIconBitmap != NULL)
                        *_width += fIconBitmap->Bounds().Width();
                else
                        *_width += be_control_look->ComposeIconSize(B_LARGE_ICON).Width();
        }

        if (_height != NULL) {
                *_height = be_control_look->DefaultLabelSpacing();
                if (fIconBitmap != NULL)
                        *_height += fIconBitmap->Bounds().Height();
                else
                        *_height += be_control_look->ComposeIconSize(B_LARGE_ICON).Height();
        }
}


BSize
TAlertView::MaxSize()
{
        return BSize(MinSize().width, B_SIZE_UNLIMITED);
}


void
TAlertView::Draw(BRect updateRect)
{
        // Here's the fun stuff
        BRect stripeRect = Bounds();
        stripeRect.right = kIconStripeWidthFactor * be_control_look->DefaultLabelSpacing();
        SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
        FillRect(stripeRect);

        if (fIconBitmap == NULL)
                return;

        SetDrawingMode(B_OP_ALPHA);
        SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
        DrawBitmapAsync(fIconBitmap, BPoint(be_control_look->DefaultLabelSpacing() * 3,
                be_control_look->DefaultLabelSpacing()));
}


//      #pragma mark - _BAlertFilter_


_BAlertFilter_::_BAlertFilter_(BAlert* alert)
        : BMessageFilter(B_KEY_DOWN),
        fAlert(alert)
{
}


_BAlertFilter_::~_BAlertFilter_()
{
}


filter_result
_BAlertFilter_::Filter(BMessage* msg, BHandler** target)
{
        if (msg->what == B_KEY_DOWN) {
                char byte;
                if (msg->FindInt8("byte", (int8*)&byte) == B_OK) {
                        for (int i = 0; i < fAlert->CountButtons(); ++i) {
                                if (byte == fAlert->Shortcut(i) && fAlert->ButtonAt(i)) {
                                        char space = ' ';
                                        fAlert->ButtonAt(i)->KeyDown(&space, 1);

                                        return B_SKIP_MESSAGE;
                                }
                        }
                }
        }

        return B_DISPATCH_MESSAGE;
}