// 28 october 2014 package ui import ( "strconv" "unsafe" ) // #include "winapi_windows.h" import "C" // TODO do we have to manually monitor user changes to the edit control? // TODO WS_EX_CLIENTEDGE on the updown? type spinbox struct { hwndEdit C.HWND hwndUpDown C.HWND changed *event // updown state updownVisible bool // keep these here to avoid having to get them out value int min int max int } func newSpinbox(min int, max int) Spinbox { s := new(spinbox) s.hwndEdit = C.newControl(editclass, C.textfieldStyle | C.ES_NUMBER, C.textfieldExtStyle) s.changed = newEvent() s.updownVisible = true // initially shown s.min = min s.max = max s.value = s.min s.remakeUpDown() C.controlSetControlFont(s.hwndEdit) C.setSpinboxEditSubclass(s.hwndEdit, unsafe.Pointer(s)) return s } func (s *spinbox) cap() { if s.value < s.min { s.value = s.min } if s.value > s.max { s.value = s.max } } func (s *spinbox) Value() int { return s.value } func (s *spinbox) SetValue(value int) { // UDM_SETPOS32 is documented to do what we want, but since we're keeping a copy of value we need to do it anyway s.value = value s.cap() C.SendMessageW(s.hwndUpDown, C.UDM_SETPOS32, 0, C.LPARAM(s.value)) } func (s *spinbox) OnChanged(e func()) { s.changed.set(e) } //export spinboxUpDownClicked func spinboxUpDownClicked(data unsafe.Pointer, nud *C.NMUPDOWN) { // this is where we do custom increments s := (*spinbox)(data) s.value = int(nud.iPos + nud.iDelta) // this can go above or below the bounds (the spinbox only rejects invalid values after the UDN_DELTAPOS notification is processed) // because we have a copy of the value, we need to fix that here s.cap() s.changed.fire() } //export spinboxEditChanged func spinboxEditChanged(data unsafe.Pointer) { // we're basically on our own here s := (*spinbox)(unsafe.Pointer(data)) // this basically does what OS X does: values too low get clamped to the minimum, values too high get clamped to the maximum, and deleting everything clamps to the minimum value, err := strconv.Atoi(getWindowText(s.hwndEdit)) if err != nil { // best we can do fo rnow in this case :S // a partial atoi() like in C would be more optimal // it handles the deleting everything case just fine value = s.min } s.value = value s.cap() C.SendMessageW(s.hwndUpDown, C.UDM_SETPOS32, 0, C.LPARAM(s.value)) // TODO position the insertion caret at the end (or wherever is appropriate) s.changed.fire() } func (s *spinbox) setParent(p *controlParent) { C.controlSetParent(s.hwndEdit, p.hwnd) C.controlSetParent(s.hwndUpDown, p.hwnd) } // an up-down control will only properly position itself the first time // stupidly, there are no messages to force a size calculation, nor can I seem to reset the buddy window to force a new position // alas, we have to make a new up/down control each time :( // TODO we'll need to store a copy of the current position and range for this func (s *spinbox) remakeUpDown() { // destroying the previous one, setting the parent properly, and subclassing are handled here s.hwndUpDown = C.newUpDown(s.hwndUpDown, unsafe.Pointer(s)) // for this to work, hwndUpDown needs to have rect [0 0 0 0] C.moveWindow(s.hwndUpDown, 0, 0, 0, 0) C.SendMessageW(s.hwndUpDown, C.UDM_SETBUDDY, C.WPARAM(uintptr(unsafe.Pointer(s.hwndEdit))), 0) C.SendMessageW(s.hwndUpDown, C.UDM_SETRANGE32, C.WPARAM(s.min), C.LPARAM(s.max)) C.SendMessageW(s.hwndUpDown, C.UDM_SETPOS32, 0, C.LPARAM(s.value)) if s.updownVisible { C.ShowWindow(s.hwndUpDown, C.SW_SHOW) } } // use the same height as normal text fields // TODO constrain the width somehow func (s *spinbox) preferredSize(d *sizing) (width, height int) { return fromdlgunitsX(textfieldWidth, d), fromdlgunitsY(textfieldHeight, d) } func (s *spinbox) resize(x int, y int, width int, height int, d *sizing) { C.moveWindow(s.hwndEdit, C.int(x), C.int(y), C.int(width), C.int(height)) s.remakeUpDown() } func (s *spinbox) nTabStops() int { // TODO does the up-down control count? return 1 } // TODO be sure to modify this when we add Show()/Hide() func (s *spinbox) containerShow() { C.ShowWindow(s.hwndEdit, C.SW_SHOW) C.ShowWindow(s.hwndUpDown, C.SW_SHOW) s.updownVisible = true } // TODO be sure to modify this when we add Show()/Hide() func (s *spinbox) containerHide() { C.ShowWindow(s.hwndEdit, C.SW_HIDE) C.ShowWindow(s.hwndUpDown, C.SW_HIDE) s.updownVisible = false }