Added file drag-and-drop into the message box

This commit is contained in:
diamondburned 2020-07-17 11:01:40 -07:00
parent 4c173773bf
commit 7d1078446a
5 changed files with 116 additions and 28 deletions

View File

@ -66,6 +66,8 @@ type Container struct {
Scroll *gtk.ScrolledWindow
Box *gtk.Box
enabled bool
// states
files []File
items map[string]gtk.IWidget
@ -127,6 +129,19 @@ func (c *Container) SetMarginStart(margin int) {
c.Box.SetMarginStart(margin)
}
// Enabled returns whether or not the container allows attachments.
func (c *Container) Enabled() bool {
return c.enabled
}
func (c *Container) SetEnabled(enabled bool) {
// Set the enabled state; reset the container if we're disabling the
// attachment box.
if c.enabled = enabled; !enabled {
c.Reset()
}
}
// Files returns the list of attachments
func (c *Container) Files() []File {
return c.files

View File

@ -95,19 +95,31 @@ type Field struct {
text *gtk.TextView // const
buffer *gtk.TextBuffer // const
UserID string
Sender cchat.ServerMessageSender
editor cchat.ServerMessageEditor
typer cchat.ServerMessageTypingIndicator
send *gtk.Button
attach *gtk.Button
ctrl Controller
// states
// Embed a state field which allows us to easily reset it.
fieldState
}
type fieldState struct {
UserID string
Sender cchat.ServerMessageSender
upload bool // true if server supports files
editor cchat.ServerMessageEditor
typer cchat.ServerMessageTypingIndicator
editingID string // never empty
lastTyped time.Time
typerDura time.Duration
}
func (s *fieldState) Reset() {
*s = fieldState{}
}
var inputFieldCSS = primitives.PrepareCSS(`
.input-field { margin: 3px 5px }
`)
@ -123,22 +135,23 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
field.TextScroll.Show()
primitives.AddClass(field.TextScroll, "scrolled-input")
attach, _ := gtk.ButtonNewFromIconName("mail-attachment-symbolic", gtk.ICON_SIZE_BUTTON)
attach.SetRelief(gtk.RELIEF_NONE)
attach.Show()
primitives.AddClass(attach, "attach-button")
field.attach, _ = gtk.ButtonNewFromIconName("mail-attachment-symbolic", gtk.ICON_SIZE_BUTTON)
field.attach.SetRelief(gtk.RELIEF_NONE)
field.attach.SetSensitive(false)
// Only show this if the server supports it (upload == true).
primitives.AddClass(field.attach, "attach-button")
send, _ := gtk.ButtonNewFromIconName("mail-send-symbolic", gtk.ICON_SIZE_BUTTON)
send.SetRelief(gtk.RELIEF_NONE)
send.Show()
primitives.AddClass(send, "send-button")
field.send, _ = gtk.ButtonNewFromIconName("mail-send-symbolic", gtk.ICON_SIZE_BUTTON)
field.send.SetRelief(gtk.RELIEF_NONE)
field.send.Show()
primitives.AddClass(field.send, "send-button")
// Keep this number the same as size-allocate below -------v
field.FieldBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
field.FieldBox.PackStart(field.Username, false, false, 0)
field.FieldBox.PackStart(attach, false, false, 0)
field.FieldBox.PackStart(field.attach, false, false, 0)
field.FieldBox.PackStart(field.TextScroll, true, true, 0)
field.FieldBox.PackStart(send, false, false, 0)
field.FieldBox.PackStart(field.send, false, false, 0)
field.FieldBox.Show()
primitives.AddClass(field.FieldBox, "input-field")
primitives.AttachCSS(field.FieldBox, inputFieldCSS)
@ -156,16 +169,16 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
// Bind text events.
text.Connect("key-press-event", field.keyDown)
// Bind the send button.
send.Connect("clicked", field.sendInput)
field.send.Connect("clicked", field.sendInput)
// Bind the attach button.
attach.Connect("clicked", func() { gts.SpawnUploader("", field.Attachments.AddFiles) })
field.attach.Connect("clicked", func() { gts.SpawnUploader("", field.Attachments.AddFiles) })
// Connect to the field's revealer. On resize, we want the attachments
// carousel to have the same padding too.
field.Username.Connect("size-allocate", func(w gtk.IWidget) {
// Calculate the left width: from the left of the message box to the
// right of the attach button, covering the username container.
var leftWidth = 5*2 + attach.GetAllocatedWidth() + w.ToWidget().GetAllocatedWidth()
var leftWidth = 5*2 + field.attach.GetAllocatedWidth() + w.ToWidget().GetAllocatedWidth()
// Set the autocompleter's left margin to be the same.
field.Attachments.SetMarginStart(leftWidth)
})
@ -179,13 +192,7 @@ func (f *Field) Reset() {
// doing this just in case.
f.text.SetSensitive(false)
f.UserID = ""
f.Sender = nil
f.editor = nil
f.typer = nil
f.lastTyped = time.Time{}
f.typerDura = 0
f.fieldState.Reset()
f.Username.Reset()
// reset the input
@ -209,6 +216,9 @@ func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSende
// Allow typer to be nil.
f.typer, _ = sender.(cchat.ServerMessageTypingIndicator)
// See if we can upload files.
_, f.upload = sender.(cchat.ServerMessageAttachmentSender)
// Populate the duration state if typer is not nil.
if f.typer != nil {
f.typerDura = f.typer.TypingTimeout()
@ -216,6 +226,23 @@ func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSende
}
}
func (f *Field) SetAllowUpload(allow bool) {
f.upload = allow
// Don't allow clicks on the attachment button if allow is false.
f.attach.SetSensitive(allow)
// Disable the attachmetn carousel for good measure, which also prevents
// drag-and-drops.
f.Attachments.SetEnabled(allow)
// Show the attachment button if we can, else hide it.
if f.upload {
f.attach.Show()
} else {
f.attach.Hide()
}
}
// 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)

View File

@ -78,6 +78,12 @@ func (f *Field) keyDown(tv *gtk.TextView, ev *gdk.Event) bool {
// Ctrl+V is paste.
case key == gdk.KEY_v && bithas(mask, cntrlMask):
// As this pasting is for image attachments, don't accept it if iwe
// don't allow attachments.
if !f.upload {
return false
}
// Is there an image in the clipboard?
if !gts.Clipboard.WaitIsImageAvailable() {
// No.

View File

@ -16,6 +16,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/typing"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/drag"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/menu"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
@ -84,6 +85,9 @@ func NewView() *View {
primitives.AddClass(view.Box, "message-view")
// Bind a file drag-and-drop box into the main view box.
drag.BindFileDest(view.Box, view.InputView.Attachments.AddFiles)
// placeholder logo
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128))
logo.Show()

View File

@ -1,13 +1,18 @@
package drag
import (
"net/url"
"strings"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
func NewTargetEntry(target string) gtk.TargetEntry {
e, _ := gtk.TargetEntryNew(target, gtk.TARGET_SAME_APP, 0)
func NewTargetEntry(target string, f gtk.TargetFlags, info uint) gtk.TargetEntry {
e, _ := gtk.TargetEntryNew(target, f, info)
return *e
}
@ -44,6 +49,35 @@ type Draggable interface {
primitives.Connector
}
func BindFileDest(dg Draggable, file func(path []string)) {
var dragEntries = []gtk.TargetEntry{
NewTargetEntry("text/uri-list", gtk.TARGET_OTHER_APP, 1),
}
dg.DragDestSet(gtk.DEST_DEFAULT_ALL, dragEntries, gdk.ACTION_COPY)
dg.Connect("drag-data-received",
func(_ gtk.IWidget, ctx *gdk.DragContext, x, y uint, data *gtk.SelectionData) {
// Get the files in form of line-delimited URIs
var uris = strings.Fields(string(data.GetData()))
// Create a path slice that we decode URIs into.
var paths = uris[:0]
// Decode the URIs.
for _, uri := range uris {
u, err := url.Parse(uri)
if err != nil {
log.Error(errors.Wrapf(err, "Failed parsing URI %q", uri))
continue
}
paths = append(paths, u.Path)
}
file(paths)
},
)
}
// Swapper is the type for a swap function.
type Swapper = func(targetID, movingID string)
@ -56,8 +90,10 @@ type Swapper = func(targetID, movingID string)
// ID will be taken from the main draggable.
func BindDraggable(dg MainDraggable, icon string, fn Swapper, draggers ...Draggable) {
var atom = "data_" + icon
var dragEntries = []gtk.TargetEntry{NewTargetEntry(atom)}
var dragAtom = gdk.GdkAtomIntern(atom, false)
var dragEntries = []gtk.TargetEntry{
NewTargetEntry(atom, gtk.TARGET_SAME_APP, 0),
}
// Set the ID for Find().
dg.SetName(dg.ID())