upgraded libhandy; new cchat auth API dialog

This commit is contained in:
diamondburned 2020-10-27 23:04:00 -07:00
parent 18c9e9535c
commit cf4f8ce245
10 changed files with 506 additions and 278 deletions

6
go.mod
View File

@ -7,9 +7,9 @@ replace github.com/gotk3/gotk3 => github.com/diamondburned/gotk3 v0.0.0-20200816
require (
github.com/Xuanwo/go-locale v1.0.0
github.com/alecthomas/chroma v0.7.3
github.com/diamondburned/cchat v0.3.7
github.com/diamondburned/cchat-discord v0.0.0-20201023215116-2209348d23bd
github.com/diamondburned/cchat-mock v0.0.0-20201023061026-155813b08c2c
github.com/diamondburned/cchat v0.3.11
github.com/diamondburned/cchat-discord v0.0.0-20201027213927-37165658e17c
github.com/diamondburned/cchat-mock v0.0.0-20201027204251-4f6dfbfc2424
github.com/diamondburned/gspell v0.0.0-20200830182722-77e5d27d6894
github.com/diamondburned/handy v0.0.0-20200829011954-4667e7a918f4
github.com/diamondburned/imgutil v0.0.0-20200710174014-8a3be144a972

12
go.sum
View File

@ -89,6 +89,10 @@ github.com/diamondburned/cchat v0.3.3/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A
github.com/diamondburned/cchat v0.3.5/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.7 h1:0t3FkbzC/pBRAR3w0uYznJ+7dYqcR1M48a9wgz4JkIg=
github.com/diamondburned/cchat v0.3.7/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.8 h1:vgFe8giVfwsAO+WpTYsTDIXvRUN48osVPNu0pZNvPEk=
github.com/diamondburned/cchat v0.3.8/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg=
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401 h1:llmx/8UiJoTcHUw+GE5/rESVVmmnLh1HEPx3wRj+oQY=
github.com/diamondburned/cchat-discord v0.0.0-20200719175346-af912db55401/go.mod h1:+hSrIVYj5tIPLAorDsHj2Tbt2fWlZtOanzfEUHX53HM=
github.com/diamondburned/cchat-discord v0.0.0-20200730000036-2c93cdc1974e h1:EA5Vg0x57qLURJP80XhABBW+X0sbQSh2gw5qvPbZTs4=
@ -119,6 +123,10 @@ github.com/diamondburned/cchat-discord v0.0.0-20201015062850-090259a6b4ca h1:36M
github.com/diamondburned/cchat-discord v0.0.0-20201015062850-090259a6b4ca/go.mod h1:S0PDR6aj2qE871JSy94YvwtprQJCWwkIJWzRu7S1Asc=
github.com/diamondburned/cchat-discord v0.0.0-20201023215116-2209348d23bd h1:OspKIwR8s5tYf6OLh2LR6mMS4Xv0eOuPGGTzze9h7dA=
github.com/diamondburned/cchat-discord v0.0.0-20201023215116-2209348d23bd/go.mod h1:7M/aCFl4EKe/rQEgyXiAWeAydaJoqqmyiSv086TOwE4=
github.com/diamondburned/cchat-discord v0.0.0-20201027050455-7ce513cf68e6 h1:uJSI/6U6SDi7FNL1cx1QQ9qADJQ19pG519HJ31Cfv5Y=
github.com/diamondburned/cchat-discord v0.0.0-20201027050455-7ce513cf68e6/go.mod h1:NdURsIvOA+Sxk0QHzaRTX5dBmoRLr/K5u3vSfzrI3n4=
github.com/diamondburned/cchat-discord v0.0.0-20201027213927-37165658e17c h1:x55N39x3lrGDrovVLR0sVyzNQzrUJsXWQfFZFNdYGi4=
github.com/diamondburned/cchat-discord v0.0.0-20201027213927-37165658e17c/go.mod h1:utku2TVk4IrnyuwNehgaIaAvtB6l4AO5RbzF+Uii8ko=
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b h1:sq0MXjJc3yAOZvuolRxOpKQNvpMLyTmsECxQqdYgF5E=
github.com/diamondburned/cchat-mock v0.0.0-20200709231652-ad222ce5a74b/go.mod h1:+bAf0m2o5qH54DmYJ/lR1HeITV53ol0JaoKyFFx3m3E=
github.com/diamondburned/cchat-mock v0.0.0-20201004204741-b841407af381 h1:8JWNJMgoa3fL2py3gXSeC3NiAC+39EZp+JmvaoDBTUU=
@ -131,6 +139,10 @@ github.com/diamondburned/cchat-mock v0.0.0-20201014202453-b9838fab0ab0 h1:Gwceon
github.com/diamondburned/cchat-mock v0.0.0-20201014202453-b9838fab0ab0/go.mod h1:hYNki0Ic/d7zFVXTJIjp/td1W4OpxDNcVY8layxgTyc=
github.com/diamondburned/cchat-mock v0.0.0-20201023061026-155813b08c2c h1:9dACu5WbTPHLzGEY9sDrXCF5DW6AV2s7R85OoIXiTbo=
github.com/diamondburned/cchat-mock v0.0.0-20201023061026-155813b08c2c/go.mod h1:hYNki0Ic/d7zFVXTJIjp/td1W4OpxDNcVY8layxgTyc=
github.com/diamondburned/cchat-mock v0.0.0-20201027045549-6d6056f11a5f h1:g3C5VHwrO6e3+5Z/+s+hN9yprTnI7PXS4jTVnaEH/pQ=
github.com/diamondburned/cchat-mock v0.0.0-20201027045549-6d6056f11a5f/go.mod h1:HwztxR71SXp2yqYPreLgWEBBVZrA/eMXMp5v7+9P5PY=
github.com/diamondburned/cchat-mock v0.0.0-20201027204251-4f6dfbfc2424 h1:VGsH+Xq34EB1lzcHrDHgiE0/6DoeKju2N3JhfCWSqB8=
github.com/diamondburned/cchat-mock v0.0.0-20201027204251-4f6dfbfc2424/go.mod h1:ylbEOpFJ6uPfjkw/+xpNvFfUDqkQobCJOAQwEt2AiDg=
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/gotk3 v0.0.0-20200816224505-3cd69b83a48a h1:wEldljb421/Jp84RNb0zBfqmiWt/TTQzUE6R1ap6UuQ=

View File

@ -1,16 +1,16 @@
package preferences
import (
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/log"
"github.com/diamondburned/cchat-gtk/internal/ui/config"
"github.com/diamondburned/cchat-gtk/internal/ui/dialog"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
type Dialog struct {
*gtk.Dialog
*dialog.Dialog
switcher *gtk.StackSwitcher
stack *gtk.Stack
@ -18,6 +18,10 @@ type Dialog struct {
func NewDialog() *Dialog {
stack, _ := gtk.StackNew()
stack.SetMarginTop(8)
stack.SetMarginBottom(8)
stack.SetMarginStart(16)
stack.SetMarginEnd(16)
stack.Show()
switcher, _ := gtk.StackSwitcherNew()
@ -29,18 +33,9 @@ func NewDialog() *Dialog {
h.SetCustomTitle(switcher)
h.Show()
d, _ := gts.NewModalDialog()
d := dialog.NewCSD(stack, h)
d.SetDefaultSize(400, 300)
d.SetTitle("Preferences")
d.SetTitlebar(h)
b, _ := d.GetContentArea()
b.SetMarginTop(8)
b.SetMarginBottom(8)
b.SetMarginStart(16)
b.SetMarginEnd(16)
b.PackStart(stack, true, true, 0)
b.Show()
return &Dialog{
Dialog: d,

View File

@ -3,12 +3,13 @@ package dialog
import (
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/gotk3/gotk3/glib"
"github.com/gotk3/gotk3/gtk"
)
type Dialog = gtk.Dialog
type Modal struct {
*gtk.Dialog
*Dialog
Cancel *gtk.Button
Action *gtk.Button
Header *gtk.HeaderBar
@ -26,20 +27,20 @@ func ShowModal(body gtk.IWidget, title, button string, clicked func(m *Modal)) {
func NewModal(body gtk.IWidget, title, button string, clicked func(m *Modal)) *Modal {
cancel, _ := gtk.ButtonNewWithMnemonic("_Cancel")
cancel.Show()
cancel.SetHAlign(gtk.ALIGN_START)
cancel.SetRelief(gtk.RELIEF_NONE)
cancel.Show()
action, _ := gtk.ButtonNewWithMnemonic(button)
action.Show()
action.SetHAlign(gtk.ALIGN_END)
action.SetRelief(gtk.RELIEF_NONE)
action.Show()
header, _ := gtk.HeaderBarNew()
header.Show()
header.SetTitle(title)
header.PackStart(cancel)
header.PackEnd(action)
header.Show()
primitives.AddClass(header, "modal-header")
primitives.AttachCSS(header, headerCSS)
@ -60,7 +61,7 @@ func NewModal(body gtk.IWidget, title, button string, clicked func(m *Modal)) *M
func NewCSD(body, header gtk.IWidget) *gtk.Dialog {
dialog := newCSD(body, header)
dialog.Connect("response", func(_ *glib.Object, resp gtk.ResponseType) {
dialog.Connect("response", func(_ *gtk.Dialog, resp gtk.ResponseType) {
if resp == gtk.RESPONSE_DELETE_EVENT {
dialog.Destroy()
}
@ -69,7 +70,10 @@ func NewCSD(body, header gtk.IWidget) *gtk.Dialog {
}
func newCSD(body, header gtk.IWidget) *gtk.Dialog {
dialog, _ := gts.NewEmptyModalDialog()
dialog, err := gts.NewEmptyModalDialog()
if err != nil {
panic(err)
}
dialog.SetDefaultSize(450, 300)
dialog.Add(body)

View File

@ -11,7 +11,6 @@ import (
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/completion"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/scrollinput"
"github.com/diamondburned/gspell"
"github.com/gotk3/gotk3/gtk"
"github.com/pkg/errors"
)
@ -93,6 +92,9 @@ func (v *InputView) SetMessenger(session cchat.Session, messenger cchat.Messenge
}
}
// wrapSpellCheck is a no-op but is replaced by gspell in ./spellcheck.go.
var wrapSpellCheck = func(textView *gtk.TextView) {}
type Field struct {
// Box contains the field box and the attachment container.
*gtk.Box
@ -104,9 +106,8 @@ type Field struct {
Username *username.Container
TextScroll *gtk.ScrolledWindow
text *gtk.TextView // const
speller *gspell.TextView // const
buffer *gtk.TextBuffer // const
text *gtk.TextView // const
buffer *gtk.TextBuffer // const
send *gtk.Button
attach *gtk.Button
@ -150,8 +151,6 @@ var scrolledInputCSS = primitives.PrepareClassCSS("scrolled-input", `
func NewField(text *gtk.TextView, ctrl Controller) *Field {
field := &Field{text: text, ctrl: ctrl}
field.buffer, _ = text.GetBuffer()
field.speller = gspell.GetFromGtkTextView(text)
field.speller.BasicSetup()
field.Username = username.NewContainer()
field.Username.Show()

View File

@ -0,0 +1,15 @@
// +build !nogspell
package input
import (
"github.com/diamondburned/gspell"
"github.com/gotk3/gotk3/gtk"
)
func init() {
wrapSpellCheck = func(textView *gtk.TextView) {
speller := gspell.GetFromGtkTextView(textView)
speller.BasicSetup()
}
}

View File

@ -1,267 +1,163 @@
package auth
import (
"html"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/gts"
"github.com/diamondburned/cchat-gtk/internal/ui/dialog"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/spinner"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk"
)
type Dialog struct {
*dialog.Modal
*dialog.Dialog
header *gtk.HeaderBar
backRev *gtk.Revealer
back *gtk.Button // might be hidden
stack *gtk.Stack
spinner *spinner.Boxed
leaflet *handy.Leaflet
stageList *StageList
request *RequestStack // might be nil
Auther cchat.Authenticator
onAuth func(cchat.Session)
stack *gtk.Stack // dialog stack
scroll *gtk.ScrolledWindow
body *gtk.Box
label *gtk.Label
// filled on spin()
request *Request
}
// NewDialog makes a new authentication dialog. Auth() is called when the user
// is authenticated successfully inside the Gtk main thread.
func NewDialog(name text.Rich, auther cchat.Authenticator, auth func(cchat.Session)) *Dialog {
label, _ := gtk.LabelNew("")
label.SetMarginStart(10)
label.SetMarginEnd(10)
label.SetMarginTop(10)
label.SetMarginBottom(10)
label.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.Show()
box.Add(label)
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.Show()
sw.Add(box)
spinner, _ := gtk.SpinnerNew()
spinner.Show()
spinner.Start()
spinner.SetSizeRequest(50, 50)
stack, _ := gtk.StackNew()
stack.Show()
stack.SetVExpand(true)
stack.SetHExpand(true)
stack.AddNamed(sw, "main")
stack.AddNamed(spinner, "spinner")
func NewDialog(name text.Rich, authers []cchat.Authenticator, auth func(cchat.Session)) *Dialog {
d := &Dialog{
Auther: auther,
Auther: nil,
onAuth: auth,
stack: stack,
scroll: sw,
body: box,
label: label,
}
d.Modal = dialog.NewModal(stack, "Log in to "+name.Content, "Log in", d.ok)
d.Modal.SetDefaultSize(400, 300)
d.spin(nil)
d.Show()
d.spinner = spinner.NewVisible()
d.spinner.SetSizeRequest(50, 50)
d.spinner.Stop()
d.spinner.Show()
d.request = NewRequestStack()
d.request.SetHExpand(true)
d.request.SetName("request")
d.request.Show()
d.leaflet = handy.LeafletNew()
d.leaflet.SetCanSwipeBack(true)
d.leaflet.SetCanSwipeForward(false)
d.leaflet.SetVExpand(true)
d.leaflet.SetTransitionType(handy.LeafletTransitionTypeSlide)
d.leaflet.Show()
d.stack, _ = gtk.StackNew()
d.stack.SetVExpand(true)
d.stack.SetHExpand(true)
d.stack.AddNamed(d.leaflet, "leaflet")
d.stack.AddNamed(d.spinner, "spinner")
d.stack.SetVisibleChildName("leaflet")
d.stack.Show()
d.back, _ = gtk.ButtonNewFromIconName("go-previous-symbolic", gtk.ICON_SIZE_BUTTON)
d.back.Show()
d.back.Connect("clicked", func() {
// If check just in case.
if d.stageList != nil {
d.leaflet.SetVisibleChild(d.stageList)
d.backRev.SetRevealChild(false)
}
})
d.backRev, _ = gtk.RevealerNew()
d.backRev.SetTransitionDuration(50)
d.backRev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_RIGHT)
d.backRev.Add(d.back)
d.backRev.Show()
d.header, _ = gtk.HeaderBarNew()
d.header.SetShowCloseButton(true)
d.header.SetTitle("Log in to " + name.Content)
d.header.PackStart(d.backRev)
d.header.Show()
d.setAuthers(authers)
primitives.LeafletOnFold(d.leaflet, func(folded bool) {
visibleChildName := primitives.GetName(d.leaflet.GetVisibleChild().ToWidget())
if folded && visibleChildName == "request" {
d.backRev.SetRevealChild(true)
} else {
d.backRev.SetRevealChild(false)
}
})
d.Dialog = dialog.NewCSD(d.stack, d.header)
d.Dialog.SetDefaultSize(500, 350)
d.Dialog.Show()
return d
}
func (d *Dialog) runOnAuth(ses cchat.Session) {
// finalize
d.Destroy()
d.onAuth(ses)
func (d *Dialog) setAuthers(authers []cchat.Authenticator) {
primitives.RemoveChildren(d.leaflet)
d.request.SetRequest(nil, nil)
d.stageList = NewStageList(authers, d.setAuther)
d.stageList.SetName("stagelist")
d.stageList.Show()
d.leaflet.Add(d.stageList)
d.leaflet.Add(d.request)
d.stageList.SelectFirst()
d.leaflet.SetVisibleChild(d.stageList)
d.backRev.SetRevealChild(false)
}
func (d *Dialog) spin(err error) {
// Print the error.
if err != nil {
d.label.SetMarkup(`<span color="red">` + html.EscapeString(err.Error()) + `</span>`)
} else {
d.label.SetText("")
}
// Restore the old widget states.
d.stack.SetVisibleChildName("main")
d.Dialog.SetSensitive(true)
form := d.Auther.AuthenticateForm()
// See if we need to remove the current request page. We should keep
// everything the same if the key matches, as then forms aren't reset.
if d.request != nil {
if d.request.equalEntries(form) {
return
}
d.body.Remove(d.request)
}
d.request = NewRequest(form)
d.body.Add(d.request)
func (d *Dialog) setAuther(auther cchat.Authenticator) {
d.Auther = auther
d.request.SetRequest(auther, d.onContinue)
d.backRev.SetRevealChild(d.leaflet.GetFolded())
d.leaflet.SetVisibleChild(d.request)
}
func (d *Dialog) ok(m *dialog.Modal) {
// Disable the buttons.
func (d *Dialog) onContinue() {
request := d.request.Request()
values := request.values()
auther := d.Auther
d.Dialog.SetSensitive(false)
// Switch to the spinner screen.
d.back.Hide()
d.stack.SetVisibleChildName("spinner")
// Get the values of all fields.
var values = d.request.values()
d.spinner.Start()
gts.Async(func() (func(), error) {
s, err := d.Auther.Authenticate(values)
if err != nil {
return func() { d.spin(err) }, nil
}
s, err := auther.Authenticate(values)
return func() { d.runOnAuth(s) }, nil
return func() {
if err == nil {
d.Destroy()
d.onAuth(s)
return
}
d.spinner.Stop()
d.stack.SetVisibleChildName("leaflet")
d.back.Show()
d.Dialog.SetSensitive(true)
if nextStage := err.NextStage(); nextStage != nil {
d.setAuthers(nextStage)
} else {
request.SetError(err)
}
}, err
})
}
type Request struct {
*gtk.Grid
entries []cchat.AuthenticateEntry
labels []*gtk.Label
texts []Texter
}
func NewRequest(authEntries []cchat.AuthenticateEntry) *Request {
grid, _ := gtk.GridNew()
grid.Show()
grid.SetRowSpacing(7)
grid.SetColumnHomogeneous(true)
grid.SetColumnSpacing(5)
req := &Request{
Grid: grid,
entries: authEntries,
labels: make([]*gtk.Label, len(authEntries)),
texts: make([]Texter, len(authEntries)),
}
for i, authEntry := range req.entries {
label, texter := newEntry(authEntry)
req.labels[i] = label
req.texts[i] = texter
grid.Attach(label, 0, i, 1, 1)
grid.Attach(texter, 1, i, 3, 1) // triple the width
}
return req
}
// equalEntries compares the current request with a list of entries. It returns
// false if there are inequalities.
func (r *Request) equalEntries(entries []cchat.AuthenticateEntry) bool {
if len(r.entries) != len(entries) {
return false
}
for i, entry := range r.entries {
if entry != entries[i] {
return false
}
}
return true
}
func (r *Request) values() []string {
var values = make([]string, len(r.entries))
for i, texter := range r.texts {
values[i] = texter.GetText()
}
return values
}
func newEntry(authEntry cchat.AuthenticateEntry) (*gtk.Label, Texter) {
label, _ := gtk.LabelNew(authEntry.Name)
label.Show()
label.SetXAlign(1) // right align
label.SetJustify(gtk.JUSTIFY_RIGHT)
label.SetLineWrap(true)
var texter Texter
if authEntry.Multiline {
texter = NewMultilineInput()
} else {
var input = NewEntryInput()
if authEntry.Secret {
input.SetInputPurpose(gtk.INPUT_PURPOSE_PASSWORD)
input.SetVisibility(false)
input.SetInvisibleChar('●')
} else {
// usually; this is just an assumption
input.SetInputPurpose(gtk.INPUT_PURPOSE_EMAIL)
}
texter = input
}
return label, texter
}
type Texter interface {
gtk.IWidget
GetText() string
SetText(string)
}
type EntryInput struct {
*gtk.Entry
}
var _ Texter = (*EntryInput)(nil)
func NewEntryInput() EntryInput {
input, _ := gtk.EntryNew()
input.SetVAlign(gtk.ALIGN_CENTER)
input.Show()
return EntryInput{
input,
}
}
func (i EntryInput) GetText() (text string) {
text, _ = i.Entry.GetText()
return
}
type MultilineInput struct {
*gtk.TextView
Buffer *gtk.TextBuffer
}
var _ Texter = (*MultilineInput)(nil)
func NewMultilineInput() MultilineInput {
view, _ := gtk.TextViewNew()
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
view.SetEditable(true)
view.Show()
buf, _ := view.GetBuffer()
return MultilineInput{view, buf}
}
func (i MultilineInput) GetText() (text string) {
start, end := i.Buffer.GetBounds()
text, _ = i.Buffer.GetText(start, end, true)
return
}
func (i MultilineInput) SetText(text string) {
i.Buffer.SetText(text)
}

View File

@ -0,0 +1,271 @@
package auth
import (
"html"
"strings"
"unicode"
"unicode/utf8"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-gtk/internal/ui/primitives/singlestack"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/pango"
)
type RequestStack struct {
*singlestack.Stack
request *Request
iconBox *gtk.Box
icon *gtk.Image
}
func NewRequestStack() *RequestStack {
icon, _ := gtk.ImageNewFromIconName("document-edit-symbolic", gtk.ICON_SIZE_DIALOG)
icon.SetHAlign(gtk.ALIGN_CENTER)
icon.SetVAlign(gtk.ALIGN_CENTER)
icon.SetHExpand(true)
icon.SetVExpand(true)
icon.SetOpacity(0.5)
icon.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.SetHExpand(true)
box.SetVExpand(true)
box.Add(icon)
box.Show()
stack := singlestack.NewStack()
stack.SetTransitionDuration(50)
stack.SetTransitionType(gtk.STACK_TRANSITION_TYPE_CROSSFADE)
stack.SetHExpand(true)
stack.Add(box)
return &RequestStack{
Stack: stack,
iconBox: box,
icon: icon,
}
}
// SetRequest sets the request into the stack. If auther is nil, then the
// placeholder icon is displayed. If auther is not nil, then Show() will be
// called.
func (rs *RequestStack) SetRequest(auther cchat.Authenticator, done func()) {
if auther == nil {
rs.request = nil
rs.Stack.Add(rs.iconBox)
} else {
rs.request = NewRequest(auther, done)
rs.request.Show()
rs.Stack.Add(rs.request)
}
}
func (rs *RequestStack) Request() *Request {
return rs.request
}
// Request is a single page of authenticator fields.
type Request struct {
*gtk.ScrolledWindow
Box *gtk.Box
Grid *gtk.Grid
ErrRev *gtk.Revealer
ErrLabel *gtk.Label
auther cchat.Authenticator
labels []*gtk.Label
texts []Texter
}
func NewRequest(auther cchat.Authenticator, done func()) *Request {
authEntries := auther.AuthenticateForm()
errLabel, _ := gtk.LabelNew("")
errLabel.SetUseMarkup(true)
errLabel.SetMarginTop(8)
errLabel.SetMarginStart(8)
errLabel.SetMarginEnd(8)
errLabel.SetLineWrap(true)
errLabel.SetLineWrapMode(pango.WRAP_WORD_CHAR)
errLabel.Show()
errRev, _ := gtk.RevealerNew()
errRev.SetTransitionDuration(50)
errRev.SetTransitionType(gtk.REVEALER_TRANSITION_TYPE_SLIDE_DOWN)
errRev.Add(errLabel)
errRev.SetRevealChild(false)
errRev.Show()
grid, _ := gtk.GridNew()
grid.SetRowSpacing(7)
grid.SetColumnHomogeneous(false)
grid.SetColumnSpacing(5)
grid.SetMarginStart(12)
grid.SetMarginEnd(12)
grid.SetMarginTop(8)
grid.Show()
continueBtn, _ := gtk.ButtonNewWithLabel("Continue")
continueBtn.SetHAlign(gtk.ALIGN_CENTER)
continueBtn.Connect("clicked", done)
continueBtn.SetBorderWidth(12)
continueBtn.Show()
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 0)
box.PackStart(errRev, false, false, 0)
box.PackStart(grid, true, true, 0)
box.PackStart(continueBtn, false, false, 0)
box.Show()
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
sw.SetHExpand(true)
sw.SetVExpand(true)
sw.Add(box)
req := &Request{
ScrolledWindow: sw,
Box: box,
Grid: grid,
ErrRev: errRev,
ErrLabel: errLabel,
auther: auther,
labels: make([]*gtk.Label, len(authEntries)),
texts: make([]Texter, len(authEntries)),
}
for i, authEntry := range authEntries {
label, texter := newEntry(authEntry)
req.labels[i] = label
req.texts[i] = texter
grid.Attach(label, 0, i, 1, 1)
grid.Attach(texter, 1, i, 1, 1)
}
return req
}
// SetError prints the error. If err is nil, then the label is cleared.
func (r *Request) SetError(err error) {
var markup string
if err != nil {
builder := strings.Builder{}
builder.WriteString(`<span color="red"><b>Error!</b>`)
builder.WriteByte('\n')
builder.WriteString(html.EscapeString(capitalizeFirst(err.Error())))
builder.WriteString(`</span>`)
markup = builder.String()
}
// Reveal if err is not nil.
r.ErrRev.SetRevealChild(err != nil)
r.ErrLabel.SetMarkup(markup)
}
// capitalizeFirst capitalizes the first letter.
func capitalizeFirst(str string) string {
r, l := utf8.DecodeRuneInString(str)
if l > 0 {
return string(unicode.ToUpper(r)) + str[l:]
}
return str
}
func (r *Request) values() []string {
var values = make([]string, len(r.texts))
for i, texter := range r.texts {
values[i] = texter.GetText()
}
return values
}
func newEntry(authEntry cchat.AuthenticateEntry) (*gtk.Label, Texter) {
label, _ := gtk.LabelNew(authEntry.Name)
label.SetXAlign(1) // right align
label.SetJustify(gtk.JUSTIFY_RIGHT)
label.SetLineWrap(true)
label.Show()
var texter Texter
if authEntry.Multiline {
texter = NewMultilineInput()
} else {
var input = NewEntryInput()
if authEntry.Secret {
input.SetInputPurpose(gtk.INPUT_PURPOSE_PASSWORD)
input.SetVisibility(false)
input.SetInvisibleChar('●')
} else {
// usually; this is just an assumption
input.SetInputPurpose(gtk.INPUT_PURPOSE_EMAIL)
}
texter = input
}
return label, texter
}
type Texter interface {
gtk.IWidget
GetText() string
SetText(string)
}
type EntryInput struct {
*gtk.Entry
}
var _ Texter = (*EntryInput)(nil)
func NewEntryInput() EntryInput {
input, _ := gtk.EntryNew()
input.SetVAlign(gtk.ALIGN_CENTER)
input.SetHExpand(true)
input.Show()
return EntryInput{
input,
}
}
func (i EntryInput) GetText() (text string) {
text, _ = i.Entry.GetText()
return
}
type MultilineInput struct {
*gtk.TextView
Buffer *gtk.TextBuffer
}
var _ Texter = (*MultilineInput)(nil)
func NewMultilineInput() MultilineInput {
view, _ := gtk.TextViewNew()
view.SetWrapMode(gtk.WRAP_WORD_CHAR)
view.SetEditable(true)
view.SetHExpand(true)
view.Show()
buf, _ := view.GetBuffer()
return MultilineInput{view, buf}
}
func (i MultilineInput) GetText() (text string) {
start, end := i.Buffer.GetBounds()
text, _ = i.Buffer.GetText(start, end, true)
return
}
func (i MultilineInput) SetText(text string) {
i.Buffer.SetText(text)
}

View File

@ -0,0 +1,50 @@
package auth
import (
"github.com/diamondburned/cchat"
"github.com/diamondburned/handy"
"github.com/gotk3/gotk3/gtk"
)
type StageList struct {
*gtk.ScrolledWindow
ListBox *gtk.ListBox
}
func NewStageList(authers []cchat.Authenticator, fn func(cchat.Authenticator)) *StageList {
list, _ := gtk.ListBoxNew()
list.SetSelectionMode(gtk.SELECTION_BROWSE)
list.SetActivateOnSingleClick(true)
list.Connect("row-activated", func(_ *gtk.ListBox, row *gtk.ListBoxRow) {
fn(authers[row.GetIndex()])
})
list.Show()
for _, auth := range authers {
row := handy.ActionRowNew()
row.SetActivatable(true)
row.SetTitle(auth.Name().String())
row.SetSubtitle(auth.Description().String())
row.Show()
list.Add(row)
}
sw, _ := gtk.ScrolledWindowNew(nil, nil)
sw.SetPolicy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
sw.SetSizeRequest(200, 0)
sw.SetHAlign(gtk.ALIGN_FILL)
sw.SetHExpand(false)
sw.Add(list)
return &StageList{
ScrolledWindow: sw,
ListBox: list,
}
}
func (slist *StageList) SelectFirst() {
if first := slist.ListBox.GetRowAtIndex(0); first != nil {
first.Activate()
}
}

View File

@ -1,26 +1,12 @@
{ pkgs ? import <nixpkgs> {} }:
let libhandy = pkgs.libhandy.overrideAttrs(old: {
name = "libhandy-0.90.0";
src = builtins.fetchGit {
url = "https://gitlab.gnome.org/GNOME/libhandy.git";
rev = "c7aaf6f4f50b64ee55fcfee84000e9525fc5f93a";
};
patches = [];
buildInputs = old.buildInputs ++ (with pkgs; [
gnome3.librsvg
gdk-pixbuf
]);
});
in pkgs.stdenv.mkDerivation rec {
pkgs.stdenv.mkDerivation rec {
name = "cchat-gtk";
version = "0.0.2";
buildInputs =
[ libhandy ]
++ (with pkgs; [ gnome3.gspell gnome3.glib gnome3.gtk ]);
buildInputs = with pkgs; [
libhandy gnome3.gspell gnome3.glib gnome3.gtk
];
nativeBuildInputs = with pkgs; [
pkgconfig go