2020-05-26 06:51:06 +00:00
|
|
|
package input
|
|
|
|
|
|
|
|
import (
|
|
|
|
"github.com/diamondburned/cchat"
|
2020-06-17 22:58:38 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
2020-06-13 07:29:32 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/completion"
|
2020-07-01 19:56:32 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/messages/input/username"
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
2020-07-01 01:09:22 +00:00
|
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
|
2020-05-26 06:51:06 +00:00
|
|
|
"github.com/gotk3/gotk3/gtk"
|
2020-06-17 22:58:38 +00:00
|
|
|
"github.com/pkg/errors"
|
2020-05-26 06:51:06 +00:00
|
|
|
)
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Controller is an interface to control message containers.
|
|
|
|
type Controller interface {
|
|
|
|
AddPresendMessage(msg PresendMessage) (onErr func(error))
|
2020-06-17 22:58:38 +00:00
|
|
|
LatestMessageFrom(userID string) (messageID string, ok bool)
|
2020-06-13 07:29:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type InputView struct {
|
|
|
|
*Field
|
|
|
|
Completer *completion.View
|
|
|
|
}
|
|
|
|
|
2020-07-01 19:56:32 +00:00
|
|
|
var textCSS = primitives.PrepareCSS(`
|
2020-07-04 04:41:12 +00:00
|
|
|
.message-input, .message-input * {
|
2020-07-01 19:56:32 +00:00
|
|
|
background-color: transparent;
|
|
|
|
}
|
2020-07-04 04:41:12 +00:00
|
|
|
|
|
|
|
.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;
|
|
|
|
}
|
2020-07-01 19:56:32 +00:00
|
|
|
`)
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
func NewView(ctrl Controller) *InputView {
|
|
|
|
text, _ := gtk.TextViewNew()
|
|
|
|
text.SetSensitive(false)
|
|
|
|
text.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
2020-07-04 04:41:12 +00:00
|
|
|
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)
|
2020-06-13 07:29:32 +00:00
|
|
|
text.Show()
|
|
|
|
|
2020-07-01 19:56:32 +00:00
|
|
|
primitives.AddClass(text, "message-input")
|
|
|
|
primitives.AttachCSS(text, textCSS)
|
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
// Bind the text event handler to text first.
|
|
|
|
c := completion.New(text)
|
|
|
|
|
|
|
|
// Bind the input callback later.
|
|
|
|
f := NewField(text, ctrl)
|
|
|
|
f.Show()
|
|
|
|
|
2020-07-01 19:56:32 +00:00
|
|
|
primitives.AddClass(f, "input-field")
|
2020-06-13 07:29:32 +00:00
|
|
|
|
2020-07-01 01:34:13 +00:00
|
|
|
return &InputView{f, c}
|
2020-06-13 07:29:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-05-26 06:51:06 +00:00
|
|
|
type Field struct {
|
2020-06-04 23:00:41 +00:00
|
|
|
*gtk.Box
|
2020-07-01 19:56:32 +00:00
|
|
|
Username *username.Container
|
2020-06-04 23:00:41 +00:00
|
|
|
|
|
|
|
TextScroll *gtk.ScrolledWindow
|
|
|
|
text *gtk.TextView
|
|
|
|
buffer *gtk.TextBuffer
|
|
|
|
|
|
|
|
UserID string
|
2020-06-13 07:29:32 +00:00
|
|
|
Sender cchat.ServerMessageSender
|
2020-06-17 22:58:38 +00:00
|
|
|
editor cchat.ServerMessageEditor
|
2020-05-26 06:51:06 +00:00
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
ctrl Controller
|
2020-06-17 22:58:38 +00:00
|
|
|
|
|
|
|
// editing state
|
|
|
|
editingID string // never empty
|
2020-06-04 23:00:41 +00:00
|
|
|
}
|
|
|
|
|
2020-07-04 04:41:12 +00:00
|
|
|
var scrollinputCSS = primitives.PrepareCSS(`
|
|
|
|
.scrolled-input {
|
|
|
|
margin: 5px;
|
|
|
|
}
|
|
|
|
`)
|
2020-06-04 23:00:41 +00:00
|
|
|
|
2020-06-13 07:29:32 +00:00
|
|
|
func NewField(text *gtk.TextView, ctrl Controller) *Field {
|
2020-07-01 19:56:32 +00:00
|
|
|
username := username.NewContainer()
|
2020-06-04 23:00:41 +00:00
|
|
|
username.Show()
|
|
|
|
|
2020-05-26 06:51:06 +00:00
|
|
|
buf, _ := text.GetBuffer()
|
|
|
|
|
2020-07-01 01:09:22 +00:00
|
|
|
sw := scrollinput.NewV(text, 150)
|
2020-06-04 23:00:41 +00:00
|
|
|
sw.Show()
|
2020-05-26 06:51:06 +00:00
|
|
|
|
2020-07-04 04:41:12 +00:00
|
|
|
primitives.AddClass(sw, "scrolled-input")
|
|
|
|
primitives.AttachCSS(sw, scrollinputCSS)
|
|
|
|
|
2020-06-04 23:00:41 +00:00
|
|
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 0)
|
2020-06-06 00:47:28 +00:00
|
|
|
box.PackStart(username, false, false, 0)
|
2020-06-04 23:00:41 +00:00
|
|
|
box.PackStart(sw, true, true, 0)
|
|
|
|
box.Show()
|
|
|
|
|
|
|
|
field := &Field{
|
2020-07-01 19:56:32 +00:00
|
|
|
Box: box,
|
|
|
|
Username: username,
|
|
|
|
// typing: typing,
|
2020-06-04 23:00:41 +00:00
|
|
|
TextScroll: sw,
|
|
|
|
text: text,
|
|
|
|
buffer: buf,
|
|
|
|
ctrl: ctrl,
|
2020-05-26 06:51:06 +00:00
|
|
|
}
|
2020-06-04 23:00:41 +00:00
|
|
|
|
|
|
|
text.SetFocusHAdjustment(sw.GetHAdjustment())
|
|
|
|
text.SetFocusVAdjustment(sw.GetVAdjustment())
|
|
|
|
text.Connect("key-press-event", field.keyDown)
|
|
|
|
|
2020-07-01 19:56:32 +00:00
|
|
|
// // 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())
|
|
|
|
// })
|
|
|
|
|
2020-06-04 23:00:41 +00:00
|
|
|
return field
|
2020-05-26 06:51:06 +00:00
|
|
|
}
|
|
|
|
|
2020-06-07 07:06:13 +00:00
|
|
|
// Reset prepares the field before SetSender() is called.
|
|
|
|
func (f *Field) Reset() {
|
|
|
|
// Paranoia.
|
|
|
|
f.text.SetSensitive(false)
|
|
|
|
|
|
|
|
f.UserID = ""
|
2020-06-13 07:29:32 +00:00
|
|
|
f.Sender = nil
|
2020-06-17 22:58:38 +00:00
|
|
|
f.editor = nil
|
2020-07-01 19:56:32 +00:00
|
|
|
f.Username.Reset()
|
2020-06-07 07:06:13 +00:00
|
|
|
|
|
|
|
// reset the input
|
|
|
|
f.buffer.Delete(f.buffer.GetBounds())
|
|
|
|
}
|
|
|
|
|
2020-05-26 06:51:06 +00:00
|
|
|
// SetSender changes the sender of the input field. If nil, the input will be
|
2020-06-07 07:06:13 +00:00
|
|
|
// disabled. Reset() should be called first.
|
2020-06-04 23:00:41 +00:00
|
|
|
func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSender) {
|
2020-06-07 04:27:28 +00:00
|
|
|
// Update the left username container in the input.
|
2020-07-01 19:56:32 +00:00
|
|
|
f.Username.Update(session, sender)
|
2020-06-17 22:58:38 +00:00
|
|
|
f.UserID = session.ID()
|
2020-06-04 23:00:41 +00:00
|
|
|
|
|
|
|
// Set the sender.
|
2020-06-07 07:06:13 +00:00
|
|
|
if sender != nil {
|
2020-06-13 07:29:32 +00:00
|
|
|
f.Sender = sender
|
2020-06-07 07:06:13 +00:00
|
|
|
f.text.SetSensitive(true)
|
2020-06-17 22:58:38 +00:00
|
|
|
|
2020-06-28 23:01:08 +00:00
|
|
|
// Allow editor to be nil.
|
|
|
|
ed, ok := sender.(cchat.ServerMessageEditor)
|
|
|
|
if !ok {
|
|
|
|
log.Printlnf("Editor is not implemented for %T", sender)
|
2020-06-17 22:58:38 +00:00
|
|
|
}
|
2020-06-28 23:01:08 +00:00
|
|
|
f.editor = ed
|
2020-06-17 22:58:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-28 23:01:08 +00:00
|
|
|
// 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)
|
2020-06-17 22:58:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *Field) StartEditing(msgID string) bool {
|
|
|
|
// Do we support message editing? If not, exit.
|
2020-06-28 23:01:08 +00:00
|
|
|
if !f.Editable(msgID) {
|
2020-06-17 22:58:38 +00:00
|
|
|
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
|
2020-06-07 07:06:13 +00:00
|
|
|
}
|
2020-06-17 22:58:38 +00:00
|
|
|
|
|
|
|
f.editingID = ""
|
|
|
|
f.clearText()
|
|
|
|
|
|
|
|
return true
|
2020-05-26 06:51:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2020-06-17 22:58:38 +00:00
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|