diff options
Diffstat (limited to 'src/libui_sdl/libui/windows/fontdialog.cpp')
-rw-r--r-- | src/libui_sdl/libui/windows/fontdialog.cpp | 686 |
1 files changed, 686 insertions, 0 deletions
diff --git a/src/libui_sdl/libui/windows/fontdialog.cpp b/src/libui_sdl/libui/windows/fontdialog.cpp new file mode 100644 index 0000000..603a17d --- /dev/null +++ b/src/libui_sdl/libui/windows/fontdialog.cpp @@ -0,0 +1,686 @@ +// 14 april 2016 +#include "uipriv_windows.hpp" + +// TODOs +// - quote the Choose Font sample here for reference +// - the Choose Font sample defaults to Regular/Italic/Bold/Bold Italic in some case (no styles?); do we? find out what the case is +// - do we set initial family and style topmost as well? +// - this should probably just handle IDWriteFonts + +struct fontDialog { + HWND hwnd; + HWND familyCombobox; + HWND styleCombobox; + HWND sizeCombobox; + + struct fontDialogParams *params; + + fontCollection *fc; + + RECT sampleRect; + HWND sampleBox; + + // we store the current selections in case an invalid string is typed in (partial or nonexistent or invalid number) + // on OK, these are what are read + LRESULT curFamily; + LRESULT curStyle; + double curSize; + + // these are finding the style that's closest to the previous one (these fields) when changing a font + DWRITE_FONT_WEIGHT weight; + DWRITE_FONT_STYLE style; + DWRITE_FONT_STRETCH stretch; +}; + +static LRESULT cbAddString(HWND cb, const WCHAR *str) +{ + LRESULT lr; + + lr = SendMessageW(cb, CB_ADDSTRING, 0, (LPARAM) str); + if (lr == (LRESULT) CB_ERR || lr == (LRESULT) CB_ERRSPACE) + logLastError(L"error adding item to combobox"); + return lr; +} + +static LRESULT cbInsertString(HWND cb, const WCHAR *str, WPARAM pos) +{ + LRESULT lr; + + lr = SendMessageW(cb, CB_INSERTSTRING, pos, (LPARAM) str); + if (lr != (LRESULT) pos) + logLastError(L"error inserting item to combobox"); + return lr; +} + +static LRESULT cbGetItemData(HWND cb, WPARAM item) +{ + LRESULT data; + + data = SendMessageW(cb, CB_GETITEMDATA, item, 0); + if (data == (LRESULT) CB_ERR) + logLastError(L"error getting combobox item data for font dialog"); + return data; +} + +static void cbSetItemData(HWND cb, WPARAM item, LPARAM data) +{ + if (SendMessageW(cb, CB_SETITEMDATA, item, data) == (LRESULT) CB_ERR) + logLastError(L"error setting combobox item data"); +} + +static BOOL cbGetCurSel(HWND cb, LRESULT *sel) +{ + LRESULT n; + + n = SendMessageW(cb, CB_GETCURSEL, 0, 0); + if (n == (LRESULT) CB_ERR) + return FALSE; + if (sel != NULL) + *sel = n; + return TRUE; +} + +static void cbSetCurSel(HWND cb, WPARAM item) +{ + if (SendMessageW(cb, CB_SETCURSEL, item, 0) != (LRESULT) item) + logLastError(L"error selecting combobox item"); +} + +static LRESULT cbGetCount(HWND cb) +{ + LRESULT n; + + n = SendMessageW(cb, CB_GETCOUNT, 0, 0); + if (n == (LRESULT) CB_ERR) + logLastError(L"error getting combobox item count"); + return n; +} + +static void cbWipeAndReleaseData(HWND cb) +{ + IUnknown *obj; + LRESULT i, n; + + n = cbGetCount(cb); + for (i = 0; i < n; i++) { + obj = (IUnknown *) cbGetItemData(cb, (WPARAM) i); + obj->Release(); + } + SendMessageW(cb, CB_RESETCONTENT, 0, 0); +} + +static WCHAR *cbGetItemText(HWND cb, WPARAM item) +{ + LRESULT len; + WCHAR *text; + + // note: neither message includes the terminating L'\0' + len = SendMessageW(cb, CB_GETLBTEXTLEN, item, 0); + if (len == (LRESULT) CB_ERR) + logLastError(L"error getting item text length from combobox"); + text = (WCHAR *) uiAlloc((len + 1) * sizeof (WCHAR), "WCHAR[]"); + if (SendMessageW(cb, CB_GETLBTEXT, item, (LPARAM) text) != len) + logLastError(L"error getting item text from combobox"); + return text; +} + +static BOOL cbTypeToSelect(HWND cb, LRESULT *posOut, BOOL restoreAfter) +{ + WCHAR *text; + LRESULT pos; + DWORD selStart, selEnd; + + // start by saving the current selection as setting the item will change the selection + SendMessageW(cb, CB_GETEDITSEL, (WPARAM) (&selStart), (LPARAM) (&selEnd)); + text = windowText(cb); + pos = SendMessageW(cb, CB_FINDSTRINGEXACT, (WPARAM) (-1), (LPARAM) text); + if (pos == (LRESULT) CB_ERR) { + uiFree(text); + return FALSE; + } + cbSetCurSel(cb, (WPARAM) pos); + if (posOut != NULL) + *posOut = pos; + if (restoreAfter) + if (SendMessageW(cb, WM_SETTEXT, 0, (LPARAM) text) != (LRESULT) TRUE) + logLastError(L"error restoring old combobox text"); + uiFree(text); + // and restore the selection like above + // TODO isn't there a 32-bit version of this + if (SendMessageW(cb, CB_SETEDITSEL, 0, MAKELPARAM(selStart, selEnd)) != (LRESULT) TRUE) + logLastError(L"error restoring combobox edit selection"); + return TRUE; +} + +static void wipeStylesBox(struct fontDialog *f) +{ + cbWipeAndReleaseData(f->styleCombobox); +} + +static WCHAR *fontStyleName(struct fontCollection *fc, IDWriteFont *font) +{ + IDWriteLocalizedStrings *str; + WCHAR *wstr; + HRESULT hr; + + hr = font->GetFaceNames(&str); + if (hr != S_OK) + logHRESULT(L"error getting font style name for font dialog", hr); + wstr = fontCollectionCorrectString(fc, str); + str->Release(); + return wstr; +} + +static void queueRedrawSampleText(struct fontDialog *f) +{ + // TODO TRUE? + invalidateRect(f->sampleBox, NULL, TRUE); +} + +static void styleChanged(struct fontDialog *f) +{ + LRESULT pos; + BOOL selected; + IDWriteFont *font; + + selected = cbGetCurSel(f->styleCombobox, &pos); + if (!selected) // on deselect, do nothing + return; + f->curStyle = pos; + + font = (IDWriteFont *) cbGetItemData(f->styleCombobox, (WPARAM) (f->curStyle)); + // these are for the nearest match when changing the family; see below + f->weight = font->GetWeight(); + f->style = font->GetStyle(); + f->stretch = font->GetStretch(); + + queueRedrawSampleText(f); +} + +static void styleEdited(struct fontDialog *f) +{ + if (cbTypeToSelect(f->styleCombobox, &(f->curStyle), FALSE)) + styleChanged(f); +} + +static void familyChanged(struct fontDialog *f) +{ + LRESULT pos; + BOOL selected; + IDWriteFontFamily *family; + IDWriteFont *font, *matchFont; + DWRITE_FONT_WEIGHT weight; + DWRITE_FONT_STYLE style; + DWRITE_FONT_STRETCH stretch; + UINT32 i, n; + UINT32 matching; + WCHAR *label; + HRESULT hr; + + selected = cbGetCurSel(f->familyCombobox, &pos); + if (!selected) // on deselect, do nothing + return; + f->curFamily = pos; + + family = (IDWriteFontFamily *) cbGetItemData(f->familyCombobox, (WPARAM) (f->curFamily)); + + // for the nearest style match + // when we select a new family, we want the nearest style to the previously selected one to be chosen + // this is how the Choose Font sample does it + hr = family->GetFirstMatchingFont( + f->weight, + f->stretch, + f->style, + &matchFont); + if (hr != S_OK) + logHRESULT(L"error finding first matching font to previous style in font dialog", hr); + // we can't just compare pointers; a "newly created" object comes out + // the Choose Font sample appears to do this instead + weight = matchFont->GetWeight(); + style = matchFont->GetStyle(); + stretch = matchFont->GetStretch(); + matchFont->Release(); + + // TODO test mutliple streteches; all the fonts I have have only one stretch value? + wipeStylesBox(f); + n = family->GetFontCount(); + matching = 0; // a safe/suitable default just in case + for (i = 0; i < n; i++) { + hr = family->GetFont(i, &font); + if (hr != S_OK) + logHRESULT(L"error getting font for filling styles box", hr); + label = fontStyleName(f->fc, font); + pos = cbAddString(f->styleCombobox, label); + uiFree(label); + cbSetItemData(f->styleCombobox, (WPARAM) pos, (LPARAM) font); + if (font->GetWeight() == weight && + font->GetStyle() == style && + font->GetStretch() == stretch) + matching = i; + } + + // and now, load the match + cbSetCurSel(f->styleCombobox, (WPARAM) matching); + styleChanged(f); +} + +// TODO search language variants like the sample does +static void familyEdited(struct fontDialog *f) +{ + if (cbTypeToSelect(f->familyCombobox, &(f->curFamily), FALSE)) + familyChanged(f); +} + +static const struct { + const WCHAR *text; + double value; +} defaultSizes[] = { + { L"8", 8 }, + { L"9", 9 }, + { L"10", 10 }, + { L"11", 11 }, + { L"12", 12 }, + { L"14", 14 }, + { L"16", 16 }, + { L"18", 18 }, + { L"20", 20 }, + { L"22", 22 }, + { L"24", 24 }, + { L"26", 26 }, + { L"28", 28 }, + { L"36", 36 }, + { L"48", 48 }, + { L"72", 72 }, + { NULL, 0 }, +}; + +static void sizeChanged(struct fontDialog *f) +{ + LRESULT pos; + BOOL selected; + + selected = cbGetCurSel(f->sizeCombobox, &pos); + if (!selected) // on deselect, do nothing + return; + f->curSize = defaultSizes[pos].value; + queueRedrawSampleText(f); +} + +static void sizeEdited(struct fontDialog *f) +{ + WCHAR *wsize; + double size; + + // handle type-to-selection + if (cbTypeToSelect(f->sizeCombobox, NULL, FALSE)) { + sizeChanged(f); + return; + } + // selection not chosen, try to parse the typing + wsize = windowText(f->sizeCombobox); + // this is what the Choose Font dialog does; it swallows errors while the real ChooseFont() is not lenient (and only checks on OK) + size = wcstod(wsize, NULL); + if (size <= 0) // don't change on invalid size + return; + f->curSize = size; + queueRedrawSampleText(f); +} + +static void fontDialogDrawSampleText(struct fontDialog *f, ID2D1RenderTarget *rt) +{ + D2D1_COLOR_F color; + D2D1_BRUSH_PROPERTIES props; + ID2D1SolidColorBrush *black; + IDWriteFont *font; + IDWriteLocalizedStrings *sampleStrings; + BOOL exists; + WCHAR *sample; + WCHAR *family; + IDWriteTextFormat *format; + D2D1_RECT_F rect; + HRESULT hr; + + color.r = 0.0; + color.g = 0.0; + color.b = 0.0; + color.a = 1.0; + ZeroMemory(&props, sizeof (D2D1_BRUSH_PROPERTIES)); + props.opacity = 1.0; + // identity matrix + props.transform._11 = 1; + props.transform._22 = 1; + hr = rt->CreateSolidColorBrush( + &color, + &props, + &black); + if (hr != S_OK) + logHRESULT(L"error creating solid brush", hr); + + font = (IDWriteFont *) cbGetItemData(f->styleCombobox, (WPARAM) f->curStyle); + hr = font->GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_SAMPLE_TEXT, &sampleStrings, &exists); + if (hr != S_OK) + exists = FALSE; + if (exists) { + sample = fontCollectionCorrectString(f->fc, sampleStrings); + sampleStrings->Release(); + } else + sample = L"The quick brown fox jumps over the lazy dog."; + + // DirectWrite doesn't allow creating a text format from a font; we need to get this ourselves + family = cbGetItemText(f->familyCombobox, f->curFamily); + hr = dwfactory->CreateTextFormat(family, + NULL, + font->GetWeight(), + font->GetStyle(), + font->GetStretch(), + // typographic points are 1/72 inch; this parameter is 1/96 inch + // fortunately Microsoft does this too, in https://msdn.microsoft.com/en-us/library/windows/desktop/dd371554%28v=vs.85%29.aspx + f->curSize * (96.0 / 72.0), + // see http://stackoverflow.com/questions/28397971/idwritefactorycreatetextformat-failing and https://msdn.microsoft.com/en-us/library/windows/desktop/dd368203.aspx + // TODO use the current locale again? + L"", + &format); + if (hr != S_OK) + logHRESULT(L"error creating IDWriteTextFormat", hr); + uiFree(family); + + rect.left = 0; + rect.top = 0; + rect.right = realGetSize(rt).width; + rect.bottom = realGetSize(rt).height; + rt->DrawText(sample, wcslen(sample), + format, + &rect, + black, + // TODO really? + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL); + + format->Release(); + if (exists) + uiFree(sample); + black->Release(); +} + +static LRESULT CALLBACK fontDialogSampleSubProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + ID2D1RenderTarget *rt; + struct fontDialog *f; + + switch (uMsg) { + case msgD2DScratchPaint: + rt = (ID2D1RenderTarget *) lParam; + f = (struct fontDialog *) dwRefData; + fontDialogDrawSampleText(f, rt); + return 0; + case WM_NCDESTROY: + if (RemoveWindowSubclass(hwnd, fontDialogSampleSubProc, uIdSubclass) == FALSE) + logLastError(L"error removing font dialog sample text subclass"); + break; + } + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} + +static void setupInitialFontDialogState(struct fontDialog *f) +{ + WCHAR wsize[512]; // this should be way more than enough + LRESULT pos; + + // first let's load the size + // the real font dialog: + // - if the chosen font size is in the list, it selects that item AND makes it topmost + // - if the chosen font size is not in the list, don't bother + // we'll simulate it by setting the text to a %f representation, then pretending as if it was entered + // TODO is 512 the correct number to pass to _snwprintf()? + // TODO will this revert to scientific notation? + _snwprintf(wsize, 512, L"%g", f->params->size); + // TODO make this a setWindowText() + if (SendMessageW(f->sizeCombobox, WM_SETTEXT, 0, (LPARAM) wsize) != (LRESULT) TRUE) + logLastError(L"error setting size combobox to initial font size"); + sizeEdited(f); + if (cbGetCurSel(f->sizeCombobox, &pos)) + if (SendMessageW(f->sizeCombobox, CB_SETTOPINDEX, (WPARAM) pos, 0) != 0) + logLastError(L"error making chosen size topmost in the size combobox"); + + // now we set the family and style + // we do this by first setting the previous style attributes, then simulating a font entered + f->weight = f->params->font->GetWeight(); + f->style = f->params->font->GetStyle(); + f->stretch = f->params->font->GetStretch(); + if (SendMessageW(f->familyCombobox, WM_SETTEXT, 0, (LPARAM) (f->params->familyName)) != (LRESULT) TRUE) + logLastError(L"error setting family combobox to initial font family"); + familyEdited(f); +} + +static struct fontDialog *beginFontDialog(HWND hwnd, LPARAM lParam) +{ + struct fontDialog *f; + UINT32 i, nFamilies; + IDWriteFontFamily *family; + WCHAR *wname; + LRESULT pos; + HWND samplePlacement; + HRESULT hr; + + f = uiNew(struct fontDialog); + f->hwnd = hwnd; + f->params = (struct fontDialogParams *) lParam; + + f->familyCombobox = getDlgItem(f->hwnd, rcFontFamilyCombobox); + f->styleCombobox = getDlgItem(f->hwnd, rcFontStyleCombobox); + f->sizeCombobox = getDlgItem(f->hwnd, rcFontSizeCombobox); + + f->fc = loadFontCollection(); + nFamilies = f->fc->fonts->GetFontFamilyCount(); + for (i = 0; i < nFamilies; i++) { + hr = f->fc->fonts->GetFontFamily(i, &family); + if (hr != S_OK) + logHRESULT(L"error getting font family", hr); + wname = fontCollectionFamilyName(f->fc, family); + pos = cbAddString(f->familyCombobox, wname); + uiFree(wname); + cbSetItemData(f->familyCombobox, (WPARAM) pos, (LPARAM) family); + } + + for (i = 0; defaultSizes[i].text != NULL; i++) + cbInsertString(f->sizeCombobox, defaultSizes[i].text, (WPARAM) i); + + samplePlacement = getDlgItem(f->hwnd, rcFontSamplePlacement); + uiWindowsEnsureGetWindowRect(samplePlacement, &(f->sampleRect)); + mapWindowRect(NULL, f->hwnd, &(f->sampleRect)); + uiWindowsEnsureDestroyWindow(samplePlacement); + f->sampleBox = newD2DScratch(f->hwnd, &(f->sampleRect), (HMENU) rcFontSamplePlacement, fontDialogSampleSubProc, (DWORD_PTR) f); + + setupInitialFontDialogState(f); + return f; +} + +static void endFontDialog(struct fontDialog *f, INT_PTR code) +{ + wipeStylesBox(f); + cbWipeAndReleaseData(f->familyCombobox); + fontCollectionFree(f->fc); + if (EndDialog(f->hwnd, code) == 0) + logLastError(L"error ending font dialog"); + uiFree(f); +} + +static INT_PTR tryFinishDialog(struct fontDialog *f, WPARAM wParam) +{ + IDWriteFontFamily *family; + + // cancelling + if (LOWORD(wParam) != IDOK) { + endFontDialog(f, 1); + return TRUE; + } + + // OK + destroyFontDialogParams(f->params); + f->params->font = (IDWriteFont *) cbGetItemData(f->styleCombobox, f->curStyle); + // we need to save font from being destroyed with the combobox + f->params->font->AddRef(); + f->params->size = f->curSize; + family = (IDWriteFontFamily *) cbGetItemData(f->familyCombobox, f->curFamily); + f->params->familyName = fontCollectionFamilyName(f->fc, family); + f->params->styleName = fontStyleName(f->fc, f->params->font); + endFontDialog(f, 2); + return TRUE; +} + +static INT_PTR CALLBACK fontDialogDlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + struct fontDialog *f; + + f = (struct fontDialog *) GetWindowLongPtrW(hwnd, DWLP_USER); + if (f == NULL) { + if (uMsg == WM_INITDIALOG) { + f = beginFontDialog(hwnd, lParam); + SetWindowLongPtrW(hwnd, DWLP_USER, (LONG_PTR) f); + return TRUE; + } + return FALSE; + } + + switch (uMsg) { + case WM_COMMAND: + SetWindowLongPtrW(f->hwnd, DWLP_MSGRESULT, 0); // just in case + switch (LOWORD(wParam)) { + case IDOK: + case IDCANCEL: + if (HIWORD(wParam) != BN_CLICKED) + return FALSE; + return tryFinishDialog(f, wParam); + case rcFontFamilyCombobox: + if (HIWORD(wParam) == CBN_SELCHANGE) { + familyChanged(f); + return TRUE; + } + if (HIWORD(wParam) == CBN_EDITCHANGE) { + familyEdited(f); + return TRUE; + } + return FALSE; + case rcFontStyleCombobox: + if (HIWORD(wParam) == CBN_SELCHANGE) { + styleChanged(f); + return TRUE; + } + if (HIWORD(wParam) == CBN_EDITCHANGE) { + styleEdited(f); + return TRUE; + } + return FALSE; + case rcFontSizeCombobox: + if (HIWORD(wParam) == CBN_SELCHANGE) { + sizeChanged(f); + return TRUE; + } + if (HIWORD(wParam) == CBN_EDITCHANGE) { + sizeEdited(f); + return TRUE; + } + return FALSE; + } + return FALSE; + } + return FALSE; +} + +BOOL showFontDialog(HWND parent, struct fontDialogParams *params) +{ + switch (DialogBoxParamW(hInstance, MAKEINTRESOURCE(rcFontDialog), parent, fontDialogDlgProc, (LPARAM) params)) { + case 1: // cancel + return FALSE; + case 2: // ok + // make the compiler happy by putting the return after the switch + break; + default: + logLastError(L"error running font dialog"); + } + return TRUE; +} + +static IDWriteFontFamily *tryFindFamily(IDWriteFontCollection *fc, const WCHAR *name) +{ + UINT32 index; + BOOL exists; + IDWriteFontFamily *family; + HRESULT hr; + + hr = fc->FindFamilyName(name, &index, &exists); + if (hr != S_OK) + logHRESULT(L"error finding font family for font dialog", hr); + if (!exists) + return NULL; + hr = fc->GetFontFamily(index, &family); + if (hr != S_OK) + logHRESULT(L"error extracting found font family for font dialog", hr); + return family; +} + +void loadInitialFontDialogParams(struct fontDialogParams *params) +{ + struct fontCollection *fc; + IDWriteFontFamily *family; + IDWriteFont *font; + HRESULT hr; + + // Our preferred font is Arial 10 Regular. + // 10 comes from the official font dialog. + // Arial Regular is a reasonable, if arbitrary, default; it's similar to the defaults on other systems. + // If Arial isn't found, we'll use Helvetica and then MS Sans Serif as fallbacks, and if not, we'll just grab the first font family in the collection. + + // We need the correct localized name for Regular (and possibly Arial too? let's say yes to be safe), so let's grab the strings from DirectWrite instead of hardcoding them. + fc = loadFontCollection(); + family = tryFindFamily(fc->fonts, L"Arial"); + if (family == NULL) { + family = tryFindFamily(fc->fonts, L"Helvetica"); + if (family == NULL) { + family = tryFindFamily(fc->fonts, L"MS Sans Serif"); + if (family == NULL) { + hr = fc->fonts->GetFontFamily(0, &family); + if (hr != S_OK) + logHRESULT(L"error getting first font out of font collection (worst case scenario)", hr); + } + } + } + + // next part is simple: just get the closest match to regular + hr = family->GetFirstMatchingFont( + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + &font); + if (hr != S_OK) + logHRESULT(L"error getting Regular font from Arial", hr); + + params->font = font; + params->size = 10; + params->familyName = fontCollectionFamilyName(fc, family); + params->styleName = fontStyleName(fc, font); + + // don't release font; we still need it + family->Release(); + fontCollectionFree(fc); +} + +void destroyFontDialogParams(struct fontDialogParams *params) +{ + params->font->Release(); + uiFree(params->familyName); + uiFree(params->styleName); +} + +WCHAR *fontDialogParamsToString(struct fontDialogParams *params) +{ + WCHAR *text; + + // TODO dynamically allocate + text = (WCHAR *) uiAlloc(512 * sizeof (WCHAR), "WCHAR[]"); + _snwprintf(text, 512, L"%s %s %g", + params->familyName, + params->styleName, + params->size); + return text; +} |