mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-01-22 01:46:47 +00:00
Added file drag-and-drop into the message box
This commit is contained in:
parent
4c173773bf
commit
7d1078446a
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue