mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-11-26 06:07:26 +00:00
Replaced the message completer with a similar popover
This commit is contained in:
parent
d1d7288879
commit
3dd5f02de2
|
|
@ -3,175 +3,61 @@ package completion
|
||||||
import (
|
import (
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
"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/primitives/completion"
|
||||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||||
"github.com/diamondburned/cchat/utils/split"
|
|
||||||
"github.com/diamondburned/imgutil"
|
"github.com/diamondburned/imgutil"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ImageSize = 20
|
ImageSize = 25
|
||||||
ImagePadding = 10
|
ImagePadding = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
// var completionQueue chan func()
|
|
||||||
|
|
||||||
// func init() {
|
|
||||||
// completionQueue = make(chan func(), 1)
|
|
||||||
// go func() {
|
|
||||||
// for fn := range completionQueue {
|
|
||||||
// fn()
|
|
||||||
// }
|
|
||||||
// }()
|
|
||||||
// }
|
|
||||||
|
|
||||||
type View struct {
|
type View struct {
|
||||||
*gtk.Revealer
|
*completion.Completer
|
||||||
Scroll *gtk.ScrolledWindow
|
entries []cchat.CompletionEntry
|
||||||
|
|
||||||
List *gtk.ListBox
|
|
||||||
entries []cchat.CompletionEntry
|
|
||||||
|
|
||||||
text *gtk.TextView
|
|
||||||
buffer *gtk.TextBuffer
|
|
||||||
|
|
||||||
// state
|
|
||||||
completer cchat.ServerMessageSendCompleter
|
completer cchat.ServerMessageSendCompleter
|
||||||
offset int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(text *gtk.TextView) *View {
|
func New(text *gtk.TextView) *View {
|
||||||
list, _ := gtk.ListBoxNew()
|
v := &View{}
|
||||||
list.SetSelectionMode(gtk.SELECTION_BROWSE)
|
c := completion.NewCompleter(text, v)
|
||||||
list.Show()
|
v.Completer = c
|
||||||
|
|
||||||
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", completion.KeyDownHandler(list, text.GrabFocus))
|
|
||||||
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) {
|
|
||||||
completion.SwapWord(v.buffer, v.entries[r.GetIndex()].Raw, v.offset)
|
|
||||||
v.Clear()
|
|
||||||
v.text.GrabFocus() // TODO: remove, maybe not needed
|
|
||||||
})
|
|
||||||
|
|
||||||
return v
|
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() {
|
func (v *View) Reset() {
|
||||||
v.SetCompleter(nil)
|
v.SetCompleter(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) SetCompleter(completer cchat.ServerMessageSendCompleter) {
|
func (v *View) SetCompleter(completer cchat.ServerMessageSendCompleter) {
|
||||||
v.Clear()
|
v.Clear()
|
||||||
|
v.Hide()
|
||||||
v.completer = completer
|
v.completer = completer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) Clear() {
|
func (v *View) Update(words []string, i int) []gtk.IWidget {
|
||||||
// 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 we don't have a completer, then don't run.
|
||||||
if v.completer == nil {
|
if v.completer == nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
text, offset := v.getInputState()
|
v.entries = v.completer.CompleteMessage(words, i)
|
||||||
words, index := split.SpaceIndexed(text, offset)
|
|
||||||
|
|
||||||
// If the input is empty.
|
var widgets = make([]gtk.IWidget, len(v.entries))
|
||||||
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 {
|
for i, entry := range v.entries {
|
||||||
l := rich.NewLabel(entry.Text)
|
l := rich.NewLabel(entry.Text)
|
||||||
l.Show()
|
l.Show()
|
||||||
|
|
||||||
img, _ := gtk.ImageNew()
|
img, _ := gtk.ImageNew()
|
||||||
img.SetSizeRequest(ImageSize, ImageSize)
|
|
||||||
img.Show()
|
|
||||||
|
|
||||||
// Do we have an icon?
|
// Do we have an icon?
|
||||||
if entry.IconURL != "" {
|
if entry.IconURL != "" {
|
||||||
|
img.SetSizeRequest(ImageSize, ImageSize)
|
||||||
|
img.Show()
|
||||||
httputil.AsyncImageSized(img, entry.IconURL, ImageSize, ImageSize, imgutil.Round(true))
|
httputil.AsyncImageSized(img, entry.IconURL, ImageSize, ImageSize, imgutil.Round(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,19 +66,12 @@ func (v *View) Run() {
|
||||||
b.PackStart(l, true, true, 0)
|
b.PackStart(l, true, true, 0)
|
||||||
b.Show()
|
b.Show()
|
||||||
|
|
||||||
r, _ := gtk.ListBoxRowNew()
|
widgets[i] = b
|
||||||
r.Add(b)
|
|
||||||
r.Show()
|
|
||||||
|
|
||||||
v.List.Add(r)
|
|
||||||
|
|
||||||
// Select the first item.
|
|
||||||
if i == 0 {
|
|
||||||
v.List.SelectRow(r)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return widgets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) getInputState() (string, int) {
|
func (v *View) Word(i int) string {
|
||||||
return completion.State(v.buffer)
|
return v.entries[i].Raw
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ type Controller interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputView struct {
|
type InputView struct {
|
||||||
*gtk.Box
|
|
||||||
*Field
|
*Field
|
||||||
Completer *completion.View
|
Completer *completion.View
|
||||||
}
|
}
|
||||||
|
|
@ -33,25 +32,19 @@ func NewView(ctrl Controller) *InputView {
|
||||||
|
|
||||||
// Bind the text event handler to text first.
|
// Bind the text event handler to text first.
|
||||||
c := completion.New(text)
|
c := completion.New(text)
|
||||||
c.Show()
|
|
||||||
|
|
||||||
// Bind the input callback later.
|
// Bind the input callback later.
|
||||||
f := NewField(text, ctrl)
|
f := NewField(text, ctrl)
|
||||||
f.Show()
|
f.Show()
|
||||||
|
|
||||||
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
|
// // Connect to the field's revealer. On resize, we want the autocompleter to
|
||||||
b.PackStart(c, false, true, 0)
|
// // have the right padding too.
|
||||||
b.PackStart(f, false, false, 0)
|
// f.username.Connect("size-allocate", func(w gtk.IWidget) {
|
||||||
b.Show()
|
// // Set the autocompleter's left margin to be the same.
|
||||||
|
// c.SetMarginStart(w.ToWidget().GetAllocatedWidth())
|
||||||
|
// })
|
||||||
|
|
||||||
// Connect to the field's revealer. On resize, we want the autocompleter to
|
return &InputView{f, c}
|
||||||
// have the right padding too.
|
|
||||||
f.username.Connect("size-allocate", func(w gtk.IWidget) {
|
|
||||||
// Set the autocompleter's left margin to be the same.
|
|
||||||
c.SetMarginStart(w.ToWidget().GetAllocatedWidth())
|
|
||||||
})
|
|
||||||
|
|
||||||
return &InputView{b, f, c}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
func (v *InputView) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package completion
|
package completion
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
||||||
"github.com/diamondburned/cchat/utils/split"
|
"github.com/diamondburned/cchat/utils/split"
|
||||||
"github.com/gotk3/gotk3/gtk"
|
"github.com/gotk3/gotk3/gtk"
|
||||||
)
|
)
|
||||||
|
|
@ -30,8 +31,12 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
||||||
l, _ := gtk.ListBoxNew()
|
l, _ := gtk.ListBoxNew()
|
||||||
l.Show()
|
l.Show()
|
||||||
|
|
||||||
|
s := scrollinput.NewVScroll(150)
|
||||||
|
s.Add(l)
|
||||||
|
s.Show()
|
||||||
|
|
||||||
p := NewPopover(input)
|
p := NewPopover(input)
|
||||||
p.Add(l)
|
p.Add(s)
|
||||||
|
|
||||||
c := &Completer{
|
c := &Completer{
|
||||||
Input: input,
|
Input: input,
|
||||||
|
|
@ -52,15 +57,19 @@ func NewCompleter(input *gtk.TextView, ctrl Completeable) *Completer {
|
||||||
|
|
||||||
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
l.Connect("row-activated", func(l *gtk.ListBox, r *gtk.ListBoxRow) {
|
||||||
SwapWord(ibuf, ctrl.Word(r.GetIndex()), c.Cursor)
|
SwapWord(ibuf, ctrl.Word(r.GetIndex()), c.Cursor)
|
||||||
c.clear()
|
c.Clear()
|
||||||
c.Popover.Popdown()
|
c.Hide()
|
||||||
input.GrabFocus()
|
input.GrabFocus()
|
||||||
})
|
})
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Completer) clear() {
|
func (c *Completer) Hide() {
|
||||||
|
c.Popover.Popdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Completer) Clear() {
|
||||||
var children = c.List.GetChildren()
|
var children = c.List.GetChildren()
|
||||||
if children.Length() == 0 {
|
if children.Length() == 0 {
|
||||||
return
|
return
|
||||||
|
|
@ -74,7 +83,7 @@ func (c *Completer) clear() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Completer) complete() {
|
func (c *Completer) complete() {
|
||||||
c.clear()
|
c.Clear()
|
||||||
|
|
||||||
var widgets []gtk.IWidget
|
var widgets []gtk.IWidget
|
||||||
if len(c.Words) > 0 {
|
if len(c.Words) > 0 {
|
||||||
|
|
@ -85,7 +94,7 @@ func (c *Completer) complete() {
|
||||||
c.Popover.SetPointingTo(CursorRect(c.Input))
|
c.Popover.SetPointingTo(CursorRect(c.Input))
|
||||||
c.Popover.Popup()
|
c.Popover.Popup()
|
||||||
} else {
|
} else {
|
||||||
c.Popover.Popdown()
|
c.Hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, widget := range widgets {
|
for i, widget := range widgets {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ var popoverCSS = primitives.PrepareCSS(`
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MinPopoverWidth = 250
|
MinPopoverWidth = 300
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewPopover(relto gtk.IWidget) *gtk.Popover {
|
func NewPopover(relto gtk.IWidget) *gtk.Popover {
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,21 @@ package scrollinput
|
||||||
|
|
||||||
import "github.com/gotk3/gotk3/gtk"
|
import "github.com/gotk3/gotk3/gtk"
|
||||||
|
|
||||||
|
func NewVScroll(maxHeight int) *gtk.ScrolledWindow {
|
||||||
|
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
||||||
|
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||||
|
sw.SetProperty("propagate-natural-height", true)
|
||||||
|
sw.SetProperty("max-content-height", maxHeight)
|
||||||
|
|
||||||
|
return sw
|
||||||
|
}
|
||||||
|
|
||||||
func NewV(text *gtk.TextView, maxHeight int) *gtk.ScrolledWindow {
|
func NewV(text *gtk.TextView, maxHeight int) *gtk.ScrolledWindow {
|
||||||
// Wrap mode needed since we're not doing horizontal scrolling.
|
// Wrap mode needed since we're not doing horizontal scrolling.
|
||||||
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||||
|
|
||||||
sw, _ := gtk.ScrolledWindowNew(nil, nil)
|
sw := NewVScroll(maxHeight)
|
||||||
sw.Add(text)
|
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
|
return sw
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue