mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-03-22 01:49:26 +00:00
Added completion into commander
This commit is contained in:
parent
47e3e67b95
commit
d1d7288879
|
@ -4,10 +4,10 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat/utils/split"
|
||||
"github.com/diamondburned/imgutil"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
|
@ -77,7 +77,7 @@ func New(text *gtk.TextView) *View {
|
|||
buffer: buffer,
|
||||
}
|
||||
|
||||
text.Connect("key-press-event", v.inputKeyDown)
|
||||
text.Connect("key-press-event", completion.KeyDownHandler(list, text.GrabFocus))
|
||||
buffer.Connect("changed", func() {
|
||||
// Clear the list first.
|
||||
v.Clear()
|
||||
|
@ -86,22 +86,9 @@ func New(text *gtk.TextView) *View {
|
|||
})
|
||||
|
||||
list.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||
// Get iter for word replacing.
|
||||
start, end := getWordIters(v.buffer, v.offset)
|
||||
|
||||
// Get the selected word.
|
||||
i := r.GetIndex()
|
||||
entry := v.entries[i]
|
||||
|
||||
// Replace the word.
|
||||
v.buffer.Delete(start, end)
|
||||
v.buffer.Insert(start, entry.Raw+" ")
|
||||
|
||||
// Clear the list.
|
||||
completion.SwapWord(v.buffer, v.entries[r.GetIndex()].Raw, v.offset)
|
||||
v.Clear()
|
||||
|
||||
// Reset the focus.
|
||||
v.text.GrabFocus()
|
||||
v.text.GrabFocus() // TODO: remove, maybe not needed
|
||||
})
|
||||
|
||||
return v
|
||||
|
@ -207,81 +194,5 @@ func (v *View) Run() {
|
|||
}
|
||||
|
||||
func (v *View) getInputState() (string, int) {
|
||||
// obtain current state
|
||||
mark := v.buffer.GetInsert()
|
||||
iter := v.buffer.GetIterAtMark(mark)
|
||||
|
||||
// obtain the input string and the current cursor position
|
||||
start, end := v.buffer.GetBounds()
|
||||
text, _ := v.buffer.GetText(start, end, true)
|
||||
offset := iter.GetOffset()
|
||||
|
||||
return text, offset
|
||||
}
|
||||
|
||||
// inputKeyDown handles keypresses such as Enter and movements.
|
||||
func (v *View) inputKeyDown(_ *gtk.TextView, ev *gdk.Event) (stop bool) {
|
||||
// Do we have any entries? If not, don't bother.
|
||||
if len(v.entries) == 0 {
|
||||
// passthrough.
|
||||
return false
|
||||
}
|
||||
|
||||
var evKey = gdk.EventKeyNewFromEvent(ev)
|
||||
var key = evKey.KeyVal()
|
||||
|
||||
switch key {
|
||||
// Did we press an arrow key?
|
||||
case gdk.KEY_Up, gdk.KEY_Down:
|
||||
// Yes, start moving the list up and down.
|
||||
i := v.List.GetSelectedRow().GetIndex()
|
||||
|
||||
switch key {
|
||||
case gdk.KEY_Up:
|
||||
if i--; i < 0 {
|
||||
i = len(v.entries) - 1
|
||||
}
|
||||
case gdk.KEY_Down:
|
||||
if i++; i >= len(v.entries) {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
|
||||
row := v.List.GetRowAtIndex(i)
|
||||
row.GrabFocus() // scroll
|
||||
v.List.SelectRow(row) // select
|
||||
v.text.GrabFocus() // unfocus
|
||||
|
||||
// Did we press the Enter or Tab key?
|
||||
case gdk.KEY_Return, gdk.KEY_Tab:
|
||||
// Activate the current row.
|
||||
row := v.List.GetSelectedRow()
|
||||
row.Activate()
|
||||
|
||||
default:
|
||||
// don't passthrough events if none matches.
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getWordIters(buf *gtk.TextBuffer, offset int) (start, end *gtk.TextIter) {
|
||||
iter := buf.GetIterAtOffset(offset)
|
||||
|
||||
var ok bool
|
||||
|
||||
// Seek backwards for space or start-of-line:
|
||||
_, start, ok = iter.BackwardSearch(" ", gtk.TEXT_SEARCH_TEXT_ONLY, nil)
|
||||
if !ok {
|
||||
start = buf.GetStartIter()
|
||||
}
|
||||
|
||||
// Seek forwards for space or end-of-line:
|
||||
_, end, ok = iter.ForwardSearch(" ", gtk.TEXT_SEARCH_TEXT_ONLY, nil)
|
||||
if !ok {
|
||||
end = buf.GetEndIter()
|
||||
}
|
||||
|
||||
return
|
||||
return completion.State(v.buffer)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -87,11 +88,7 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
|||
|
||||
buf, _ := text.GetBuffer()
|
||||
|
||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||
sw.Add(text)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
sw.SetProperty("propagate-natural-height", true)
|
||||
sw.SetProperty("max-content-height", 150)
|
||||
sw := scrollinput.NewV(text, 150)
|
||||
sw.Show()
|
||||
|
||||
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||||
|
|
102
internal/ui/primitives/completion/completer.go
Normal file
102
internal/ui/primitives/completion/completer.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package completion
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat/utils/split"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type Completeable interface {
|
||||
Update([]string, int) []gtk.IWidget
|
||||
Word(i int) string
|
||||
}
|
||||
|
||||
type Completer struct {
|
||||
ctrl Completeable
|
||||
|
||||
Input *gtk.TextView
|
||||
List *gtk.ListBox
|
||||
Popover *gtk.Popover
|
||||
|
||||
Words []string
|
||||
Index int
|
||||
Cursor int
|
||||
}
|
||||
|
||||
func WrapCompleter(input *gtk.TextView, ctrl Completeable) {
|
||||
NewCompleter(input, ctrl)
|
||||
}
|
||||
|
||||
func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
||||
l, _ := gtk.ListBoxNew()
|
||||
l.Show()
|
||||
|
||||
p := NewPopover(input)
|
||||
p.Add(l)
|
||||
|
||||
c := &Completer{
|
||||
Input: input,
|
||||
List: l,
|
||||
Popover: p,
|
||||
ctrl: ctrl,
|
||||
}
|
||||
|
||||
input.Connect("key-press-event", KeyDownHandler(l, input.GrabFocus))
|
||||
|
||||
ibuf, _ := input.GetBuffer()
|
||||
ibuf.Connect("changed", func() {
|
||||
t, v := State(ibuf)
|
||||
c.Cursor = v
|
||||
c.Words, c.Index = split.SpaceIndexed(t, v)
|
||||
c.complete()
|
||||
})
|
||||
|
||||
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||
SwapWord(ibuf, ctrl.Word(r.GetIndex()), c.Cursor)
|
||||
c.clear()
|
||||
c.Popover.Popdown()
|
||||
input.GrabFocus()
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Completer) clear() {
|
||||
var children = c.List.GetChildren()
|
||||
if children.Length() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
children.Foreach(func(i interface{}) {
|
||||
w := i.(gtk.IWidget).ToWidget()
|
||||
c.List.Remove(w)
|
||||
w.Destroy()
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Completer) complete() {
|
||||
c.clear()
|
||||
|
||||
var widgets []gtk.IWidget
|
||||
if len(c.Words) > 0 {
|
||||
widgets = c.ctrl.Update(c.Words, c.Index)
|
||||
}
|
||||
|
||||
if len(widgets) > 0 {
|
||||
c.Popover.SetPointingTo(CursorRect(c.Input))
|
||||
c.Popover.Popup()
|
||||
} else {
|
||||
c.Popover.Popdown()
|
||||
}
|
||||
|
||||
for i, widget := range widgets {
|
||||
r, _ := gtk.ListBoxRowNew()
|
||||
r.Add(widget)
|
||||
r.Show()
|
||||
|
||||
c.List.Add(r)
|
||||
|
||||
if i == 0 {
|
||||
c.List.SelectRow(r)
|
||||
}
|
||||
}
|
||||
}
|
125
internal/ui/primitives/completion/utils.go
Normal file
125
internal/ui/primitives/completion/utils.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package completion
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
var popoverCSS = primitives.PrepareCSS(`
|
||||
popover {
|
||||
border-radius: 0;
|
||||
}
|
||||
`)
|
||||
|
||||
const (
|
||||
MinPopoverWidth = 250
|
||||
)
|
||||
|
||||
func NewPopover(relto gtk.IWidget) *gtk.Popover {
|
||||
p, _ := gtk.PopoverNew(relto)
|
||||
p.SetSizeRequest(MinPopoverWidth, -1)
|
||||
p.SetModal(false)
|
||||
p.SetPosition(gtk.POS_TOP)
|
||||
primitives.AttachCSS(p, popoverCSS)
|
||||
return p
|
||||
}
|
||||
|
||||
type KeyDownHandlerFn = func(gtk.IWidget, *gdk.Event) bool
|
||||
|
||||
func KeyDownHandler(l *gtk.ListBox, focus func()) KeyDownHandlerFn {
|
||||
return func(w gtk.IWidget, ev *gdk.Event) bool {
|
||||
// Do we have any entries? If not, don't bother.
|
||||
var length = int(l.GetChildren().Length())
|
||||
if length == 0 {
|
||||
// passthrough.
|
||||
return false
|
||||
}
|
||||
|
||||
var evKey = gdk.EventKeyNewFromEvent(ev)
|
||||
var key = evKey.KeyVal()
|
||||
|
||||
switch key {
|
||||
// Did we press an arrow key?
|
||||
case gdk.KEY_Up, gdk.KEY_Down:
|
||||
// Yes, start moving the list up and down.
|
||||
i := l.GetSelectedRow().GetIndex()
|
||||
|
||||
switch key {
|
||||
case gdk.KEY_Up:
|
||||
if i--; i < 0 {
|
||||
i = length - 1
|
||||
}
|
||||
case gdk.KEY_Down:
|
||||
if i++; i >= length {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
|
||||
row := l.GetRowAtIndex(i)
|
||||
row.GrabFocus() // scroll
|
||||
l.SelectRow(row) // select
|
||||
focus() // unfocus
|
||||
|
||||
// Did we press the Enter or Tab key?
|
||||
case gdk.KEY_Return, gdk.KEY_Tab:
|
||||
// Activate the current row.
|
||||
l.GetSelectedRow().Activate()
|
||||
focus()
|
||||
|
||||
default:
|
||||
// passthrough events if none matches.
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func SwapWord(b *gtk.TextBuffer, word string, offset int) {
|
||||
// Get iter for word replacing.
|
||||
start, end := GetWordIters(b, offset)
|
||||
b.Delete(start, end)
|
||||
b.Insert(start, word+" ")
|
||||
}
|
||||
|
||||
func CursorRect(i *gtk.TextView) gdk.Rectangle {
|
||||
r, _ := i.GetCursorLocations(nil)
|
||||
x, _ := i.BufferToWindowCoords(gtk.TEXT_WINDOW_WIDGET, r.GetX(), r.GetY())
|
||||
r.SetX(x)
|
||||
r.SetY(0)
|
||||
return *r
|
||||
}
|
||||
|
||||
func State(buf *gtk.TextBuffer) (string, int) {
|
||||
// obtain current state
|
||||
mark := buf.GetInsert()
|
||||
iter := buf.GetIterAtMark(mark)
|
||||
|
||||
// obtain the input string and the current cursor position
|
||||
start, end := buf.GetBounds()
|
||||
text, _ := buf.GetText(start, end, true)
|
||||
offset := iter.GetOffset()
|
||||
|
||||
return text, offset
|
||||
}
|
||||
|
||||
func GetWordIters(buf *gtk.TextBuffer, offset int) (start, end *gtk.TextIter) {
|
||||
iter := buf.GetIterAtOffset(offset)
|
||||
|
||||
var ok bool
|
||||
|
||||
// Seek backwards for space or start-of-line:
|
||||
_, start, ok = iter.BackwardSearch(" ", gtk.TEXT_SEARCH_TEXT_ONLY, nil)
|
||||
if !ok {
|
||||
start = buf.GetStartIter()
|
||||
}
|
||||
|
||||
// Seek forwards for space or end-of-line:
|
||||
_, end, ok = iter.ForwardSearch(" ", gtk.TEXT_SEARCH_TEXT_ONLY, nil)
|
||||
if !ok {
|
||||
end = buf.GetEndIter()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
27
internal/ui/primitives/scrollinput/scrollinput.go
Normal file
27
internal/ui/primitives/scrollinput/scrollinput.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package scrollinput
|
||||
|
||||
import "github.com/gotk3/gotk3/gtk"
|
||||
|
||||
func NewV(text *gtk.TextView, maxHeight int) *gtk.ScrolledWindow {
|
||||
// Wrap mode needed since we're not doing horizontal scrolling.
|
||||
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||
|
||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||
sw.Add(text)
|
||||
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
sw.SetProperty("propagate-natural-height", true)
|
||||
sw.SetProperty("max-content-height", maxHeight)
|
||||
|
||||
return sw
|
||||
}
|
||||
|
||||
func NewH(text *gtk.TextView) *gtk.ScrolledWindow {
|
||||
text.SetHExpand(true)
|
||||
|
||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||
sw.Add(text)
|
||||
sw.SetPolicy(gtk.POLICY_EXTERNAL, gtk.POLICY_NEVER)
|
||||
sw.SetProperty("propagate-natural-width", true)
|
||||
|
||||
return sw
|
||||
}
|
62
internal/ui/service/session/commander/buffer.go
Normal file
62
internal/ui/service/session/commander/buffer.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package commander
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
type SessionCommander interface {
|
||||
cchat.Session
|
||||
cchat.Commander
|
||||
}
|
||||
|
||||
type Buffer struct {
|
||||
*gtk.TextBuffer
|
||||
svcname string
|
||||
cmder SessionCommander
|
||||
}
|
||||
|
||||
// NewBuffer creates a new buffer with the given SessionCommander, or returns
|
||||
// nil if cmder is nil.
|
||||
func NewBuffer(svc cchat.Service, cmder SessionCommander) *Buffer {
|
||||
if cmder == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, _ := gtk.TextBufferNew(nil)
|
||||
b.CreateTag("error", map[string]interface{}{
|
||||
"foreground": "#FF0000",
|
||||
})
|
||||
b.CreateTag("system", map[string]interface{}{
|
||||
"foreground": "#808080",
|
||||
})
|
||||
return &Buffer{b, svc.Name().Content, cmder}
|
||||
}
|
||||
|
||||
// WriteError is not thread-safe.
|
||||
func (b *Buffer) WriteError(err error) {
|
||||
b.InsertWithTagByName(b.GetEndIter(), err.Error()+"\n", "error")
|
||||
}
|
||||
|
||||
// WriteSystem is not thread-safe.
|
||||
func (b *Buffer) WriteSystem(bytes []byte) {
|
||||
b.InsertWithTagByName(b.GetEndIter(), string(bytes), "system")
|
||||
}
|
||||
|
||||
// Printlnf is not thread-safe.
|
||||
func (b *Buffer) Printlnf(f string, v ...interface{}) {
|
||||
b.WriteSystem([]byte(fmt.Sprintf(f+"\n", v...)))
|
||||
}
|
||||
|
||||
// Write is thread-safe.
|
||||
func (b *Buffer) Write(bytes []byte) (int, error) {
|
||||
gts.ExecAsync(func() { b.Insert(b.GetEndIter(), string(bytes)) })
|
||||
return len(bytes), nil
|
||||
}
|
||||
|
||||
func (b *Buffer) ShowDialog() {
|
||||
SpawnDialog(b)
|
||||
}
|
|
@ -9,62 +9,15 @@ import (
|
|||
"github.com/diamondburned/cchat-gtk/internal/gts"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
|
||||
"github.com/google/shlex"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SessionCommander interface {
|
||||
cchat.Session
|
||||
cchat.Commander
|
||||
}
|
||||
|
||||
type Buffer struct {
|
||||
*gtk.TextBuffer
|
||||
svcname string
|
||||
cmder SessionCommander
|
||||
}
|
||||
|
||||
// NewBuffer creates a new buffer with the given SessionCommander, or returns
|
||||
// nil if cmder is nil.
|
||||
func NewBuffer(svc cchat.Service, cmder SessionCommander) *Buffer {
|
||||
if cmder == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, _ := gtk.TextBufferNew(nil)
|
||||
b.CreateTag("error", map[string]interface{}{
|
||||
"foreground": "#FF0000",
|
||||
})
|
||||
return &Buffer{b, svc.Name().Content, cmder}
|
||||
}
|
||||
|
||||
// WriteError is not thread-safe.
|
||||
func (b *Buffer) WriteError(err error) {
|
||||
b.InsertWithTagByName(b.GetEndIter(), err.Error()+"\n", "error")
|
||||
}
|
||||
|
||||
// WriteUnsafe is not thread-safe.
|
||||
func (b *Buffer) WriteUnsafe(bytes []byte) {
|
||||
b.Insert(b.GetEndIter(), string(bytes))
|
||||
}
|
||||
|
||||
// Printlnf is not thread-safe.
|
||||
func (b *Buffer) Printlnf(f string, v ...interface{}) {
|
||||
b.WriteUnsafe([]byte(fmt.Sprintf(f+"\n", v...)))
|
||||
}
|
||||
|
||||
// Write is thread-safe.
|
||||
func (b *Buffer) Write(bytes []byte) (int, error) {
|
||||
gts.ExecAsync(func() { b.WriteUnsafe(bytes) })
|
||||
return len(bytes), nil
|
||||
}
|
||||
|
||||
func (b *Buffer) ShowDialog() {
|
||||
SpawnDialog(b)
|
||||
}
|
||||
|
||||
var entryCSS = primitives.PrepareCSS(`
|
||||
var monospace = primitives.PrepareCSS(`
|
||||
* {
|
||||
font-family: monospace;
|
||||
border-radius: 0;
|
||||
|
@ -73,9 +26,15 @@ var entryCSS = primitives.PrepareCSS(`
|
|||
|
||||
type Session struct {
|
||||
*gtk.Box
|
||||
words []string
|
||||
|
||||
cmder cchat.Commander
|
||||
buffer *Buffer
|
||||
cmplt *completer
|
||||
|
||||
inputbuf *gtk.TextBuffer
|
||||
|
||||
// words []string
|
||||
// index int
|
||||
}
|
||||
|
||||
func SpawnDialog(buf *Buffer) {
|
||||
|
@ -98,69 +57,73 @@ func SpawnDialog(buf *Buffer) {
|
|||
}
|
||||
|
||||
func NewSession(cmder cchat.Commander, buf *Buffer) *Session {
|
||||
v, _ := gtk.TextViewNewWithBuffer(buf.TextBuffer)
|
||||
v.SetEditable(false)
|
||||
v.SetProperty("monospace", true)
|
||||
v.SetBorderWidth(8)
|
||||
v.SetPixelsAboveLines(1)
|
||||
v.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||
v.Show()
|
||||
view, _ := gtk.TextViewNewWithBuffer(buf.TextBuffer)
|
||||
view.SetEditable(false)
|
||||
view.SetProperty("monospace", true)
|
||||
view.SetPixelsAboveLines(1)
|
||||
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||
view.Show()
|
||||
|
||||
s := autoscroll.NewScrolledWindow()
|
||||
s.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
s.Add(v)
|
||||
s.Show()
|
||||
scroll := autoscroll.NewScrolledWindow()
|
||||
scroll.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
scroll.Add(view)
|
||||
scroll.Show()
|
||||
|
||||
i, _ := gtk.EntryNew()
|
||||
primitives.AttachCSS(i, entryCSS)
|
||||
i.Show()
|
||||
input, _ := gtk.TextViewNew()
|
||||
input.SetSizeRequest(-1, 35) // magic height 35px
|
||||
primitives.AttachCSS(input, monospace)
|
||||
input.Show()
|
||||
|
||||
inputbuf, _ := input.GetBuffer()
|
||||
|
||||
inputscroll := scrollinput.NewH(input)
|
||||
inputscroll.Show()
|
||||
|
||||
sep, _ := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL)
|
||||
sep.Show()
|
||||
|
||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
||||
b.PackStart(s, true, true, 0)
|
||||
b.PackStart(i, false, false, 0)
|
||||
b.PackStart(scroll, true, true, 0)
|
||||
b.PackStart(sep, false, false, 0)
|
||||
b.PackStart(inputscroll, false, false, 0)
|
||||
|
||||
session := &Session{
|
||||
Box: b,
|
||||
cmder: cmder,
|
||||
buffer: buf,
|
||||
Box: b,
|
||||
cmder: cmder,
|
||||
buffer: buf,
|
||||
cmplt: newCompleter(input, cmder),
|
||||
inputbuf: inputbuf,
|
||||
}
|
||||
|
||||
i.Connect("activate", session.inputActivate)
|
||||
// Split words on typing to provide live errors.
|
||||
i.Connect("changed", func(i *gtk.Entry) {
|
||||
t, _ := i.GetText()
|
||||
input.Connect("key-press-event", session.inputActivate)
|
||||
input.GrabFocus()
|
||||
|
||||
w, err := shlex.Split(t)
|
||||
if err != nil {
|
||||
i.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "dialog-error")
|
||||
i.SetIconTooltipText(gtk.ENTRY_ICON_SECONDARY, err.Error())
|
||||
session.words = nil
|
||||
} else {
|
||||
i.SetIconFromIconName(gtk.ENTRY_ICON_SECONDARY, "")
|
||||
session.words = w
|
||||
}
|
||||
})
|
||||
|
||||
// Focus on the input by default.
|
||||
i.GrabFocus()
|
||||
primitives.AddClass(b, "commander")
|
||||
primitives.AddClass(view, "command-buffer")
|
||||
primitives.AddClass(input, "command-input")
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func (s *Session) inputActivate(e *gtk.Entry) {
|
||||
// If the input is empty, then ignore.
|
||||
if len(s.words) == 0 {
|
||||
return
|
||||
func (s *Session) inputActivate(v *gtk.TextView, ev *gdk.Event) bool {
|
||||
// If the keypress is not enter, then ignore.
|
||||
if kev := gdk.EventKeyNewFromEvent(ev); kev.KeyVal() != gdk.KEY_Return {
|
||||
return false
|
||||
}
|
||||
|
||||
r, err := s.cmder.RunCommand(s.words)
|
||||
// If the input is empty, then ignore.
|
||||
if len(s.cmplt.Words) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
r, err := s.cmder.RunCommand(s.cmplt.Words)
|
||||
if err != nil {
|
||||
s.buffer.WriteError(err)
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
// Clear the entry.
|
||||
e.SetText("")
|
||||
s.inputbuf.Delete(s.inputbuf.GetBounds())
|
||||
|
||||
var then = time.Now()
|
||||
s.buffer.Printlnf("%s: Running command...", then.Format(time.Kitchen))
|
||||
|
@ -182,4 +145,50 @@ func (s *Session) inputActivate(e *gtk.Entry) {
|
|||
)
|
||||
})
|
||||
}()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type completer struct {
|
||||
*completion.Completer
|
||||
|
||||
completer cchat.CommandCompleter
|
||||
choices []string
|
||||
}
|
||||
|
||||
func newCompleter(input *gtk.TextView, v cchat.Commander) *completer {
|
||||
completer := &completer{}
|
||||
completer.Completer = completion.NewCompleter(input, completer)
|
||||
|
||||
c, ok := v.(cchat.CommandCompleter)
|
||||
if ok {
|
||||
completer.completer = c
|
||||
}
|
||||
|
||||
return completer
|
||||
}
|
||||
|
||||
func (c *completer) Update(words []string, offset int) []gtk.IWidget {
|
||||
if c.completer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.choices = c.completer.CompleteCommand(words, offset)
|
||||
var widgets = make([]gtk.IWidget, 0, len(c.choices))
|
||||
|
||||
for _, choice := range c.choices {
|
||||
l, _ := gtk.LabelNew(choice)
|
||||
l.SetXAlign(0)
|
||||
l.SetEllipsize(pango.ELLIPSIZE_END)
|
||||
primitives.AttachCSS(l, monospace)
|
||||
l.Show()
|
||||
|
||||
widgets = append(widgets, l)
|
||||
}
|
||||
|
||||
return widgets
|
||||
}
|
||||
|
||||
func (c *completer) Word(i int) string {
|
||||
return c.choices[i]
|
||||
}
|
||||
|
|
|
@ -11,3 +11,7 @@ headerbar { padding: 0; }
|
|||
popover > box {
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.command-buffer, .command-input {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue