mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
widget: add ReadOnly mode to editor
This commit provides a new ReadOnly boolean on the editor. If set, the editor functions as a selectable label. User interaction cannot change the contents of the editor (though application code can still use the API). Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
+34
-17
@@ -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)
|
||||
|
||||
+72
-5
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user