1/*
2 * Copyright 2001-2020 Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT license.
4 *
5 * Authors:
6 *		Stephan A��mus, superstippi@gmx.de
7 *		Stefano Ceccherini, burton666@libero.it
8 *		DarkWyrm, bpmagic@columbus.rr.com
9 *		Marc Flerackers, mflerackers@androme.be
10 *		John Scipione, jscipione@gmail.com
11 */
12
13
14#include <ScrollBar.h>
15
16#include <algorithm>
17
18#include <math.h>
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22
23#include <ControlLook.h>
24#include <LayoutUtils.h>
25#include <Message.h>
26#include <OS.h>
27#include <Shape.h>
28#include <Window.h>
29
30#include <binary_compatibility/Interface.h>
31
32
33//#define TRACE_SCROLLBAR
34#ifdef TRACE_SCROLLBAR
35#	define TRACE(x...) printf(x)
36#else
37#	define TRACE(x...)
38#endif
39
40
41typedef enum {
42	ARROW_LEFT = 0,
43	ARROW_RIGHT,
44	ARROW_UP,
45	ARROW_DOWN,
46	ARROW_NONE
47} arrow_direction;
48
49
50#define SCROLL_BAR_MAXIMUM_KNOB_SIZE	50
51#define SCROLL_BAR_MINIMUM_KNOB_SIZE	9
52
53#define SBC_SCROLLBYVALUE	0
54#define SBC_SETDOUBLE		1
55#define SBC_SETPROPORTIONAL	2
56#define SBC_SETSTYLE		3
57
58// Quick constants for determining which arrow is down and are defined with
59// respect to double arrow mode. ARROW1 and ARROW4 refer to the outer pair of
60// arrows and ARROW2 and ARROW3 refer to the inner ones. ARROW1 points left/up
61// and ARROW4 points right/down.
62#define ARROW1	0
63#define ARROW2	1
64#define ARROW3	2
65#define ARROW4	3
66#define THUMB	4
67#define NOARROW	-1
68
69
70static const bigtime_t kRepeatDelay = 300000;
71
72
73// Because the R5 version kept a lot of data on server-side, we need to kludge
74// our way into binary compatibility
75class BScrollBar::Private {
76public:
77	Private(BScrollBar* scrollBar)
78	:
79	fScrollBar(scrollBar),
80	fEnabled(true),
81	fRepeaterThread(-1),
82	fExitRepeater(false),
83	fRepeaterDelay(0),
84	fThumbFrame(0.0, 0.0, -1.0, -1.0),
85	fDoRepeat(false),
86	fClickOffset(0.0, 0.0),
87	fThumbInc(0.0),
88	fStopValue(0.0),
89	fUpArrowsEnabled(true),
90	fDownArrowsEnabled(true),
91	fBorderHighlighted(false),
92	fButtonDown(NOARROW)
93	{
94#ifdef TEST_MODE
95		fScrollBarInfo.proportional = true;
96		fScrollBarInfo.double_arrows = true;
97		fScrollBarInfo.knob = 0;
98		fScrollBarInfo.min_knob_size = 15;
99#else
100		get_scroll_bar_info(&fScrollBarInfo);
101#endif
102
103		fScrollBarInfo.min_knob_size = (int32)(fScrollBarInfo.min_knob_size *
104				(be_plain_font->Size() / 12.0f));
105	}
106
107	~Private()
108	{
109		if (fRepeaterThread >= 0) {
110			status_t dummy;
111			fExitRepeater = true;
112			wait_for_thread(fRepeaterThread, &dummy);
113		}
114	}
115
116	void DrawScrollBarButton(BScrollBar* owner, arrow_direction direction,
117		BRect frame, bool down = false);
118
119	static int32 button_repeater_thread(void* data);
120
121	int32 ButtonRepeaterThread();
122
123	BScrollBar*			fScrollBar;
124	bool				fEnabled;
125
126	// TODO: This should be a static, initialized by
127	// _init_interface_kit() at application startup-time,
128	// like BMenu::sMenuInfo
129	scroll_bar_info		fScrollBarInfo;
130
131	thread_id			fRepeaterThread;
132	volatile bool		fExitRepeater;
133	bigtime_t			fRepeaterDelay;
134
135	BRect				fThumbFrame;
136	volatile bool		fDoRepeat;
137	BPoint				fClickOffset;
138
139	float				fThumbInc;
140	float				fStopValue;
141
142	bool				fUpArrowsEnabled;
143	bool				fDownArrowsEnabled;
144
145	bool				fBorderHighlighted;
146
147	int8				fButtonDown;
148};
149
150
151// This thread is spawned when a button is initially pushed and repeatedly scrolls
152// the scrollbar by a little bit after a short delay
153int32
154BScrollBar::Private::button_repeater_thread(void* data)
155{
156	BScrollBar::Private* privateData = (BScrollBar::Private*)data;
157	return privateData->ButtonRepeaterThread();
158}
159
160
161int32
162BScrollBar::Private::ButtonRepeaterThread()
163{
164	// Wait a bit before auto scrolling starts. As long as the user releases
165	// and presses the button again while the repeat delay has not yet
166	// triggered, the value is pushed into the future, so we need to loop such
167	// that repeating starts at exactly the correct delay after the last
168	// button press.
169	while (fRepeaterDelay > system_time() && !fExitRepeater)
170		snooze_until(fRepeaterDelay, B_SYSTEM_TIMEBASE);
171
172	// repeat loop
173	while (!fExitRepeater) {
174		if (fScrollBar->LockLooper()) {
175			if (fDoRepeat) {
176				float value = fScrollBar->Value() + fThumbInc;
177				if (fButtonDown == NOARROW) {
178					// in this case we want to stop when we're under the mouse
179					if (fThumbInc > 0.0 && value <= fStopValue)
180						fScrollBar->SetValue(value);
181					if (fThumbInc < 0.0 && value >= fStopValue)
182						fScrollBar->SetValue(value);
183				} else
184					fScrollBar->SetValue(value);
185			}
186
187			fScrollBar->UnlockLooper();
188		}
189
190		snooze(25000);
191	}
192
193	// tell scrollbar we're gone
194	if (fScrollBar->LockLooper()) {
195		fRepeaterThread = -1;
196		fScrollBar->UnlockLooper();
197	}
198
199	return 0;
200}
201
202
203//	#pragma mark - BScrollBar
204
205
206BScrollBar::BScrollBar(BRect frame, const char* name, BView* target,
207	float min, float max, orientation direction)
208	:
209	BView(frame, name, B_FOLLOW_NONE,
210		B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS),
211	fMin(min),
212	fMax(max),
213	fSmallStep(1.0f),
214	fLargeStep(10.0f),
215	fValue(0),
216	fProportion(0.0f),
217	fTarget(NULL),
218	fOrientation(direction)
219{
220	SetViewColor(B_TRANSPARENT_COLOR);
221
222	fPrivateData = new BScrollBar::Private(this);
223
224	SetTarget(target);
225	SetEventMask(B_NO_POINTER_HISTORY);
226
227	_UpdateThumbFrame();
228	_UpdateArrowButtons();
229
230	SetResizingMode(direction == B_VERTICAL
231		? B_FOLLOW_TOP_BOTTOM | B_FOLLOW_RIGHT
232		: B_FOLLOW_LEFT_RIGHT | B_FOLLOW_BOTTOM);
233}
234
235
236BScrollBar::BScrollBar(const char* name, BView* target,
237	float min, float max, orientation direction)
238	:
239	BView(name, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS),
240	fMin(min),
241	fMax(max),
242	fSmallStep(1.0f),
243	fLargeStep(10.0f),
244	fValue(0),
245	fProportion(0.0f),
246	fTarget(NULL),
247	fOrientation(direction)
248{
249	SetViewColor(B_TRANSPARENT_COLOR);
250
251	fPrivateData = new BScrollBar::Private(this);
252
253	SetTarget(target);
254	SetEventMask(B_NO_POINTER_HISTORY);
255
256	_UpdateThumbFrame();
257	_UpdateArrowButtons();
258}
259
260
261BScrollBar::BScrollBar(BMessage* data)
262	:
263	BView(data),
264	fTarget(NULL)
265{
266	fPrivateData = new BScrollBar::Private(this);
267
268	// TODO: Does the BeOS implementation try to find the target
269	// by name again? Does it archive the name at all?
270	if (data->FindFloat("_range", 0, &fMin) < B_OK)
271		fMin = 0.0f;
272
273	if (data->FindFloat("_range", 1, &fMax) < B_OK)
274		fMax = 0.0f;
275
276	if (data->FindFloat("_steps", 0, &fSmallStep) < B_OK)
277		fSmallStep = 1.0f;
278
279	if (data->FindFloat("_steps", 1, &fLargeStep) < B_OK)
280		fLargeStep = 10.0f;
281
282	if (data->FindFloat("_val", &fValue) < B_OK)
283		fValue = 0.0;
284
285	int32 orientation;
286	if (data->FindInt32("_orient", &orientation) < B_OK) {
287		fOrientation = B_VERTICAL;
288	} else
289		fOrientation = (enum orientation)orientation;
290
291	if ((Flags() & B_SUPPORTS_LAYOUT) == 0) {
292		// just to make sure
293		SetResizingMode(fOrientation == B_VERTICAL
294			? B_FOLLOW_TOP_BOTTOM | B_FOLLOW_RIGHT
295			: B_FOLLOW_LEFT_RIGHT | B_FOLLOW_BOTTOM);
296	}
297
298	if (data->FindFloat("_prop", &fProportion) < B_OK)
299		fProportion = 0.0;
300
301	_UpdateThumbFrame();
302	_UpdateArrowButtons();
303}
304
305
306BScrollBar::~BScrollBar()
307{
308	SetTarget((BView*)NULL);
309	delete fPrivateData;
310}
311
312
313BArchivable*
314BScrollBar::Instantiate(BMessage* data)
315{
316	if (validate_instantiation(data, "BScrollBar"))
317		return new BScrollBar(data);
318	return NULL;
319}
320
321
322status_t
323BScrollBar::Archive(BMessage* data, bool deep) const
324{
325	status_t err = BView::Archive(data, deep);
326	if (err != B_OK)
327		return err;
328
329	err = data->AddFloat("_range", fMin);
330	if (err != B_OK)
331		return err;
332
333	err = data->AddFloat("_range", fMax);
334	if (err != B_OK)
335		return err;
336
337	err = data->AddFloat("_steps", fSmallStep);
338	if (err != B_OK)
339		return err;
340
341	err = data->AddFloat("_steps", fLargeStep);
342	if (err != B_OK)
343		return err;
344
345	err = data->AddFloat("_val", fValue);
346	if (err != B_OK)
347		return err;
348
349	err = data->AddInt32("_orient", (int32)fOrientation);
350	if (err != B_OK)
351		return err;
352
353	err = data->AddFloat("_prop", fProportion);
354
355	return err;
356}
357
358
359void
360BScrollBar::AllAttached()
361{
362	BView::AllAttached();
363}
364
365
366void
367BScrollBar::AllDetached()
368{
369	BView::AllDetached();
370}
371
372
373void
374BScrollBar::AttachedToWindow()
375{
376	BView::AttachedToWindow();
377}
378
379
380void
381BScrollBar::DetachedFromWindow()
382{
383	BView::DetachedFromWindow();
384}
385
386
387void
388BScrollBar::Draw(BRect updateRect)
389{
390	rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR);
391
392	uint32 flags = 0;
393	bool scrollingEnabled = fMin < fMax
394		&& fProportion >= 0.0f && fProportion < 1.0f;
395	if (scrollingEnabled)
396		flags |= BControlLook::B_PARTIALLY_ACTIVATED;
397
398	if (!fPrivateData->fEnabled || !scrollingEnabled)
399		flags |= BControlLook::B_DISABLED;
400
401	bool isFocused = fPrivateData->fBorderHighlighted;
402	if (isFocused)
403		flags |= BControlLook::B_FOCUSED;
404
405	bool doubleArrows = _DoubleArrows();
406
407	// draw border
408	BRect rect = Bounds();
409	be_control_look->DrawScrollBarBorder(this, rect, updateRect, base, flags,
410		fOrientation);
411
412	// inset past border
413	rect.InsetBy(1, 1);
414
415	// draw arrows buttons
416	BRect thumbBG = rect;
417	if (fOrientation == B_HORIZONTAL) {
418		BRect buttonFrame(rect.left, rect.top,
419			rect.left + rect.Height(), rect.bottom);
420
421		be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect,
422			base, flags | (fPrivateData->fButtonDown == ARROW1
423				? BControlLook::B_ACTIVATED : 0),
424			BControlLook::B_LEFT_ARROW, fOrientation,
425			fPrivateData->fButtonDown == ARROW1);
426
427		if (doubleArrows) {
428			buttonFrame.OffsetBy(rect.Height() + 1, 0.0f);
429			be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect,
430				base, flags | (fPrivateData->fButtonDown == ARROW2
431					? BControlLook::B_ACTIVATED : 0),
432				BControlLook::B_RIGHT_ARROW, fOrientation,
433				fPrivateData->fButtonDown == ARROW2);
434
435			buttonFrame.OffsetTo(rect.right - ((rect.Height() * 2) + 1),
436				rect.top);
437			be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect,
438				base, flags | (fPrivateData->fButtonDown == ARROW3
439					? BControlLook::B_ACTIVATED : 0),
440				BControlLook::B_LEFT_ARROW, fOrientation,
441				fPrivateData->fButtonDown == ARROW3);
442
443			thumbBG.left += rect.Height() * 2 + 2;
444			thumbBG.right -= rect.Height() * 2 + 2;
445		} else {
446			thumbBG.left += rect.Height() + 1;
447			thumbBG.right -= rect.Height() + 1;
448		}
449
450		buttonFrame.OffsetTo(rect.right - rect.Height(), rect.top);
451		be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect,
452			base, flags | (fPrivateData->fButtonDown == ARROW4
453				? BControlLook::B_ACTIVATED : 0),
454			BControlLook::B_RIGHT_ARROW, fOrientation,
455			fPrivateData->fButtonDown == ARROW4);
456	} else {
457		BRect buttonFrame(rect.left, rect.top, rect.right,
458			rect.top + rect.Width());
459
460		be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect,
461			base, flags | (fPrivateData->fButtonDown == ARROW1
462				? BControlLook::B_ACTIVATED : 0),
463			BControlLook::B_UP_ARROW, fOrientation,
464			fPrivateData->fButtonDown == ARROW1);
465
466		if (doubleArrows) {
467			buttonFrame.OffsetBy(0, rect.Width() + 1);
468			be_control_look->DrawScrollBarButton(this, buttonFrame,
469				updateRect, base, flags | (fPrivateData->fButtonDown == ARROW2
470					? BControlLook::B_ACTIVATED : 0),
471				BControlLook::B_DOWN_ARROW, fOrientation,
472				fPrivateData->fButtonDown == ARROW2);
473
474			buttonFrame.OffsetTo(rect.left, rect.bottom
475				- ((rect.Width() * 2) + 1));
476			be_control_look->DrawScrollBarButton(this, buttonFrame,
477				updateRect, base, flags | (fPrivateData->fButtonDown == ARROW3
478					? BControlLook::B_ACTIVATED : 0),
479				BControlLook::B_UP_ARROW, fOrientation,
480				fPrivateData->fButtonDown == ARROW3);
481
482			thumbBG.top += rect.Width() * 2 + 2;
483			thumbBG.bottom -= rect.Width() * 2 + 2;
484		} else {
485			thumbBG.top += rect.Width() + 1;
486			thumbBG.bottom -= rect.Width() + 1;
487		}
488
489		buttonFrame.OffsetTo(rect.left, rect.bottom - rect.Width());
490		be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect,
491			base, flags | (fPrivateData->fButtonDown == ARROW4
492				? BControlLook::B_ACTIVATED : 0),
493			BControlLook::B_DOWN_ARROW, fOrientation,
494			fPrivateData->fButtonDown == ARROW4);
495	}
496
497	// fill background besides the thumb
498	rect = fPrivateData->fThumbFrame;
499	if (fOrientation == B_HORIZONTAL) {
500		BRect leftOfThumb(thumbBG.left, thumbBG.top,
501			rect.left - 1, thumbBG.bottom);
502		BRect rightOfThumb(rect.right + 1, thumbBG.top,
503			thumbBG.right, thumbBG.bottom);
504		be_control_look->DrawScrollBarBackground(this, leftOfThumb,
505			rightOfThumb, updateRect, base, flags, fOrientation);
506	} else {
507		BRect topOfThumb(thumbBG.left, thumbBG.top,
508			thumbBG.right, rect.top - 1);
509		BRect bottomOfThumb(thumbBG.left, rect.bottom + 1,
510			thumbBG.right, thumbBG.bottom);
511		be_control_look->DrawScrollBarBackground(this, topOfThumb,
512			bottomOfThumb, updateRect, base, flags, fOrientation);
513	}
514
515	// draw thumb
516	rect = fPrivateData->fThumbFrame;
517	be_control_look->DrawScrollBarThumb(this, rect, updateRect,
518		ui_color(B_SCROLL_BAR_THUMB_COLOR), flags, fOrientation,
519		fPrivateData->fScrollBarInfo.knob);
520}
521
522
523void
524BScrollBar::FrameMoved(BPoint newPosition)
525{
526	BView::FrameMoved(newPosition);
527}
528
529
530void
531BScrollBar::FrameResized(float newWidth, float newHeight)
532{
533	_UpdateThumbFrame();
534}
535
536
537void
538BScrollBar::MessageReceived(BMessage* message)
539{
540	switch(message->what) {
541		case B_VALUE_CHANGED:
542		{
543			int32 value;
544			if (message->FindInt32("value", &value) == B_OK)
545				ValueChanged(value);
546
547			break;
548		}
549
550		case B_MOUSE_WHEEL_CHANGED:
551		{
552			// Must handle this here since BView checks for the existence of
553			// scrollbars, which a scrollbar itself does not have
554			float deltaX = 0.0f;
555			float deltaY = 0.0f;
556			message->FindFloat("be:wheel_delta_x", &deltaX);
557			message->FindFloat("be:wheel_delta_y", &deltaY);
558
559			if (deltaX == 0.0f && deltaY == 0.0f)
560				break;
561
562			if (deltaX != 0.0f && deltaY == 0.0f)
563				deltaY = deltaX;
564
565			ScrollWithMouseWheelDelta(this, deltaY);
566		}
567
568		default:
569			BView::MessageReceived(message);
570	}
571}
572
573
574void
575BScrollBar::MouseDown(BPoint where)
576{
577	if (!fPrivateData->fEnabled || fMin == fMax)
578		return;
579
580	SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
581
582	int32 buttons;
583	if (Looper() == NULL || Looper()->CurrentMessage() == NULL
584		|| Looper()->CurrentMessage()->FindInt32("buttons", &buttons) != B_OK) {
585		buttons = B_PRIMARY_MOUSE_BUTTON;
586	}
587
588	if (buttons & B_SECONDARY_MOUSE_BUTTON) {
589		// special absolute scrolling: move thumb to where we clicked
590		fPrivateData->fButtonDown = THUMB;
591		fPrivateData->fClickOffset
592			= fPrivateData->fThumbFrame.LeftTop() - where;
593		if (Orientation() == B_HORIZONTAL) {
594			fPrivateData->fClickOffset.x
595				= -fPrivateData->fThumbFrame.Width() / 2;
596		} else {
597			fPrivateData->fClickOffset.y
598				= -fPrivateData->fThumbFrame.Height() / 2;
599		}
600
601		SetValue(_ValueFor(where + fPrivateData->fClickOffset));
602		return;
603	}
604
605	// hit test for the thumb
606	if (fPrivateData->fThumbFrame.Contains(where)) {
607		fPrivateData->fButtonDown = THUMB;
608		fPrivateData->fClickOffset
609			= fPrivateData->fThumbFrame.LeftTop() - where;
610		Invalidate(fPrivateData->fThumbFrame);
611		return;
612	}
613
614	// hit test for arrows or empty area
615	float scrollValue = 0.0;
616
617	// pressing the shift key scrolls faster
618	float buttonStepSize
619		= (modifiers() & B_SHIFT_KEY) != 0 ? fLargeStep : fSmallStep;
620
621	fPrivateData->fButtonDown = _ButtonFor(where);
622	switch (fPrivateData->fButtonDown) {
623		case ARROW1:
624			scrollValue = -buttonStepSize;
625			break;
626
627		case ARROW2:
628			scrollValue = buttonStepSize;
629			break;
630
631		case ARROW3:
632			scrollValue = -buttonStepSize;
633			break;
634
635		case ARROW4:
636			scrollValue = buttonStepSize;
637			break;
638
639		case NOARROW:
640			// we hit the empty area, figure out which side of the thumb
641			if (fOrientation == B_VERTICAL) {
642				if (where.y < fPrivateData->fThumbFrame.top)
643					scrollValue = -fLargeStep;
644				else
645					scrollValue = fLargeStep;
646			} else {
647				if (where.x < fPrivateData->fThumbFrame.left)
648					scrollValue = -fLargeStep;
649				else
650					scrollValue = fLargeStep;
651			}
652			_UpdateTargetValue(where);
653			break;
654	}
655	if (scrollValue != 0.0) {
656		SetValue(fValue + scrollValue);
657		Invalidate(_ButtonRectFor(fPrivateData->fButtonDown));
658
659		// launch the repeat thread
660		if (fPrivateData->fRepeaterThread == -1) {
661			fPrivateData->fExitRepeater = false;
662			fPrivateData->fRepeaterDelay = system_time() + kRepeatDelay;
663			fPrivateData->fThumbInc = scrollValue;
664			fPrivateData->fDoRepeat = true;
665			fPrivateData->fRepeaterThread = spawn_thread(
666				fPrivateData->button_repeater_thread, "scroll repeater",
667				B_NORMAL_PRIORITY, fPrivateData);
668			resume_thread(fPrivateData->fRepeaterThread);
669		} else {
670			fPrivateData->fExitRepeater = false;
671			fPrivateData->fRepeaterDelay = system_time() + kRepeatDelay;
672			fPrivateData->fDoRepeat = true;
673		}
674	}
675}
676
677
678void
679BScrollBar::MouseMoved(BPoint where, uint32 code, const BMessage* dragMessage)
680{
681	if (!fPrivateData->fEnabled || fMin >= fMax || fProportion >= 1.0f
682		|| fProportion < 0.0f) {
683		return;
684	}
685
686	if (fPrivateData->fButtonDown != NOARROW) {
687		if (fPrivateData->fButtonDown == THUMB) {
688			SetValue(_ValueFor(where + fPrivateData->fClickOffset));
689		} else {
690			// suspend the repeating if the mouse is not over the button
691			bool repeat = _ButtonRectFor(fPrivateData->fButtonDown).Contains(
692				where);
693			if (fPrivateData->fDoRepeat != repeat) {
694				fPrivateData->fDoRepeat = repeat;
695				Invalidate(_ButtonRectFor(fPrivateData->fButtonDown));
696			}
697		}
698	} else {
699		// update the value at which we want to stop repeating
700		if (fPrivateData->fDoRepeat) {
701			_UpdateTargetValue(where);
702			// we might have to turn arround
703			if ((fValue < fPrivateData->fStopValue
704					&& fPrivateData->fThumbInc < 0)
705				|| (fValue > fPrivateData->fStopValue
706					&& fPrivateData->fThumbInc > 0)) {
707				fPrivateData->fThumbInc = -fPrivateData->fThumbInc;
708			}
709		}
710	}
711}
712
713
714void
715BScrollBar::MouseUp(BPoint where)
716{
717	if (fPrivateData->fButtonDown == THUMB)
718		Invalidate(fPrivateData->fThumbFrame);
719	else
720		Invalidate(_ButtonRectFor(fPrivateData->fButtonDown));
721
722	fPrivateData->fButtonDown = NOARROW;
723	fPrivateData->fExitRepeater = true;
724	fPrivateData->fDoRepeat = false;
725}
726
727
728void
729BScrollBar::WindowActivated(bool active)
730{
731	fPrivateData->fEnabled = active;
732	Invalidate();
733}
734
735
736void
737BScrollBar::SetValue(float value)
738{
739	if (value > fMax)
740		value = fMax;
741	else if (value < fMin)
742		value = fMin;
743	else if (isnan(value) || isinf(value))
744		return;
745
746	value = roundf(value);
747	if (value == fValue)
748		return;
749
750	TRACE("BScrollBar(%s)::SetValue(%.1f)\n", Name(), value);
751
752	fValue = value;
753
754	_UpdateThumbFrame();
755	_UpdateArrowButtons();
756
757	ValueChanged(fValue);
758}
759
760
761float
762BScrollBar::Value() const
763{
764	return fValue;
765}
766
767
768void
769BScrollBar::ValueChanged(float newValue)
770{
771	TRACE("BScrollBar(%s)::ValueChanged(%.1f)\n", Name(), newValue);
772
773	if (fTarget != NULL) {
774		// cache target bounds
775		BRect targetBounds = fTarget->Bounds();
776		// if vertical, check bounds top and scroll if different from newValue
777		if (fOrientation == B_VERTICAL && targetBounds.top != newValue)
778			fTarget->ScrollBy(0.0, newValue - targetBounds.top);
779
780		// if horizontal, check bounds left and scroll if different from newValue
781		if (fOrientation == B_HORIZONTAL && targetBounds.left != newValue)
782			fTarget->ScrollBy(newValue - targetBounds.left, 0.0);
783	}
784
785	TRACE(" -> %.1f\n", newValue);
786
787	SetValue(newValue);
788}
789
790
791void
792BScrollBar::SetProportion(float value)
793{
794	if (value < 0.0f)
795		value = 0.0f;
796	else if (value > 1.0f)
797		value = 1.0f;
798
799	if (value == fProportion)
800		return;
801
802	TRACE("BScrollBar(%s)::SetProportion(%.1f)\n", Name(), value);
803
804	bool oldEnabled = fPrivateData->fEnabled && fMin < fMax
805		&& fProportion < 1.0f && fProportion >= 0.0f;
806
807	fProportion = value;
808
809	bool newEnabled = fPrivateData->fEnabled && fMin < fMax
810		&& fProportion < 1.0f && fProportion >= 0.0f;
811
812	_UpdateThumbFrame();
813
814	if (oldEnabled != newEnabled)
815		Invalidate();
816}
817
818
819float
820BScrollBar::Proportion() const
821{
822	return fProportion;
823}
824
825
826void
827BScrollBar::SetRange(float min, float max)
828{
829	if (min > max || isnanf(min) || isnanf(max)
830		|| isinff(min) || isinff(max)) {
831		min = 0.0f;
832		max = 0.0f;
833	}
834
835	min = roundf(min);
836	max = roundf(max);
837
838	if (fMin == min && fMax == max)
839		return;
840
841	TRACE("BScrollBar(%s)::SetRange(min=%.1f, max=%.1f)\n", Name(), min, max);
842
843	fMin = min;
844	fMax = max;
845
846	if (fValue < fMin || fValue > fMax)
847		SetValue(fValue);
848	else {
849		_UpdateThumbFrame();
850		Invalidate();
851	}
852}
853
854
855void
856BScrollBar::GetRange(float* min, float* max) const
857{
858	if (min != NULL)
859		*min = fMin;
860
861	if (max != NULL)
862		*max = fMax;
863}
864
865
866void
867BScrollBar::SetSteps(float smallStep, float largeStep)
868{
869	// Under R5, steps can be set only after being attached to a window,
870	// probably because the data is kept server-side. We'll just remove
871	// that limitation... :P
872
873	// The BeBook also says that we need to specify an integer value even
874	// though the step values are floats. For the moment, we'll just make
875	// sure that they are integers
876	smallStep = roundf(smallStep);
877	largeStep = roundf(largeStep);
878	if (fSmallStep == smallStep && fLargeStep == largeStep)
879		return;
880
881	TRACE("BScrollBar(%s)::SetSteps(small=%.1f, large=%.1f)\n", Name(),
882		smallStep, largeStep);
883
884	fSmallStep = smallStep;
885	fLargeStep = largeStep;
886
887	if (fProportion == 0.0) {
888		// special case, proportion is based on fLargeStep if it was never
889		// set, so it means we need to invalidate here
890		_UpdateThumbFrame();
891		Invalidate();
892	}
893
894	// TODO: test use of fractional values and make them work properly if
895	// they don't
896}
897
898
899void
900BScrollBar::GetSteps(float* smallStep, float* largeStep) const
901{
902	if (smallStep != NULL)
903		*smallStep = fSmallStep;
904
905	if (largeStep != NULL)
906		*largeStep = fLargeStep;
907}
908
909
910void
911BScrollBar::SetTarget(BView* target)
912{
913	if (fTarget) {
914		// unset the previous target's scrollbar pointer
915		if (fOrientation == B_VERTICAL)
916			fTarget->fVerScroller = NULL;
917		else
918			fTarget->fHorScroller = NULL;
919	}
920
921	fTarget = target;
922	if (fTarget) {
923		if (fOrientation == B_VERTICAL)
924			fTarget->fVerScroller = this;
925		else
926			fTarget->fHorScroller = this;
927	}
928}
929
930
931void
932BScrollBar::SetTarget(const char* targetName)
933{
934	// NOTE 1: BeOS implementation crashes for targetName == NULL
935	// NOTE 2: BeOS implementation also does not modify the target
936	// if it can't be found
937	if (targetName == NULL)
938		return;
939
940	if (Window() == NULL)
941		debugger("Method requires window and doesn't have one");
942
943	BView* target = Window()->FindView(targetName);
944	if (target != NULL)
945		SetTarget(target);
946}
947
948
949BView*
950BScrollBar::Target() const
951{
952	return fTarget;
953}
954
955
956void
957BScrollBar::SetOrientation(orientation direction)
958{
959	if (fOrientation == direction)
960		return;
961
962	fOrientation = direction;
963	InvalidateLayout();
964	Invalidate();
965}
966
967
968orientation
969BScrollBar::Orientation() const
970{
971	return fOrientation;
972}
973
974
975status_t
976BScrollBar::SetBorderHighlighted(bool highlight)
977{
978	if (fPrivateData->fBorderHighlighted == highlight)
979		return B_OK;
980
981	fPrivateData->fBorderHighlighted = highlight;
982
983	BRect dirty(Bounds());
984	if (fOrientation == B_HORIZONTAL)
985		dirty.bottom = dirty.top;
986	else
987		dirty.right = dirty.left;
988
989	Invalidate(dirty);
990
991	return B_OK;
992}
993
994
995void
996BScrollBar::GetPreferredSize(float* _width, float* _height)
997{
998	if (fOrientation == B_VERTICAL) {
999		if (_width)
1000			*_width = be_control_look->GetScrollBarWidth(B_VERTICAL);
1001
1002		if (_height)
1003			*_height = _MinSize().Height();
1004	} else if (fOrientation == B_HORIZONTAL) {
1005		if (_width)
1006			*_width = _MinSize().Width();
1007
1008		if (_height)
1009			*_height = be_control_look->GetScrollBarWidth(B_HORIZONTAL);
1010	}
1011}
1012
1013
1014void
1015BScrollBar::ResizeToPreferred()
1016{
1017	BView::ResizeToPreferred();
1018}
1019
1020
1021
1022void
1023BScrollBar::MakeFocus(bool focus)
1024{
1025	BView::MakeFocus(focus);
1026}
1027
1028
1029BSize
1030BScrollBar::MinSize()
1031{
1032	return BLayoutUtils::ComposeSize(ExplicitMinSize(), _MinSize());
1033}
1034
1035
1036BSize
1037BScrollBar::MaxSize()
1038{
1039	BSize maxSize;
1040	GetPreferredSize(&maxSize.width, &maxSize.height);
1041	if (fOrientation == B_HORIZONTAL)
1042		maxSize.width = B_SIZE_UNLIMITED;
1043	else
1044		maxSize.height = B_SIZE_UNLIMITED;
1045	return BLayoutUtils::ComposeSize(ExplicitMaxSize(), maxSize);
1046}
1047
1048
1049BSize
1050BScrollBar::PreferredSize()
1051{
1052	BSize preferredSize;
1053	GetPreferredSize(&preferredSize.width, &preferredSize.height);
1054	return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), preferredSize);
1055}
1056
1057
1058status_t
1059BScrollBar::GetSupportedSuites(BMessage* message)
1060{
1061	return BView::GetSupportedSuites(message);
1062}
1063
1064
1065BHandler*
1066BScrollBar::ResolveSpecifier(BMessage* message, int32 index,
1067	BMessage* specifier, int32 what, const char* property)
1068{
1069	return BView::ResolveSpecifier(message, index, specifier, what, property);
1070}
1071
1072
1073status_t
1074BScrollBar::Perform(perform_code code, void* _data)
1075{
1076	switch (code) {
1077		case PERFORM_CODE_MIN_SIZE:
1078			((perform_data_min_size*)_data)->return_value
1079				= BScrollBar::MinSize();
1080
1081			return B_OK;
1082
1083		case PERFORM_CODE_MAX_SIZE:
1084			((perform_data_max_size*)_data)->return_value
1085				= BScrollBar::MaxSize();
1086
1087			return B_OK;
1088
1089		case PERFORM_CODE_PREFERRED_SIZE:
1090			((perform_data_preferred_size*)_data)->return_value
1091				= BScrollBar::PreferredSize();
1092
1093			return B_OK;
1094
1095		case PERFORM_CODE_LAYOUT_ALIGNMENT:
1096			((perform_data_layout_alignment*)_data)->return_value
1097				= BScrollBar::LayoutAlignment();
1098
1099			return B_OK;
1100
1101		case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH:
1102			((perform_data_has_height_for_width*)_data)->return_value
1103				= BScrollBar::HasHeightForWidth();
1104
1105			return B_OK;
1106
1107		case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH:
1108		{
1109			perform_data_get_height_for_width* data
1110				= (perform_data_get_height_for_width*)_data;
1111			BScrollBar::GetHeightForWidth(data->width, &data->min, &data->max,
1112				&data->preferred);
1113
1114			return B_OK;
1115		}
1116
1117		case PERFORM_CODE_SET_LAYOUT:
1118		{
1119			perform_data_set_layout* data = (perform_data_set_layout*)_data;
1120			BScrollBar::SetLayout(data->layout);
1121
1122			return B_OK;
1123		}
1124
1125		case PERFORM_CODE_LAYOUT_INVALIDATED:
1126		{
1127			perform_data_layout_invalidated* data
1128				= (perform_data_layout_invalidated*)_data;
1129			BScrollBar::LayoutInvalidated(data->descendants);
1130
1131			return B_OK;
1132		}
1133
1134		case PERFORM_CODE_DO_LAYOUT:
1135		{
1136			BScrollBar::DoLayout();
1137
1138			return B_OK;
1139		}
1140	}
1141
1142	return BView::Perform(code, _data);
1143}
1144
1145
1146void BScrollBar::_ReservedScrollBar1() {}
1147void BScrollBar::_ReservedScrollBar2() {}
1148void BScrollBar::_ReservedScrollBar3() {}
1149void BScrollBar::_ReservedScrollBar4() {}
1150
1151
1152BScrollBar&
1153BScrollBar::operator=(const BScrollBar&)
1154{
1155	return *this;
1156}
1157
1158
1159bool
1160BScrollBar::_DoubleArrows() const
1161{
1162	if (!fPrivateData->fScrollBarInfo.double_arrows)
1163		return false;
1164
1165	// if there is not enough room, switch to single arrows even though
1166	// double arrows is specified
1167	if (fOrientation == B_HORIZONTAL) {
1168		return Bounds().Width() > (Bounds().Height() + 1) * 4
1169			+ fPrivateData->fScrollBarInfo.min_knob_size * 2;
1170	} else {
1171		return Bounds().Height() > (Bounds().Width() + 1) * 4
1172			+ fPrivateData->fScrollBarInfo.min_knob_size * 2;
1173	}
1174}
1175
1176
1177void
1178BScrollBar::_UpdateThumbFrame()
1179{
1180	BRect bounds = Bounds();
1181	bounds.InsetBy(1.0, 1.0);
1182
1183	BRect oldFrame = fPrivateData->fThumbFrame;
1184	fPrivateData->fThumbFrame = bounds;
1185	float minSize = fPrivateData->fScrollBarInfo.min_knob_size;
1186	float maxSize;
1187	float buttonSize;
1188
1189	// assume square buttons
1190	if (fOrientation == B_VERTICAL) {
1191		maxSize = bounds.Height();
1192		buttonSize = bounds.Width() + 1.0;
1193	} else {
1194		maxSize = bounds.Width();
1195		buttonSize = bounds.Height() + 1.0;
1196	}
1197
1198	if (_DoubleArrows()) {
1199		// subtract the size of four buttons
1200		maxSize -= buttonSize * 4;
1201	} else {
1202		// subtract the size of two buttons
1203		maxSize -= buttonSize * 2;
1204	}
1205	// visual adjustments (room for darker line between thumb and buttons)
1206	maxSize--;
1207
1208	float thumbSize;
1209	if (fPrivateData->fScrollBarInfo.proportional) {
1210		float proportion = fProportion;
1211		if (fMin >= fMax || proportion > 1.0 || proportion < 0.0)
1212			proportion = 1.0;
1213
1214		if (proportion == 0.0) {
1215			// Special case a proportion of 0.0, use the large step value
1216			// in that case (NOTE: fMin == fMax already handled above)
1217			// This calculation is based on the assumption that "large step"
1218			// scrolls by one "page size".
1219			proportion = fLargeStep / (2 * (fMax - fMin));
1220			if (proportion > 1.0)
1221				proportion = 1.0;
1222		}
1223		thumbSize = maxSize * proportion;
1224		if (thumbSize < minSize)
1225			thumbSize = minSize;
1226	} else
1227		thumbSize = minSize;
1228
1229	thumbSize = floorf(thumbSize + 0.5);
1230	thumbSize--;
1231
1232	// the thumb can be scrolled within the remaining area "maxSize - thumbSize - 1.0"
1233	float offset = 0.0;
1234	if (fMax > fMin) {
1235		offset = floorf(((fValue - fMin) / (fMax - fMin))
1236			* (maxSize - thumbSize - 1.0));
1237	}
1238
1239	if (_DoubleArrows()) {
1240		offset += buttonSize * 2;
1241	} else
1242		offset += buttonSize;
1243
1244	// visual adjustments (room for darker line between thumb and buttons)
1245	offset++;
1246
1247	if (fOrientation == B_VERTICAL) {
1248		fPrivateData->fThumbFrame.bottom = fPrivateData->fThumbFrame.top
1249			+ thumbSize;
1250		fPrivateData->fThumbFrame.OffsetBy(0.0, offset);
1251	} else {
1252		fPrivateData->fThumbFrame.right = fPrivateData->fThumbFrame.left
1253			+ thumbSize;
1254		fPrivateData->fThumbFrame.OffsetBy(offset, 0.0);
1255	}
1256
1257	if (Window() != NULL) {
1258		BRect invalid = oldFrame.IsValid()
1259			? oldFrame | fPrivateData->fThumbFrame
1260			: fPrivateData->fThumbFrame;
1261		// account for those two dark lines
1262		if (fOrientation == B_HORIZONTAL)
1263			invalid.InsetBy(-2.0, 0.0);
1264		else
1265			invalid.InsetBy(0.0, -2.0);
1266
1267		Invalidate(invalid);
1268	}
1269}
1270
1271
1272float
1273BScrollBar::_ValueFor(BPoint where) const
1274{
1275	BRect bounds = Bounds();
1276	bounds.InsetBy(1.0f, 1.0f);
1277
1278	float offset;
1279	float thumbSize;
1280	float maxSize;
1281	float buttonSize;
1282
1283	if (fOrientation == B_VERTICAL) {
1284		offset = where.y;
1285		thumbSize = fPrivateData->fThumbFrame.Height();
1286		maxSize = bounds.Height();
1287		buttonSize = bounds.Width() + 1.0f;
1288	} else {
1289		offset = where.x;
1290		thumbSize = fPrivateData->fThumbFrame.Width();
1291		maxSize = bounds.Width();
1292		buttonSize = bounds.Height() + 1.0f;
1293	}
1294
1295	if (_DoubleArrows()) {
1296		// subtract the size of four buttons
1297		maxSize -= buttonSize * 4;
1298		// convert point to inside of area between buttons
1299		offset -= buttonSize * 2;
1300	} else {
1301		// subtract the size of two buttons
1302		maxSize -= buttonSize * 2;
1303		// convert point to inside of area between buttons
1304		offset -= buttonSize;
1305	}
1306	// visual adjustments (room for darker line between thumb and buttons)
1307	maxSize--;
1308	offset++;
1309
1310	return roundf(fMin + (offset / (maxSize - thumbSize)
1311		* (fMax - fMin + 1.0f)));
1312}
1313
1314
1315int32
1316BScrollBar::_ButtonFor(BPoint where) const
1317{
1318	BRect bounds = Bounds();
1319	bounds.InsetBy(1.0f, 1.0f);
1320
1321	float buttonSize = fOrientation == B_VERTICAL
1322		? bounds.Width() + 1.0f
1323		: bounds.Height() + 1.0f;
1324
1325	BRect rect(bounds.left, bounds.top,
1326		bounds.left + buttonSize, bounds.top + buttonSize);
1327
1328	if (fOrientation == B_VERTICAL) {
1329		if (rect.Contains(where))
1330			return ARROW1;
1331
1332		if (_DoubleArrows()) {
1333			rect.OffsetBy(0.0, buttonSize);
1334			if (rect.Contains(where))
1335				return ARROW2;
1336
1337			rect.OffsetTo(bounds.left, bounds.bottom - 2 * buttonSize);
1338			if (rect.Contains(where))
1339				return ARROW3;
1340		}
1341		rect.OffsetTo(bounds.left, bounds.bottom - buttonSize);
1342		if (rect.Contains(where))
1343			return ARROW4;
1344	} else {
1345		if (rect.Contains(where))
1346			return ARROW1;
1347
1348		if (_DoubleArrows()) {
1349			rect.OffsetBy(buttonSize, 0.0);
1350			if (rect.Contains(where))
1351				return ARROW2;
1352
1353			rect.OffsetTo(bounds.right - 2 * buttonSize, bounds.top);
1354			if (rect.Contains(where))
1355				return ARROW3;
1356		}
1357		rect.OffsetTo(bounds.right - buttonSize, bounds.top);
1358		if (rect.Contains(where))
1359			return ARROW4;
1360	}
1361
1362	return NOARROW;
1363}
1364
1365
1366BRect
1367BScrollBar::_ButtonRectFor(int32 button) const
1368{
1369	BRect bounds = Bounds();
1370	bounds.InsetBy(1.0f, 1.0f);
1371
1372	float buttonSize = fOrientation == B_VERTICAL
1373		? bounds.Width() + 1.0f
1374		: bounds.Height() + 1.0f;
1375
1376	BRect rect(bounds.left, bounds.top,
1377		bounds.left + buttonSize - 1.0f, bounds.top + buttonSize - 1.0f);
1378
1379	if (fOrientation == B_VERTICAL) {
1380		switch (button) {
1381			case ARROW1:
1382				break;
1383
1384			case ARROW2:
1385				rect.OffsetBy(0.0, buttonSize);
1386				break;
1387
1388			case ARROW3:
1389				rect.OffsetTo(bounds.left, bounds.bottom - 2 * buttonSize + 1);
1390				break;
1391
1392			case ARROW4:
1393				rect.OffsetTo(bounds.left, bounds.bottom - buttonSize + 1);
1394				break;
1395		}
1396	} else {
1397		switch (button) {
1398			case ARROW1:
1399				break;
1400
1401			case ARROW2:
1402				rect.OffsetBy(buttonSize, 0.0);
1403				break;
1404
1405			case ARROW3:
1406				rect.OffsetTo(bounds.right - 2 * buttonSize + 1, bounds.top);
1407				break;
1408
1409			case ARROW4:
1410				rect.OffsetTo(bounds.right - buttonSize + 1, bounds.top);
1411				break;
1412		}
1413	}
1414
1415	return rect;
1416}
1417
1418
1419void
1420BScrollBar::_UpdateTargetValue(BPoint where)
1421{
1422	if (fOrientation == B_VERTICAL) {
1423		fPrivateData->fStopValue = _ValueFor(BPoint(where.x, where.y
1424			- fPrivateData->fThumbFrame.Height() / 2.0));
1425	} else {
1426		fPrivateData->fStopValue = _ValueFor(BPoint(where.x
1427			- fPrivateData->fThumbFrame.Width() / 2.0, where.y));
1428	}
1429}
1430
1431
1432void
1433BScrollBar::_UpdateArrowButtons()
1434{
1435	bool upEnabled = fValue > fMin;
1436	if (fPrivateData->fUpArrowsEnabled != upEnabled) {
1437		fPrivateData->fUpArrowsEnabled = upEnabled;
1438		Invalidate(_ButtonRectFor(ARROW1));
1439		if (_DoubleArrows())
1440			Invalidate(_ButtonRectFor(ARROW3));
1441	}
1442
1443	bool downEnabled = fValue < fMax;
1444	if (fPrivateData->fDownArrowsEnabled != downEnabled) {
1445		fPrivateData->fDownArrowsEnabled = downEnabled;
1446		Invalidate(_ButtonRectFor(ARROW4));
1447		if (_DoubleArrows())
1448			Invalidate(_ButtonRectFor(ARROW2));
1449	}
1450}
1451
1452
1453status_t
1454control_scrollbar(scroll_bar_info* info, BScrollBar* bar)
1455{
1456	if (bar == NULL || info == NULL)
1457		return B_BAD_VALUE;
1458
1459	if (bar->fPrivateData->fScrollBarInfo.double_arrows
1460			!= info->double_arrows) {
1461		bar->fPrivateData->fScrollBarInfo.double_arrows = info->double_arrows;
1462
1463		int8 multiplier = (info->double_arrows) ? 1 : -1;
1464
1465		if (bar->fOrientation == B_VERTICAL) {
1466			bar->fPrivateData->fThumbFrame.OffsetBy(0, multiplier
1467				* B_H_SCROLL_BAR_HEIGHT);
1468		} else {
1469			bar->fPrivateData->fThumbFrame.OffsetBy(multiplier
1470				* B_V_SCROLL_BAR_WIDTH, 0);
1471		}
1472	}
1473
1474	bar->fPrivateData->fScrollBarInfo.proportional = info->proportional;
1475
1476	// TODO: Figure out how proportional relates to the size of the thumb
1477
1478	// TODO: Add redraw code to reflect the changes
1479
1480	if (info->knob >= 0 && info->knob <= 2)
1481		bar->fPrivateData->fScrollBarInfo.knob = info->knob;
1482	else
1483		return B_BAD_VALUE;
1484
1485	if (info->min_knob_size >= SCROLL_BAR_MINIMUM_KNOB_SIZE
1486			&& info->min_knob_size <= SCROLL_BAR_MAXIMUM_KNOB_SIZE) {
1487		bar->fPrivateData->fScrollBarInfo.min_knob_size = info->min_knob_size;
1488	} else
1489		return B_BAD_VALUE;
1490
1491	return B_OK;
1492}
1493
1494
1495BSize
1496BScrollBar::_MinSize() const
1497{
1498	BSize minSize;
1499	if (fOrientation == B_HORIZONTAL) {
1500		minSize.width = 2 * B_V_SCROLL_BAR_WIDTH
1501			+ 2 * fPrivateData->fScrollBarInfo.min_knob_size;
1502		minSize.height = B_H_SCROLL_BAR_HEIGHT;
1503	} else {
1504		minSize.width = B_V_SCROLL_BAR_WIDTH;
1505		minSize.height = 2 * B_H_SCROLL_BAR_HEIGHT
1506			+ 2 * fPrivateData->fScrollBarInfo.min_knob_size;
1507	}
1508
1509	return minSize;
1510}
1511