mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2024-11-16 11:12:44 +00:00
288 lines
6 KiB
Go
288 lines
6 KiB
Go
|
package completion
|
||
|
|
||
|
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/rich"
|
||
|
"github.com/diamondburned/cchat/utils/split"
|
||
|
"github.com/diamondburned/imgutil"
|
||
|
"github.com/gotk3/gotk3/gdk"
|
||
|
"github.com/gotk3/gotk3/gtk"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
ImageSize = 20
|
||
|
ImagePadding = 10
|
||
|
)
|
||
|
|
||
|
// var completionQueue chan func()
|
||
|
|
||
|
// func init() {
|
||
|
// completionQueue = make(chan func(), 1)
|
||
|
// go func() {
|
||
|
// for fn := range completionQueue {
|
||
|
// fn()
|
||
|
// }
|
||
|
// }()
|
||
|
// }
|
||
|
|
||
|
type View struct {
|
||
|
*gtk.Revealer
|
||
|
Scroll *gtk.ScrolledWindow
|
||
|
|
||
|
List *gtk.ListBox
|
||
|
entries []cchat.CompletionEntry
|
||
|
|
||
|
text *gtk.TextView
|
||
|
buffer *gtk.TextBuffer
|
||
|
|
||
|
// state
|
||
|
completer cchat.ServerMessageSendCompleter
|
||
|
offset int
|
||
|
}
|
||
|
|
||
|
func New(text *gtk.TextView) *View {
|
||
|
list, _ := gtk.ListBoxNew()
|
||
|
list.SetSelectionMode(gtk.SELECTION_BROWSE)
|
||
|
list.Show()
|
||
|
|
||
|
primitives.AddClass(list, "completer")
|
||
|
|
||
|
scroll, _ := gtk.ScrolledWindowNew(nil, nil)
|
||
|
scroll.Add(list)
|
||
|
scroll.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||
|
scroll.SetProperty("propagate-natural-height", true)
|
||
|
scroll.SetProperty("max-content-height", 250)
|
||
|
scroll.Show()
|
||
|
|
||
|
// Bind scroll adjustments.
|
||
|
list.SetFocusHAdjustment(scroll.GetHAdjustment())
|
||
|
list.SetFocusVAdjustment(scroll.GetVAdjustment())
|
||
|
|
||
|
rev, _ := gtk.RevealerNew()
|
||
|
rev.SetRevealChild(false)
|
||
|
rev.SetTransitionDuration(50)
|
||
|
rev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_UP)
|
||
|
rev.Add(scroll)
|
||
|
rev.Show()
|
||
|
|
||
|
buffer, _ := text.GetBuffer()
|
||
|
|
||
|
v := &View{
|
||
|
Revealer: rev,
|
||
|
Scroll: scroll,
|
||
|
List: list,
|
||
|
text: text,
|
||
|
buffer: buffer,
|
||
|
}
|
||
|
|
||
|
text.Connect("key-press-event", v.inputKeyDown)
|
||
|
buffer.Connect("changed", func() {
|
||
|
// Clear the list first.
|
||
|
v.Clear()
|
||
|
// Re-run the list.
|
||
|
v.Run()
|
||
|
})
|
||
|
|
||
|
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.
|
||
|
v.Clear()
|
||
|
|
||
|
// Reset the focus.
|
||
|
v.text.GrabFocus()
|
||
|
})
|
||
|
|
||
|
return v
|
||
|
}
|
||
|
|
||
|
// SetMarginStart sets the left margin but account for images as well.
|
||
|
func (v *View) SetMarginStart(pad int) {
|
||
|
pad = pad - (ImagePadding*2 + ImageSize - 2) // subtracting 2 for no reason
|
||
|
if pad < 0 {
|
||
|
pad = 0
|
||
|
}
|
||
|
v.Revealer.SetMarginStart(pad)
|
||
|
}
|
||
|
|
||
|
func (v *View) Reset() {
|
||
|
v.SetCompleter(nil)
|
||
|
}
|
||
|
|
||
|
func (v *View) SetCompleter(completer cchat.ServerMessageSendCompleter) {
|
||
|
v.Clear()
|
||
|
v.completer = completer
|
||
|
}
|
||
|
|
||
|
func (v *View) Clear() {
|
||
|
// Do we have anything in the slice? If not, then we don't need to run
|
||
|
// again. We do need to keep RevealChild consistent with this, however.
|
||
|
if v.entries == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Since we don't store the widgets inside the list, we'll manually iterate
|
||
|
// and remove.
|
||
|
v.List.GetChildren().Foreach(func(i interface{}) {
|
||
|
w := i.(gtk.IWidget).ToWidget()
|
||
|
v.List.Remove(w)
|
||
|
w.Destroy()
|
||
|
})
|
||
|
|
||
|
// Set entries to nil to free up the slice.
|
||
|
v.entries = nil
|
||
|
// Set offset to 0 to reset.
|
||
|
v.offset = 0
|
||
|
|
||
|
// Hide the list.
|
||
|
v.SetRevealChild(false)
|
||
|
}
|
||
|
|
||
|
func (v *View) Run() {
|
||
|
// If we don't have a completer, then don't run.
|
||
|
if v.completer == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
text, offset := v.getInputState()
|
||
|
words, index := split.SpaceIndexed(text, offset)
|
||
|
|
||
|
// If the input is empty.
|
||
|
if len(words) == 0 {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
v.offset = offset
|
||
|
v.entries = v.completer.CompleteMessage(words, index)
|
||
|
|
||
|
if len(v.entries) == 0 {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Reveal if needed be.
|
||
|
v.SetRevealChild(true)
|
||
|
|
||
|
// TODO: make entries reuse pixbuf.
|
||
|
|
||
|
for i, entry := range v.entries {
|
||
|
l := rich.NewLabel(entry.Text)
|
||
|
l.Show()
|
||
|
|
||
|
img, _ := gtk.ImageNew()
|
||
|
img.SetSizeRequest(ImageSize, ImageSize)
|
||
|
img.Show()
|
||
|
|
||
|
// Do we have an icon?
|
||
|
if entry.IconURL != "" {
|
||
|
httputil.AsyncImageSized(img, entry.IconURL, ImageSize, ImageSize, imgutil.Round(true))
|
||
|
}
|
||
|
|
||
|
b, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
||
|
b.PackStart(img, false, false, ImagePadding)
|
||
|
b.PackStart(l, true, true, 0)
|
||
|
b.Show()
|
||
|
|
||
|
r, _ := gtk.ListBoxRowNew()
|
||
|
r.Add(b)
|
||
|
r.Show()
|
||
|
|
||
|
v.List.Add(r)
|
||
|
|
||
|
// Select the first item.
|
||
|
if i == 0 {
|
||
|
v.List.SelectRow(r)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|