395 lines
9.2 KiB
Go
395 lines
9.2 KiB
Go
|
// Copyright 2010 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 (
|
||
|
"sync"
|
||
|
"syscall"
|
||
|
"unsafe"
|
||
|
|
||
|
"github.com/lxn/win"
|
||
|
)
|
||
|
|
||
|
type TextEdit struct {
|
||
|
WidgetBase
|
||
|
readOnlyChangedPublisher EventPublisher
|
||
|
textChangedPublisher EventPublisher
|
||
|
textColor Color
|
||
|
compactHeight bool
|
||
|
margins Size // in native pixels
|
||
|
lastHeight int
|
||
|
origWordbreakProcPtr uintptr
|
||
|
}
|
||
|
|
||
|
func NewTextEdit(parent Container) (*TextEdit, error) {
|
||
|
return NewTextEditWithStyle(parent, 0)
|
||
|
}
|
||
|
|
||
|
func NewTextEditWithStyle(parent Container, style uint32) (*TextEdit, error) {
|
||
|
te := new(TextEdit)
|
||
|
|
||
|
if err := InitWidget(
|
||
|
te,
|
||
|
parent,
|
||
|
"EDIT",
|
||
|
win.WS_TABSTOP|win.WS_VISIBLE|win.ES_MULTILINE|win.ES_WANTRETURN|style,
|
||
|
win.WS_EX_CLIENTEDGE); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
te.origWordbreakProcPtr = te.SendMessage(win.EM_GETWORDBREAKPROC, 0, 0)
|
||
|
|
||
|
te.GraphicsEffects().Add(InteractionEffect)
|
||
|
te.GraphicsEffects().Add(FocusEffect)
|
||
|
|
||
|
te.MustRegisterProperty("ReadOnly", NewProperty(
|
||
|
func() interface{} {
|
||
|
return te.ReadOnly()
|
||
|
},
|
||
|
func(v interface{}) error {
|
||
|
return te.SetReadOnly(v.(bool))
|
||
|
},
|
||
|
te.readOnlyChangedPublisher.Event()))
|
||
|
|
||
|
te.MustRegisterProperty("Text", NewProperty(
|
||
|
func() interface{} {
|
||
|
return te.Text()
|
||
|
},
|
||
|
func(v interface{}) error {
|
||
|
return te.SetText(assertStringOr(v, ""))
|
||
|
},
|
||
|
te.textChangedPublisher.Event()))
|
||
|
|
||
|
return te, nil
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) applyFont(font *Font) {
|
||
|
te.WidgetBase.applyFont(font)
|
||
|
|
||
|
te.updateMargins()
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) updateMargins() {
|
||
|
// 56 works at least from 96 to 192 DPI, so until a better solution comes up, this is it.
|
||
|
defaultSize := te.dialogBaseUnitsToPixels(Size{56, 12})
|
||
|
|
||
|
var rc win.RECT
|
||
|
te.SendMessage(win.EM_GETRECT, 0, uintptr(unsafe.Pointer(&rc)))
|
||
|
|
||
|
if te.hasExtendedStyleBits(win.WS_EX_CLIENTEDGE) {
|
||
|
width := te.WidthPixels()
|
||
|
if width == 0 {
|
||
|
width = defaultSize.Width
|
||
|
}
|
||
|
te.margins.Width = width - int(rc.Right-rc.Left)
|
||
|
} else {
|
||
|
te.margins.Width = int(rc.Left) * 2
|
||
|
}
|
||
|
|
||
|
lineHeight := te.calculateTextSizeImpl("gM").Height
|
||
|
te.margins.Height = defaultSize.Height - lineHeight
|
||
|
}
|
||
|
|
||
|
var drawTextCompatibleEditWordbreakProcPtr uintptr
|
||
|
|
||
|
func init() {
|
||
|
AppendToWalkInit(func() {
|
||
|
drawTextCompatibleEditWordbreakProcPtr = syscall.NewCallback(drawTextCompatibleEditWordbreakProc)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func drawTextCompatibleEditWordbreakProc(lpch *uint16, ichCurrent, cch, code uintptr) uintptr {
|
||
|
switch code {
|
||
|
case win.WB_LEFT:
|
||
|
for i := int(ichCurrent); i >= 0; i-- {
|
||
|
if *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(lpch)) + uintptr(i)*2)) == 32 {
|
||
|
return uintptr(i)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
case win.WB_RIGHT:
|
||
|
for i := int(ichCurrent); i < int(cch); i++ {
|
||
|
if *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(lpch)) + uintptr(i)*2)) == 32 {
|
||
|
return uintptr(i)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
case win.WB_ISDELIMITER:
|
||
|
if *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(lpch)) + ichCurrent*2)) == 32 {
|
||
|
return 1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) Text() string {
|
||
|
return te.text()
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) TextLength() int {
|
||
|
return int(te.SendMessage(win.WM_GETTEXTLENGTH, 0, 0))
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) SetText(text string) (err error) {
|
||
|
if text == te.Text() {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
var oldLineCount int
|
||
|
if te.compactHeight {
|
||
|
oldLineCount = int(te.SendMessage(win.EM_GETLINECOUNT, 0, 0))
|
||
|
}
|
||
|
err = te.setText(text)
|
||
|
if te.compactHeight {
|
||
|
if newLineCount := int(te.SendMessage(win.EM_GETLINECOUNT, 0, 0)); newLineCount != oldLineCount {
|
||
|
te.RequestLayout()
|
||
|
}
|
||
|
}
|
||
|
te.textChangedPublisher.Publish()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) CompactHeight() bool {
|
||
|
return te.compactHeight
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) SetCompactHeight(enabled bool) {
|
||
|
if enabled == te.compactHeight {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
te.compactHeight = enabled
|
||
|
|
||
|
var ptr uintptr
|
||
|
if enabled {
|
||
|
te.updateMargins()
|
||
|
ptr = drawTextCompatibleEditWordbreakProcPtr
|
||
|
} else {
|
||
|
ptr = te.origWordbreakProcPtr
|
||
|
}
|
||
|
te.SendMessage(win.EM_SETWORDBREAKPROC, 0, ptr)
|
||
|
|
||
|
te.RequestLayout()
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) TextAlignment() Alignment1D {
|
||
|
switch win.GetWindowLong(te.hWnd, win.GWL_STYLE) & (win.ES_LEFT | win.ES_CENTER | win.ES_RIGHT) {
|
||
|
case win.ES_CENTER:
|
||
|
return AlignCenter
|
||
|
|
||
|
case win.ES_RIGHT:
|
||
|
return AlignFar
|
||
|
}
|
||
|
|
||
|
return AlignNear
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) SetTextAlignment(alignment Alignment1D) error {
|
||
|
if alignment == AlignDefault {
|
||
|
alignment = AlignNear
|
||
|
}
|
||
|
|
||
|
var bit uint32
|
||
|
|
||
|
switch alignment {
|
||
|
case AlignCenter:
|
||
|
bit = win.ES_CENTER
|
||
|
|
||
|
case AlignFar:
|
||
|
bit = win.ES_RIGHT
|
||
|
|
||
|
default:
|
||
|
bit = win.ES_LEFT
|
||
|
}
|
||
|
|
||
|
return te.setAndClearStyleBits(bit, win.ES_LEFT|win.ES_CENTER|win.ES_RIGHT)
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) MaxLength() int {
|
||
|
return int(te.SendMessage(win.EM_GETLIMITTEXT, 0, 0))
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) SetMaxLength(value int) {
|
||
|
te.SendMessage(win.EM_SETLIMITTEXT, uintptr(value), 0)
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) ScrollToCaret() {
|
||
|
te.SendMessage(win.EM_SCROLLCARET, 0, 0)
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) TextSelection() (start, end int) {
|
||
|
te.SendMessage(win.EM_GETSEL, uintptr(unsafe.Pointer(&start)), uintptr(unsafe.Pointer(&end)))
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) SetTextSelection(start, end int) {
|
||
|
te.SendMessage(win.EM_SETSEL, uintptr(start), uintptr(end))
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) ReplaceSelectedText(text string, canUndo bool) {
|
||
|
te.SendMessage(win.EM_REPLACESEL,
|
||
|
uintptr(win.BoolToBOOL(canUndo)),
|
||
|
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))))
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) AppendText(value string) {
|
||
|
s, e := te.TextSelection()
|
||
|
l := te.TextLength()
|
||
|
te.SetTextSelection(l, l)
|
||
|
te.ReplaceSelectedText(value, false)
|
||
|
te.SetTextSelection(s, e)
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) ReadOnly() bool {
|
||
|
return te.hasStyleBits(win.ES_READONLY)
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) SetReadOnly(readOnly bool) error {
|
||
|
if 0 == te.SendMessage(win.EM_SETREADONLY, uintptr(win.BoolToBOOL(readOnly)), 0) {
|
||
|
return newError("SendMessage(EM_SETREADONLY)")
|
||
|
}
|
||
|
|
||
|
te.readOnlyChangedPublisher.Publish()
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) TextChanged() *Event {
|
||
|
return te.textChangedPublisher.Event()
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) TextColor() Color {
|
||
|
return te.textColor
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) SetTextColor(c Color) {
|
||
|
te.textColor = c
|
||
|
|
||
|
te.Invalidate()
|
||
|
}
|
||
|
|
||
|
// ContextMenuLocation returns carret position in screen coordinates in native pixels.
|
||
|
func (te *TextEdit) ContextMenuLocation() Point {
|
||
|
idx := int(te.SendMessage(win.EM_GETCARETINDEX, 0, 0))
|
||
|
if idx < 0 {
|
||
|
start, end := te.TextSelection()
|
||
|
idx = (start + end) / 2
|
||
|
}
|
||
|
res := uint32(te.SendMessage(win.EM_POSFROMCHAR, uintptr(idx), 0))
|
||
|
pt := win.POINT{int32(win.LOWORD(res)), int32(win.HIWORD(res))}
|
||
|
windowTrimToClientBounds(te.hWnd, &pt)
|
||
|
return pointPixelsFromPOINT(pt)
|
||
|
}
|
||
|
|
||
|
func (*TextEdit) NeedsWmSize() bool {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
|
||
|
switch msg {
|
||
|
case win.WM_COMMAND:
|
||
|
switch win.HIWORD(uint32(wParam)) {
|
||
|
case win.EN_CHANGE:
|
||
|
if te.compactHeight {
|
||
|
if createLayoutItemForWidget(te).(MinSizer).MinSize().Height != te.HeightPixels() {
|
||
|
te.RequestLayout()
|
||
|
}
|
||
|
}
|
||
|
te.textChangedPublisher.Publish()
|
||
|
}
|
||
|
|
||
|
case win.WM_GETDLGCODE:
|
||
|
if wParam == win.VK_RETURN {
|
||
|
return win.DLGC_WANTALLKEYS
|
||
|
}
|
||
|
|
||
|
return win.DLGC_HASSETSEL | win.DLGC_WANTARROWS | win.DLGC_WANTCHARS
|
||
|
|
||
|
case win.WM_KEYDOWN:
|
||
|
if Key(wParam) == KeyA && ControlDown() {
|
||
|
te.SetTextSelection(0, -1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return te.WidgetBase.WndProc(hwnd, msg, wParam, lParam)
|
||
|
}
|
||
|
|
||
|
func (te *TextEdit) CreateLayoutItem(ctx *LayoutContext) LayoutItem {
|
||
|
if te.margins.Width <= 0 {
|
||
|
te.updateMargins()
|
||
|
}
|
||
|
|
||
|
return &textEditLayoutItem{
|
||
|
width2Height: make(map[int]int),
|
||
|
compactHeight: te.compactHeight,
|
||
|
margins: te.margins,
|
||
|
text: te.Text(),
|
||
|
font: te.Font(),
|
||
|
minWidth: te.calculateTextSizeImpl("W").Width,
|
||
|
nonCompactHeightMinSize: te.dialogBaseUnitsToPixels(Size{20, 12}),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type textEditLayoutItem struct {
|
||
|
LayoutItemBase
|
||
|
mutex sync.Mutex
|
||
|
width2Height map[int]int // in native pixels
|
||
|
nonCompactHeightMinSize Size // in native pixels
|
||
|
margins Size // in native pixels
|
||
|
text string
|
||
|
font *Font
|
||
|
minWidth int // in native pixels
|
||
|
compactHeight bool
|
||
|
}
|
||
|
|
||
|
func (li *textEditLayoutItem) LayoutFlags() LayoutFlags {
|
||
|
flags := ShrinkableHorz | GrowableHorz | GreedyHorz
|
||
|
if !li.compactHeight {
|
||
|
flags |= GreedyVert | GrowableVert | ShrinkableVert
|
||
|
}
|
||
|
return flags
|
||
|
}
|
||
|
|
||
|
func (li *textEditLayoutItem) IdealSize() Size {
|
||
|
if li.compactHeight {
|
||
|
return li.MinSize()
|
||
|
} else {
|
||
|
return SizeFrom96DPI(Size{100, 100}, li.ctx.dpi)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (li *textEditLayoutItem) MinSize() Size {
|
||
|
if li.compactHeight {
|
||
|
width := IntFrom96DPI(100, li.ctx.dpi)
|
||
|
return Size{width, li.HeightForWidth(width)}
|
||
|
} else {
|
||
|
return li.nonCompactHeightMinSize
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (li *textEditLayoutItem) HasHeightForWidth() bool {
|
||
|
return li.compactHeight
|
||
|
}
|
||
|
|
||
|
func (li *textEditLayoutItem) HeightForWidth(width int) int {
|
||
|
li.mutex.Lock()
|
||
|
defer li.mutex.Unlock()
|
||
|
|
||
|
if height, ok := li.width2Height[width]; ok {
|
||
|
return height
|
||
|
}
|
||
|
|
||
|
size := calculateTextSize(li.text, li.font, li.ctx.dpi, width-li.margins.Width, li.handle)
|
||
|
size.Height += li.margins.Height
|
||
|
size.Height = maxi(size.Height, li.nonCompactHeightMinSize.Height)
|
||
|
|
||
|
li.width2Height[width] = size.Height
|
||
|
|
||
|
return size.Height
|
||
|
}
|