summaryrefslogtreecommitdiff
path: root/alf/alfcombobox.cpp
blob: 3ce732651a138582a6d9ea7361807cd355fb4964 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
#include "alfpriv.h"

// Win32s doesn't like using the original message numbers for custom messages
#define ALF_CB_INSERTSTRING     (ALF_WM__BASE + 200)
#define ALF_CB_ADDSTRING        (ALF_WM__BASE + 201)
#define ALF_CB_GETCURSEL        (ALF_WM__BASE + 202)
#define ALF_CB_SETCURSEL        (ALF_WM__BASE + 203)
#define ALF_CB_GETLBTEXTLEN     (ALF_WM__BASE + 204)
#define ALF_CB_GETLBTEXT        (ALF_WM__BASE + 205)
#define ALF_CB_DELETESTRING     (ALF_WM__BASE + 206)
#define ALF_CB_GETSTYLE         (ALF_WM__BASE + 207)
#define ALF_CB_FINDSTRINGEXACT  (ALF_WM__BASE + 208)

TCHAR *_alf_comboClass = NULL;

typedef struct {
    DWORD  comboStyle;
} ALFComboCreateParams;

typedef struct {
    HWND hwndChild;
    int  dpi;
} ALFComboPriv;

static ALFComboPriv *
ALF_Combo_InitializePriv(HWND hwnd, ALFComboCreateParams *params)
{
    ALFComboPriv *priv = ALF_New(ALFComboPriv, 1);
    priv->dpi = 96;

    DWORD comboStyle = params->comboStyle | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS;

    priv->hwndChild = CreateWindowEx(0,
                                     TEXT("COMBOBOX"),
                                     TEXT(""),
                                     WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP | comboStyle,
                                     0, 0, 100, 200 /* FIXME needed for commctl32 v5, what is the best value here? */,
                                     hwnd,
                                     (HMENU) GetWindowLongPtrW(hwnd, GWLP_ID),
                                     ALF_HINSTANCE,
                                     NULL);

    return priv;
}

static void
ALF_Combo_FreePriv(ALFComboPriv *priv)
{
    ALF_Free(priv);
}

static int
ALF__ComboItemHeight(HWND hwnd)
{
    int height = 0;
    HDC hDc = GetDC(hwnd);
    HFONT font = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0);
    HFONT oldfont = SelectFont(hDc, font);

    TEXTMETRIC tm;
    ZeroMemory(&tm, sizeof(tm));

    if (GetTextMetrics(hDc, &tm)) {
        height = tm.tmHeight;
    }

    SelectFont(hDc, oldfont);

    if (!ALF_Compat_IsMinWindowsVersion(4, 0)) {
            // baseline alignment for NT3.x/Win32s
            // ideally, this would only be applied for the ODS_COMOBOXEDIT state,
            // but that state is not sent pre-Win95.
            height += 2;
        }

    ReleaseDC(hwnd, hDc);
    return height;
}

static LRESULT CALLBACK
ALF__ComboWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    ALFComboPriv *priv = (ALFComboPriv *)GetWindowLongPtr(hwnd, 0);

    if (uMsg == WM_CREATE) {
        ALFComboCreateParams *params = (ALFComboCreateParams*)((CREATESTRUCT*)lParam)->lpCreateParams;

        priv = ALF_Combo_InitializePriv(hwnd, params);
        SetWindowLongPtr(hwnd, 0, (LONG_PTR)priv);
    }

    if (uMsg == WM_DESTROY) {
        ALF_Combo_FreePriv(priv);
        priv = NULL;
        SetWindowLongPtr(hwnd, 0, 0);
    }

    if (uMsg == WM_ENABLE && priv) {
        EnableWindow(priv->hwndChild, (BOOL)wParam);
        return 0;
    }
    if (uMsg == WM_SETTEXT && priv) {
        SetWindowText(priv->hwndChild, (TCHAR*)lParam);
        ALF_InvalidateLayout(GetParent(hwnd));
    }
    if (uMsg == WM_GETTEXTLENGTH && priv) {
        return (LRESULT)GetWindowTextLength(priv->hwndChild);
    }
    if (uMsg == WM_GETTEXT && priv) {
        return GetWindowText(priv->hwndChild, (TCHAR*)lParam, (int)wParam);
    }
    if (uMsg == WM_SETFONT && priv) {
        SendMessage(priv->hwndChild, WM_SETFONT, wParam, lParam);
        SendMessage(priv->hwndChild, CB_SETITEMHEIGHT, (WPARAM)0, (LPARAM)ALF__ComboItemHeight(hwnd));

        ALF_InvalidateLayout(GetParent(hwnd));

        return 0;
    }
    if (uMsg == WM_GETFONT && priv) {
        return SendMessage(priv->hwndChild, WM_GETFONT, wParam, lParam);
    }
    if (uMsg == ALF_CB_GETSTYLE && priv) {
        return GetWindowLong(priv->hwndChild, GWL_STYLE);
    }
    if ((uMsg == ALF_CB_FINDSTRINGEXACT || uMsg == CB_FINDSTRINGEXACT) && priv) {
        return SendMessage(priv->hwndChild, CB_FINDSTRINGEXACT, wParam, lParam);
    }
    if ((uMsg == ALF_CB_ADDSTRING || uMsg == CB_ADDSTRING) && priv) {
        return SendMessage(priv->hwndChild, CB_ADDSTRING, wParam, lParam);
    }
    if ((uMsg == ALF_CB_INSERTSTRING || uMsg == CB_INSERTSTRING) && priv) {
        return SendMessage(priv->hwndChild, CB_INSERTSTRING, wParam, lParam);
    }
    if ((uMsg == ALF_CB_DELETESTRING || uMsg == CB_DELETESTRING) && priv) {
        return SendMessage(priv->hwndChild, CB_DELETESTRING, wParam, lParam);
    }
    if ((uMsg == ALF_CB_GETCURSEL || uMsg == CB_GETCURSEL) && priv) {
        return SendMessage(priv->hwndChild, CB_GETCURSEL, wParam, lParam);
    }
    if ((uMsg == ALF_CB_SETCURSEL || uMsg == CB_SETCURSEL) && priv) {
        LRESULT curSel = SendMessage(priv->hwndChild, CB_GETCURSEL, 0, 0);
        if ((WPARAM)curSel != wParam) {
            LRESULT newSel = SendMessage(priv->hwndChild, CB_SETCURSEL, wParam, 0);
            SendMessage(hwnd, WM_COMMAND, MAKEWPARAM(GetWindowLong(priv->hwndChild, GWL_ID), CBN_SELCHANGE), (LPARAM)priv->hwndChild);
            return newSel;
        }
    }
    if ((uMsg == ALF_CB_GETLBTEXTLEN || uMsg == CB_GETLBTEXTLEN) && priv) {
        return SendMessage(priv->hwndChild, CB_GETLBTEXTLEN, wParam, lParam);
    }
    if ((uMsg == ALF_CB_GETLBTEXT || uMsg == CB_GETLBTEXT) && priv) {
        return SendMessage(priv->hwndChild, CB_GETLBTEXT, wParam, lParam);
    }

    if (uMsg == WM_COMMAND && priv && (HWND)lParam == priv->hwndChild) {
        // XXX: for whatever reason, Win95 (and only win95 - doesn't happen on NT)
        // sends a wrong ID value in WPARAM. We fix it by replacing it with our own id.
        return SendMessage(GetParent(hwnd), WM_COMMAND, MAKEWPARAM(GetWindowLong(hwnd, GWL_ID), HIWORD(wParam)), (LPARAM)hwnd);
    }

    if (uMsg == ALF_WM_QUERYSIZE) {
        HDC hDc = GetDC(hwnd);
        HFONT font = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0);

        if (font) {
            HFONT oldfont = SelectFont(hDc, font);

            TEXTMETRIC tm;
            ZeroMemory(&tm, sizeof(tm));

            if (GetTextMetrics(hDc, &tm)) {
                SIZE *ps = (SIZE*)lParam;
                int cx = ALF_CentipointsToPixels(12000, priv->dpi);
                int cy = ps->cy = tm.tmHeight + 2*ALF_Compat_GetSystemMetricsForDpi(
                            SM_CYEDGE, (UINT)priv->dpi)
                        + 4 /* padding internal to the edit control */;

                if (ps->cx < cx)
                    ps->cx = cx;

                if (ps->cy < cy)
                    ps->cy = cy;
            }

            SelectFont(hDc, oldfont);
        }

        ReleaseDC(hwnd, hDc);
        return 0;
    }

    if (uMsg == WM_MEASUREITEM) {
        PMEASUREITEMSTRUCT lpmis = (LPMEASUREITEMSTRUCT) lParam;

        lpmis->itemHeight = (UINT)ALF__ComboItemHeight(hwnd);

        return TRUE;
    }

    if (uMsg == WM_DRAWITEM) {
        LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT) lParam;

        if (lpdis->itemID != (UINT)-1) { // Empty item
            DWORD clrForeground = SetTextColor(lpdis->hDC,
                GetSysColor(lpdis->itemState & (ODS_SELECTED | ODS_FOCUS) ?
                COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT));

            DWORD clrBackground = SetBkColor(lpdis->hDC,
                GetSysColor(lpdis->itemState & (ODS_SELECTED | ODS_FOCUS) ?
                COLOR_HIGHLIGHT : COLOR_WINDOW));

            LONG y = lpdis->rcItem.top;
            LONG x = lpdis->rcItem.left;

            if (!ALF_Compat_IsMinWindowsVersion(4, 0)) {
                // baseline alignment for NT3.x/Win32s
                // ideally, this would only be applied for the ODS_COMOBOXEDIT state,
                // but that state is not sent pre-Win95.
                y += 1;
            }

            // add left padding like an edit control
            TEXTMETRIC tm;
            ZeroMemory(&tm, sizeof(tm));
            if (GetTextMetrics(lpdis->hDC, &tm)) {
                if (tm.tmPitchAndFamily & (TMPF_VECTOR | TMPF_TRUETYPE))
                    x += ALF_GetAveCharWidth(lpdis->hDC) / 2;
            }

            // Get and display the text for the list item.
            int len = (int)SendMessage(lpdis->hwndItem, CB_GETLBTEXTLEN, (WPARAM)lpdis->itemID, 0);
            if (len != CB_ERR) {
                TCHAR *buf = (TCHAR*)LocalAlloc(LPTR, (SIZE_T)(len+1) * sizeof(TCHAR));

                SendMessage(lpdis->hwndItem, CB_GETLBTEXT,
                    lpdis->itemID, (LPARAM)buf);

                ExtTextOut(lpdis->hDC, x, y,
                    ETO_CLIPPED | ETO_OPAQUE, &lpdis->rcItem,
                    buf, (UINT)lstrlen(buf), NULL);

                LocalFree(buf);
            }

            // Restore the previous colors.
            SetTextColor(lpdis->hDC, clrForeground);
            SetBkColor(lpdis->hDC, clrBackground);
        }

        // If the item has the focus, draw the focus rectangle.
        if ((lpdis->itemState & ODS_FOCUS) && !(lpdis->itemState & ODS_NOFOCUSRECT))
            DrawFocusRect(lpdis->hDC, &lpdis->rcItem);

        return TRUE;
    }

    if (uMsg == WM_WINDOWPOSCHANGED && priv) {
        WINDOWPOS *pos = (WINDOWPOS *)lParam;
        if (!(pos->flags & SWP_NOSIZE)) {
            // XXX: When resizing the combo box, it will improperly draw a selection.
            // this appears to be a well-known bug that is still not fixed, even in Win10.
            // workaround based on https://stackoverflow.com/questions/49603893/how-to-deal-with-invalidly-painted-combobox-control-in-win32-winapi
            LRESULT sel = SendMessage(priv->hwndChild, CB_GETEDITSEL, 0, 0);
            SendMessage(priv->hwndChild, CB_SETEDITSEL, 0, -1);
            // SWP_NOCOPYBITS because NT 3.51 doesn't properly repaint the drop-down button
            SetWindowPos(priv->hwndChild, NULL, 0, 0, pos->cx, 200, SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOCOPYBITS);
            SendMessage(priv->hwndChild, CB_SETEDITSEL, 0, (LPARAM)sel);

            int heightOffset = 0;
            if (ALF_Compat_IsMinWindowsVersion(4, 0)) {
                heightOffset = - 2*ALF_Compat_GetSystemMetricsForDpi(
                            SM_CYEDGE, (UINT)priv->dpi)
                            - 2;
            }

            SendMessage(priv->hwndChild, CB_SETITEMHEIGHT, (WPARAM)-1, pos->cy + heightOffset);
            SendMessage(priv->hwndChild, CB_SETITEMHEIGHT, (WPARAM)0, (LPARAM)ALF__ComboItemHeight(hwnd));
        }
    }

    if (uMsg == ALF_WM_DPICHANGE && priv) {
        priv->dpi = (int)lParam;
        ALF_InvalidateLayout(GetParent(hwnd));
        return TRUE;
    }

    if (!ALF_Compat_IsMinWindowsVersion(4, 0)) {
        // HACK to draw the space between edit control and button as COLOR_BTNFACE
        if (uMsg == WM_CTLCOLORLISTBOX && priv && !(GetWindowLong(priv->hwndChild, GWL_STYLE) & 1)) {
            return (LRESULT)GetSysColorBrush(COLOR_BTNFACE);
        }
    }

    if (ALF_ShouldMessageBubble(hwnd, uMsg, wParam, lParam)) {
        return SendMessage(GetParent(hwnd), uMsg, wParam, lParam);
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}


void
ALF_RegisterComboClass(void)
{
    WNDCLASS cls;
    ZeroMemory(&cls, sizeof(cls));

    TCHAR  classNameBuf[256];
    ALF_BuildUniqueName(classNameBuf, TEXT("ALFComboBox."), (ULONG_PTR)&_alf_comboClass);

    cls.hInstance = ALF_HINSTANCE;
    cls.hCursor = LoadCursor(NULL, (LPTSTR)IDC_ARROW);
    cls.hbrBackground = (HBRUSH)(COLOR_BTNFACE+1);
    cls.lpszClassName = classNameBuf;
    cls.cbWndExtra = sizeof(void*);
    cls.lpfnWndProc = ALF__ComboWindowProc;

    ATOM classatom = RegisterClass(&cls);
    if (!classatom)
        MessageBox(NULL, TEXT("FATAL: Could not register Combo class"), NULL, MB_OK);

    _alf_comboClass = MAKEINTATOM(classatom);
}

static HWND
ALF_InternalAddComboBox(HWND win, WORD id, int x, int y, DWORD style, const TCHAR *defaultText)
{
    ALFComboCreateParams cp;
    cp.comboStyle = style;

    HWND hwndCombo = CreateWindowEx(WS_EX_CONTROLPARENT,
                                    _alf_comboClass,
                                    TEXT(""),
                                    WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
                                    0, 0, 0, 0,
                                    win,
                                    (HMENU)(ULONG_PTR)id,
                                    ALF_HINSTANCE,
                                    &cp);

    if (defaultText)
        SetWindowText(hwndCombo, defaultText);

    ALF_AddWidget(win, x, y, hwndCombo, 0, 0, ALF_LAYOUT_SIZE_QUERY | ALF_LAYOUT_INHERITFONT | ALF_LAYOUT_SENDDPICHANGE);

    return hwndCombo;
}

HWND
ALF_AddEditableComboBox(HWND win, WORD id, int x, int y, const TCHAR *defaultText)
{
    return ALF_InternalAddComboBox(win, id, x, y, CBS_DROPDOWN, defaultText);
}

HWND
ALF_AddSelectionOnlyComboBox(HWND win, WORD id, int x, int y)
{
    return ALF_InternalAddComboBox(win, id, x, y, CBS_DROPDOWNLIST, NULL);
}

int /* index */
ALF_ComboBoxAddString(HWND combo, const TCHAR *text)
{
    return (int)SendMessage(combo, ALF_CB_ADDSTRING, 0, (LPARAM)text);
}

TCHAR *
ALF_ComboBoxCurrentText(HWND combo)
{
    LRESULT style = SendMessage(combo, ALF_CB_GETSTYLE, 0, 0);
    if (style & CBS_DROPDOWNLIST) {
        int i = ALF_ComboBoxCurrentIndex(combo);
        if (i >= 0) {
            return ALF_ComboBoxStringForIndex(combo, i);
        }
    }

    return ALF_Text(combo);
}

void
ALF_ComboBoxSetText(HWND combo, const TCHAR *text)
{
    LRESULT index = SendMessage(combo, ALF_CB_FINDSTRINGEXACT, (WPARAM)-1, (LPARAM)text);
    if (index >= 0) {
        SendMessage(combo, ALF_CB_SETCURSEL, (WPARAM)index, 0);
    } else {
        SetWindowText(combo, text);
    }
}

int
ALF_ComboBoxCurrentIndex(HWND combo)
{
    return (int)SendMessage(combo, ALF_CB_GETCURSEL, 0, 0);
}

void
ALF_ComboBoxSetCurrentIndex(HWND combo, int index)
{
    SendMessage(combo, ALF_CB_SETCURSEL, (WPARAM)index, 0);
}

TCHAR * // free with ALF_Free
ALF_ComboBoxStringForIndex(HWND combo, int index)
{
    int len = (int)SendMessage(combo, ALF_CB_GETLBTEXTLEN, (WPARAM)index, 0);
    if (len > 0) {
        TCHAR* buf = ALF_New(TCHAR, (SIZE_T)len + 1);
        if (SendMessage(combo, ALF_CB_GETLBTEXT, (WPARAM)index, (LPARAM)buf)) {
            return buf;
        } else {
            ALF_Free(buf);
        }
    }

    return NULL;
}

void
ALF_ComboBoxInsertString(HWND combo, int index, const TCHAR *text)
{
    SendMessage(combo, ALF_CB_INSERTSTRING, (WPARAM)index, (LPARAM)text);
}

void
ALF_ComboBoxRemoveString(HWND combo, int index)
{
    SendMessage(combo, ALF_CB_DELETESTRING, (WPARAM)index, 0);
}