730 lines
16 KiB
C++
730 lines
16 KiB
C++
// windatepicker.cpp
|
|
//
|
|
// Copyright (C) 2005, Chris Laurel <claurel@shatters.net>
|
|
//
|
|
// Windows front end for Celestia.
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License
|
|
// as published by the Free Software Foundation; either version 2
|
|
// of the License, or (at your option) any later version.
|
|
|
|
#include <windows.h>
|
|
#include <commctrl.h>
|
|
#include "celengine/astro.h"
|
|
#include "celutil/util.h"
|
|
#include "celutil/winutil.h"
|
|
|
|
|
|
// DatePicker is a Win32 control for setting the date. It replaces the
|
|
// date picker from commctl, adding a number of features appropriate
|
|
// for astronomical applications:
|
|
//
|
|
// - The standard Windows date picker does not permit setting years
|
|
// prior to 1752, the point that the US and UK switched to the
|
|
// Gregorian calendar. Celestia's date picker allows setting any
|
|
// year from -9999 to 9999.
|
|
//
|
|
// - Astronomical year conventions are used for dates before the
|
|
// year 1. This means that the year 0 is not omitted, and the year
|
|
// 2 BCE is entered as -1.
|
|
//
|
|
// - The first adoption of the Gregorian calendar was in 1582, when
|
|
// days 5-14 were skipped in the month of October. All dates are
|
|
// based on the initial 1582 reform, even though most countries
|
|
// didn't adopt the Gregorian calendar until many years later.
|
|
//
|
|
// - No invalid date is permitted, including the skipped days in
|
|
// October 1582.
|
|
|
|
static char* Months[12] =
|
|
{
|
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
|
};
|
|
|
|
enum DatePickerField
|
|
{
|
|
InvalidField = -1,
|
|
DayField = 0,
|
|
MonthField = 1,
|
|
YearField = 2,
|
|
NumFields = 3,
|
|
};
|
|
|
|
class DatePicker
|
|
{
|
|
public:
|
|
DatePicker(HWND _hwnd, CREATESTRUCT& cs);
|
|
~DatePicker();
|
|
|
|
LRESULT paint(HDC hdc);
|
|
void redraw(HDC hdc);
|
|
LRESULT keyDown(DWORD vkcode, LPARAM lParam);
|
|
LRESULT killFocus(HWND lostFocus);
|
|
LRESULT setFocus(HWND lostFocus);
|
|
LRESULT enable(bool);
|
|
LRESULT leftButtonDown(WORD key, int x, int y);
|
|
LRESULT notify(int id, const NMHDR& nmhdr);
|
|
LRESULT command(WPARAM wParam, LPARAM lParam);
|
|
LRESULT resize(WORD flags, int width, int height);
|
|
|
|
bool sendNotify(UINT code);
|
|
bool notifyDateChanged();
|
|
|
|
LRESULT setSystemTime(DWORD flag, SYSTEMTIME* sysTime);
|
|
LRESULT getSystemTime(SYSTEMTIME* sysTime);
|
|
|
|
LRESULT destroy();
|
|
|
|
private:
|
|
int getFieldWidth(DatePickerField field, HDC hdc);
|
|
void incrementField();
|
|
void decrementField();
|
|
|
|
private:
|
|
HWND hwnd;
|
|
HWND parent;
|
|
astro::Date date;
|
|
DatePickerField selectedField;
|
|
char textBuffer[64];
|
|
HFONT hFont;
|
|
DWORD style;
|
|
|
|
bool haveFocus;
|
|
bool firstDigit;
|
|
|
|
RECT fieldRects[NumFields];
|
|
RECT clientRect;
|
|
};
|
|
|
|
|
|
DatePicker::DatePicker(HWND _hwnd, CREATESTRUCT& cs) :
|
|
hwnd(_hwnd),
|
|
parent(cs.hwndParent),
|
|
date(1970, 10, 25),
|
|
selectedField(YearField),
|
|
haveFocus(false),
|
|
firstDigit(true)
|
|
{
|
|
textBuffer[0] = '\0';
|
|
|
|
hFont = reinterpret_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
|
|
|
|
clientRect.left = 0;
|
|
clientRect.right = 0;
|
|
clientRect.top = 0;
|
|
clientRect.bottom = 0;
|
|
}
|
|
|
|
|
|
DatePicker::~DatePicker()
|
|
{
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::paint(HDC hdc)
|
|
{
|
|
PAINTSTRUCT ps;
|
|
|
|
if (!hdc)
|
|
{
|
|
hdc = BeginPaint(hwnd, &ps);
|
|
redraw(hdc);
|
|
EndPaint(hwnd, &ps);
|
|
}
|
|
else
|
|
{
|
|
redraw(hdc);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
void
|
|
DatePicker::redraw(HDC hdc)
|
|
{
|
|
RECT rect;
|
|
GetClientRect(hwnd, &rect);
|
|
|
|
SelectObject(hdc, hFont);
|
|
SetTextColor(hdc, RGB(0, 0, 0));
|
|
SetBkMode(hdc, TRANSPARENT);
|
|
|
|
char dayBuf[32];
|
|
char monthBuf[32];
|
|
char yearBuf[32];
|
|
|
|
bind_textdomain_codeset("celestia", CurrentCP());
|
|
sprintf(dayBuf, "%02d", date.day);
|
|
sprintf(monthBuf, "%s", _(Months[date.month - 1]));
|
|
sprintf(yearBuf, "%5d", date.year);
|
|
bind_textdomain_codeset("celestia", "UTF8");
|
|
|
|
char* fieldText[NumFields];
|
|
fieldText[DayField] = dayBuf;
|
|
fieldText[MonthField] = monthBuf;
|
|
fieldText[YearField] = yearBuf;
|
|
|
|
int right = 2;
|
|
for (unsigned int i = 0; i < NumFields; i++)
|
|
{
|
|
SIZE size;
|
|
GetTextExtentPoint(hdc, fieldText[i], strlen(fieldText[i]), &size);
|
|
int fieldWidth = getFieldWidth(DatePickerField(i), hdc);
|
|
fieldRects[i].left = right;
|
|
fieldRects[i].right = right + fieldWidth;
|
|
fieldRects[i].top = rect.top;
|
|
fieldRects[i].bottom = rect.bottom;
|
|
|
|
right = fieldRects[i].right;
|
|
|
|
if (i == selectedField && haveFocus)
|
|
{
|
|
RECT r = fieldRects[i];
|
|
r.top = (clientRect.bottom - size.cy) / 2;
|
|
r.bottom = r.top + size.cy + 1;
|
|
|
|
HBRUSH hbrush = CreateSolidBrush(GetSysColor(COLOR_HIGHLIGHT));
|
|
FillRect(hdc, &r, hbrush);
|
|
DeleteObject(hbrush);
|
|
|
|
SetTextColor(hdc, GetSysColor(COLOR_HIGHLIGHTTEXT));
|
|
}
|
|
else
|
|
{
|
|
SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT));
|
|
}
|
|
|
|
DrawText(hdc, fieldText[i], strlen(fieldText[i]), &fieldRects[i], DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
|
|
}
|
|
}
|
|
|
|
|
|
static bool isLeapYear(unsigned int year)
|
|
{
|
|
if (year > 1582)
|
|
{
|
|
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
|
|
}
|
|
else
|
|
{
|
|
return year % 4 == 0;
|
|
}
|
|
}
|
|
|
|
|
|
static unsigned int daysInMonth(unsigned int month, unsigned int year)
|
|
{
|
|
static unsigned int daysPerMonth[12] =
|
|
{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
|
|
|
if (month == 2)
|
|
return isLeapYear(year) ? 29 : 28;
|
|
else
|
|
return daysPerMonth[month - 1];
|
|
}
|
|
|
|
|
|
static void clampToValidDate(astro::Date& date)
|
|
{
|
|
int days = (int) daysInMonth(date.month, date.year);
|
|
if (date.day > days)
|
|
date.day = days;
|
|
|
|
// 10 days skipped in Gregorian calendar reform
|
|
if (date.year == 1582 && date.month == 10 && date.day > 4 && date.day < 15)
|
|
{
|
|
if (date.day < 10)
|
|
date.day = 4;
|
|
else
|
|
date.day = 15;
|
|
}
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::keyDown(DWORD vkcode, LPARAM flags)
|
|
{
|
|
if (!haveFocus)
|
|
return 0;
|
|
|
|
if (vkcode >= '0' && vkcode <= '9')
|
|
{
|
|
unsigned int digit = vkcode - '0';
|
|
|
|
if (firstDigit)
|
|
{
|
|
switch (selectedField)
|
|
{
|
|
case DayField:
|
|
if (digit != 0)
|
|
date.day = digit;
|
|
break;
|
|
case MonthField:
|
|
if (digit != 0)
|
|
date.month = digit;
|
|
break;
|
|
case YearField:
|
|
if (digit != 0)
|
|
date.year = digit;
|
|
break;
|
|
}
|
|
firstDigit = false;
|
|
}
|
|
else
|
|
{
|
|
switch (selectedField)
|
|
{
|
|
case DayField:
|
|
{
|
|
unsigned int day = date.day * 10 + digit;
|
|
if (day >= 10)
|
|
firstDigit = true;
|
|
if (day > daysInMonth(date.month, date.year))
|
|
day = 1;
|
|
date.day = day;
|
|
}
|
|
break;
|
|
|
|
case MonthField:
|
|
{
|
|
unsigned int month = date.month * 10 + digit;
|
|
if (month > 1)
|
|
firstDigit = true;
|
|
if (month > 12)
|
|
month = 1;
|
|
date.month = month;
|
|
}
|
|
break;
|
|
|
|
case YearField:
|
|
{
|
|
unsigned int year = date.year * 10 + digit;
|
|
if (year >= 1000)
|
|
firstDigit = true;
|
|
if (year <= 9999)
|
|
date.year = year;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
clampToValidDate(date);
|
|
notifyDateChanged();
|
|
}
|
|
else if (vkcode == VK_SUBTRACT || vkcode == VK_OEM_MINUS)
|
|
{
|
|
if (selectedField == YearField)
|
|
{
|
|
date.year = -date.year;
|
|
clampToValidDate(date);
|
|
notifyDateChanged();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
firstDigit = true;
|
|
|
|
switch (vkcode)
|
|
{
|
|
case VK_LEFT:
|
|
if ((int) selectedField == 0)
|
|
selectedField = DatePickerField((int) NumFields - 1);
|
|
else
|
|
selectedField = DatePickerField((int) selectedField - 1);
|
|
break;
|
|
|
|
case VK_RIGHT:
|
|
if ((int) selectedField == (int) NumFields - 1)
|
|
selectedField = DatePickerField(0);
|
|
else
|
|
selectedField = DatePickerField((int) selectedField + 1);
|
|
break;
|
|
|
|
case VK_UP:
|
|
incrementField();
|
|
notifyDateChanged();
|
|
break;
|
|
|
|
case VK_DOWN:
|
|
decrementField();
|
|
notifyDateChanged();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
InvalidateRect(hwnd, NULL, TRUE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::leftButtonDown(WORD key, int x, int y)
|
|
{
|
|
POINT pt;
|
|
pt.x = x;
|
|
pt.y = y;
|
|
|
|
if (PtInRect(&fieldRects[DayField], pt))
|
|
selectedField = DayField;
|
|
else if (PtInRect(&fieldRects[MonthField], pt))
|
|
selectedField = MonthField;
|
|
else if (PtInRect(&fieldRects[YearField], pt))
|
|
selectedField = YearField;
|
|
|
|
InvalidateRect(hwnd, NULL, TRUE);
|
|
|
|
::SetFocus(hwnd); // note that this is the Win32 API function, not the class method
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::setFocus(HWND lostFocus)
|
|
{
|
|
if (!haveFocus)
|
|
{
|
|
sendNotify(NM_SETFOCUS);
|
|
haveFocus = true;
|
|
}
|
|
|
|
firstDigit = true;
|
|
|
|
InvalidateRect(hwnd, NULL, TRUE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::killFocus(HWND lostFocus)
|
|
{
|
|
if (haveFocus)
|
|
{
|
|
sendNotify(NM_KILLFOCUS);
|
|
haveFocus = false;
|
|
}
|
|
|
|
InvalidateRect(hwnd, NULL, TRUE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::enable(bool b)
|
|
{
|
|
if (!b)
|
|
style &= ~WS_DISABLED;
|
|
else
|
|
style |= WS_DISABLED;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::notify(int id, const NMHDR& nmhdr)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::command(WPARAM, LPARAM)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
bool
|
|
DatePicker::sendNotify(UINT code)
|
|
{
|
|
NMHDR nmhdr;
|
|
|
|
ZeroMemory(&nmhdr, sizeof(nmhdr));
|
|
nmhdr.hwndFrom = hwnd;
|
|
nmhdr.idFrom = GetWindowLongPtr(hwnd, GWLP_ID);
|
|
nmhdr.code = code;
|
|
|
|
return SendMessage(parent, WM_NOTIFY,
|
|
nmhdr.idFrom,
|
|
reinterpret_cast<LPARAM>(&nmhdr)) ? true : false;
|
|
}
|
|
|
|
|
|
bool
|
|
DatePicker::notifyDateChanged()
|
|
{
|
|
NMDATETIMECHANGE change;
|
|
|
|
ZeroMemory(&change, sizeof(change));
|
|
change.nmhdr.hwndFrom = hwnd;
|
|
change.nmhdr.idFrom = GetWindowLongPtr(hwnd, GWLP_ID);
|
|
change.nmhdr.code = DTN_DATETIMECHANGE;
|
|
change.st.wYear = date.year;
|
|
change.st.wMonth = date.month;
|
|
change.st.wDay = date.day;
|
|
|
|
return SendMessage(parent, WM_NOTIFY,
|
|
change.nmhdr.idFrom,
|
|
reinterpret_cast<LPARAM>(&change)) ? true : false;
|
|
}
|
|
|
|
|
|
int
|
|
DatePicker::getFieldWidth(DatePickerField field, HDC hdc)
|
|
{
|
|
char* maxWidthText = "\0";
|
|
|
|
switch (field)
|
|
{
|
|
case YearField:
|
|
maxWidthText = "-2222 ";
|
|
break;
|
|
|
|
case MonthField:
|
|
maxWidthText = " Oct ";
|
|
break;
|
|
|
|
case DayField:
|
|
maxWidthText = "22 ";
|
|
break;
|
|
}
|
|
|
|
SIZE size;
|
|
GetTextExtentPoint32(hdc, maxWidthText, strlen(maxWidthText), &size);
|
|
|
|
return size.cx;
|
|
}
|
|
|
|
|
|
void
|
|
DatePicker::incrementField()
|
|
{
|
|
switch (selectedField)
|
|
{
|
|
case YearField:
|
|
date.year++;
|
|
clampToValidDate(date);
|
|
break;
|
|
case MonthField:
|
|
date.month++;
|
|
if (date.month > 12)
|
|
date.month = 1;
|
|
clampToValidDate(date);
|
|
break;
|
|
case DayField:
|
|
date.day++;
|
|
if (date.day > (int) daysInMonth(date.month, date.year))
|
|
date.day = 1;
|
|
// Skip 10 days deleted in Gregorian calendar reform
|
|
if (date.year == 1582 && date.month == 10 && date.day == 5)
|
|
date.day = 15;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
DatePicker::decrementField()
|
|
{
|
|
switch (selectedField)
|
|
{
|
|
case YearField:
|
|
date.year--;
|
|
clampToValidDate(date);
|
|
break;
|
|
case MonthField:
|
|
date.month--;
|
|
if (date.month < 1)
|
|
date.month = 12;
|
|
clampToValidDate(date);
|
|
break;
|
|
case DayField:
|
|
date.day--;
|
|
if (date.day < 1)
|
|
date.day = daysInMonth(date.month, date.year);
|
|
// Skip 10 days deleted in Gregorian calendar reform
|
|
if (date.year == 1582 && date.month == 10 && date.day == 14)
|
|
date.day = 4;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::destroy()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::resize(WORD flags, int width, int height)
|
|
{
|
|
clientRect.bottom = height;
|
|
clientRect.right = width;
|
|
|
|
InvalidateRect(hwnd, NULL, FALSE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::setSystemTime(DWORD flag, SYSTEMTIME* sysTime)
|
|
{
|
|
date.year = (int16_t) sysTime->wYear;
|
|
date.month = sysTime->wMonth;
|
|
date.day = sysTime->wDay;
|
|
|
|
InvalidateRect(hwnd, NULL, TRUE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
LRESULT
|
|
DatePicker::getSystemTime(SYSTEMTIME* sysTime)
|
|
{
|
|
if (sysTime != NULL)
|
|
{
|
|
sysTime->wYear = date.year;
|
|
sysTime->wMonth = date.month;
|
|
sysTime->wDay = date.day;
|
|
}
|
|
|
|
return GDT_VALID;
|
|
}
|
|
|
|
|
|
static LRESULT
|
|
DatePickerNCCreate(HWND hwnd, CREATESTRUCT& cs)
|
|
{
|
|
DWORD exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
|
|
|
|
exStyle |= WS_EX_CLIENTEDGE;
|
|
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle);
|
|
|
|
return DefWindowProc(hwnd, WM_NCCREATE, 0, reinterpret_cast<LPARAM>(&cs));
|
|
}
|
|
|
|
|
|
static LRESULT
|
|
DatePickerCreate(HWND hwnd, CREATESTRUCT& cs)
|
|
{
|
|
DatePicker* dp = new DatePicker(hwnd, cs);
|
|
SetWindowLongPtr(hwnd, 0, reinterpret_cast<DWORD_PTR>(dp));
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static LRESULT WINAPI
|
|
DatePickerProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
DatePicker* dp = reinterpret_cast<DatePicker*>(GetWindowLongPtr(hwnd, 0));
|
|
|
|
if (!dp && (uMsg != WM_CREATE) && (uMsg != WM_NCCREATE))
|
|
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
|
|
|
|
|
switch (uMsg)
|
|
{
|
|
case DTM_SETSYSTEMTIME:
|
|
return dp->setSystemTime(wParam, reinterpret_cast<SYSTEMTIME*>(lParam));
|
|
break;
|
|
|
|
case DTM_GETSYSTEMTIME:
|
|
return dp->getSystemTime(reinterpret_cast<SYSTEMTIME*>(lParam));
|
|
break;
|
|
|
|
case WM_NOTIFY:
|
|
return dp->notify((int) wParam, *reinterpret_cast<NMHDR*>(lParam));
|
|
break;
|
|
|
|
case WM_ENABLE:
|
|
return dp->enable(wParam != 0 ? true : false);
|
|
break;
|
|
|
|
//case WM_ERASEBKGND:
|
|
//break;
|
|
|
|
case WM_PAINT:
|
|
return dp->paint(reinterpret_cast<HDC>(wParam));
|
|
break;
|
|
|
|
case WM_GETDLGCODE:
|
|
return DLGC_WANTARROWS | DLGC_WANTCHARS;
|
|
|
|
case WM_KEYDOWN:
|
|
return dp->keyDown(wParam, lParam);
|
|
break;
|
|
|
|
case WM_KILLFOCUS:
|
|
return dp->killFocus(reinterpret_cast<HWND>(wParam));
|
|
break;
|
|
|
|
case WM_SETFOCUS:
|
|
return dp->setFocus(reinterpret_cast<HWND>(wParam));
|
|
break;
|
|
|
|
case WM_NCCREATE:
|
|
return DatePickerNCCreate(hwnd, *reinterpret_cast<CREATESTRUCT*>(lParam));
|
|
break;
|
|
|
|
case WM_SIZE:
|
|
return dp->resize(wParam, LOWORD(lParam), HIWORD(lParam));
|
|
break;
|
|
|
|
case WM_LBUTTONDOWN:
|
|
return dp->leftButtonDown((WORD) wParam, LOWORD(lParam), HIWORD(lParam));
|
|
break;
|
|
|
|
case WM_LBUTTONUP:
|
|
return 0;
|
|
break;
|
|
|
|
case WM_CREATE:
|
|
return DatePickerCreate(hwnd, *reinterpret_cast<CREATESTRUCT*>(lParam));
|
|
break;
|
|
|
|
case WM_DESTROY:
|
|
return dp->destroy();
|
|
break;
|
|
|
|
case WM_COMMAND:
|
|
return dp->command(wParam, lParam);
|
|
break;
|
|
|
|
default:
|
|
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
RegisterDatePicker()
|
|
{
|
|
WNDCLASS wc;
|
|
|
|
ZeroMemory(&wc, sizeof(wc));
|
|
wc.style = CS_GLOBALCLASS;
|
|
wc.lpfnWndProc = DatePickerProc;
|
|
wc.cbClsExtra = 0;
|
|
wc.cbWndExtra = sizeof(DatePicker*);
|
|
wc.hCursor = LoadCursor(0, IDC_ARROW);
|
|
wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
|
|
wc.lpszClassName = "CelestiaDatePicker";
|
|
|
|
RegisterClass(&wc);
|
|
}
|