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/diamondburned/cchat-gtk/internal/ui/messages/input/username" "github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput" "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 { *Field Completer *completion.View } var textCSS = primitives.PrepareCSS(` .message-input, .message-input * { background-color: transparent; } .message-input * { background-color: @theme_base_color; border: 1px solid alpha(@theme_fg_color, 0.2); border-radius: 4px; transition: linear 50ms border-color; } .message-input:focus * { border-color: @theme_selected_bg_color; } `) func NewView(ctrl Controller) *InputView { text, _ := gtk.TextViewNew() text.SetSensitive(false) text.SetWrapMode(gtk.WRAP_WORD_CHAR) text.SetVAlign(gtk.ALIGN_START) text.SetProperty("top-margin", 4) text.SetProperty("bottom-margin", 4) text.SetProperty("left-margin", 8) text.SetProperty("right-margin", 8) text.Show() primitives.AddClass(text, "message-input") primitives.AttachCSS(text, textCSS) // Bind the text event handler to text first. c := completion.New(text) // Bind the input callback later. f := NewField(text, ctrl) f.Show() primitives.AddClass(f, "input-field") return &InputView{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 *username.Container 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 } var scrollinputCSS = primitives.PrepareCSS(` .scrolled-input { margin: 5px; } `) func NewField(text *gtk.TextView, ctrl Controller) *Field { username := username.NewContainer() username.Show() buf, _ := text.GetBuffer() sw := scrollinput.NewV(text, 150) sw.Show() primitives.AddClass(sw, "scrolled-input") primitives.AttachCSS(sw, scrollinputCSS) 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, // typing: typing, TextScroll: sw, text: text, buffer: buf, ctrl: ctrl, } text.SetFocusHAdjustment(sw.GetHAdjustment()) text.SetFocusVAdjustment(sw.GetVAdjustment()) text.Connect("key-press-event", field.keyDown) // // 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 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() }