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:
parent
4c173773bf
commit
7d1078446a
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in a new issue