summaryrefslogtreecommitdiff
path: root/prefsize_windows.go
blob: 07a633fcd59269cfc11fe12eb3a56d62e2681fe2 (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
// 24 february 2014

package ui

import (
	"fmt"
//	"syscall"
	"unsafe"
)

// For Windows, Microsoft just hands you a list of preferred control sizes as part of the MSDN documentation and tells you to roll with it.
// These sizes are given in "dialog units", which are independent of the font in use.
// We need to convert these into standard pixels, which requires we get the device context of the OS window.
// References:
// - http://msdn.microsoft.com/en-us/library/windows/desktop/aa511279.aspx#controlsizing for control sizes
// - http://msdn.microsoft.com/en-us/library/ms645502%28VS.85%29.aspx - the calculation needed
// - http://support.microsoft.com/kb/125681 - to get the base X and Y
// (thanks to http://stackoverflow.com/questions/58620/default-button-size)
// For push buttons, date/time pickers, links (which we don't use), toolbars, and rebars (another type of toolbar), Common Controls version 6 provides convenient methods to use instead, falling back to the old way if it fails.

// As we are left with incomplete data, an arbitrary size will be chosen
const (
	defaultWidth = 100		// 2 * preferred width of buttons
)

type dlgunits struct {
	width	int
	height	int
	longest	bool		// TODO actually use this
	getsize	uintptr
}

var stdDlgSizes = [nctypes]dlgunits{
	c_button:		dlgunits{
		width:	50,
		height:	14,
		getsize:	_BCM_GETIDEALSIZE,
	},
	c_checkbox:	dlgunits{
		// widtdh is not defined here so assume longest
		longest:	true,
		height:	10,
	},
	c_combobox:	dlgunits{
		// technically the height of a combobox has to include the drop-down list (this is a historical accident: originally comboboxes weren't drop-down)
		// but since we're forcing Common Controls version 6, we can take advantage of one of its mechanisms to automatically fix this mistake (bad practice but whatever)
		// see also: http://blogs.msdn.com/b/oldnewthing/archive/2006/03/10/548537.aspx
		// note that the Microsoft guidelines pages don't take the list size into account
		longest:	true,
		height:	12,		// from the Visual Studio 2012 offline docs's Win32 layout page; the online page above says 14
	},
	c_lineedit:	dlgunits{
		longest:	true,
		height:	14,
	},
	c_label:		dlgunits{
		longest:	true,
		height:	8,
	},
	c_listbox:		dlgunits{
		longest:	true,
		// height is not clearly defined here ("an integral number of items (3 items minimum)") so just use a three-line edit control
		height:	14 + 10 + 10,
	},
	c_progressbar:		dlgunits{
		width:	237,		// the first reference says 107 also works; TODO decide which to use
		height:	8,
	},
	// TODO area
}

var (
	_selectObject = gdi32.NewProc("SelectObject")
	_getTextExtentPoint32 = gdi32.NewProc("GetTextExtentPoint32W")
	_getTextMetrics = gdi32.NewProc("GetTextMetricsW")
	_getWindowDC = user32.NewProc("GetWindowDC")
	_releaseDC = user32.NewProc("ReleaseDC")
)

// This function runs on uitask; call the functions directly.
func (s *sysData) preferredSize() (width int, height int) {
	if msg := stdDlgSizes[s.ctype].getsize; msg != 0 {
		var size _SIZE

		r1, _, _ := _sendMessage.Call(
			uintptr(s.hwnd),
			msg,
			uintptr(0),
			uintptr(unsafe.Pointer(&size)))
		if r1 != uintptr(_FALSE) {		// success
			return int(size.cx), int(size.cy)
		}
		// otherwise the message approach failed, so fall back to the regular approach
		println("message failed; falling back")
	}

	var dc _HANDLE
	var tm _TEXTMETRICS
	var baseX, baseY int

	// TODO use GetDC() and not GetWindowDC()?
	r1, _, err := _getWindowDC.Call(uintptr(s.hwnd))
	if r1 == 0 {		// failure
		panic(fmt.Errorf("error getting DC for preferred size calculations: %v", err))
	}
	dc = _HANDLE(r1)
	r1, _, err = _selectObject.Call(
		uintptr(dc),
		uintptr(controlFont))
	if r1 == 0  {		// failure
		panic(fmt.Errorf("error loading control font into device context for preferred size calculation: %v", err))
	}
	r1, _, err = _getTextMetrics.Call(
		uintptr(dc),
		uintptr(unsafe.Pointer(&tm)))
	if r1 == 0 {		// failure
		panic(fmt.Errorf("error getting text metrics for preferred size calculations: %v", err))
	}
	baseX = int(tm.tmAveCharWidth)		// TODO not optimal; third reference has better way
	baseY = int(tm.tmHeight)
	r1, _, err = _releaseDC.Call(
		uintptr(s.hwnd),
		uintptr(dc))
	if r1 == 0 {		// failure
		panic(fmt.Errorf("error releasing DC for preferred size calculations: %v", err))
	}

	// now that we have the conversion factors...
	width = stdDlgSizes[s.ctype].width
	if width == 0 {
		width = defaultWidth
	}
	height = stdDlgSizes[s.ctype].height
	width = muldiv(width, baseX, 4)		// equivalent to right of rect
	height = muldiv(height, baseY, 8)		// equivalent to bottom of rect
	return width, height
}

var (
	_mulDiv = kernel32.NewProc("MulDiv")
)

func muldiv(ma int, mb int, div int) int {
	// div will not be 0 in the usages above
	// we also ignore overflow; that isn't likely to happen for our use case anytime soon
	r1, _, _ := _mulDiv.Call(
		uintptr(int32(ma)),
		uintptr(int32(mb)),
		uintptr(int32(div)))
	return int(int32(r1))
}

// List Boxes do not dynamically handle horizontal scrollbars.
// We have to manually handle this ourselves.
// TODO make this run on the main thread when we switch to uitask taking function literals
// TODO this is inefficient; some caching would be nice
func recalcListboxWidth(hwnd _HWND) {
	var size _SIZE

	ret := make(chan uiret)
	defer close(ret)
	uitask <- &uimsg{
		call:		_sendMessage,
		p:		[]uintptr{
			uintptr(hwnd),
			uintptr(_LB_GETCOUNT),
			uintptr(0),
			uintptr(0),
		},
		ret:		ret,
	}
	r := <-ret
	if r.ret == uintptr(_LB_ERR) {		// failure
		panic(fmt.Errorf("error getting number of items for Listbox width calculations: %v", r.err))
	}
	n := int(r.ret)
	uitask <- &uimsg{
		call:		_getWindowDC,
		p:		[]uintptr{uintptr(hwnd)},
		ret:		ret,
	}
	r = <-ret
	if r.ret == 0 {		// failure
		panic(fmt.Errorf("error getting DC for Listbox width calculations: %v", r.err))
	}
	dc := _HANDLE(r.ret)
	uitask <- &uimsg{
		call:		_selectObject,
		p:		[]uintptr{
			uintptr(dc),
			uintptr(controlFont),
		},
		ret:		ret,
	}
	r = <-ret
	if r.ret == 0  {		// failure
		panic(fmt.Errorf("error loading control font into device context for Listbox width calculation: %v", r.err))
	}
	hextent := uintptr(0)
	for i := 0; i < n; i++ {
		uitask <- &uimsg{
			call:		_sendMessage,
			p:		[]uintptr{
				uintptr(hwnd),
				uintptr(_LB_GETTEXTLEN),
				uintptr(_WPARAM(i)),
				uintptr(0),
			},
			ret:		ret,
		}
		r := <-ret
		if r.ret == uintptr(_LB_ERR) {
			panic("UI library internal error: LB_ERR from LB_GETTEXTLEN in what we know is a valid listbox index (came from LB_GETSELITEMS)")
		}
		str := make([]uint16, r.ret)
		uitask <- &uimsg{
			call:		_sendMessage,
			p:		[]uintptr{
				uintptr(hwnd),
				uintptr(_LB_GETTEXT),
				uintptr(_WPARAM(i)),
				uintptr(_LPARAM(unsafe.Pointer(&str[0]))),
			},
			ret:		ret,
		}
		r = <-ret
		if r.ret == uintptr(_LB_ERR) {
			panic("UI library internal error: LB_ERR from LB_GETTEXT in what we know is a valid listbox index (came from LB_GETSELITEMS)")
		}
		// r.ret is still the length of the string; this time without the null terminator
		uitask <- &uimsg{
			call:		_getTextExtentPoint32,
			p:		[]uintptr{
				uintptr(dc),
				uintptr(unsafe.Pointer(&str[0])),
				r.ret,
				uintptr(unsafe.Pointer(&size)),
			},
			ret:		ret,
		}
		r = <-ret
		if r.ret == 0 {		// failure
			panic(fmt.Errorf("error getting width of item %d text for Listbox width calculation: %v", i, r.err))
		}
		if hextent < uintptr(size.cx) {
			hextent = uintptr(size.cx)
		}
	}
	uitask <- &uimsg{
		call:		_releaseDC,
		p:		[]uintptr{
			uintptr(hwnd),
			uintptr(dc),
		},
		ret:		ret,
	}
	r = <-ret
	if r.ret == 0 {		// failure
		panic(fmt.Errorf("error releasing DC for Listbox width calculations: %v", r.err))
	}
	uitask <- &uimsg{
		call:		_sendMessage,
		p:		[]uintptr{
			uintptr(hwnd),
			uintptr(_LB_SETHORIZONTALEXTENT),
			hextent,
			uintptr(0),
		},
		ret:		ret,
	}
	<-ret
}

type _SIZE struct {
	cx	int32		// originally LONG
	cy	int32
}

type _TEXTMETRICS struct {
	tmHeight				int32
	tmAscent				int32
	tmDescent			int32
	tmInternalLeading		int32
	tmExternalLeading		int32
	tmAveCharWidth		int32
	tmMaxCharWidth		int32
	tmWeight				int32
	tmOverhang			int32
	tmDigitizedAspectX		int32
	tmDigitizedAspectY		int32
	tmFirstChar			uint16
	tmLastChar			uint16
	tmDefaultChar			uint16
	tmBreakChar			uint16
	tmItalic				byte
	tmUnderlined			byte
	tmStruckOut			byte
	tmPitchAndFamily		byte
	tmCharSet			byte
}