erm/vendor/github.com/lxn/walk/numberedit.go

928 lines
20 KiB
Go
Raw Normal View History

2021-07-30 22:29:20 +00:00
// Copyright 2011 The Walk Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
package walk
import (
"bytes"
"fmt"
"math"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/lxn/win"
)
const numberEditWindowClass = `\o/ Walk_NumberEdit_Class \o/`
func init() {
AppendToWalkInit(func() {
MustRegisterWindowClass(numberEditWindowClass)
})
}
// NumberEdit is a widget that is suited to edit numeric values.
type NumberEdit struct {
WidgetBase
edit *numberLineEdit
maxValueChangedPublisher EventPublisher
minValueChangedPublisher EventPublisher
prefixChangedPublisher EventPublisher
suffixChangedPublisher EventPublisher
}
// NewNumberEdit returns a new NumberEdit widget as child of parent.
func NewNumberEdit(parent Container) (*NumberEdit, error) {
ne := new(NumberEdit)
if err := InitWidget(
ne,
parent,
numberEditWindowClass,
win.WS_VISIBLE,
win.WS_EX_CONTROLPARENT); err != nil {
return nil, err
}
var succeeded bool
defer func() {
if !succeeded {
ne.Dispose()
}
}()
var err error
if ne.edit, err = newNumberLineEdit(ne); err != nil {
return nil, err
}
ne.edit.applyFont(ne.Font())
ne.SetRange(-math.MaxFloat64, math.MaxFloat64)
if err = ne.SetValue(0); err != nil {
return nil, err
}
ne.GraphicsEffects().Add(InteractionEffect)
ne.GraphicsEffects().Add(FocusEffect)
ne.MustRegisterProperty("MaxValue", NewProperty(
func() interface{} {
return ne.MaxValue()
},
func(v interface{}) error {
return ne.SetRange(ne.MinValue(), assertFloat64Or(v, 0.0))
},
ne.minValueChangedPublisher.Event()))
ne.MustRegisterProperty("MinValue", NewProperty(
func() interface{} {
return ne.MinValue()
},
func(v interface{}) error {
return ne.SetRange(assertFloat64Or(v, 0.0), ne.MaxValue())
},
ne.maxValueChangedPublisher.Event()))
ne.MustRegisterProperty("Prefix", NewProperty(
func() interface{} {
return ne.Prefix()
},
func(v interface{}) error {
return ne.SetPrefix(assertStringOr(v, ""))
},
ne.prefixChangedPublisher.Event()))
ne.MustRegisterProperty("ReadOnly", NewProperty(
func() interface{} {
return ne.ReadOnly()
},
func(v interface{}) error {
return ne.SetReadOnly(v.(bool))
},
ne.edit.readOnlyChangedPublisher.Event()))
ne.MustRegisterProperty("Suffix", NewProperty(
func() interface{} {
return ne.Suffix()
},
func(v interface{}) error {
return ne.SetSuffix(assertStringOr(v, ""))
},
ne.suffixChangedPublisher.Event()))
ne.MustRegisterProperty("Value", NewProperty(
func() interface{} {
return ne.Value()
},
func(v interface{}) error {
return ne.SetValue(assertFloat64Or(v, 0.0))
},
ne.edit.valueChangedPublisher.Event()))
succeeded = true
return ne, nil
}
func (ne *NumberEdit) applyEnabled(enabled bool) {
ne.WidgetBase.applyEnabled(enabled)
if ne.edit == nil {
return
}
ne.edit.applyEnabled(enabled)
}
func (ne *NumberEdit) applyFont(font *Font) {
ne.WidgetBase.applyFont(font)
if ne.edit == nil {
return
}
ne.edit.applyFont(font)
}
// Decimals returns the number of decimal places in the NumberEdit.
func (ne *NumberEdit) Decimals() int {
return ne.edit.decimals
}
// SetDecimals sets the number of decimal places in the NumberEdit.
func (ne *NumberEdit) SetDecimals(decimals int) error {
if decimals < 0 || decimals > 8 {
return newError("decimals must >= 0 && <= 8")
}
ne.edit.decimals = decimals
return ne.SetValue(ne.edit.value)
}
// Prefix returns the text that appears in the NumberEdit before the number.
func (ne *NumberEdit) Prefix() string {
return syscall.UTF16ToString(ne.edit.prefix)
}
// SetPrefix sets the text that appears in the NumberEdit before the number.
func (ne *NumberEdit) SetPrefix(prefix string) error {
if prefix == ne.Prefix() {
return nil
}
p, err := syscall.UTF16FromString(prefix)
if err != nil {
return err
}
old := ne.edit.prefix
ne.edit.prefix = p[:len(p)-1]
if err := ne.edit.setTextFromValue(ne.edit.value); err != nil {
ne.edit.prefix = old
return err
}
ne.prefixChangedPublisher.Publish()
return nil
}
// PrefixChanged returns the event that is published when the prefix changed.
func (ne *NumberEdit) PrefixChanged() *Event {
return ne.prefixChangedPublisher.Event()
}
// Suffix returns the text that appears in the NumberEdit after the number.
func (ne *NumberEdit) Suffix() string {
return syscall.UTF16ToString(ne.edit.suffix)
}
// SetSuffix sets the text that appears in the NumberEdit after the number.
func (ne *NumberEdit) SetSuffix(suffix string) error {
if suffix == ne.Suffix() {
return nil
}
s, err := syscall.UTF16FromString(suffix)
if err != nil {
return err
}
old := ne.edit.suffix
ne.edit.suffix = s[:len(s)-1]
if err := ne.edit.setTextFromValue(ne.edit.value); err != nil {
ne.edit.suffix = old
return err
}
ne.suffixChangedPublisher.Publish()
return nil
}
// SuffixChanged returns the event that is published when the suffix changed.
func (ne *NumberEdit) SuffixChanged() *Event {
return ne.suffixChangedPublisher.Event()
}
// Increment returns the amount by which the NumberEdit increments or decrements
// its value, when the user presses the KeyDown or KeyUp keys, or when the mouse
// wheel is rotated.
func (ne *NumberEdit) Increment() float64 {
return ne.edit.increment
}
// SetIncrement sets the amount by which the NumberEdit increments or decrements
// its value, when the user presses the KeyDown or KeyUp keys, or when the mouse
// wheel is rotated.
func (ne *NumberEdit) SetIncrement(increment float64) error {
ne.edit.increment = increment
return nil
}
// MinValue returns the minimum value the NumberEdit will accept.
func (ne *NumberEdit) MinValue() float64 {
return ne.edit.minValue
}
// MinValue returns the maximum value the NumberEdit will accept.
func (ne *NumberEdit) MaxValue() float64 {
return ne.edit.maxValue
}
// SetRange sets the minimum and maximum values the NumberEdit will accept.
//
// If the current value is out of this range, it will be adjusted.
func (ne *NumberEdit) SetRange(min, max float64) error {
if min > max {
return newError(fmt.Sprintf("invalid range - min: %f, max: %f", min, max))
}
minChanged := min != ne.edit.minValue
maxChanged := max != ne.edit.maxValue
ne.edit.minValue = min
ne.edit.maxValue = max
if min != max {
if ne.edit.value < min {
if err := ne.edit.setValue(min, true); err != nil {
return err
}
} else if ne.edit.value > max {
if err := ne.edit.setValue(max, true); err != nil {
return err
}
}
}
if minChanged {
ne.minValueChangedPublisher.Publish()
}
if maxChanged {
ne.maxValueChangedPublisher.Publish()
}
return nil
}
// Value returns the value of the NumberEdit.
func (ne *NumberEdit) Value() float64 {
return ne.edit.value
}
// SetValue sets the value of the NumberEdit.
func (ne *NumberEdit) SetValue(value float64) error {
if ne.edit.minValue != ne.edit.maxValue &&
(value < ne.edit.minValue || value > ne.edit.maxValue) {
return newError("value out of range")
}
return ne.edit.setValue(value, true)
}
// ValueChanged returns an Event that can be used to track changes to Value.
func (ne *NumberEdit) ValueChanged() *Event {
return ne.edit.valueChangedPublisher.Event()
}
// SetFocus sets the keyboard input focus to the NumberEdit.
func (ne *NumberEdit) SetFocus() error {
if win.SetFocus(ne.edit.hWnd) == 0 {
return lastError("SetFocus")
}
return nil
}
// TextSelection returns the range of the current text selection of the
// NumberEdit.
func (ne *NumberEdit) TextSelection() (start, end int) {
return ne.edit.TextSelection()
}
// SetTextSelection sets the range of the current text selection of the
// NumberEdit.
func (ne *NumberEdit) SetTextSelection(start, end int) {
ne.edit.SetTextSelection(start, end)
}
// ReadOnly returns whether the NumberEdit is in read-only mode.
func (ne *NumberEdit) ReadOnly() bool {
return ne.edit.ReadOnly()
}
// SetReadOnly sets whether the NumberEdit is in read-only mode.
func (ne *NumberEdit) SetReadOnly(readOnly bool) error {
if readOnly != ne.ReadOnly() {
ne.invalidateBorderInParent()
}
return ne.edit.SetReadOnly(readOnly)
}
// Background returns the background Brush of the NumberEdit.
//
// By default this is nil.
func (ne *NumberEdit) Background() Brush {
return ne.edit.Background()
}
// SetBackground sets the background Brush of the NumberEdit.
func (ne *NumberEdit) SetBackground(bg Brush) {
ne.edit.SetBackground(bg)
}
// TextColor returns the Color used to draw the text of the NumberEdit.
func (ne *NumberEdit) TextColor() Color {
return ne.edit.TextColor()
}
// TextColor sets the Color used to draw the text of the NumberEdit.
func (ne *NumberEdit) SetTextColor(c Color) {
ne.edit.SetTextColor(c)
}
func (*NumberEdit) NeedsWmSize() bool {
return true
}
// WndProc is the window procedure of the NumberEdit.
//
// When implementing your own WndProc to add or modify behavior, call the
// WndProc of the embedded NumberEdit for messages you don't handle yourself.
func (ne *NumberEdit) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case win.WM_CTLCOLOREDIT, win.WM_CTLCOLORSTATIC:
if hBrush := ne.handleWMCTLCOLOR(wParam, lParam); hBrush != 0 {
return hBrush
}
case win.WM_WINDOWPOSCHANGED:
wp := (*win.WINDOWPOS)(unsafe.Pointer(lParam))
if wp.Flags&win.SWP_NOSIZE != 0 {
break
}
if ne.edit == nil {
break
}
cb := ne.ClientBoundsPixels()
if err := ne.edit.SetBoundsPixels(cb); err != nil {
break
}
}
return ne.WidgetBase.WndProc(hwnd, msg, wParam, lParam)
}
func (ne *NumberEdit) CreateLayoutItem(ctx *LayoutContext) LayoutItem {
return &numberEditLayoutItem{
idealSize: ne.dialogBaseUnitsToPixels(Size{50, 12}),
minSize: ne.dialogBaseUnitsToPixels(Size{20, 12}),
}
}
type numberEditLayoutItem struct {
LayoutItemBase
idealSize Size // in native pixels
minSize Size // in native pixels
}
func (*numberEditLayoutItem) LayoutFlags() LayoutFlags {
return ShrinkableHorz | GrowableHorz
}
func (li *numberEditLayoutItem) IdealSize() Size {
return li.idealSize
}
func (li *numberEditLayoutItem) MinSize() Size {
return li.minSize
}
type numberLineEdit struct {
*LineEdit
buf *bytes.Buffer
prefix []uint16
suffix []uint16
value float64
minValue float64
maxValue float64
increment float64
decimals int
valueChangedPublisher EventPublisher
inEditMode bool
}
func newNumberLineEdit(parent Widget) (*numberLineEdit, error) {
nle := &numberLineEdit{
buf: new(bytes.Buffer),
increment: 1,
}
var err error
if nle.LineEdit, err = newLineEdit(parent); err != nil {
return nil, err
}
succeeded := false
defer func() {
if !succeeded {
nle.Dispose()
}
}()
if err := nle.LineEdit.setAndClearStyleBits(win.ES_RIGHT, win.ES_LEFT|win.ES_CENTER); err != nil {
return nil, err
}
if err := InitWrapperWindow(nle); err != nil {
return nil, err
}
succeeded = true
return nle, nil
}
func (nle *numberLineEdit) TextColor() Color {
return nle.LineEdit.TextColor()
}
func (nle *numberLineEdit) SetTextColor(c Color) {
nle.LineEdit.SetTextColor(c)
}
func (nle *numberLineEdit) setValue(value float64, setText bool) error {
if setText {
if err := nle.setTextFromValue(value); err != nil {
return err
}
}
if value == nle.value {
return nil
}
nle.value = value
nle.valueChangedPublisher.Publish()
return nil
}
func (nle *numberLineEdit) setTextFromValue(value float64) error {
nle.buf.Reset()
nle.buf.WriteString(syscall.UTF16ToString(nle.prefix))
if nle.decimals > 0 {
nle.buf.WriteString(FormatFloatGrouped(value, nle.decimals))
} else {
nle.buf.WriteString(FormatFloat(value, nle.decimals))
}
nle.buf.WriteString(syscall.UTF16ToString(nle.suffix))
return nle.SetText(nle.buf.String())
}
func (nle *numberLineEdit) endEdit() error {
if err := nle.setTextFromValue(nle.value); err != nil {
return err
}
nle.inEditMode = false
return nil
}
func (nle *numberLineEdit) processChar(text []uint16, start, end int, key Key, char uint16) {
hadSelection := start != end
if !nle.inEditMode {
var groupSepsBeforeStart int
if nle.decimals > 0 {
groupSepsBeforeStart = uint16CountUint16(text[:start], groupSepUint16)
}
if hadSelection {
text = append(text[:start], text[end:]...)
}
if nle.decimals > 0 {
text = uint16RemoveUint16(text, groupSepUint16)
start -= groupSepsBeforeStart
}
nle.inEditMode = true
} else {
if hadSelection {
text = append(text[:start], text[end:]...)
}
}
end = start
switch key {
case KeyBack:
if !hadSelection && start > 0 {
start -= 1
text = append(text[:start], text[start+1:]...)
}
case KeyDelete:
if !hadSelection && start < len(text) {
text = append(text[:start], text[start+1:]...)
}
default:
t := make([]uint16, len(text[:start]), len(text)+1)
copy(t, text[:start])
t = append(t, char)
text = append(t, text[start:]...)
start += 1
}
nle.buf.Reset()
str := syscall.UTF16ToString(text)
nle.buf.WriteString(syscall.UTF16ToString(nle.prefix))
nle.buf.WriteString(str)
nle.buf.WriteString(syscall.UTF16ToString(nle.suffix))
nle.SetText(nle.buf.String())
start += len(nle.prefix)
nle.SetTextSelection(start, start)
nle.tryUpdateValue(false)
}
func (nle *numberLineEdit) tryUpdateValue(setText bool) bool {
t := nle.textUTF16()
t = t[len(nle.prefix) : len(t)-len(nle.suffix)]
text := strings.Replace(syscall.UTF16ToString(t), decimalSepS, ".", 1)
switch text {
case "", ".":
text = "0"
}
if value, err := strconv.ParseFloat(text, 64); err == nil {
if nle.minValue == nle.maxValue || value >= nle.minValue && value <= nle.maxValue {
return nle.setValue(value, setText) == nil
}
}
return false
}
func (nle *numberLineEdit) selectNumber() {
nle.SetTextSelection(len(nle.prefix), len(nle.textUTF16())-len(nle.suffix))
}
func (nle *numberLineEdit) textUTF16() []uint16 {
textLength := nle.SendMessage(win.WM_GETTEXTLENGTH, 0, 0)
buf := make([]uint16, textLength+1)
nle.SendMessage(win.WM_GETTEXT, uintptr(textLength+1), uintptr(unsafe.Pointer(&buf[0])))
return buf[:len(buf)-1]
}
func (nle *numberLineEdit) incrementValue(delta float64) {
value := nle.value + delta
if nle.minValue != nle.maxValue {
if value < nle.minValue {
value = nle.minValue
} else if value > nle.maxValue {
value = nle.maxValue
}
}
nle.setValue(value, true)
nle.selectNumber()
}
func (nle *numberLineEdit) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case win.WM_CHAR:
if nle.ReadOnly() {
break
}
if AltDown() {
return 0
}
if ControlDown() {
if wParam == 1 {
// Ctrl+A
return 0
}
break
}
char := uint16(wParam)
text := nle.textUTF16()
text = text[len(nle.prefix) : len(text)-len(nle.suffix)]
start, end := nle.TextSelection()
start -= len(nle.prefix)
end -= len(nle.prefix)
if Key(wParam) == KeyBack {
nle.processChar(text, start, end, KeyBack, 0)
return 0
}
switch char {
case uint16('0'), uint16('1'), uint16('2'), uint16('3'), uint16('4'), uint16('5'), uint16('6'), uint16('7'), uint16('8'), uint16('9'):
if start == end && nle.decimals > 0 {
if i := uint16IndexUint16(text, decimalSepUint16); i > -1 && i < len(text)-nle.decimals && start > i {
return 0
}
}
nle.processChar(text, start, end, 0, char)
return 0
case uint16('-'):
if nle.minValue != nle.maxValue && nle.minValue >= 0 {
return 0
}
if start > 0 || uint16ContainsUint16(text, uint16('-')) && end == 0 {
return 0
}
nle.processChar(text, start, end, 0, char)
return 0
case decimalSepUint16:
if nle.decimals == 0 {
return 0
}
if start == 0 && end == 0 && len(text) > 0 && text[0] == '-' {
return 0
}
if end < len(text)-nle.decimals {
return 0
}
if i := uint16IndexUint16(text, decimalSepUint16); i > -1 && i <= start || i > end {
return 0
}
nle.processChar(text, start, end, 0, char)
return 0
default:
return 0
}
case win.WM_KEYDOWN:
switch Key(wParam) {
case KeyA:
if ControlDown() {
nle.selectNumber()
return 0
}
case KeyDelete:
if nle.ReadOnly() {
break
}
text := nle.textUTF16()
text = text[len(nle.prefix) : len(text)-len(nle.suffix)]
start, end := nle.TextSelection()
start -= len(nle.prefix)
end -= len(nle.prefix)
nle.processChar(text, start, end, KeyDelete, 0)
return 0
case KeyDown:
if nle.ReadOnly() || nle.increment <= 0 {
return 0
}
nle.incrementValue(-nle.increment)
return 0
case KeyEnd:
start, end := nle.TextSelection()
end = len(nle.textUTF16()) - len(nle.suffix)
if !ShiftDown() {
start = end
}
nle.SetTextSelection(start, end)
return 0
case KeyHome:
start, end := nle.TextSelection()
start = len(nle.prefix)
if !ShiftDown() {
end = start
}
nle.SetTextSelection(start, end)
return 0
case KeyLeft:
var pos win.POINT
win.GetCaretPos(&pos)
lParam := uintptr(win.MAKELONG(uint16(pos.X), uint16(pos.Y)))
i := int(win.LOWORD(uint32(nle.SendMessage(win.EM_CHARFROMPOS, 0, lParam))))
if min := len(nle.prefix); i <= min {
if !ShiftDown() {
nle.SetTextSelection(min, min)
}
return 0
}
case KeyReturn:
if nle.ReadOnly() {
break
}
if nle.inEditMode {
nle.endEdit()
nle.selectNumber()
return 0
}
case KeyRight:
var pos win.POINT
win.GetCaretPos(&pos)
lParam := uintptr(win.MAKELONG(uint16(pos.X), uint16(pos.Y)))
i := int(win.LOWORD(uint32(nle.SendMessage(win.EM_CHARFROMPOS, 0, lParam))))
if max := len(nle.textUTF16()) - len(nle.suffix); i >= max {
if !ShiftDown() {
nle.SetTextSelection(max, max)
}
return 0
}
case KeyUp:
if nle.ReadOnly() || nle.increment <= 0 {
return 0
}
nle.incrementValue(nle.increment)
return 0
}
case win.WM_GETDLGCODE:
if !nle.inEditMode {
if form := ancestor(nle); form != nil {
if dlg, ok := form.(dialogish); ok {
if dlg.DefaultButton() != nil {
// If the NumberEdit lives in a Dialog that has a
// DefaultButton, we won't swallow the return key.
break
}
}
}
}
if wParam == win.VK_RETURN {
return win.DLGC_WANTALLKEYS
}
case win.WM_KILLFOCUS:
nle.onFocusChanged()
nle.endEdit()
case win.WM_LBUTTONDOWN:
i := int(win.LOWORD(uint32(nle.SendMessage(win.EM_CHARFROMPOS, 0, lParam))))
if min := len(nle.prefix); i < min {
nle.SetFocus()
nle.SetTextSelection(min, min)
return 0
}
if max := len(nle.textUTF16()) - len(nle.suffix); i > max {
nle.SetFocus()
nle.SetTextSelection(max, max)
return 0
}
case win.WM_LBUTTONDBLCLK:
nle.selectNumber()
return 0
case win.WM_MOUSEMOVE:
i := int(win.LOWORD(uint32(nle.SendMessage(win.EM_CHARFROMPOS, 0, lParam))))
if min := len(nle.prefix); i < min {
return 0
}
if max := len(nle.textUTF16()) - len(nle.suffix); i > max {
return 0
}
case win.WM_MOUSEWHEEL:
if nle.ReadOnly() || nle.increment <= 0 {
break
}
delta := float64(int16(win.HIWORD(uint32(wParam))))
nle.incrementValue(delta / 120 * nle.increment)
return 0
case win.WM_PASTE:
if nle.ReadOnly() {
break
}
ret := nle.LineEdit.WndProc(hwnd, msg, wParam, lParam)
if !nle.tryUpdateValue(true) {
nle.setTextFromValue(nle.value)
}
nle.selectNumber()
return ret
case win.WM_SETFOCUS:
nle.onFocusChanged()
nle.selectNumber()
case win.EM_SETSEL:
start := int(wParam)
end := int(lParam)
adjusted := false
if min := len(nle.prefix); start < min {
start = min
adjusted = true
}
if max := len(nle.textUTF16()) - len(nle.suffix); end < 0 || end > max {
end = max
adjusted = true
}
if adjusted {
nle.SetTextSelection(start, end)
return 0
}
}
return nle.LineEdit.WndProc(hwnd, msg, wParam, lParam)
}
func (nle *numberLineEdit) onFocusChanged() {
if ne := windowFromHandle(win.GetParent(nle.hWnd)); ne != nil {
if wnd := windowFromHandle(win.GetParent(ne.Handle())); wnd != nil {
if _, ok := wnd.(Container); ok {
ne.(Widget).AsWidgetBase().invalidateBorderInParent()
}
}
}
}
func (ne *NumberEdit) SetToolTipText(s string) error {
return ne.edit.SetToolTipText(s)
}