From 76d4b8d69aa498eb51ac895535148bf49152bb07 Mon Sep 17 00:00:00 2001 From: diamondburned Date: Wed, 8 Jul 2020 02:07:00 -0700 Subject: [PATCH] Several minor changes and bug fixes This commit added a confirmation dialog when clicking on links. It also fixes some bugs. I forgot the rest. --- go.mod | 8 ++- go.sum | 14 ++++ internal/c/README.md | 6 ++ internal/c/gtkp/gtkp.go | 23 +++++++ internal/c/labelutils/labelutils.go | 101 ++++++++++++++++++++++++++++ internal/ui/dialog/dialog.go | 23 +++---- internal/ui/imgview/imgview.go | 79 ++++++++++++++++++++-- internal/ui/rich/parser/markup.go | 21 +++--- 8 files changed, 247 insertions(+), 28 deletions(-) create mode 100644 internal/c/README.md create mode 100644 internal/c/gtkp/gtkp.go create mode 100644 internal/c/labelutils/labelutils.go diff --git a/go.mod b/go.mod index 6920eb1..02bf564 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,15 @@ go 1.14 replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d +replace github.com/diamondburned/cchat-discord => ../cchat-discord/ + require ( github.com/Xuanwo/go-locale v0.2.0 github.com/alecthomas/chroma v0.7.3 - github.com/diamondburned/cchat v0.0.40 - github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03 + github.com/diamondburned/cchat v0.0.42 + github.com/diamondburned/cchat-discord v0.0.0-20200708083530-d0e43cc63b03 github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 - github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516 + github.com/diamondburned/imgutil v0.0.0-20200708012333-53c9e45dd28b github.com/goodsign/monday v1.0.0 github.com/gotk3/gotk3 v0.4.1-0.20200524052254-cb2aa31c6194 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 diff --git a/go.sum b/go.sum index 5ef7aad..ab440d6 100644 --- a/go.sum +++ b/go.sum @@ -46,22 +46,36 @@ github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb h1:Ja/niwykeFoSk github.com/diamondburned/aqs v0.0.0-20200704043812-99b676ee44eb/go.mod h1:q1MbMBfZrv7xqV8n7LgMwhHs3oBbNwWJes8exs2AmDs= github.com/diamondburned/arikawa v0.9.5 h1:P1ffsp+NHT22wWKYFVC8CdlGRLzPuUV9FcCBKOCJpCI= github.com/diamondburned/arikawa v0.9.5/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= +github.com/diamondburned/arikawa v0.9.6 h1:6TpfTKa2btoVQGxojNqv8g2YC0tIc/tX5w/OCVZPF5Q= +github.com/diamondburned/arikawa v0.9.6/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660= github.com/diamondburned/cchat v0.0.40 h1:38gPyJnnDoNDHrXcV8Qchfv3y6jlS3Fzz/6FY0BPH6I= github.com/diamondburned/cchat v0.0.40/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= +github.com/diamondburned/cchat v0.0.41 h1:6y32s2wWTiDw4hWN/Gna6ay3uUrRAW5V8Cj0/xLKovw= +github.com/diamondburned/cchat v0.0.41/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= +github.com/diamondburned/cchat v0.0.42 h1:FVMLy9hOTxKju8OWDBIStrekbgTHCaH8+GVnV4LOByg= +github.com/diamondburned/cchat v0.0.42/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU= github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03 h1:F5TL7GPRU/D4ldVkS0haY3SiHPtf1Kby/4nbYpm//MQ= github.com/diamondburned/cchat-discord v0.0.0-20200703190659-fbf95b9b6c03/go.mod h1:p0X6QUH0mxK8yEW0+a4QA77ClAmoxz8CvgbnobMtWQA= +github.com/diamondburned/cchat-discord v0.0.0-20200708083530-d0e43cc63b03 h1:Xx4ioFTurT6qTxzTL8QlsH3E5VskLxHPJ8RwmaKhObA= +github.com/diamondburned/cchat-discord v0.0.0-20200708083530-d0e43cc63b03/go.mod h1:hjaCeQfhSqANH+ZtxXPMWzpgobb4syy2VcqDK4PXcPU= github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3 h1:xr07/2cwINyrMqh92pQQJVDfQqG0u6gHAK+ZcGfpSew= github.com/diamondburned/cchat-mock v0.0.0-20200704044009-f587c4904aa3/go.mod h1:SRu3OOeggELFr2Wd3/+SpYV1eNcvSk2LBhM70NOZSG8= github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d h1:Ha/I6PMKi+B4hpWclwlXj0tUMehR7Q0TNxPczzBwzPI= github.com/diamondburned/gotk3 v0.0.0-20200630065217-97aeb06d705d/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516 h1:6j4oZahbNdVhSEInRfeYbgDpx1FXDfJy6CcUVyWOuVY= github.com/diamondburned/imgutil v0.0.0-20200704034004-40dbfc732516/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ= +github.com/diamondburned/imgutil v0.0.0-20200708012333-53c9e45dd28b h1:iYKHGvWzNFBIRTSY8Pd5g301YDGWMfs3fh1VS0iBSj0= +github.com/diamondburned/imgutil v0.0.0-20200708012333-53c9e45dd28b/go.mod h1:kBQKaukR/LyCfhED99/T4/XxUMDNEEzf1Fx6vreD3RQ= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I= github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A= +github.com/diamondburned/ningen v0.1.1-0.20200708090333-227e90d19851 h1:xf1aLPnwK/Yn2z7dBIgQROSVOEc2wtivgnnwBItdEVM= +github.com/diamondburned/ningen v0.1.1-0.20200708090333-227e90d19851/go.mod h1:FNezDLQIhoDS+RkXLSQ7dJNrt6BW/nVl1krzDgWMQwg= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= diff --git a/internal/c/README.md b/internal/c/README.md new file mode 100644 index 0000000..b332c35 --- /dev/null +++ b/internal/c/README.md @@ -0,0 +1,6 @@ +# c + +**HERE BE DRAGONS!!** + +This package and all its subpackages contain either C code or Cgo code. This +bugs me and I hate it. PROCEED WITH CAUTION! diff --git a/internal/c/gtkp/gtkp.go b/internal/c/gtkp/gtkp.go new file mode 100644 index 0000000..e5a1942 --- /dev/null +++ b/internal/c/gtkp/gtkp.go @@ -0,0 +1,23 @@ +package gtkp + +// #cgo pkg-config: gdk-3.0 gio-2.0 glib-2.0 gobject-2.0 gtk+-3.0 +// #include +// #include +// #include +import "C" + +import ( + "unsafe" + + "github.com/gotk3/gotk3/gtk" +) + +func LabelNoHyphens(l *gtk.Label) { + attrlist := C.pango_attr_list_new() + defer C.pango_attr_list_unref(attrlist) + + C.pango_attr_list_insert(attrlist, C.pango_attr_insert_hyphens_new(C.FALSE)) + + v := (*C.GtkLabel)(unsafe.Pointer(l.Native())) + C.gtk_label_set_attributes(v, attrlist) +} diff --git a/internal/c/labelutils/labelutils.go b/internal/c/labelutils/labelutils.go new file mode 100644 index 0000000..f87da96 --- /dev/null +++ b/internal/c/labelutils/labelutils.go @@ -0,0 +1,101 @@ +package labelutils + +// #cgo pkg-config: gdk-3.0 gio-2.0 glib-2.0 gobject-2.0 gtk+-3.0 +// #include +// #include +// #include +import "C" + +import ( + "unsafe" + + "github.com/gotk3/gotk3/gtk" + "github.com/gotk3/gotk3/pango" +) + +func gbool(b bool) C.gboolean { + if b { + return C.gboolean(C.TRUE) + } else { + return C.gboolean(C.FALSE) + } +} + +type Attribute = func() *C.PangoAttribute + +func InsertHyphens(hyphens bool) Attribute { + return func() *C.PangoAttribute { + return C.pango_attr_insert_hyphens_new(gbool(hyphens)) + } +} + +func Scale(factor float64) Attribute { + return func() *C.PangoAttribute { + return C.pango_attr_scale_new(C.double(factor)) + } +} + +func Underline(underline pango.Underline) Attribute { + return func() *C.PangoAttribute { + return C.pango_attr_underline_new(C.PangoUnderline(underline)) + } +} + +func Strikethrough(strikethrough bool) Attribute { + return func() *C.PangoAttribute { + return C.pango_attr_strikethrough_new(gbool(strikethrough)) + } +} + +const u16divu8 = 65535 / 255 + +func rgb(hex uint32) (r, g, b uint16) { + r = uint16(hex>>16&255) * u16divu8 + g = uint16(hex>>8&255) * u16divu8 + b = uint16(hex&255) * u16divu8 + return +} + +func Background(hex uint32) Attribute { + r, g, b := rgb(hex) + + return func() *C.PangoAttribute { + return C.pango_attr_background_new(C.guint16(r), C.guint16(g), C.guint16(b)) + } +} + +func Foreground(hex uint32) Attribute { + r, g, b := rgb(hex) + + return func() *C.PangoAttribute { + return C.pango_attr_foreground_new(C.guint16(r), C.guint16(g), C.guint16(b)) + } +} + +func Style(style pango.Style) Attribute { + return func() *C.PangoAttribute { + return C.pango_attr_style_new(C.PangoStyle(style)) + } +} + +func Family(family string) Attribute { + return func() *C.PangoAttribute { + str := C.CString(family) + defer C.free(unsafe.Pointer(str)) + return C.pango_attr_family_new(str) + } +} + +func AddAttr(l *gtk.Label, attrs ...Attribute) { + attrlist := C.pango_attr_list_new() + defer C.pango_attr_list_unref(attrlist) + + for _, attr := range attrs { + // attr() should not unref; insert is transfer-full + // https://discourse.gnome.org/t/pango-how-to-turn-off-hyphenation-for-char-wrapping/2101/2 + C.pango_attr_list_insert(attrlist, attr()) + } + + v := (*C.GtkLabel)(unsafe.Pointer(l.Native())) + C.gtk_label_set_attributes(v, attrlist) +} diff --git a/internal/ui/dialog/dialog.go b/internal/ui/dialog/dialog.go index 8d21ec9..c773631 100644 --- a/internal/ui/dialog/dialog.go +++ b/internal/ui/dialog/dialog.go @@ -6,7 +6,11 @@ import ( "github.com/gotk3/gotk3/gtk" ) -func NewModal(body gtk.IWidget, title, label string, callback func()) *gtk.Dialog { +func ShowModal(body gtk.IWidget, title, button string, callback func()) { + NewModal(body, title, title, callback).Show() +} + +func NewModal(body gtk.IWidget, title, button string, callback func()) *gtk.Dialog { cancel, _ := gtk.ButtonNew() cancel.Show() cancel.SetHAlign(gtk.ALIGN_START) @@ -17,10 +21,12 @@ func NewModal(body gtk.IWidget, title, label string, callback func()) *gtk.Dialo action.Show() action.SetHAlign(gtk.ALIGN_END) action.SetRelief(gtk.RELIEF_NONE) - action.SetLabel(label) + action.SetLabel(button) header, _ := gtk.HeaderBarNew() header.Show() + header.SetMarginStart(5) + header.SetMarginEnd(5) header.SetTitle(title) header.PackStart(cancel) header.PackEnd(action) @@ -44,20 +50,13 @@ func NewCSD(body, header gtk.IWidget) *gtk.Dialog { } func newCSD(body, header gtk.IWidget) *gtk.Dialog { - dialog, _ := gtk.DialogNew() - dialog.SetModal(true) - dialog.SetTransientFor(gts.App.Window) - - if area, _ := dialog.GetContentArea(); area != nil { - dialog.Remove(area) - } + dialog, _ := gts.NewEmptyModalDialog() + dialog.SetDefaultSize(450, 300) + dialog.Add(body) if oldh, _ := dialog.GetHeaderBar(); oldh != nil { dialog.Remove(oldh) } - - dialog.Add(body) - dialog.SetTitlebar(header) return dialog diff --git a/internal/ui/imgview/imgview.go b/internal/ui/imgview/imgview.go index 3ca37fd..69ebf6e 100644 --- a/internal/ui/imgview/imgview.go +++ b/internal/ui/imgview/imgview.go @@ -1,15 +1,23 @@ package imgview import ( + "fmt" + "html" "net/url" "path" "strings" + "github.com/diamondburned/cchat-gtk/internal/c/labelutils" "github.com/diamondburned/cchat-gtk/internal/gts/httputil" + "github.com/diamondburned/cchat-gtk/internal/log" + "github.com/diamondburned/cchat-gtk/internal/ui/dialog" "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" + "github.com/gotk3/gotk3/pango" + "github.com/pkg/errors" + "github.com/skratchdot/open-golang/open" ) const ( @@ -44,7 +52,8 @@ func BindTooltip(connector WidgetConnector) { r.SetX(int(x)) r.SetY(int(y)) - // Make a new image that's asynchronously fetched. + // Make a new image that's asynchronously fetched inside a button. + // This allows us to make it clickable. img, _ := gtk.ImageNewFromIconName("image-loading", gtk.ICON_SIZE_BUTTON) img.SetMarginStart(5) img.SetMarginEnd(5) @@ -56,20 +65,80 @@ func BindTooltip(connector WidgetConnector) { var w, h = parser.FragmentImageSize(uri, MaxWidth, MaxHeight) httputil.AsyncImageSized(img, uri, w, h) + btn, _ := gtk.ButtonNew() + btn.Add(img) + btn.SetRelief(gtk.RELIEF_NONE) + btn.Connect("clicked", func() { PromptOpen(uri) }) + btn.Show() + p, _ := gtk.PopoverNew(c) p.SetPointingTo(r) p.Connect("closed", img.Destroy) // on close, destroy image - p.Add(img) + p.Add(btn) p.Popup() - return true - default: - return false + PromptOpen(uri) } + + // Never let Gtk open the dialog. + return true }) } +const urlPrompt = `This link leads to the following URL: +%[1]s +Click Open to proceed.` + +var warnLabelCSS = primitives.PrepareCSS(` + label { + padding: 4px 8px; + } +`) + +// PromptOpen shows a dialog asking if the URL should be opened. +func PromptOpen(uri string) { + // Format the prompt body. + l, _ := gtk.LabelNew("") + l.SetJustify(gtk.JUSTIFY_CENTER) + l.SetLineWrap(true) + l.SetLineWrapMode(pango.WRAP_WORD_CHAR) + l.Show() + l.SetMarkup(fmt.Sprintf(urlPrompt, html.EscapeString(uri))) + + // Style the label. + primitives.AttachCSS(l, warnLabelCSS) + + // Disable hyphens on line wraps. + labelutils.AddAttr(l, labelutils.InsertHyphens(false)) + + open := func() { + if err := open.Start(uri); err != nil { + log.Error(errors.Wrap(err, "Failed to open URL after confirm")) + } + } + + // Prompt the user if they want to open the URL. + dlg := dialog.NewModal(l, "Caution", "Open", open) + dlg.SetSizeRequest(350, 100) + + // Add a class to the dialog to allow theming. + primitives.AddClass(dlg, "url-warning") + + // On link click, close the dialog. + l.Connect("activate-link", func(l *gtk.Label, uri string) bool { + // Close the dialog. + dlg.Destroy() + // Open the link anyway. + open() + // Return true since we handled the event. + return true + }) + + // Show the dialog. + dlg.Show() +} + // ext parses and sanitizes the extension to something comparable. func ext(uri string) string { u, err := url.Parse(uri) diff --git a/internal/ui/rich/parser/markup.go b/internal/ui/rich/parser/markup.go index 31774ce..27d0bb2 100644 --- a/internal/ui/rich/parser/markup.go +++ b/internal/ui/rich/parser/markup.go @@ -64,27 +64,32 @@ func RenderMarkup(content text.Rich) string { for _, segment := range content.Segments { start, end := segment.Bounds() - switch segment := segment.(type) { - case text.Linker: + if segment, ok := segment.(text.Linker); ok { appended.Addf(start, ``, html.EscapeString(segment.Link())) appended.Add(end, "") + } - case text.Imager: + if segment, ok := segment.(text.Imager); ok { // Ends don't matter with images. appended.Add(start, composeImageMarkup(segment)) + } - case text.Colorer: + if segment, ok := segment.(text.Colorer); ok { appended.Span(start, end, fmt.Sprintf(`color="#%06X"`, segment.Color())) + } - case text.Attributor: + if segment, ok := segment.(text.Attributor); ok { appended.Span(start, end, markupAttr(segment.Attribute())) + } - case text.Codeblocker: + if segment, ok := segment.(text.Codeblocker); ok { // Syntax highlight the codeblock. hl.Segments(&appended, content.Content, segment) + } - case text.Quoteblocker: - // TODO: pls. + // TODO: make this not shit. Maybe make it somehow not rely on green + // arrows. Or maybe. + if _, ok := segment.(text.Quoteblocker); ok { appended.Span(start, end, `color="#789922"`) } }