diff --git a/go.mod b/go.mod index 6995552..1cc2675 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 47fdb4e..cc5b11e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/ui/config/preferences/preferences.go b/internal/ui/config/preferences/preferences.go index c9acea1..a042937 100644 --- a/internal/ui/config/preferences/preferences.go +++ b/internal/ui/config/preferences/preferences.go @@ -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, diff --git a/internal/ui/dialog/dialog.go b/internal/ui/dialog/dialog.go index ae41ade..9344d94 100644 --- a/internal/ui/dialog/dialog.go +++ b/internal/ui/dialog/dialog.go @@ -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) diff --git a/internal/ui/messages/input/input.go b/internal/ui/messages/input/input.go index c7eed75..b5cf4f6 100644 --- a/internal/ui/messages/input/input.go +++ b/internal/ui/messages/input/input.go @@ -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() diff --git a/internal/ui/messages/input/spellcheck.go b/internal/ui/messages/input/spellcheck.go new file mode 100644 index 0000000..8781d5c --- /dev/null +++ b/internal/ui/messages/input/spellcheck.go @@ -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() + } +} diff --git a/internal/ui/service/auth/auth.go b/internal/ui/service/auth/auth.go index 1770913..472c428 100644 --- a/internal/ui/service/auth/auth.go +++ b/internal/ui/service/auth/auth.go @@ -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(`` + html.EscapeString(err.Error()) + ``) - } 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) -} diff --git a/internal/ui/service/auth/entries.go b/internal/ui/service/auth/entries.go new file mode 100644 index 0000000..db38725 --- /dev/null +++ b/internal/ui/service/auth/entries.go @@ -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(`Error!`) + builder.WriteByte('\n') + builder.WriteString(html.EscapeString(capitalizeFirst(err.Error()))) + builder.WriteString(``) + 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) +} diff --git a/internal/ui/service/auth/stagelist.go b/internal/ui/service/auth/stagelist.go new file mode 100644 index 0000000..a8ec640 --- /dev/null +++ b/internal/ui/service/auth/stagelist.go @@ -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() + } +} diff --git a/shell.nix b/shell.nix index 983a96d..7059abc 100644 --- a/shell.nix +++ b/shell.nix @@ -1,26 +1,12 @@ { pkgs ? import {} }: -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