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