Просмотр исходного кода

gui.Edit: Added possibility to select text

- shift + left/right expands/shrinks selection to left/right
- shift + home/end expands selection to beginning/end of text
- ctrl + a selects everything
- shift + delete removes everything (even when nothing is selected)
- selected text can be deleted with delete or backspace
- selected text can be overwritten by inputting new text while text is selected
- selected text is highlighted even when the edit box has no focus
- SelectedText() method to get only the selected text.
- everything should mimic a "normal" text field (so everything should behave like expected)
- NO mouse selection yet
count 3 лет назад
Родитель
Сommit
748000dd0e
3 измененных файлов с 281 добавлено и 43 удалено
  1. 261 39
      gui/edit.go
  2. 2 2
      gui/label.go
  3. 18 2
      text/font.go

+ 261 - 39
gui/edit.go

@@ -21,6 +21,8 @@ type Edit struct {
 	placeHolder string // place holder string
 	text        string // current edit text
 	col         int    // current column
+	selStart    int    // start column of selection. always < selEnd. if selStart == selEnd then nothing is selected.
+	selEnd      int    // end column of selection. always > selStart. if selStart == selEnd then nothing is selected.
 	focus       bool   // key focus flag
 	cursorOver  bool
 	blinkID     int
@@ -63,6 +65,8 @@ func NewEdit(width int, placeHolder string) *Edit {
 	ed.text = ""
 	ed.MaxLength = 80
 	ed.col = 0
+	ed.selStart = 0
+	ed.selEnd = 0
 	ed.focus = false
 
 	ed.Label.initialize("", StyleDefault().Font)
@@ -80,10 +84,13 @@ func NewEdit(width int, placeHolder string) *Edit {
 }
 
 // SetText sets this edit text
-func (ed *Edit) SetText(text string) *Edit {
+func (ed *Edit) SetText(newText string) *Edit {
 
 	// Remove new lines from text
-	ed.text = strings.Replace(text, "\n", "", -1)
+	ed.text = strings.Replace(newText, "\n", "", -1)
+	ed.col = text.StrCount(ed.text)
+	ed.selStart = ed.col
+	ed.selEnd = ed.col
 	ed.update()
 	return ed
 }
@@ -94,6 +101,28 @@ func (ed *Edit) Text() string {
 	return ed.text
 }
 
+// SelectedText returns the currently selected text
+// or empty string when nothing is selected
+func (ed *Edit) SelectedText() string {
+
+	if ed.selStart == ed.selEnd {
+		return ""
+	}
+
+	s := ""
+	charNum := 0
+	for _, currentRune := range ed.text {
+		if charNum >= ed.selEnd {
+			break
+		}
+		if charNum >= ed.selStart {
+			s += string(currentRune)
+		}
+		charNum++
+	}
+	return s
+}
+
 // SetFontSize sets label font size (overrides Label.SetFontSize)
 func (ed *Edit) SetFontSize(size float64) *Edit {
 
@@ -124,36 +153,165 @@ func (ed *Edit) CursorPos(col int) {
 
 	if col <= text.StrCount(ed.text) {
 		ed.col = col
+		ed.selStart = col
+		ed.selEnd = col
 		ed.redraw(ed.focus)
 	}
 }
 
+// SetSelection selects the text between start and end
+func (ed *Edit) SetSelection(start, end int) {
+
+	// make sure end is bigger than start
+	if start > end {
+		start, end = end, start
+	}
+
+	if start < 0 {
+		start = 0
+	}
+	if end > text.StrCount(ed.text) {
+		end = text.StrCount(ed.text)
+	}
+
+	ed.selStart = start
+	ed.selEnd = end
+	ed.col = end
+	ed.redraw(ed.focus)
+}
+
 // CursorLeft moves the edit cursor one character left if possible
+// If text is selected the cursor is moved to the beginning of the selection instead
+// and the selection is removed
 func (ed *Edit) CursorLeft() {
 
-	if ed.col > 0 {
-		ed.col--
+	if ed.selStart == ed.selEnd {
+		// no selection
+		// move cursor to the left if possible
+		if ed.col > 0 {
+			ed.col--
+			ed.selStart = ed.col
+			ed.selEnd = ed.col
+			ed.redraw(ed.focus)
+		}
+	} else {
+		// reset selection and move cursor to start of selection
+		ed.col = ed.selStart
+		ed.selStart = ed.col
+		ed.selEnd = ed.col
 		ed.redraw(ed.focus)
 	}
 }
 
 // CursorRight moves the edit cursor one character right if possible
+// If text is selected the cursor is moved to the end of the selection instead
+// and the selection is removed
 func (ed *Edit) CursorRight() {
 
-	if ed.col < text.StrCount(ed.text) {
-		ed.col++
+	if ed.selStart == ed.selEnd {
+		// no selection
+		// move cursor to the right if possible
+		if ed.col < text.StrCount(ed.text) {
+			ed.col++
+			ed.selStart = ed.col
+			ed.selEnd = ed.col
+			ed.redraw(ed.focus)
+		}
+	} else {
+		// reset selection and move cursor to end of selection
+		ed.col = ed.selEnd
+		ed.selStart = ed.col
+		ed.selEnd = ed.col
 		ed.redraw(ed.focus)
 	}
 }
 
-// CursorBack deletes the character at left of the cursor if possible
-func (ed *Edit) CursorBack() {
+// SelectLeft expands/shrinks the selection to the left if possible
+func (ed *Edit) SelectLeft() {
 
 	if ed.col > 0 {
-		ed.col--
-		ed.text = text.StrRemove(ed.text, ed.col)
-		ed.redraw(ed.focus)
-		ed.Dispatch(OnChange, nil)
+		if ed.col == ed.selStart {
+			// cursor is at the start of selection
+			// expand selection to the left
+			ed.col--
+			ed.selStart = ed.col
+			ed.redraw(ed.focus)
+		} else {
+			// cursor is at the end of selection:
+			// remove selection from the end
+			ed.col--
+			ed.selEnd = ed.col
+			ed.redraw(ed.focus)
+		}
+	}
+}
+
+// SelectRight expands/shrinks the selection to the right if possible
+func (ed *Edit) SelectRight() {
+
+	if ed.col < text.StrCount(ed.text) {
+		if ed.col == ed.selEnd {
+			// cursor is at the end of selection:
+			// expand selection to the right if possible
+			ed.col++
+			ed.selEnd = ed.col
+			ed.redraw(ed.focus)
+		} else {
+			// cursor is at the start of selection:
+			// remove selection from the start
+			ed.col++
+			ed.selStart = ed.col
+			ed.redraw(ed.focus)
+		}
+	}
+}
+
+// SelectHome expands the selection to the left to the beginning of the text
+func (ed *Edit) SelectHome() {
+
+	if ed.selStart < ed.col {
+		ed.selEnd = ed.selStart
+	}
+	ed.col = 0
+	ed.selStart = 0
+	ed.redraw(ed.focus)
+}
+
+// SelectEnd expands the selection to the right to the end of the text
+func (ed *Edit) SelectEnd() {
+
+	if ed.selEnd > ed.col {
+		ed.selStart = ed.selEnd
+	}
+	ed.col = text.StrCount(ed.text)
+	ed.selEnd = ed.col
+	ed.redraw(ed.focus)
+}
+
+// SelectAll selects all text
+func (ed *Edit) SelectAll() {
+
+	ed.selStart = 0
+	ed.selEnd = text.StrCount(ed.text)
+	ed.col = ed.selEnd
+	ed.redraw(ed.focus)
+}
+
+// CursorBack either deletes the character at left of the cursor if possible
+// Or if text is selected the selected text is removed all at once
+func (ed *Edit) CursorBack() {
+
+	if ed.selStart == ed.selEnd {
+		if ed.col > 0 {
+			ed.col--
+			ed.selStart = ed.col
+			ed.selEnd = ed.col
+			ed.text = text.StrRemove(ed.text, ed.col)
+			ed.redraw(ed.focus)
+			ed.Dispatch(OnChange, nil)
+		}
+	} else {
+		ed.DeleteSelection()
 	}
 }
 
@@ -161,6 +319,8 @@ func (ed *Edit) CursorBack() {
 func (ed *Edit) CursorHome() {
 
 	ed.col = 0
+	ed.selStart = ed.col
+	ed.selEnd = ed.col
 	ed.redraw(ed.focus)
 }
 
@@ -168,22 +328,55 @@ func (ed *Edit) CursorHome() {
 func (ed *Edit) CursorEnd() {
 
 	ed.col = text.StrCount(ed.text)
+	ed.selStart = ed.col
+	ed.selEnd = ed.col
 	ed.redraw(ed.focus)
 }
 
-// CursorDelete deletes the character at the right of the cursor if possible
+// CursorDelete either deletes the character at the right of the cursor if possible
+// Or if text is selected the selected text is removed all at once
 func (ed *Edit) CursorDelete() {
 
-	if ed.col < text.StrCount(ed.text) {
-		ed.text = text.StrRemove(ed.text, ed.col)
-		ed.redraw(ed.focus)
+	if ed.selStart == ed.selEnd {
+		if ed.col < text.StrCount(ed.text) {
+			ed.text = text.StrRemove(ed.text, ed.col)
+			ed.redraw(ed.focus)
+			ed.Dispatch(OnChange, nil)
+		}
+	} else {
+		ed.DeleteSelection()
+	}
+}
+
+// DeleteSelection deletes the selected characters. Does nothing if nothing is selected.
+func (ed *Edit) DeleteSelection() {
+
+	if ed.selStart == ed.selEnd {
+		return
+	}
+
+	changed := false
+	ed.col = ed.selStart
+	for ed.selEnd > ed.selStart {
+		if ed.col < text.StrCount(ed.text) {
+			changed = true
+			ed.text = text.StrRemove(ed.text, ed.col)
+			ed.selEnd--
+		}
+	}
+	if changed {
 		ed.Dispatch(OnChange, nil)
+		ed.redraw(ed.focus)
 	}
 }
 
 // CursorInput inserts the specified string at the current cursor position
+// If text is selected the selected text gets overwritten
 func (ed *Edit) CursorInput(s string) {
 
+	if ed.selStart != ed.selEnd {
+		ed.DeleteSelection()
+	}
 	if text.StrCount(ed.text) >= ed.MaxLength {
 		return
 	}
@@ -204,40 +397,65 @@ func (ed *Edit) CursorInput(s string) {
 
 	ed.text = newText
 	ed.col++
+	ed.selStart = ed.col
+	ed.selEnd = ed.col
 
 	ed.Dispatch(OnChange, nil)
 	ed.redraw(ed.focus)
 }
 
 // redraw redraws the text showing the caret if specified
+// the selection caret is always shown (when text is selected)
 func (ed *Edit) redraw(caret bool) {
 
 	line := 0
-	if !caret {
-		line = -1
-	}
-	ed.Label.setTextCaret(ed.text, editMarginX, ed.width, line, ed.col)
+	ed.Label.setTextCaret(ed.text, editMarginX, ed.width, caret, line, ed.col, ed.selStart, ed.selEnd)
 }
 
 // onKey receives subscribed key events
 func (ed *Edit) onKey(evname string, ev interface{}) {
 
 	kev := ev.(*window.KeyEvent)
-	switch kev.Key {
-	case window.KeyLeft:
-		ed.CursorLeft()
-	case window.KeyRight:
-		ed.CursorRight()
-	case window.KeyHome:
-		ed.CursorHome()
-	case window.KeyEnd:
-		ed.CursorEnd()
-	case window.KeyBackspace:
-		ed.CursorBack()
-	case window.KeyDelete:
-		ed.CursorDelete()
-	default:
-		return
+	if kev.Mods != window.ModShift && kev.Mods != window.ModControl {
+		switch kev.Key {
+		case window.KeyLeft:
+			ed.CursorLeft()
+		case window.KeyRight:
+			ed.CursorRight()
+		case window.KeyHome:
+			ed.CursorHome()
+		case window.KeyEnd:
+			ed.CursorEnd()
+		case window.KeyBackspace:
+			ed.CursorBack()
+		case window.KeyDelete:
+			ed.CursorDelete()
+		default:
+			return
+		}
+	} else if kev.Mods == window.ModShift {
+		switch kev.Key {
+		case window.KeyLeft:
+			ed.SelectLeft()
+		case window.KeyRight:
+			ed.SelectRight()
+		case window.KeyHome:
+			ed.SelectHome()
+		case window.KeyEnd:
+			ed.SelectEnd()
+		case window.KeyBackspace:
+			ed.CursorBack()
+		case window.KeyDelete:
+			ed.SelectAll()
+			ed.DeleteSelection()
+		default:
+			return
+		}
+	} else if kev.Mods == window.ModControl {
+		switch kev.Key {
+		case window.KeyA:
+			ed.SelectAll()
+		}
 	}
 }
 
@@ -256,9 +474,6 @@ func (ed *Edit) onMouse(evname string, ev interface{}) {
 		return
 	}
 
-	// Set key focus to this panel
-	Manager().SetKeyFocus(ed)
-
 	// Find clicked column
 	var nchars int
 	for nchars = 1; nchars <= text.StrCount(ed.text); nchars++ {
@@ -273,6 +488,13 @@ func (ed *Edit) onMouse(evname string, ev interface{}) {
 		ed.blinkID = Manager().SetInterval(750*time.Millisecond, nil, ed.blink)
 	}
 	ed.CursorPos(nchars - 1)
+
+	// Set key focus to this panel
+	// Set the focus AFTER the mouse selection is handled
+	// Otherwise the OnFocus event would fire before the cursor is set.
+	// That way the OnFocus handler could NOT influence the selection
+	// Because it would be overridden/cleared directly afterwards.
+	Manager().SetKeyFocus(ed)
 }
 
 // onCursor receives subscribed cursor events
@@ -336,7 +558,7 @@ func (ed *Edit) applyStyle(s *EditStyle) {
 
 	if !ed.focus && len(ed.text) == 0 && len(ed.placeHolder) > 0 {
 		ed.Label.SetColor4(&s.HolderColor)
-		ed.Label.setTextCaret(ed.placeHolder, editMarginX, ed.width, -1, ed.col)
+		ed.Label.setTextCaret(ed.placeHolder, editMarginX, ed.width, false, -1, ed.col, ed.selStart, ed.selEnd)
 	} else {
 		ed.Label.SetColor4(&s.FgColor)
 		ed.redraw(ed.focus)

+ 2 - 2
gui/label.go

@@ -213,7 +213,7 @@ func (l *Label) LineSpacing() float64 {
 // setTextCaret sets the label text and draws a caret at the
 // specified line and column.
 // It is normally used by the Edit widget.
-func (l *Label) setTextCaret(msg string, mx, width, line, col int) {
+func (l *Label) setTextCaret(msg string, mx, width int, drawCaret bool, line, col, selStart, selEnd int) {
 
 	// Set font properties
 	l.font.SetAttributes(&l.style.FontAttributes)
@@ -222,7 +222,7 @@ func (l *Label) setTextCaret(msg string, mx, width, line, col int) {
 	// Create canvas and draw text
 	_, height := l.font.MeasureText(msg)
 	canvas := text.NewCanvas(width, height, &l.style.BgColor)
-	canvas.DrawTextCaret(mx, 0, msg, l.font, line, col)
+	canvas.DrawTextCaret(mx, 0, msg, l.font, drawCaret, line, col, selStart, selEnd)
 
 	// Creates texture if if doesnt exist.
 	if l.tex == nil {

+ 18 - 2
text/font.go

@@ -270,7 +270,7 @@ func (c Canvas) DrawText(x, y int, text string, f *Font) {
 // the specified line and column.
 // The supplied text string can contain line break escape sequences (\n).
 // TODO Implement caret as a gui.Panel in gui.Edit
-func (c Canvas) DrawTextCaret(x, y int, text string, f *Font, line, col int) error {
+func (c Canvas) DrawTextCaret(x, y int, text string, f *Font, drawCaret bool, line, col, selStart, selEnd int) error {
 
 	// Creates drawer
 	f.updateFace()
@@ -284,9 +284,25 @@ func (c Canvas) DrawTextCaret(x, y int, text string, f *Font, line, col int) err
 	lines := strings.Split(text, "\n")
 	for l, s := range lines {
 		d.Dot = fixed.P(x, py)
+		if selStart != selEnd && l == line && selEnd <= StrCount(s) {
+			width, _ := f.MeasureText(StrPrefix(s, selStart))
+			widthEnd, _ := f.MeasureText(StrPrefix(s, selEnd))
+			// Draw selection caret
+			// TODO This will not work when the selection spans multiple lines
+			// Currently there is no multiline edit text
+			// Once there is, this needs to change
+			caretH := int(f.attrib.PointSize) + 2
+			caretY := int(d.Dot.Y>>6) - int(f.attrib.PointSize) + 2
+			color := Color4RGBA(&math32.Color4{0, 0, 1, 0.5}) // Hardcoded to blue, alpha 50%
+			for w := width; w < widthEnd; w++ {
+				for j := caretY; j < caretY+caretH; j++ {
+					c.RGBA.Set(x+w, j, color)
+				}
+			}
+		}
 		d.DrawString(s)
 		// Checks for caret position
-		if l == line && col <= StrCount(s) {
+		if drawCaret && l == line && col <= StrCount(s) {
 			width, _ := f.MeasureText(StrPrefix(s, col))
 			// Draw caret vertical line
 			caretH := int(f.attrib.PointSize) + 2