⛏️ index : haiku.git

/*
 * Copyright 2019-2025, Haiku, Inc.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Preetpal Kaur <preetpalok123@gmail.com>
 *		Pawan Yerramilli <me@pawanyerramilli.com>
 *		Samuel Rodríguez Pérez <samuelrp84@gmail.com>
 */


#include "InputTouchpadPrefView.h"

#include <stdio.h>

#include <Alert.h>
#include <Box.h>
#include <Catalog.h>
#include <CheckBox.h>
#include <ControlLook.h>
#include <File.h>
#include <FindDirectory.h>
#include <Input.h>
#include <LayoutBuilder.h>
#include <Locale.h>
#include <MenuField.h>
#include <MenuItem.h>
#include <Message.h>
#include <Path.h>
#include <Screen.h>
#include <SeparatorView.h>
#include <SpaceLayoutItem.h>
#include <Window.h>

#include <keyboard_mouse_driver.h>


const uint32 SCROLL_X_DRAG = 'sxdr';
const uint32 SCROLL_Y_DRAG = 'sydr';

#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "TouchpadPrefView"


TouchpadView::TouchpadView(BRect frame)
	:
	BView(frame, "TouchpadView", B_FOLLOW_NONE, B_WILL_DRAW)
{
	fXTracking = false;
	fYTracking = false;
	fOffScreenView = NULL;
	fOffScreenBitmap = NULL;

	fPrefRect = frame;
	fPadRect = fPrefRect;
	fPadRect.InsetBy(10, 10);
	fXScrollRange = fPadRect.Width();
	fYScrollRange = fPadRect.Height();
}


TouchpadView::~TouchpadView()
{
	delete fOffScreenBitmap;
}


void
TouchpadView::Draw(BRect updateRect)
{
	DrawSliders();
}


void
TouchpadView::MouseDown(BPoint point)
{
	if (fXScrollDragZone.Contains(point)) {
		fXTracking = true;
		fOldXScrollRange = fXScrollRange;
		SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
	}

	if (fYScrollDragZone.Contains(point)) {
		fYTracking = true;
		fOldYScrollRange = fYScrollRange;
		SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
	}
}


void
TouchpadView::MouseUp(BPoint point)
{
	if (!fXTracking && !fYTracking)
		return;

	fXTracking = false;
	fYTracking = false;

	const float kSoftScrollLimit = 0.7;

	int32 result = 0;
	if (GetRightScrollRatio() > kSoftScrollLimit
		|| GetBottomScrollRatio() > kSoftScrollLimit) {
		BAlert* alert = new BAlert(B_TRANSLATE("Please confirm"),
			B_TRANSLATE(
				"The new scroll area is very large and may impede "
				"normal mouse operation. Do you really want to change it?"),
			B_TRANSLATE("OK"), B_TRANSLATE("Cancel"), NULL, B_WIDTH_AS_USUAL,
			B_WARNING_ALERT);
		alert->SetShortcut(1, B_ESCAPE);
		result = alert->Go();
	}

	if (result == 0) {
		BMessage msg(SCROLL_AREA_CHANGED);
		Invoke(&msg);
	} else {
		if (GetRightScrollRatio() > kSoftScrollLimit)
			fXScrollRange = fOldXScrollRange;
		if (GetBottomScrollRatio() > kSoftScrollLimit)
			fYScrollRange = fOldYScrollRange;
		DrawSliders();
	}
}


void
TouchpadView::AttachedToWindow()
{
	if (!fOffScreenView)
		fOffScreenView = new BView(Bounds(), "", B_FOLLOW_ALL, B_WILL_DRAW);

	if (!fOffScreenBitmap) {
		fOffScreenBitmap = new BBitmap(Bounds(), B_CMAP8, true, false);

		if (fOffScreenBitmap && fOffScreenView)
			fOffScreenBitmap->AddChild(fOffScreenView);
	}
}


void
TouchpadView::SetValues(float rightRange, float bottomRange)
{
	fXScrollRange = fPadRect.Width() * (1 - rightRange);
	fYScrollRange = fPadRect.Height() * (1 - bottomRange);
	Invalidate();
}


void
TouchpadView::GetPreferredSize(float* width, float* height)
{
	if (width != NULL)
		*width = fPrefRect.Width();
	if (height != NULL)
		*height = fPrefRect.Height();
}


void
TouchpadView::MouseMoved(BPoint point, uint32 transit, const BMessage* message)
{
	if (fXTracking) {
		if (point.x > fPadRect.right)
			fXScrollRange = fPadRect.Width();
		else if (point.x < fPadRect.left)
			fXScrollRange = 0;
		else
			fXScrollRange = point.x - fPadRect.left;

		DrawSliders();
	}

	if (fYTracking) {
		if (point.y > fPadRect.bottom)
			fYScrollRange = fPadRect.Height();
		else if (point.y < fPadRect.top)
			fYScrollRange = 0;
		else
			fYScrollRange = point.y - fPadRect.top;

		DrawSliders();
	}
}


void
TouchpadView::DrawSliders()
{
	BView* view = fOffScreenView != NULL ? fOffScreenView : this;

	if (!LockLooper())
		return;

	if (fOffScreenBitmap->Lock()) {
		view->SetHighColor(ui_color(B_PANEL_BACKGROUND_COLOR));
		view->FillRect(Bounds());
		view->SetHighColor(100, 100, 100);
		view->FillRoundRect(fPadRect, 4, 4);

		int32 dragSize = 3; // half drag size

		// scroll areas
		view->SetHighColor(145, 100, 100);
		BRect rightRect(fPadRect.left + fXScrollRange, fPadRect.top,
			fPadRect.right, fPadRect.bottom);
		view->FillRoundRect(rightRect, 4, 4);

		BRect bottomRect(fPadRect.left, fPadRect.top + fYScrollRange,
			fPadRect.right, fPadRect.bottom);
		view->FillRoundRect(bottomRect, 4, 4);

		// Stroke Rect
		view->SetHighColor(100, 100, 100);
		view->SetPenSize(2);
		view->StrokeRoundRect(fPadRect, 4, 4);

		// x scroll range line
		view->SetHighColor(200, 0, 0);
		view->StrokeLine(BPoint(fPadRect.left + fXScrollRange, fPadRect.top),
			BPoint(fPadRect.left + fXScrollRange, fPadRect.bottom));

		fXScrollDragZone = BRect(fPadRect.left + fXScrollRange - dragSize,
			fPadRect.top - dragSize, fPadRect.left + fXScrollRange + dragSize,
			fPadRect.bottom + dragSize);
		BRect xscrollDragZone1 = BRect(fPadRect.left + fXScrollRange - dragSize,
			fPadRect.top - dragSize, fPadRect.left + fXScrollRange + dragSize,
			fPadRect.top + dragSize);
		view->FillRect(xscrollDragZone1);
		BRect xscrollDragZone2 = BRect(fPadRect.left + fXScrollRange - dragSize,
			fPadRect.bottom - dragSize,
			fPadRect.left + fXScrollRange + dragSize,
			fPadRect.bottom + dragSize);
		view->FillRect(xscrollDragZone2);

		// y scroll range line
		view->StrokeLine(BPoint(fPadRect.left, fPadRect.top + fYScrollRange),
			BPoint(fPadRect.right, fPadRect.top + fYScrollRange));

		fYScrollDragZone = BRect(fPadRect.left - dragSize,
			fPadRect.top + fYScrollRange - dragSize, fPadRect.right + dragSize,
			fPadRect.top + fYScrollRange + dragSize);
		BRect yscrollDragZone1 = BRect(fPadRect.left - dragSize,
			fPadRect.top + fYScrollRange - dragSize, fPadRect.left + dragSize,
			fPadRect.top + fYScrollRange + dragSize);
		view->FillRect(yscrollDragZone1);
		BRect yscrollDragZone2 = BRect(fPadRect.right - dragSize,
			fPadRect.top + fYScrollRange - dragSize, fPadRect.right + dragSize,
			fPadRect.top + fYScrollRange + dragSize);
		view->FillRect(yscrollDragZone2);

		view->Sync();
		fOffScreenBitmap->Unlock();
		DrawBitmap(fOffScreenBitmap, B_ORIGIN);
	}

	UnlockLooper();
}


//	#pragma mark - TouchpadPrefView


TouchpadPrefView::TouchpadPrefView(BInputDevice* dev)
	:
	BGroupView(),
	fTouchpadPref(dev)
{
	SetupView();
	// set view values
	SetValues(&fTouchpadPref.Settings());
}


TouchpadPrefView::~TouchpadPrefView()
{
}


void
TouchpadPrefView::MessageReceived(BMessage* message)
{
	touchpad_settings& settings = fTouchpadPref.Settings();

	switch (message->what) {
		case SCROLL_AREA_CHANGED:
			settings.scroll_rightrange = fTouchpadView->GetRightScrollRatio();
			settings.scroll_bottomrange = fTouchpadView->GetBottomScrollRatio();
			fRevertButton->SetEnabled(true);
			fTouchpadPref.UpdateRunningSettings();
			break;

		case SCROLL_CONTROL_CHANGED:
			settings.scroll_reverse = fScrollReverseBox->Value() == B_CONTROL_ON;
			settings.scroll_twofinger = fTwoFingerBox->Value() == B_CONTROL_ON;
			settings.scroll_twofinger_horizontal
				= fTwoFingerHorizontalBox->Value() == B_CONTROL_ON;
			settings.scroll_acceleration = fScrollAccelSlider->Value();
			settings.scroll_xstepsize = (20 - fScrollStepXSlider->Value()) * 3;
			settings.scroll_ystepsize = (20 - fScrollStepYSlider->Value()) * 3;
			fTwoFingerHorizontalBox->SetEnabled(settings.scroll_twofinger);
			fRevertButton->SetEnabled(true);
			fTouchpadPref.UpdateRunningSettings();
			break;

		case TAP_CONTROL_CHANGED:
			settings.tapgesture_sensibility = fTapSlider->Value();
			fRevertButton->SetEnabled(true);
			fTouchpadPref.UpdateRunningSettings();
			break;

		case PADBLOCK_TIME_CHANGED:
			settings.padblocker_threshold = fPadBlockerSlider->Value();
			// The maximum value means "disabled", but in the settings file that
			// must be stored as 0
			if (settings.padblocker_threshold == 1000)
				settings.padblocker_threshold = 0;
			fRevertButton->SetEnabled(true);
			fTouchpadPref.UpdateRunningSettings();
			break;

		case PAD_SPEED_CHANGED:
		{
			fTouchpadPref.SetSpeed(fSpeedSlider->Value());
			fRevertButton->SetEnabled(true);
			break;
		}
		case PAD_ACCELERATION_CHANGED:
		{
			fTouchpadPref.SetAcceleration(fAccelSlider->Value());
			fRevertButton->SetEnabled(true);
			break;
		}
		case DEFAULT_SETTINGS:
			fTouchpadPref.Defaults();
			fRevertButton->SetEnabled(true);
			fTouchpadPref.UpdateRunningSettings();
			SetValues(&settings);
			break;

		case REVERT_SETTINGS:
			fTouchpadPref.Revert();
			fTouchpadPref.UpdateRunningSettings();
			fRevertButton->SetEnabled(false);
			SetValues(&settings);
			break;

		default:
			BView::MessageReceived(message);
	}
}


void
TouchpadPrefView::AttachedToWindow()
{
	fTouchpadView->SetTarget(this);
	fScrollReverseBox->SetTarget(this);
	fTwoFingerBox->SetTarget(this);
	fTwoFingerHorizontalBox->SetTarget(this);
	fScrollStepXSlider->SetTarget(this);
	fScrollStepYSlider->SetTarget(this);
	fScrollAccelSlider->SetTarget(this);
	fPadBlockerSlider->SetTarget(this);
	fTapSlider->SetTarget(this);
	fSpeedSlider->SetTarget(this);
	fAccelSlider->SetTarget(this);
	fDefaultButton->SetTarget(this);
	fRevertButton->SetTarget(this);
	BSize size = PreferredSize();
	Window()->ResizeTo(size.width, size.height);

	BPoint position = fTouchpadPref.WindowPosition();
	// center window on screen if it had a bad position
	if (position.x < 0 && position.y < 0)
		Window()->CenterOnScreen();
	else
		Window()->MoveTo(position);
}


void
TouchpadPrefView::DetachedFromWindow()
{
	fTouchpadPref.SetWindowPosition(Window()->Frame().LeftTop());
}


void
TouchpadPrefView::SetupView()
{
	SetLayout(new BGroupLayout(B_VERTICAL));
	BBox* scrollBox = new BBox("Touchpad");
	scrollBox->SetLabel(B_TRANSLATE("Scrolling"));


	fTouchpadView = new TouchpadView(BRect(0, 0, 130, 120));
	fTouchpadView->SetExplicitMaxSize(BSize(130, 120));

	// Create the scrolling acceleration slider...
	fScrollAccelSlider = new BSlider("scroll_accel",
		B_TRANSLATE("Acceleration"),
		new BMessage(SCROLL_CONTROL_CHANGED), 0, 20, B_HORIZONTAL);
	fScrollAccelSlider->SetHashMarks(B_HASH_MARKS_BOTTOM);
	fScrollAccelSlider->SetHashMarkCount(7);
	fScrollAccelSlider->SetLimitLabels(
		B_TRANSLATE("Low"), B_TRANSLATE("High"));
	fScrollAccelSlider->SetExplicitMinSize(BSize(150, B_SIZE_UNSET));

	fScrollStepXSlider = new BSlider("scroll_stepX", B_TRANSLATE("Horizontal"),
		new BMessage(SCROLL_CONTROL_CHANGED), 0, 20, B_HORIZONTAL);
	fScrollStepXSlider->SetHashMarks(B_HASH_MARKS_BOTTOM);
	fScrollStepXSlider->SetHashMarkCount(7);
	fScrollStepXSlider->SetLimitLabels(
		B_TRANSLATE("Slow"), B_TRANSLATE("Fast"));

	fScrollStepYSlider = new BSlider("scroll_stepY", B_TRANSLATE("Vertical"),
		new BMessage(SCROLL_CONTROL_CHANGED), 0, 20, B_HORIZONTAL);
	fScrollStepYSlider->SetHashMarks(B_HASH_MARKS_BOTTOM);
	fScrollStepYSlider->SetHashMarkCount(7);
	fScrollStepYSlider->SetLimitLabels(
		B_TRANSLATE("Slow"), B_TRANSLATE("Fast"));

	fPadBlockerSlider
		= new BSlider("padblocker", B_TRANSLATE("Keyboard lock delay"),
			new BMessage(PADBLOCK_TIME_CHANGED), 5, 1000, B_HORIZONTAL);
	fPadBlockerSlider->SetHashMarks(B_HASH_MARKS_BOTTOM);
	fPadBlockerSlider->SetHashMarkCount(10);
	fPadBlockerSlider->SetLimitLabels(
		B_TRANSLATE("Quick"), B_TRANSLATE("Never"));

	fScrollReverseBox = new BCheckBox(B_TRANSLATE("Reverse scroll direction"),
		new BMessage(SCROLL_CONTROL_CHANGED));
	fTwoFingerBox = new BCheckBox(B_TRANSLATE("Two finger scrolling"),
		new BMessage(SCROLL_CONTROL_CHANGED));
	fTwoFingerHorizontalBox = new BCheckBox(B_TRANSLATE("Horizontal scrolling"),
		new BMessage(SCROLL_CONTROL_CHANGED));

	float spacing = be_control_look->DefaultItemSpacing();

	BView* scrollPrefLeftLayout
		= BLayoutBuilder::Group<>(B_VERTICAL, 0)
		.Add(fTouchpadView)
		.AddStrut(spacing)
		.Add(fScrollReverseBox)
		.Add(fTwoFingerBox)
		.AddGroup(B_HORIZONTAL, 0)
			.AddStrut(spacing * 2)
			.Add(fTwoFingerHorizontalBox)
			.End()
		.AddGlue()
		.View();

	BGroupView* scrollPrefRightLayout = new BGroupView(B_VERTICAL);
	scrollPrefRightLayout->AddChild(fScrollAccelSlider);
	scrollPrefRightLayout->AddChild(fScrollStepXSlider);
	scrollPrefRightLayout->AddChild(fScrollStepYSlider);

	BGroupLayout* scrollPrefLayout = new BGroupLayout(B_HORIZONTAL);
	scrollPrefLayout->SetSpacing(spacing);
	scrollPrefLayout->SetInsets(
		spacing, scrollBox->TopBorderOffset() * 2 + spacing, spacing, spacing);
	scrollBox->SetLayout(scrollPrefLayout);

	scrollPrefLayout->AddView(scrollPrefLeftLayout);
	scrollPrefLayout->AddItem(
		BSpaceLayoutItem::CreateVerticalStrut(spacing * 1.5));
	scrollPrefLayout->AddView(scrollPrefRightLayout);

	fTapSlider = new BSlider("tap_sens", B_TRANSLATE("Tapping sensitivity"),
		new BMessage(TAP_CONTROL_CHANGED), 0, 50, B_HORIZONTAL);
	fTapSlider->SetHashMarks(B_HASH_MARKS_BOTTOM);
	fTapSlider->SetHashMarkCount(11);
	fTapSlider->SetLimitLabels(B_TRANSLATE("Off"), B_TRANSLATE("High"));

	fSpeedSlider = new BSlider("pad_speed", B_TRANSLATE("Trackpad speed"),
		new BMessage(PAD_SPEED_CHANGED), 0, 1000, B_HORIZONTAL);
	fSpeedSlider->SetHashMarks(B_HASH_MARKS_BOTTOM);
	fSpeedSlider->SetHashMarkCount(7);
	fSpeedSlider->SetLimitLabels(B_TRANSLATE("Slow"), B_TRANSLATE("Fast"));

	fAccelSlider = new BSlider("pad_accel", B_TRANSLATE("Trackpad acceleration"),
		new BMessage(PAD_ACCELERATION_CHANGED), 0, 1000, B_HORIZONTAL);
	fAccelSlider->SetHashMarks(B_HASH_MARKS_BOTTOM);
	fAccelSlider->SetHashMarkCount(7);
	fAccelSlider->SetLimitLabels(B_TRANSLATE("Low"), B_TRANSLATE("High"));

	fDefaultButton
		= new BButton(B_TRANSLATE("Defaults"), new BMessage(DEFAULT_SETTINGS));

	fRevertButton
		= new BButton(B_TRANSLATE("Revert"), new BMessage(REVERT_SETTINGS));
	fRevertButton->SetEnabled(false);


	BLayoutBuilder::Group<>(this, B_VERTICAL)
		.SetInsets(B_USE_WINDOW_SPACING)
		.Add(scrollBox)
		.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING)
			.AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING)
				.Add(fTapSlider)
				.Add(fPadBlockerSlider)
				.End()
			.Add(new BSeparatorView(B_VERTICAL))
			.AddGroup(B_VERTICAL, B_USE_DEFAULT_SPACING)
				.Add(fSpeedSlider)
				.Add(fAccelSlider)
				.End()
			.End()
		.Add(new BSeparatorView(B_HORIZONTAL))
			.AddGroup(B_HORIZONTAL)
			.Add(fDefaultButton)
			.Add(fRevertButton)
			.AddGlue()
			.End()
		.End();
}


void
TouchpadPrefView::SetValues(touchpad_settings* settings)
{
	fTouchpadView->SetValues(settings->scroll_rightrange, settings->scroll_bottomrange);
	fScrollReverseBox->SetValue(settings->scroll_reverse ? B_CONTROL_ON : B_CONTROL_OFF);
	fTwoFingerBox->SetValue(settings->scroll_twofinger ? B_CONTROL_ON : B_CONTROL_OFF);
	fTwoFingerHorizontalBox->SetValue(
		settings->scroll_twofinger_horizontal ? B_CONTROL_ON : B_CONTROL_OFF);
	fTwoFingerHorizontalBox->SetEnabled(settings->scroll_twofinger);
	fScrollStepXSlider->SetValue(20 - settings->scroll_xstepsize / 2);
	fScrollStepYSlider->SetValue(20 - settings->scroll_ystepsize / 2);
	fScrollAccelSlider->SetValue(settings->scroll_acceleration);
	fTapSlider->SetValue(settings->tapgesture_sensibility);
	fPadBlockerSlider->SetValue(settings->padblocker_threshold);
	int32 value = int32((log(settings->trackpad_speed / 8192.0) / log(2)) * 1000 / 6);
	fSpeedSlider->SetValue(value);
	value = int32(sqrt(settings->trackpad_acceleration / 16384.0) * 1000 / 4);
	fAccelSlider->SetValue(value);
}