1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2024-10-31 19:44:23 +00:00
cchat-gtk/internal/ui/messages/input/input.go

220 lines
5.2 KiB
Go

package input
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
// Controller is an interface to control message containers.
type Controller interface {
AddPresendMessage(msg PresendMessage) (onErr func(error))
LatestMessageFrom(userID string) (messageID string, ok bool)
}
type InputView struct {
*gtk.Box
*Field
Completer *completion.View
}
func NewView(ctrl Controller) *InputView {
text, _ := gtk.TextViewNew()
text.SetSensitive(false)
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
text.SetProperty("top-margin", inputmargin)
text.SetProperty("left-margin", inputmargin)
text.SetProperty("right-margin", inputmargin)
text.SetProperty("bottom-margin", inputmargin)
text.Show()
// Bind the text event handler to text first.
c := completion.New(text)
c.Show()
// Bind the input callback later.
f := NewField(text, ctrl)
f.Show()
b, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
b.PackStart(c, false, true, 0)
b.PackStart(f, false, false, 0)
b.Show()
// Connect to the field's revealer. On resize, we want the autocompleter to
// 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) {
v.Field.SetSender(session, sender)
// Ignore ok; completer can be nil.
completer, _ := sender.(cchat.ServerMessageSendCompleter)
v.Completer.SetCompleter(completer)
}
type Field struct {
*gtk.Box
username *usernameContainer
TextScroll *gtk.ScrolledWindow
text *gtk.TextView
buffer *gtk.TextBuffer
UserID string
Sender cchat.ServerMessageSender
editor cchat.ServerMessageEditor
ctrl Controller
// editing state
editingID string // never empty
}
const inputmargin = 4
func NewField(text *gtk.TextView, ctrl Controller) *Field {
username := newUsernameContainer()
username.Show()
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.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
box.PackStart(username, false, false, 0)
box.PackStart(sw, true, true, 0)
box.Show()
field := &Field{
Box: box,
username: username,
TextScroll: sw,
text: text,
buffer: buf,
ctrl: ctrl,
}
text.SetFocusHAdjustment(sw.GetHAdjustment())
text.SetFocusVAdjustment(sw.GetVAdjustment())
text.Connect("key-press-event", field.keyDown)
return field
}
// Reset prepares the field before SetSender() is called.
func (f *Field) Reset() {
// Paranoia.
f.text.SetSensitive(false)
f.UserID = ""
f.Sender = nil
f.editor = nil
f.username.Reset()
// reset the input
f.buffer.Delete(f.buffer.GetBounds())
}
// SetSender changes the sender of the input field. If nil, the input will be
// disabled. Reset() should be called first.
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
// Update the left username container in the input.
f.username.Update(session, sender)
f.UserID = session.ID()
// Set the sender.
if sender != nil {
f.Sender = sender
f.text.SetSensitive(true)
// Allow editor to be nil.
ed, ok := sender.(cchat.ServerMessageEditor)
if !ok {
log.Printlnf("Editor is not implemented for %T", sender)
}
f.editor = ed
}
}
// Editable returns whether or not the input field can be edited.
func (f *Field) Editable(msgID string) bool {
return f.editor != nil && f.editor.MessageEditable(msgID)
}
func (f *Field) StartEditing(msgID string) bool {
// Do we support message editing? If not, exit.
if !f.Editable(msgID) {
return false
}
// Try and request the old message content for editing.
content, err := f.editor.RawMessageContent(msgID)
if err != nil {
// TODO: show error
log.Error(errors.Wrap(err, "Failed to get message content"))
return false
}
// Set the current editing state and set the input after requesting the
// content.
f.editingID = msgID
f.buffer.SetText(content)
return true
}
// StopEditing cancels the current editing message. It returns a false and does
// nothing if the editor is not editing anything.
func (f *Field) StopEditing() bool {
if f.editingID == "" {
return false
}
f.editingID = ""
f.clearText()
return true
}
// yankText cuts the text from the input field and returns it.
func (f *Field) yankText() string {
start, end := f.buffer.GetBounds()
text, _ := f.buffer.GetText(start, end, false)
if text != "" {
f.buffer.Delete(start, end)
}
return text
}
// clearText wipes the input field
func (f *Field) clearText() {
f.buffer.Delete(f.buffer.GetBounds())
}
// getText returns the text from the input, but it doesn't cut it.
func (f *Field) getText() string {
start, end := f.buffer.GetBounds()
text, _ := f.buffer.GetText(start, end, false)
return text
}
func (f *Field) textLen() int {
return f.buffer.GetCharCount()
}