root/src/preferences/input/MouseView.cpp
/*
 * Copyright 2019, Haiku, Inc.
 * Distributed under the terms of the MIT License.
 *
 * Author:
 *              Preetpal Kaur <preetpalok123@gmail.com>
 */


#include "MouseView.h"

#include <algorithm>

#include <Box.h>
#include <Button.h>
#include <Debug.h>
#include <GradientLinear.h>
#include <GradientRadial.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <PopUpMenu.h>
#include <Region.h>
#include <Shape.h>
#include <Slider.h>
#include <TextControl.h>
#include <TranslationUtils.h>
#include <TranslatorFormats.h>
#include <Window.h>

#include "InputConstants.h"
#include "MouseSettings.h"


static const int32 kButtonTop = 3;
static const int32 kMouseDownWidth = 72;
static const int32 kMouseDownHeight = 35;

#define W kMouseDownWidth / 100
static const int32 kButtonOffsets[][7] = {
        { 0, 100 * W },
        { 0, 50 * W, 100 * W },
        { 0, 35 * W, 65 * W, 100 * W },
        { 0, 25 * W, 50 * W, 75 * W, 100 * W },
        { 0, 20 * W, 40 * W, 60 * W, 80 * W, 100 * W },
        { 0, 19 * W, 34 * W, 50 * W, 66 * W, 82 * W, 100 * W }
};
#undef W

static const rgb_color kButtonTextColor = {0, 0, 0, 255};
static const rgb_color kMouseShadowColor = {100, 100, 100, 128};
static const rgb_color kMouseBodyTopColor = {0xed, 0xed, 0xed, 255};
static const rgb_color kMouseBodyBottomColor = {0x85, 0x85, 0x85, 255};
static const rgb_color kMouseOutlineColor = {0x51, 0x51, 0x51, 255};
static const rgb_color kMouseButtonOutlineColor = {0xa0, 0xa0, 0xa0, 255};
static const rgb_color kButtonPressedColor = {110, 110, 110, 110};


static const int32*
getButtonOffsets(int32 type)
{
        if ((type - 1) >= (int32)B_COUNT_OF(kButtonOffsets))
                return kButtonOffsets[2];
        return kButtonOffsets[type - 1];
}


static uint32
getMappingNumber(uint32 mapping)
{
        if (mapping == 0)
                return 0;

        int i;
        for (i = 0; mapping != 1; i++)
                mapping >>= 1;

        return i;
}


MouseView::MouseView(const MouseSettings& settings)
        :
        BView("Mouse", B_PULSE_NEEDED | B_WILL_DRAW),
        fSettings(settings),
        fType(-1),
        fButtons(0),
        fOldButtons(0)
{
        SetEventMask(B_POINTER_EVENTS, B_NO_POINTER_HISTORY);
        fScaling = std::max(1.0f, be_plain_font->Size() / 7.0f);
}


MouseView::~MouseView()
{
}


void
MouseView::SetMouseType(int32 type)
{
        fType = type;
        Invalidate();
}


void
MouseView::MouseMapUpdated()
{
        Invalidate();
}


void
MouseView::UpdateFromSettings()
{
        if (fSettings.MouseType() > 6)
                debugger("Mouse type is invalid");
        SetMouseType(fSettings.MouseType());
}


void
MouseView::GetPreferredSize(float* _width, float* _height)
{
        if (_width != NULL)
                *_width = fScaling * (kMouseDownWidth + 2);
        if (_height != NULL)
                *_height = fScaling * 104;
}


void
MouseView::AttachedToWindow()
{
        AdoptParentColors();

        UpdateFromSettings();
        _CreateButtonsPicture();

        font_height fontHeight;
        GetFontHeight(&fontHeight);
        fDigitHeight = int32(ceilf(fontHeight.ascent) + ceilf(fontHeight.descent));
        fDigitBaseline = int32(ceilf(fontHeight.ascent));
}


void
MouseView::MouseUp(BPoint)
{
        fButtons = 0;
        Invalidate(_ButtonsRect());
        fOldButtons = fButtons;
}


void
MouseView::MouseDown(BPoint where)
{
        BMessage* mouseMsg = Window()->CurrentMessage();
        fButtons = mouseMsg->FindInt32("buttons");
        int32 modifiers = mouseMsg->FindInt32("modifiers");
        if (modifiers & B_CONTROL_KEY) {
                if (modifiers & B_COMMAND_KEY)
                        fButtons = B_TERTIARY_MOUSE_BUTTON;
                else
                        fButtons = B_SECONDARY_MOUSE_BUTTON;
        }
        // Get the current clipping region before requesting any updates.
        // Otherwise those parts would be excluded from the region.
        BRegion clipping;
        GetClippingRegion(&clipping);

        if (fOldButtons != fButtons) {
                Invalidate(_ButtonsRect());
                fOldButtons = fButtons;
        }

        const int32* offset = getButtonOffsets(fType);
        int32 button = -1;
        for (int32 i = 0; i <= fType; i++) {
                if (_ButtonRect(offset, i).Contains(where)) {
                        button = i;
                        break;
                }
        }
        if (button < 0)
                return;

        if (clipping.Contains(where)) {
                button = _ConvertFromVisualOrder(button);

                BPopUpMenu menu("Mouse Map Menu");
                BMessage message(kMsgMouseMap);
                message.AddInt32("button", button);

                for (int i = 1; i < 7; i++) {
                        char tmp[2];
                        sprintf(tmp, "%d", i);
                        menu.AddItem(new BMenuItem(tmp, new BMessage(message)));
                }

                int32 mapping = fSettings.Mapping(button);
                BMenuItem* item = menu.ItemAt(getMappingNumber(mapping));
                if (item)
                        item->SetMarked(true);
                menu.SetTargetForItems(Window());

                ConvertToScreen(&where);
                menu.Go(where, true);
        }
}


void
MouseView::Draw(BRect updateFrame)
{
        SetDrawingMode(B_OP_ALPHA);
        SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
        SetScale(fScaling * 1.8);

        BShape mouseShape;
        mouseShape.MoveTo(BPoint(16, 12));
        // left
        BPoint control[3] = {BPoint(12, 16), BPoint(8, 64), BPoint(32, 64)};
        mouseShape.BezierTo(control);
        // right
        BPoint control2[3] = {BPoint(56, 64), BPoint(52, 16), BPoint(48, 12)};
        mouseShape.BezierTo(control2);
        // top
        BPoint control3[3] = {BPoint(44, 8), BPoint(20, 8), BPoint(16, 12)};
        mouseShape.BezierTo(control3);
        mouseShape.Close();

        // Draw the shadow
        SetOrigin(-17 * fScaling, -11 * fScaling);
        SetHighColor(kMouseShadowColor);
        FillShape(&mouseShape, B_SOLID_HIGH);

        // Draw the body
        SetOrigin(-21 * fScaling, -14 * fScaling);
        BGradientRadial bodyGradient(28, 24, 128);
        bodyGradient.AddColor(kMouseBodyTopColor, 0);
        bodyGradient.AddColor(kMouseBodyBottomColor, 255);

        FillShape(&mouseShape, bodyGradient);

        // Draw the outline
        SetPenSize(1 / 1.8 / fScaling);
        SetDrawingMode(B_OP_OVER);
        SetHighColor(kMouseOutlineColor);

        StrokeShape(&mouseShape, B_SOLID_HIGH);

        // bottom button border
        BShape buttonsOutline;
        buttonsOutline.MoveTo(BPoint(13, 27));
        BPoint control4[] = {BPoint(18, 30), BPoint(46, 30), BPoint(51, 27)};
        buttonsOutline.BezierTo(control4);

        SetHighColor(kMouseButtonOutlineColor);
        StrokeShape(&buttonsOutline, B_SOLID_HIGH);

        SetScale(1);
        SetOrigin(0, 0);

        mouse_map map;
        fSettings.Mapping(map);

        SetDrawingMode(B_OP_OVER);

        // All button drawing is clipped to the outline of the buttons area,
        // simplifying the code below as it can overdraw things.
        ClipToPicture(&fButtonsPicture, B_ORIGIN, false);

        // Separator between the buttons
        const int32* offset = getButtonOffsets(fType);
        for (int32 i = 1; i < fType; i++) {
                BRect buttonRect = _ButtonRect(offset, i);
                StrokeLine(buttonRect.LeftTop(), buttonRect.LeftBottom());
        }

        for (int32 i = 0; i < fType; i++) {
                // draw mapping number centered over the button

                bool pressed = (fButtons & map.button[_ConvertFromVisualOrder(i)]) != 0;
                // is button currently pressed?
                if (pressed) {
                        SetDrawingMode(B_OP_ALPHA);
                        SetHighColor(kButtonPressedColor);
                        FillRect(_ButtonRect(offset, i));
                }

                BRect border(fScaling * (offset[i] + 1), fScaling * (kButtonTop + 5),
                        fScaling * offset[i + 1] - 1,
                        fScaling * (kButtonTop + kMouseDownHeight - 4));
                if (i == 0)
                        border.left += fScaling * 5;
                if (i == fType - 1)
                        border.right -= fScaling * 4;

                char label[2] = {0};
                int32 number = getMappingNumber(map.button[_ConvertFromVisualOrder(i)]);
                label[0] = number + '1';

                SetDrawingMode(B_OP_OVER);
                SetHighColor(kButtonTextColor);
                DrawString(label,
                        BPoint(border.left + (border.Width() - StringWidth(label)) / 2,
                                border.top + fDigitBaseline
                                        + (border.IntegerHeight() - fDigitHeight) / 2));
        }

        ClipToPicture(NULL);
}


BRect
MouseView::_ButtonsRect() const
{
        return BRect(0, fScaling * kButtonTop, fScaling * kMouseDownWidth,
                fScaling * (kButtonTop + kMouseDownHeight));
}


BRect
MouseView::_ButtonRect(const int32* offsets, int index) const
{
        return BRect(fScaling * offsets[index], fScaling * kButtonTop,
                fScaling * offsets[index + 1] - 1,
                fScaling * (kButtonTop + kMouseDownHeight));
}


/** The buttons on a mouse are normally 1 (left), 2 (right), 3 (middle) so
 * we need to reorder them */
int32
MouseView::_ConvertFromVisualOrder(int32 i)
{
        if (fType < 3)
                return i;

        switch (i) {
                case 0:
                        return 0;
                case 1:
                        return 2;
                case 2:
                        return 1;
                default:
                        return i;
        }
}


void
MouseView::_CreateButtonsPicture()
{
        BeginPicture(&fButtonsPicture);
        SetScale(1.8 * fScaling);
        SetOrigin(-21 * fScaling, -14 * fScaling);

        BShape mouseShape;
        mouseShape.MoveTo(BPoint(48, 12));
        // top
        BPoint control3[3] = {BPoint(44, 8), BPoint(20, 8), BPoint(16, 12)};
        mouseShape.BezierTo(control3);
        // left
        BPoint control[3] = {BPoint(12, 16), BPoint(13, 27), BPoint(13, 27)};
        mouseShape.BezierTo(control);
        // bottom
        BPoint control4[3] = {BPoint(18, 30), BPoint(46, 30), BPoint(51, 27)};
        mouseShape.BezierTo(control4);
        // right
        BPoint control2[3] = {BPoint(51, 27), BPoint(50, 14), BPoint(48, 12)};
        mouseShape.BezierTo(control2);

        mouseShape.Close();

        SetHighColor(255, 0, 0, 255);
        FillShape(&mouseShape, B_SOLID_HIGH);

        EndPicture();
        SetOrigin(0, 0);
        SetScale(1);
}