diff --git a/widget/editor.go b/widget/editor.go index 9ae07d7b..1b796a49 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -37,6 +37,10 @@ type Editor struct { // SingleLine also sets the scrolling direction to // horizontal. SingleLine bool + // ReadOnly controls whether the contents of the editor can be altered by + // user interaction. If set to true, the editor will allow selecting text + // and copying it interactively, but not modifying it. + ReadOnly bool // Submit enabled translation of carriage return keys to SubmitEvents. // If not enabled, carriage returns are inserted as newlines in the text. Submit bool @@ -343,7 +347,7 @@ func (e *Editor) processKey(gtx layout.Context) { if !e.focused || ke.State != key.Press { break } - if e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { + if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { if !ke.Modifiers.Contain(key.ModShift) { e.events = append(e.events, SubmitEvent{ Text: e.Text(), @@ -357,6 +361,9 @@ func (e *Editor) processKey(gtx layout.Context) { case key.SnippetEvent: e.updateSnippet(gtx, ke.Start, ke.End) case key.EditEvent: + if e.ReadOnly { + break + } e.caret.scroll = true e.scroller.Stop() s := ke.Text @@ -442,18 +449,24 @@ func (e *Editor) command(gtx layout.Context, k key.Event) { } switch k.Name { case key.NameReturn, key.NameEnter: - e.append("\n") + if !e.ReadOnly { + e.append("\n") + } case key.NameDeleteBackward: - if moveByWord { - e.deleteWord(-1) - } else { - e.Delete(-1) + if !e.ReadOnly { + if moveByWord { + e.deleteWord(-1) + } else { + e.Delete(-1) + } } case key.NameDeleteForward: - if moveByWord { - e.deleteWord(1) - } else { - e.Delete(1) + if !e.ReadOnly { + if moveByWord { + e.deleteWord(1) + } else { + e.Delete(1) + } } case key.NameUpArrow: e.moveLines(-1, selAct) @@ -488,12 +501,14 @@ func (e *Editor) command(gtx layout.Context, k key.Event) { // Initiate a paste operation, by requesting the clipboard contents; other // half is in Editor.processKey() under clipboard.Event. case "V": - clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops) + if !e.ReadOnly { + clipboard.ReadOp{Tag: &e.eventKey}.Add(gtx.Ops) + } // Copy or Cut selection -- ignored if nothing selected. case "C", "X": if text := e.SelectedText(); text != "" { clipboard.WriteOp{Text: text}.Add(gtx.Ops) - if k.Name == "X" { + if k.Name == "X" && !e.ReadOnly { e.Delete(1) } } @@ -502,10 +517,12 @@ func (e *Editor) command(gtx layout.Context, k key.Event) { e.caret.end = 0 e.caret.start = e.Len() case "Z": - if k.Modifiers.Contain(key.ModShift) { - e.redo() - } else { - e.undo() + if !e.ReadOnly { + if k.Modifiers.Contain(key.ModShift) { + e.redo() + } else { + e.undo() + } } } } @@ -781,7 +798,7 @@ func (e *Editor) caretWidth(gtx layout.Context) int { } func (e *Editor) PaintCaret(gtx layout.Context) { - if !e.caret.on { + if !e.caret.on || e.ReadOnly { return } carWidth2 := e.caretWidth(gtx) diff --git a/widget/editor_test.go b/widget/editor_test.go index 703e2e5a..e8ab6312 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -92,9 +92,9 @@ func assertContents(t *testing.T, e *Editor, contents string, selectionStart, se } } -// TestEditorZeroDimensions ensures that an empty editor still reserves -// space for displaying its caret when the constraints allow for it. -func TestEditorZeroDimensions(t *testing.T) { +// TestEditorReadOnly ensures that mouse and keyboard interactions with readonly +// editors do nothing but manipulate the text selection. +func TestEditorReadOnly(t *testing.T) { gtx := layout.Context{ Ops: new(op.Ops), Constraints: layout.Constraints{ @@ -102,13 +102,80 @@ func TestEditorZeroDimensions(t *testing.T) { }, Locale: english, } + gtx.Queue = &testQueue{ + events: []event.Event{ + key.FocusEvent{Focus: true}, + }, + } cache := text.NewShaper(gofont.Collection()) fontSize := unit.Sp(10) font := text.Font{} e := new(Editor) + e.ReadOnly = true + e.SetText("The quick brown fox jumps over the lazy dog. We just need a few lines of text in the editor so that it can adequately test a few different modes of selection. The quick brown fox jumps over the lazy dog. We just need a few lines of text in the editor so that it can adequately test a few different modes of selection.") + cStart, cEnd := e.Selection() + if cStart != cEnd { + t.Errorf("unexpected initial caret positions") + } dims := e.Layout(gtx, cache, font, fontSize, nil) - if dims.Size.X < 1 || dims.Size.Y < 1 { - t.Errorf("expected empty editor to occupy enough space to display cursor, but returned dimensions %v", dims) + + // Select everything. + gtx.Ops.Reset() + gtx.Queue.(*testQueue).events = append(gtx.Queue.(*testQueue).events, key.Event{Name: "A", Modifiers: key.ModShortcut}) + dims = e.Layout(gtx, cache, font, fontSize, nil) + textContent := e.Text() + cStart2, cEnd2 := e.Selection() + if cStart2 > cEnd2 { + cStart2, cEnd2 = cEnd2, cStart2 + } + if cEnd2 != e.Len() { + t.Errorf("expected selection to contain %d runes, got %d", e.Len(), cEnd2) + } + if cStart2 != 0 { + t.Errorf("expected selection to start at rune 0, got %d", cStart2) + } + + // Type some new characters. + gtx.Ops.Reset() + gtx.Queue.(*testQueue).events = append(gtx.Queue.(*testQueue).events, key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}) + dims = e.Layout(gtx, cache, font, fontSize, nil) + textContent2 := e.Text() + if textContent2 != textContent { + t.Errorf("readonly editor modified by key.EditEvent") + } + + // Try to delete selection. + gtx.Ops.Reset() + gtx.Queue.(*testQueue).events = append(gtx.Queue.(*testQueue).events, key.Event{Name: key.NameDeleteBackward}) + dims = e.Layout(gtx, cache, font, fontSize, nil) + textContent2 = e.Text() + if textContent2 != textContent { + t.Errorf("readonly editor modified by delete key.Event") + } + + // Click and drag from the middle of the first line + // to the center. + gtx.Ops.Reset() + gtx.Queue.(*testQueue).events = append(gtx.Queue.(*testQueue).events, + pointer.Event{ + Type: pointer.Press, + Buttons: pointer.ButtonPrimary, + Position: f32.Pt(float32(dims.Size.X)*.5, 5), + }, + pointer.Event{ + Type: pointer.Drag, + Buttons: pointer.ButtonPrimary, + Position: layout.FPt(dims.Size).Mul(.5), + }, + pointer.Event{ + Type: pointer.Release, + Buttons: pointer.ButtonPrimary, + Position: layout.FPt(dims.Size).Mul(.5), + }, + ) + cStart3, cEnd3 := e.Selection() + if cStart3 == cStart2 || cEnd3 == cEnd2 { + t.Errorf("expected mouse interaction to change selection.") } }