// 14 april 2016 #include "uipriv_windows.h" struct fontDialog { HWND hwnd; HWND familyCombobox; HWND styleCombobox; HWND sizeCombobox; // TODO desc; fontCollection *fc; IDWriteFontFamily **families; UINT32 nFamilies; IDWriteGdiInterop *gdiInterop; RECT sampleRect; }; static LRESULT cbAddString(HWND cb, WCHAR *str) { LRESULT lr; lr = SendMessageW(cb, CB_ADDSTRING, 0, (LPARAM) str); if (lr == (LRESULT) CB_ERR || lr == (LRESULT) CB_ERRSPACE) logLastError("error adding item to combobox in cbAddString()"); return lr; } static LRESULT cbInsertStringAtTop(HWND cb, WCHAR *str) { LRESULT lr; lr = SendMessageW(cb, CB_INSERTSTRING, 0, (LPARAM) str); if (lr == (LRESULT) CB_ERR || lr == (LRESULT) CB_ERRSPACE) logLastError("error inserting item to combobox in cbInsertStringAtTop()"); return lr; } static void wipeStylesBox(struct fontDialog *f) { IDWriteFont *font; LRESULT i, n; n = SendMessageW(f->styleCombobox, CB_GETCOUNT, 0, 0); if (n == (LRESULT) CB_ERR) logLastError("error getting combobox item count in wipeStylesBox()"); for (i = 0; i < n; i++) { font = (IDWriteFont *) SendMessageW(f->styleCombobox, CB_GETITEMDATA, (WPARAM) i, 0); if (font == (IDWriteFont *) CB_ERR) logLastError("error getting font to release it in wipeStylesBox()"); font->Release(); } SendMessageW(f->styleCombobox, CB_RESETCONTENT, 0, 0); } static WCHAR *fontStyleName(struct fontDialog *f, IDWriteFont *font) { IDWriteLocalizedStrings *str; BOOL exists; WCHAR *wstr; HRESULT hr; // first try this; if this is present, use it... hr = font->GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_PREFERRED_SUBFAMILY_NAMES, &str, &exists); if (hr != S_OK) logHRESULT("error getting preferred subfamily string in fontStyleName()", hr); if (exists) goto good; // ...otherwise this font is good enough to be part of the main one on GDI as well, so try that name hr = font->GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_WIN32_SUBFAMILY_NAMES, &str, &exists); if (hr != S_OK) logHRESULT("error getting Win32 subfamily string in fontStyleName()", hr); // TODO what if !exists? good: wstr = fontCollectionCorrectString(f->fc, str); str->Release(); return wstr; } static void familyChanged(struct fontDialog *f) { LRESULT n; IDWriteFontList *specifics; IDWriteFont *specific; UINT32 i, ns; WCHAR *label; LRESULT pos; HRESULT hr; wipeStylesBox(f); n = SendMessageW(f->familyCombobox, CB_GETCURSEL, 0, 0); if (n == (LRESULT) CB_ERR) return; // TODO restore previous selection // TODO figure out what the correct sort order is hr = f->families[n]->GetMatchingFonts( DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STRETCH_NORMAL, DWRITE_FONT_STYLE_NORMAL, &specifics); if (hr != S_OK) logHRESULT("error getting styles for font in familyChanged()", hr); // TODO test mutliple streteches; all the fonts I have have only one stretch value ns = specifics->GetFontCount(); for (i = 0; i < ns; i++) { hr = specifics->GetFont(i, &specific); if (hr != S_OK) logHRESULT("error getting font for filling styles box in familyChanged()", hr); label = fontStyleName(f, specific); pos = cbAddString(f->styleCombobox, label); uiFree(label); if (SendMessageW(f->styleCombobox, CB_SETITEMDATA, (WPARAM) pos, (LPARAM) specific) == (LRESULT) CB_ERR) logLastError("error setting font data in styles box in familyChanged()"); } specifics->Release(); // TODO how do we preserve style selection? the real thing seems to have a very elaborate method of doing so // TODO check error SendMessageW(f->styleCombobox, CB_SETCURSEL, 0, 0); // TODO refine this a bit InvalidateRect(f->hwnd, NULL, TRUE/*TODO*/); } static struct fontDialog *beginFontDialog(HWND hwnd, LPARAM lParam) { struct fontDialog *f; UINT32 i; WCHAR *wname; LRESULT ten; HWND samplePlacement; HRESULT hr; f = uiNew(struct fontDialog); f->hwnd = hwnd; f->familyCombobox = GetDlgItem(f->hwnd, rcFontFamilyCombobox); if (f->familyCombobox == NULL) logLastError("error getting font family combobox handle in beginFontDialog()"); f->styleCombobox = GetDlgItem(f->hwnd, rcFontStyleCombobox); if (f->styleCombobox == NULL) logLastError("error getting font style combobox handle in beginFontDialog()"); f->sizeCombobox = GetDlgItem(f->hwnd, rcFontSizeCombobox); if (f->sizeCombobox == NULL) logLastError("error getting font size combobox handle in beginFontDialog()"); f->fc = loadFontCollection(); f->nFamilies = f->fc->fonts->GetFontFamilyCount(); f->families = new IDWriteFontFamily *[f->nFamilies]; for (i = 0; i < f->nFamilies; i++) { hr = f->fc->fonts->GetFontFamily(i, &(f->families[i])); if (hr != S_OK) logHRESULT("error getting font family in beginFontDialog()", hr); wname = fontCollectionFamilyName(f->fc, f->families[i]); cbAddString(f->familyCombobox, wname); uiFree(wname); } // TODO all comboboxes should select on type; these already scroll on type but not select // TODO behavior for the real thing: // - if prior size is in list, select and scroll to it // - if not, select nothing and don't scroll list at all (keep at top) // we do 8 and 9 later ten = cbAddString(f->sizeCombobox, L"10"); cbAddString(f->sizeCombobox, L"11"); cbAddString(f->sizeCombobox, L"12"); cbAddString(f->sizeCombobox, L"14"); cbAddString(f->sizeCombobox, L"16"); cbAddString(f->sizeCombobox, L"18"); cbAddString(f->sizeCombobox, L"20"); cbAddString(f->sizeCombobox, L"22"); cbAddString(f->sizeCombobox, L"24"); cbAddString(f->sizeCombobox, L"26"); cbAddString(f->sizeCombobox, L"28"); cbAddString(f->sizeCombobox, L"36"); cbAddString(f->sizeCombobox, L"48"); cbAddString(f->sizeCombobox, L"72"); if (SendMessageW(f->sizeCombobox, CB_SETCURSEL, (WPARAM) ten, 0) != ten) logLastError("error selecting 10 in the size combobox in beginFontDialog()"); // if we just use CB_ADDSTRING 8 and 9 will appear at the bottom of the list due to lexicographical sorting // if we use CB_INSERTSTRING instead it won't cbInsertStringAtTop(f->sizeCombobox, L"9"); cbInsertStringAtTop(f->sizeCombobox, L"8"); // 10 moved because of the above; figure out where it is now // we selected it earlier; getting the selection is easiest ten = SendMessageW(f->sizeCombobox, CB_GETCURSEL, 0, 0); // and finally put 10 at the top to imitate ChooseFont() if (SendMessageW(f->sizeCombobox, CB_SETTOPINDEX, (WPARAM) ten, 0) != 0) logLastError("error making 10 visible in the size combobox in beginFontDialog()"); // note: we can't add ES_NUMBER to the combobox entry (it seems to disable the entry instead?!), so we must do validation when the box is dmissed; TODO // TODO actually select Arial if (SendMessageW(f->familyCombobox, CB_SETCURSEL, (WPARAM) 0, 0) != 0) logLastError("error selecting Arial in the family combobox in beginFontDialog()"); familyChanged(f); hr = dwfactory->GetGdiInterop(&(f->gdiInterop)); if (hr != S_OK) logHRESULT("error getting GDI interop for font dialog in beginFontDialog()", hr); samplePlacement = GetDlgItem(f->hwnd, rcFontSamplePlacement); if (samplePlacement == NULL) logLastError("error getting sample placement static control handle in beginFontDialog()"); if (GetWindowRect(samplePlacement, &(f->sampleRect)) == 0) logLastError("error getting sample placement in beginFontDialog()"); mapWindowRect(NULL, f->hwnd, &(f->sampleRect)); if (DestroyWindow(samplePlacement) == 0) logLastError("error getting rid of the sample placement static control in beginFontDialog()"); return f; } static void endFontDialog(struct fontDialog *f, INT_PTR code) { UINT32 i; f->gdiInterop->Release(); wipeStylesBox(f); for (i = 0; i < f->nFamilies; i++) f->families[i]->Release(); delete[] f->families; fontCollectionFree(f->fc); if (EndDialog(f->hwnd, code) == 0) logLastError("error ending font dialog in endFontDialog()"); uiFree(f); } static INT_PTR tryFinishDialog(struct fontDialog *f, WPARAM wParam) { // cancelling if (LOWORD(wParam) != IDOK) { endFontDialog(f, 1); return TRUE; } // TODO endFontDialog(f, 2); return TRUE; } class gdiRenderer : public IDWriteTextRenderer { public: ULONG refcount; // IUnknown STDMETHODIMP QueryInterface(REFIID riid, void **ppv); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); // IDWritePixelSnapping STDMETHODIMP GetCurrentTransform(void *clientDrawingContext, DWRITE_MATRIX *transform); STDMETHODIMP GetPixelsPerDip(void *clientDrawingContext, FLOAT *pixelsPerDip); STDMETHODIMP IsPixelSnappingDisabled(void *clientDrawingContext, BOOL *isDisabled); // IDWriteTextRenderer STDMETHODIMP DrawGlyphRun( void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, DWRITE_MEASURING_MODE measuringMode, const DWRITE_GLYPH_RUN *glyphRun, const DWRITE_GLYPH_RUN_DESCRIPTION *glyphRunDescription, IUnknown *clientDrawingEffect); STDMETHODIMP DrawInlineObject(void *clientDrawingContext, FLOAT originX, FLOAT originY, IDWriteInlineObject *inlineObject, BOOL isSideways, BOOL isRightToLeft, IUnknown *clientDrawingEffect); STDMETHODIMP DrawStrikethrough(void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, const DWRITE_STRIKETHROUGH *strikethrough, IUnknown *clientDrawingEffect); STDMETHODIMP DrawUnderline(void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, const DWRITE_UNDERLINE *underline, IUnknown *clientDrawingEffect); }; STDMETHODIMP gdiRenderer::QueryInterface(REFIID riid, void **ppv) { if (ppv == NULL) return E_POINTER; if (riid == IID_IUnknown || riid == __uuidof (IDWritePixelSnapping) || riid == __uuidof (IDWriteTextRenderer)) { *ppv = static_cast(this); this->AddRef(); return S_OK; } *ppv = NULL; return E_NOINTERFACE; } STDMETHODIMP_(ULONG) gdiRenderer::AddRef() { this->refcount++; return this->refcount; } STDMETHODIMP_(ULONG) gdiRenderer::Release() { this->refcount--; if (this->refcount == 0) { delete this; return 0; } return this->refcount; } STDMETHODIMP gdiRenderer::GetCurrentTransform(void *clientDrawingContext, DWRITE_MATRIX *transform) { IDWriteBitmapRenderTarget *target = (IDWriteBitmapRenderTarget *) clientDrawingContext; return target->GetCurrentTransform(transform); } STDMETHODIMP gdiRenderer::GetPixelsPerDip(void *clientDrawingContext, FLOAT *pixelsPerDip) { IDWriteBitmapRenderTarget *target = (IDWriteBitmapRenderTarget *) clientDrawingContext; if (pixelsPerDip == NULL) return E_POINTER; *pixelsPerDip = target->GetPixelsPerDip(); return S_OK; } STDMETHODIMP gdiRenderer::IsPixelSnappingDisabled(void *clientDrawingContext, BOOL *isDisabled) { // TODO this is the MSDN recommendation if (isDisabled == NULL) return E_POINTER; *isDisabled = FALSE; return S_OK; } STDMETHODIMP gdiRenderer::DrawGlyphRun( void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, DWRITE_MEASURING_MODE measuringMode, const DWRITE_GLYPH_RUN *glyphRun, const DWRITE_GLYPH_RUN_DESCRIPTION *glyphRunDescription, IUnknown *clientDrawingEffect) { IDWriteBitmapRenderTarget *target = (IDWriteBitmapRenderTarget *) clientDrawingContext; RECT dirtyRect; IDWriteRenderingParams *rp; HRESULT hr; // TODO I cannot believe this is required; we really do need to switch to Direct2D hr = dwfactory->CreateRenderingParams(&rp); if (hr != S_OK) return hr; hr = target->DrawGlyphRun( baselineOriginX, baselineOriginY, measuringMode, glyphRun, rp, RGB(0, 0, 0), &dirtyRect); rp->Release(); if (hr != S_OK) return hr; if (SetBoundsRect(target->GetMemoryDC(), &dirtyRect, DCB_ACCUMULATE) == 0) // TODO return E_FAIL; return S_OK; } STDMETHODIMP gdiRenderer::DrawInlineObject(void *clientDrawingContext, FLOAT originX, FLOAT originY, IDWriteInlineObject *inlineObject, BOOL isSideways, BOOL isRightToLeft, IUnknown *clientDrawingEffect) { return E_NOTIMPL; } STDMETHODIMP gdiRenderer::DrawStrikethrough(void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, const DWRITE_STRIKETHROUGH *strikethrough, IUnknown *clientDrawingEffect) { return E_NOTIMPL; } STDMETHODIMP gdiRenderer::DrawUnderline(void *clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, const DWRITE_UNDERLINE *underline, IUnknown *clientDrawingEffect) { return E_NOTIMPL; } // TODO rename this function // TODO consider using Direct2D instead static void doPaint(struct fontDialog *f) { PAINTSTRUCT ps; HDC dc; IDWriteBitmapRenderTarget *target; gdiRenderer *renderer; LRESULT i; IDWriteFont *font; IDWriteLocalizedStrings *sampleStrings; BOOL exists; WCHAR *sample; WCHAR *family; WCHAR *wsize; double size; IDWriteTextFormat *format; IDWriteTextLayout *layout; HDC memoryDC; RECT memoryRect; HRESULT hr; dc = BeginPaint(f->hwnd, &ps); if (dc == NULL) logLastError("error beginning font dialog redraw in doPaint()"); hr = f->gdiInterop->CreateBitmapRenderTarget(dc, f->sampleRect.right - f->sampleRect.left, f->sampleRect.bottom - f->sampleRect.top, &target); if (hr != S_OK) logHRESULT("error creating bitmap render target for font dialog in doPaint()", hr); // TODO why is this needed? // TODO error check { RECT rdraw; rdraw.left = 0; rdraw.top = 0; rdraw.right = f->sampleRect.right - f->sampleRect.left; rdraw.bottom = f->sampleRect.bottom - f->sampleRect.top; FillRect(target->GetMemoryDC(), &rdraw, GetSysColorBrush(COLOR_BTNFACE)); } renderer = new gdiRenderer; renderer->refcount = 1; i = SendMessageW(f->styleCombobox, CB_GETCURSEL, 0, 0); if (i == (LRESULT) CB_ERR) {EndPaint(f->hwnd,&ps);return;} // TODO something more appropriate font = (IDWriteFont *) SendMessageW(f->styleCombobox, CB_GETITEMDATA, (WPARAM) i, 0); if (font == (IDWriteFont *) CB_ERR) logLastError("error getting font to draw font dialog sample in doPaint()"); // TOOD allow for a fallback 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"TODO get this from GTK+ instead of AaBbYyZz"; // TODO get this from the currently selected item family = windowText(f->familyCombobox); // TODO but NOT this wsize = windowText(f->sizeCombobox); // TODO error check? size = _wtof(wsize); uiFree(wsize); 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 size * (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("error creating IDWriteTextFormat for font dialog sample in doPaint()", hr); uiFree(family); hr = dwfactory->CreateTextLayout(sample, wcslen(sample), format, // FLOAT is float, not double, so this should work... TODO // TODO we don't want wrapping here FLT_MAX, FLT_MAX, &layout); if (hr != S_OK) logHRESULT("error creating IDWriteTextLayout for font dialog sample in doPaint()", hr); hr = layout->Draw(target, renderer, 0, 0); if (hr != S_OK) logHRESULT("error drawing font dialog sample text in doPaint()", hr); memoryDC = target->GetMemoryDC(); if (GetBoundsRect(memoryDC, &memoryRect, 0) == 0) logLastError("error getting size of memory DC for font dialog in doPaint()"); if (BitBlt(dc, f->sampleRect.left, f->sampleRect.top, memoryRect.right - memoryRect.left, memoryRect.bottom - memoryRect.top, memoryDC, 0, 0, SRCCOPY | NOMIRRORBITMAP) == 0) logLastError("error blitting sample text to font dialog in doPaint()"); layout->Release(); format->Release(); if (exists) uiFree(sample); renderer->Release(); target->Release(); EndPaint(f->hwnd, &ps); } 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) return FALSE; familyChanged(f); return TRUE; // TODO case rcFontStyleCombobox: case rcFontSizeCombobox: if (HIWORD(wParam) != CBN_SELCHANGE) return FALSE; // TODO error check; refine InvalidateRect(f->hwnd, NULL, TRUE); return TRUE; } return FALSE; case WM_PAINT: doPaint(f); SetWindowLongPtrW(f->hwnd, DWLP_MSGRESULT, 0); return TRUE; } return FALSE; } void showFontDialog(HWND parent) { switch (DialogBoxParamW(hInstance, MAKEINTRESOURCE(rcFontDialog), parent, fontDialogDlgProc, (LPARAM) NULL)) { case 1: // TODO cancel break; case 2: // TODO OK break; default: logLastError("error running font dialog in showFontDialog()"); } }