mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-03-23 18:39:22 +00:00
Reverted TextView; added image popover on click
This commit is contained in:
parent
e2d656dcae
commit
32e6ed8b21
2
go.mod
2
go.mod
|
@ -4,8 +4,6 @@ go 1.14
|
|||
|
||||
replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200619213419-0533bcce0dd6
|
||||
|
||||
replace github.com/diamondburned/cchat-discord => ../cchat-discord/
|
||||
|
||||
require (
|
||||
github.com/Xuanwo/go-locale v0.2.0
|
||||
github.com/diamondburned/cchat v0.0.34
|
||||
|
|
|
@ -50,7 +50,7 @@ func AsyncImage(img ImageContainer, url string, procs ...imgutil.Processor) {
|
|||
go syncImage(ctx, l, url, procs, gif)
|
||||
}
|
||||
|
||||
// AsyncImageSized resizes using GdkPixbuf. This method does not use the cache.
|
||||
// AsyncImageSized resizes using GdkPixbuf. This method uses the cache.
|
||||
func AsyncImageSized(img ImageContainerSizer, url string, w, h int, procs ...imgutil.Processor) {
|
||||
if url == "" {
|
||||
return
|
||||
|
|
82
internal/ui/imgview/imgview.go
Normal file
82
internal/ui/imgview/imgview.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package imgview
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat-gtk/internal/gts/httputil"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
|
||||
"github.com/gotk3/gotk3/gdk"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxWidth = 350
|
||||
MaxHeight = 350
|
||||
)
|
||||
|
||||
type WidgetConnector interface {
|
||||
gtk.IWidget
|
||||
primitives.Connector
|
||||
}
|
||||
|
||||
var _ WidgetConnector = (*gtk.Label)(nil)
|
||||
|
||||
func BindTooltip(connector WidgetConnector) {
|
||||
// This implementation doesn't seem like a good idea. First off, is the
|
||||
// closure really garbage collected? If it's not, then we have some huge
|
||||
// issues. Second, if the closure is garbage collected, then when? If it's
|
||||
// not garbage collecteed, then not only are we leaking 2 float64s per
|
||||
// message, but we're also keeping alive the widget.
|
||||
|
||||
var x, y float64
|
||||
connector.Connect("motion-notify-event", func(w gtk.IWidget, ev *gdk.Event) {
|
||||
mev := gdk.EventMotionNewFromEvent(ev)
|
||||
x, y = mev.MotionVal()
|
||||
})
|
||||
|
||||
connector.Connect("activate-link", func(c WidgetConnector, uri string) bool {
|
||||
switch ext(uri) {
|
||||
case ".jpg", ".jpeg", ".png", ".webp", ".gif":
|
||||
// Make a new rectangle to use in the popover.
|
||||
r := gdk.Rectangle{}
|
||||
r.SetX(int(x))
|
||||
r.SetY(int(y))
|
||||
|
||||
// Make a new image that's asynchronously fetched.
|
||||
img, _ := gtk.ImageNewFromIconName("image-loading", gtk.ICON_SIZE_BUTTON)
|
||||
img.SetMarginStart(5)
|
||||
img.SetMarginEnd(5)
|
||||
img.SetMarginTop(5)
|
||||
img.SetMarginBottom(5)
|
||||
img.Show()
|
||||
|
||||
// Cap the width and height if requested.
|
||||
var w, h = parser.FragmentImageSize(uri, MaxWidth, MaxHeight)
|
||||
httputil.AsyncImageSized(img, uri, w, h)
|
||||
|
||||
p, _ := gtk.PopoverNew(c)
|
||||
p.SetPointingTo(r)
|
||||
p.Connect("closed", img.Destroy) // on close, destroy image
|
||||
p.Add(img)
|
||||
p.Popup()
|
||||
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ext parses and sanitizes the extension to something comparable.
|
||||
func ext(uri string) string {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return strings.ToLower(path.Ext(uri))
|
||||
}
|
||||
|
||||
return strings.ToLower(path.Ext(u.Path))
|
||||
}
|
|
@ -24,7 +24,6 @@ func NewCollapsedMessage(msg cchat.MessageCreate) *CollapsedMessage {
|
|||
|
||||
func WrapCollapsedMessage(gc *message.GenericContainer) *CollapsedMessage {
|
||||
// Set Timestamp's padding accordingly to Avatar's.
|
||||
gc.Timestamp.SetProperty("ypad", 1) // trivial detail
|
||||
gc.Timestamp.SetSizeRequest(AvatarSize, -1)
|
||||
gc.Timestamp.SetVAlign(gtk.ALIGN_START)
|
||||
gc.Timestamp.SetMarginStart(container.ColumnSpacing * 2)
|
||||
|
|
|
@ -57,7 +57,6 @@ func WrapFullMessage(gc *message.GenericContainer) *FullMessage {
|
|||
// We don't call avatar.Show(). That's called in Attach.
|
||||
|
||||
// Style the timestamp accordingly.
|
||||
gc.Timestamp.SetProperty("ypad", 1) // trivial detail
|
||||
gc.Timestamp.SetXAlign(0.0) // left-align
|
||||
gc.Timestamp.SetVAlign(gtk.ALIGN_END) // bottom-align
|
||||
gc.Timestamp.SetMarginStart(0) // clear margins
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-gtk/internal/humanize"
|
||||
"github.com/diamondburned/cchat-gtk/internal/log"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/imgview"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich"
|
||||
"github.com/diamondburned/cchat-gtk/internal/ui/rich/parser"
|
||||
|
@ -13,7 +14,6 @@ import (
|
|||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/gotk3/gotk3/gtk"
|
||||
"github.com/gotk3/gotk3/pango"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Container interface {
|
||||
|
@ -55,8 +55,7 @@ type GenericContainer struct {
|
|||
|
||||
Timestamp *gtk.Label
|
||||
Username *gtk.Label
|
||||
Content *gtk.TextView
|
||||
CBuffer *gtk.TextBuffer
|
||||
Content *gtk.Label
|
||||
|
||||
MenuItems []menu.Item
|
||||
}
|
||||
|
@ -90,20 +89,18 @@ func NewEmptyContainer() *GenericContainer {
|
|||
user.SetMaxWidthChars(35)
|
||||
user.SetLineWrap(true)
|
||||
user.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||
user.SetHAlign(gtk.ALIGN_END)
|
||||
user.SetXAlign(1) // right align
|
||||
user.SetVAlign(gtk.ALIGN_START)
|
||||
user.SetSelectable(true)
|
||||
user.Show()
|
||||
|
||||
content, _ := gtk.TextViewNew()
|
||||
content.SetHExpand(true)
|
||||
content.SetWrapMode(gtk.WRAP_WORD_CHAR)
|
||||
content.SetCursorVisible(false)
|
||||
content.SetEditable(false)
|
||||
content, _ := gtk.LabelNew("")
|
||||
content.SetLineWrap(true)
|
||||
content.SetLineWrapMode(pango.WRAP_WORD_CHAR)
|
||||
content.SetXAlign(0) // left align
|
||||
// content.SetSelectable(true)
|
||||
content.Show()
|
||||
|
||||
cbuffer, _ := content.GetBuffer()
|
||||
|
||||
// Add CSS classes.
|
||||
primitives.AddClass(ts, "message-time")
|
||||
primitives.AddClass(user, "message-author")
|
||||
|
@ -113,31 +110,17 @@ func NewEmptyContainer() *GenericContainer {
|
|||
Timestamp: ts,
|
||||
Username: user,
|
||||
Content: content,
|
||||
CBuffer: cbuffer,
|
||||
}
|
||||
|
||||
gc.Content.SetProperty("populate-all", true)
|
||||
gc.Content.Connect("populate-popup", func(tv *gtk.TextView, popup *gtk.Widget) {
|
||||
v, err := popup.Cast()
|
||||
if err != nil {
|
||||
log.Error(errors.Wrap(err, "Failed to cast popup to IWidget"))
|
||||
return
|
||||
}
|
||||
|
||||
switch popup := v.(type) {
|
||||
case menu.MenuAppender:
|
||||
menu.MenuSeparator(popup)
|
||||
menu.MenuItems(popup, gc.MenuItems)
|
||||
|
||||
case menu.ToolbarInserter:
|
||||
menu.ToolbarSeparator(popup)
|
||||
menu.ToolbarItems(popup, gc.MenuItems)
|
||||
|
||||
default:
|
||||
log.Printlnf("Debug: typeOf(popup) = %T", popup)
|
||||
}
|
||||
gc.Content.Connect("populate-popup", func(l *gtk.Label, m *gtk.Menu) {
|
||||
menu.MenuSeparator(m)
|
||||
menu.MenuItems(m, gc.MenuItems)
|
||||
})
|
||||
|
||||
// Make up for the lack of inline images with an image popover that's shown
|
||||
// when links are clicked.
|
||||
imgview.BindTooltip(gc.Content)
|
||||
|
||||
return gc
|
||||
}
|
||||
|
||||
|
@ -182,12 +165,21 @@ func (m *GenericContainer) UpdateAuthorName(name text.Rich) {
|
|||
}
|
||||
|
||||
func (m *GenericContainer) UpdateContent(content text.Rich, edited bool) {
|
||||
// Render the content.
|
||||
parser.RenderTextBuffer(m.CBuffer, content)
|
||||
log.Println("Rendering", m.id, content)
|
||||
|
||||
var markup = parser.RenderMarkup(content)
|
||||
if edited {
|
||||
parser.AppendEditBadge(m.CBuffer, m.Time())
|
||||
markup += " " + rich.Small("(edited)")
|
||||
}
|
||||
|
||||
m.Content.SetMarkup(markup)
|
||||
|
||||
// // Render the content.
|
||||
// parser.RenderTextBuffer(m.CBuffer, content)
|
||||
|
||||
// if edited {
|
||||
// parser.AppendEditBadge(m.CBuffer, m.Time())
|
||||
// }
|
||||
}
|
||||
|
||||
// AttachMenu connects signal handlers to handle a list of menu items from
|
||||
|
|
|
@ -53,12 +53,14 @@ func (m *GenericPresendContainer) SetDone(id string) {
|
|||
|
||||
func (m *GenericPresendContainer) SetLoading() {
|
||||
m.SetSensitive(false)
|
||||
m.CBuffer.SetText(m.sendString)
|
||||
m.Content.SetText(m.sendString)
|
||||
m.Content.SetTooltipText("")
|
||||
|
||||
// m.CBuffer.SetText(m.sendString)
|
||||
}
|
||||
|
||||
func (m *GenericPresendContainer) SetSentError(err error) {
|
||||
m.SetSensitive(true) // allow events incl right clicks
|
||||
m.CBuffer.SetText(`<span color="red">` + html.EscapeString(m.sendString) + `</span>`)
|
||||
m.Content.SetMarkup(`<span color="red">` + html.EscapeString(m.sendString) + `</span>`)
|
||||
m.Content.SetTooltipText(err.Error())
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/imgutil"
|
||||
)
|
||||
|
||||
type attrAppendMap struct {
|
||||
|
@ -120,10 +122,7 @@ func RenderMarkup(content text.Rich) string {
|
|||
|
||||
case text.Imager:
|
||||
// Ends don't matter with images.
|
||||
appended.addf(start,
|
||||
`<a href="%s">%s</a>`,
|
||||
html.EscapeString(segment.Image()), html.EscapeString(segment.ImageText()),
|
||||
)
|
||||
appended.add(start, composeImageMarkup(segment))
|
||||
|
||||
case text.Colorer:
|
||||
appended.span(start, end, fmt.Sprintf(`color="#%06X"`, segment.Color()))
|
||||
|
@ -156,6 +155,47 @@ func RenderMarkup(content text.Rich) string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
// string constant for formatting width and height in URL fragments
|
||||
const f_FragmentSize = "w=%d;h=%d"
|
||||
|
||||
func composeImageMarkup(imager text.Imager) string {
|
||||
u, err := url.Parse(imager.Image())
|
||||
if err != nil {
|
||||
// If the URL is invalid, then just write a normal text.
|
||||
return html.EscapeString(imager.ImageText())
|
||||
}
|
||||
|
||||
// Override the URL fragment with our own.
|
||||
if w, h := imager.ImageSize(); w > 0 && h > 0 {
|
||||
u.Fragment = fmt.Sprintf(f_FragmentSize, w, h)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
`<a href="%s">%s</a>`,
|
||||
html.EscapeString(u.String()), html.EscapeString(imager.ImageText()),
|
||||
)
|
||||
}
|
||||
|
||||
// FragmentImageSize tries to parse the width and height encoded in the URL
|
||||
// fragment, which is inserted by the markup renderer. A pair of zero values are
|
||||
// returned if there is none. The returned width and height will be the minimum
|
||||
// of the given maxes and the encoded sizes.
|
||||
func FragmentImageSize(URL string, maxw, maxh int) (w, h int) {
|
||||
u, err := url.Parse(URL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore the error, as we can check for the integers.
|
||||
fmt.Sscanf(u.Fragment, f_FragmentSize, &w, &h)
|
||||
|
||||
if w > 0 && h > 0 {
|
||||
return imgutil.MaxSize(w, h, maxw, maxh)
|
||||
}
|
||||
|
||||
return maxw, maxh
|
||||
}
|
||||
|
||||
func span(key, value string) string {
|
||||
return "<span key=\"" + value + "\">"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue