Reverted TextView; added image popover on click

This commit is contained in:
diamondburned (Forefront) 2020-06-27 18:35:26 -07:00
parent e2d656dcae
commit 32e6ed8b21
8 changed files with 158 additions and 46 deletions

2
go.mod
View File

@ -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

View File

@ -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

View 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))
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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())
}

View File

@ -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 + "\">"
}