* Copyright 2004-2024 Haiku, Inc. All Rights Reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Mike Berg <mike@berg-net.us>
* Julun <host.haiku@gmx.de>
* Stephan Aßmus <superstippi@gmx.de>
* Clemens <mail@Clemens-Zeidler.de>
* Hamish Morrison <hamish@lavabit.com>
* John Scipione <jscipione@gmail.com>
* Niklas Poslovski <ni.pos@yandex.com>
*/
#include "AnalogClock.h"
#include <math.h>
#include <stdio.h>
#include <LayoutUtils.h>
#include <Message.h>
#include <Window.h>
#include "TimeMessages.h"
#define DRAG_DELTA_PHI 0.2
TAnalogClock::TAnalogClock(const char* name, bool drawSecondHand,
bool interactive)
:
BView(name, B_WILL_DRAW | B_DRAW_ON_CHILDREN | B_FRAME_EVENTS),
fHours(0),
fMinutes(0),
fSeconds(0),
fDirty(true),
fCenterX(0.0),
fCenterY(0.0),
fRadius(0.0),
fHourDragging(false),
fMinuteDragging(false),
fDrawSecondHand(drawSecondHand),
fInteractive(interactive),
fTimeChangeIsOngoing(false)
{
SetFlags(Flags() | B_SUBPIXEL_PRECISE);
}
TAnalogClock::~TAnalogClock()
{
}
void
TAnalogClock::Draw(BRect )
{
if (fDirty)
DrawClock();
}
void
TAnalogClock::MessageReceived(BMessage* message)
{
int32 change;
switch (message->what) {
case B_OBSERVER_NOTICE_CHANGE:
message->FindInt32(B_OBSERVE_WHAT_CHANGE, &change);
switch (change) {
case H_TM_CHANGED:
{
int32 hour = 0;
int32 minute = 0;
int32 second = 0;
if (message->FindInt32("hour", &hour) == B_OK
&& message->FindInt32("minute", &minute) == B_OK
&& message->FindInt32("second", &second) == B_OK)
SetTime(hour, minute, second);
break;
}
default:
BView::MessageReceived(message);
break;
}
break;
default:
BView::MessageReceived(message);
break;
}
}
void
TAnalogClock::MouseDown(BPoint point)
{
if (!fInteractive) {
BView::MouseDown(point);
return;
}
if (InMinuteHand(point)) {
fMinuteDragging = true;
fDirty = true;
SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
Invalidate();
return;
}
if (InHourHand(point)) {
fHourDragging = true;
fDirty = true;
SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
Invalidate();
return;
}
}
void
TAnalogClock::MouseUp(BPoint point)
{
if (!fInteractive) {
BView::MouseUp(point);
return;
}
if (fHourDragging || fMinuteDragging) {
int32 hour, minute, second;
GetTime(&hour, &minute, &second);
BMessage message(H_USER_CHANGE);
message.AddBool("time", true);
message.AddInt32("hour", hour);
message.AddInt32("minute", minute);
Window()->PostMessage(&message);
fTimeChangeIsOngoing = true;
}
fHourDragging = false;
fDirty = true;
fMinuteDragging = false;
fDirty = true;
}
void
TAnalogClock::MouseMoved(BPoint point, uint32 transit, const BMessage* message)
{
if (!fInteractive) {
BView::MouseMoved(point, transit, message);
return;
}
if (fMinuteDragging)
SetMinuteHand(point);
if (fHourDragging)
SetHourHand(point);
Invalidate();
}
void
TAnalogClock::DoLayout()
{
BRect bounds = Bounds();
fCenterX = floorf((bounds.left + bounds.right) / 2 + 0.5) + 0.5;
fCenterY = floorf((bounds.top + bounds.bottom) / 2 + 0.5) + 0.5;
fRadius = floorf((MIN(bounds.Width(), bounds.Height()) / 2.0)) - 5.5;
}
BSize
TAnalogClock::MaxSize()
{
return BLayoutUtils::ComposeSize(ExplicitMaxSize(),
BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
}
BSize
TAnalogClock::MinSize()
{
return BSize(64.f, 64.f);
}
BSize
TAnalogClock::PreferredSize()
{
return BLayoutUtils::ComposeSize(ExplicitPreferredSize(),
BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED));
}
void
TAnalogClock::SetTime(int32 hour, int32 minute, int32 second)
{
if (fHourDragging || fMinuteDragging || fTimeChangeIsOngoing)
return;
if (fHours == hour && fMinutes == minute && fSeconds == second)
return;
fHours = hour;
fMinutes = minute;
fSeconds = second;
fDirty = true;
BWindow* window = Window();
if (window && window->Lock()) {
Invalidate();
Window()->Unlock();
}
}
bool
TAnalogClock::IsChangingTime()
{
return fTimeChangeIsOngoing;
}
void
TAnalogClock::ChangeTimeFinished()
{
fTimeChangeIsOngoing = false;
}
void
TAnalogClock::GetTime(int32* hour, int32* minute, int32* second)
{
*hour = fHours;
*minute = fMinutes;
*second = fSeconds;
}
void
TAnalogClock::DrawClock()
{
if (!LockLooper())
return;
BRect bounds = Bounds();
SetHighColor(ViewColor());
FillRect(bounds);
bounds.Set(fCenterX - fRadius, fCenterY - fRadius, fCenterX + fRadius, fCenterY + fRadius);
SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR);
SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
SetPenSize(2.0);
bool isLight = LowColor().IsLight();
if (isLight)
SetHighUIColor(B_DOCUMENT_BACKGROUND_COLOR, B_DARKEN_1_TINT);
else
SetHighUIColor(B_DOCUMENT_BACKGROUND_COLOR, 0.853);
StrokeEllipse(bounds.OffsetByCopy(-1, -1));
if (isLight)
SetHighUIColor(B_DOCUMENT_BACKGROUND_COLOR, B_LIGHTEN_2_TINT);
else
SetHighUIColor(B_DOCUMENT_BACKGROUND_COLOR, 1.615);
StrokeEllipse(bounds);
if (isLight)
SetHighUIColor(B_DOCUMENT_BACKGROUND_COLOR, B_DARKEN_3_TINT);
else
SetHighUIColor(B_DOCUMENT_BACKGROUND_COLOR, 0.593);
StrokeEllipse(bounds.OffsetByCopy(1, 1));
FillEllipse(bounds, B_SOLID_LOW);
if (isLight)
SetHighUIColor(B_DOCUMENT_BACKGROUND_COLOR, B_DARKEN_2_TINT);
else
SetHighUIColor(B_DOCUMENT_BACKGROUND_COLOR, 0.705);
SetPenSize(1.0);
SetLineMode(B_BUTT_CAP, B_MITER_JOIN);
for (int32 minute = 1; minute < 60; minute++) {
if (minute % 5 == 0)
continue;
float x1 = fCenterX + sinf(minute * M_PI / 30.0) * fRadius;
float y1 = fCenterY + cosf(minute * M_PI / 30.0) * fRadius;
float x2 = fCenterX + sinf(minute * M_PI / 30.0) * (fRadius * 0.95);
float y2 = fCenterY + cosf(minute * M_PI / 30.0) * (fRadius * 0.95);
StrokeLine(BPoint(x1, y1), BPoint(x2, y2));
}
if (isLight)
SetHighUIColor(B_DOCUMENT_TEXT_COLOR, B_DARKEN_1_TINT);
else
SetHighUIColor(B_DOCUMENT_TEXT_COLOR, 0.853);
rgb_color handsColor = HighColor();
SetPenSize(2.0);
SetLineMode(B_ROUND_CAP, B_MITER_JOIN);
for (int32 hour = 0; hour < 12; hour++) {
float x1 = fCenterX + sinf(hour * M_PI / 6.0) * fRadius;
float y1 = fCenterY + cosf(hour * M_PI / 6.0) * fRadius;
float x2 = fCenterX + sinf(hour * M_PI / 6.0) * (fRadius * 0.9);
float y2 = fCenterY + cosf(hour * M_PI / 6.0) * (fRadius * 0.9);
StrokeLine(BPoint(x1, y1), BPoint(x2, y2));
}
rgb_color hourColor;
if (fHourDragging)
hourColor = ui_color(B_NAVIGATION_BASE_COLOR);
else
hourColor = handsColor;
rgb_color minuteColor;
if (fMinuteDragging)
minuteColor = ui_color(B_NAVIGATION_BASE_COLOR);
else
minuteColor = handsColor;
rgb_color secondsColor = make_color(255, 0, 0, 255);
rgb_color shadowColor = tint_color(LowColor(), (B_DARKEN_1_TINT + B_DARKEN_2_TINT) / 2);
_DrawHands(fCenterX + 1.5, fCenterY + 1.5, fRadius, shadowColor, shadowColor, shadowColor,
shadowColor);
_DrawHands(fCenterX, fCenterY, fRadius, hourColor, minuteColor, secondsColor, handsColor);
Sync();
UnlockLooper();
}
bool
TAnalogClock::InHourHand(BPoint point)
{
int32 ticks = fHours;
if (ticks > 12)
ticks -= 12;
ticks *= 5;
ticks += int32(5. * fMinutes / 60.0);
if (ticks > 60)
ticks -= 60;
return _InHand(point, ticks, fRadius * 0.7);
}
bool
TAnalogClock::InMinuteHand(BPoint point)
{
return _InHand(point, fMinutes, fRadius * 0.9);
}
void
TAnalogClock::SetHourHand(BPoint point)
{
point.x -= fCenterX;
point.y -= fCenterY;
float pointPhi = _GetPhi(point);
float hoursExact = 6.0 * pointPhi / M_PI;
if (fHours >= 12)
fHours = 12;
else
fHours = 0;
fHours += int32(hoursExact);
SetTime(fHours, fMinutes, fSeconds);
}
void
TAnalogClock::SetMinuteHand(BPoint point)
{
point.x -= fCenterX;
point.y -= fCenterY;
float pointPhi = _GetPhi(point);
float minutesExact = 30.0 * pointPhi / M_PI;
fMinutes = int32(ceilf(minutesExact));
SetTime(fHours, fMinutes, fSeconds);
}
float
TAnalogClock::_GetPhi(BPoint point)
{
if (point.x == 0 && point.y < 0)
return 2 * M_PI;
if (point.x == 0 && point.y > 0)
return M_PI;
if (point.y == 0 && point.x < 0)
return M_PI * 3 / 2;
if (point.y == 0 && point.x > 0)
return M_PI / 2;
float pointPhi = atanf(-1. * point.y / point.x);
if (point.y < 0. && point.x > 0.)
pointPhi = M_PI / 2. - pointPhi;
if (point.y > 0. && point.x > 0.)
pointPhi = M_PI / 2 - pointPhi;
if (point.y > 0. && point.x < 0.)
pointPhi = (M_PI * 3. / 2. - pointPhi);
if (point.y < 0. && point.x < 0.)
pointPhi = 3. / 2. * M_PI - pointPhi;
return pointPhi;
}
bool
TAnalogClock::_InHand(BPoint point, int32 ticks, float radius)
{
point.x -= fCenterX;
point.y -= fCenterY;
float pRadius = sqrt(pow(point.x, 2) + pow(point.y, 2));
if (radius < pRadius)
return false;
float pointPhi = _GetPhi(point);
float handPhi = M_PI / 30.0 * ticks;
float delta = pointPhi - handPhi;
if (fabs(delta) > DRAG_DELTA_PHI)
return false;
return true;
}
void
TAnalogClock::_DrawHands(float x, float y, float radius,
rgb_color hourColor, rgb_color minuteColor,
rgb_color secondsColor, rgb_color knobColor)
{
float offsetX;
float offsetY;
SetHighColor(hourColor);
SetPenSize(4.0);
float hours = fHours + float(fMinutes) / 60.0;
offsetX = (radius * 0.7) * sinf((hours * M_PI) / 6.0);
offsetY = (radius * 0.7) * cosf((hours * M_PI) / 6.0);
StrokeLine(BPoint(x, y), BPoint(x + offsetX, y - offsetY));
SetHighColor(minuteColor);
SetPenSize(3.0);
float minutes = fMinutes + float(fSeconds) / 60.0;
offsetX = (radius * 0.9) * sinf((minutes * M_PI) / 30.0);
offsetY = (radius * 0.9) * cosf((minutes * M_PI) / 30.0);
StrokeLine(BPoint(x, y), BPoint(x + offsetX, y - offsetY));
if (fDrawSecondHand) {
SetHighColor(secondsColor);
SetPenSize(1.0);
offsetX = (radius * 0.95) * sinf((fSeconds * M_PI) / 30.0);
offsetY = (radius * 0.95) * cosf((fSeconds * M_PI) / 30.0);
StrokeLine(BPoint(x, y), BPoint(x + offsetX, y - offsetY));
}
SetHighColor(knobColor);
FillEllipse(BPoint(x, y), radius * 0.06, radius * 0.06);
}