1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-gtk.git synced 2025-03-20 08:59:18 +00:00

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 Scroll *gtk.ScrolledWindow
Box *gtk.Box Box *gtk.Box
enabled bool
// states // states
files []File files []File
items map[string]gtk.IWidget items map[string]gtk.IWidget
@ -127,6 +129,19 @@ func (c *Container) SetMarginStart(margin int) {
c.Box.SetMarginStart(margin) 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 // Files returns the list of attachments
func (c *Container) Files() []File { func (c *Container) Files() []File {
return c.files return c.files

View file

@ -95,19 +95,31 @@ type Field struct {
text *gtk.TextView // const text *gtk.TextView // const
buffer *gtk.TextBuffer // const buffer *gtk.TextBuffer // const
UserID string send *gtk.Button
Sender cchat.ServerMessageSender attach *gtk.Button
editor cchat.ServerMessageEditor
typer cchat.ServerMessageTypingIndicator
ctrl Controller 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 editingID string // never empty
lastTyped time.Time lastTyped time.Time
typerDura time.Duration typerDura time.Duration
} }
func (s *fieldState) Reset() {
*s = fieldState{}
}
var inputFieldCSS = primitives.PrepareCSS(` var inputFieldCSS = primitives.PrepareCSS(`
.input-field { margin: 3px 5px } .input-field { margin: 3px 5px }
`) `)
@ -123,22 +135,23 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
field.TextScroll.Show() field.TextScroll.Show()
primitives.AddClass(field.TextScroll, "scrolled-input") primitives.AddClass(field.TextScroll, "scrolled-input")
attach, _ := gtk.ButtonNewFromIconName("mail-attachment-symbolic", gtk.ICON_SIZE_BUTTON) field.attach, _ = gtk.ButtonNewFromIconName("mail-attachment-symbolic", gtk.ICON_SIZE_BUTTON)
attach.SetRelief(gtk.RELIEF_NONE) field.attach.SetRelief(gtk.RELIEF_NONE)
attach.Show() field.attach.SetSensitive(false)
primitives.AddClass(attach, "attach-button") // 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) field.send, _ = gtk.ButtonNewFromIconName("mail-send-symbolic", gtk.ICON_SIZE_BUTTON)
send.SetRelief(gtk.RELIEF_NONE) field.send.SetRelief(gtk.RELIEF_NONE)
send.Show() field.send.Show()
primitives.AddClass(send, "send-button") primitives.AddClass(field.send, "send-button")
// Keep this number the same as size-allocate below -------v // Keep this number the same as size-allocate below -------v
field.FieldBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5) field.FieldBox, _ = gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
field.FieldBox.PackStart(field.Username, false, false, 0) 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(field.TextScroll, true, true, 0)
field.FieldBox.PackStart(send, false, false, 0) field.FieldBox.PackStart(field.send, false, false, 0)
field.FieldBox.Show() field.FieldBox.Show()
primitives.AddClass(field.FieldBox, "input-field") primitives.AddClass(field.FieldBox, "input-field")
primitives.AttachCSS(field.FieldBox, inputFieldCSS) primitives.AttachCSS(field.FieldBox, inputFieldCSS)
@ -156,16 +169,16 @@ func NewField(text *gtk.TextView, ctrl Controller) *Field {
// Bind text events. // Bind text events.
text.Connect("key-press-event", field.keyDown) text.Connect("key-press-event", field.keyDown)
// Bind the send button. // Bind the send button.
send.Connect("clicked", field.sendInput) field.send.Connect("clicked", field.sendInput)
// Bind the attach button. // 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 // Connect to the field's revealer. On resize, we want the attachments
// carousel to have the same padding too. // carousel to have the same padding too.
field.Username.Connect("size-allocate", func(w gtk.IWidget) { field.Username.Connect("size-allocate", func(w gtk.IWidget) {
// Calculate the left width: from the left of the message box to the // Calculate the left width: from the left of the message box to the
// right of the attach button, covering the username container. // 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. // Set the autocompleter's left margin to be the same.
field.Attachments.SetMarginStart(leftWidth) field.Attachments.SetMarginStart(leftWidth)
}) })
@ -179,13 +192,7 @@ func (f *Field) Reset() {
// doing this just in case. // doing this just in case.
f.text.SetSensitive(false) f.text.SetSensitive(false)
f.UserID = "" f.fieldState.Reset()
f.Sender = nil
f.editor = nil
f.typer = nil
f.lastTyped = time.Time{}
f.typerDura = 0
f.Username.Reset() f.Username.Reset()
// reset the input // reset the input
@ -209,6 +216,9 @@ func (f *Field) SetSender(session cchat.Session, sender cchat.ServerMessageSende
// Allow typer to be nil. // Allow typer to be nil.
f.typer, _ = sender.(cchat.ServerMessageTypingIndicator) 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. // Populate the duration state if typer is not nil.
if f.typer != nil { if f.typer != nil {
f.typerDura = f.typer.TypingTimeout() 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. // Editable returns whether or not the input field can be edited.
func (f *Field) Editable(msgID string) bool { func (f *Field) Editable(msgID string) bool {
return f.editor != nil && f.editor.MessageEditable(msgID) 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. // Ctrl+V is paste.
case key == gdk.KEY_v && bithas(mask, cntrlMask): 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? // Is there an image in the clipboard?
if !gts.Clipboard.WaitIsImageAvailable() { if !gts.Clipboard.WaitIsImageAvailable() {
// No. // No.

View file

@ -16,6 +16,7 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/messages/typing" "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"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/autoscroll" "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/diamondburned/cchat-gtk/internal/ui/primitives/menu"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -84,6 +85,9 @@ func NewView() *View {
primitives.AddClass(view.Box, "message-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 // placeholder logo
logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128)) logo, _ := gtk.ImageNewFromPixbuf(icons.Logo256Variant2(128))
logo.Show() logo.Show()

View file

@ -1,13 +1,18 @@
package drag package drag
import ( import (
"net/url"
"strings"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives" "github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/gdk"
"github.com/gotk3/gotk3/gtk" "github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
) )
func NewTargetEntry(target string) gtk.TargetEntry { func NewTargetEntry(target string, f gtk.TargetFlags, info uint) gtk.TargetEntry {
e, _ := gtk.TargetEntryNew(target, gtk.TARGET_SAME_APP, 0) e, _ := gtk.TargetEntryNew(target, f, info)
return *e return *e
} }
@ -44,6 +49,35 @@ type Draggable interface {
primitives.Connector 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. // Swapper is the type for a swap function.
type Swapper = func(targetID, movingID string) type Swapper = func(targetID, movingID string)
@ -56,8 +90,10 @@ type Swapper = func(targetID, movingID string)
// ID will be taken from the main draggable. // ID will be taken from the main draggable.
func BindDraggable(dg MainDraggable, icon string, fn Swapper, draggers ...Draggable) { func BindDraggable(dg MainDraggable, icon string, fn Swapper, draggers ...Draggable) {
var atom = "data_" + icon var atom = "data_" + icon
var dragEntries = []gtk.TargetEntry{NewTargetEntry(atom)}
var dragAtom = gdk.GdkAtomIntern(atom, false) var dragAtom = gdk.GdkAtomIntern(atom, false)
var dragEntries = []gtk.TargetEntry{
NewTargetEntry(atom, gtk.TARGET_SAME_APP, 0),
}
// Set the ID for Find(). // Set the ID for Find().
dg.SetName(dg.ID()) dg.SetName(dg.ID())