diff --git a/README.md b/README.md index 70fbbb0..989a247 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Command Line User Interface (Console UI inspired by TurboVision) with built-in t ## Current version -The current version is 0.9.0 RC1. Please see details in [changelog](./changelog). +The current version is 0.9.0 RC2. Please see details in [changelog](./changelog). ## Applications that uses the library * Terminal FB2 reader(termfb2): https://github.com/VladimirMarkelov/termfb2 @@ -39,6 +39,7 @@ The current version is 0.9.0 RC1. Please see details in [changelog](./changelog) * SparkChart (Show tabular data as a bar graph) * GridView (Table to show structured data - only virtual and readonly mode with scroll support) * ![FilePicker](/docs/fselect.md) +* LoginDialog - a simple authorization dialog with two fields: Username and Password ## Screenshots The main demo (theme changing and radio group control) diff --git a/changelog b/changelog index e1e0c8f..a557697 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,6 @@ +2018-08-13 - version 0.9.0 RC2 +[+] New control: LoginDialog (a dialog with username and password fields) + 2018-08-04 - version 0.9.0 RC1 [+] New control: File Picker (a dialog for file save/load operations) [+] New property for Label: TextDisplay. It defines which part of Label title diff --git a/demos/logindlg/logindlg.go b/demos/logindlg/logindlg.go new file mode 100644 index 0000000..bef9a01 --- /dev/null +++ b/demos/logindlg/logindlg.go @@ -0,0 +1,92 @@ +package main + +import ( + ui "github.com/VladimirMarkelov/clui" +) + +func createView() { + view := ui.AddWindow(0, 0, 30, 7, "Login dialog") + view.SetPack(ui.Vertical) + view.SetGaps(0, 1) + view.SetPaddings(2, 2) + + frmOpts := ui.CreateFrame(view, 1, 1, ui.BorderNone, ui.Fixed) + frmOpts.SetPack(ui.Horizontal) + cbCheck := ui.CreateCheckBox(frmOpts, ui.AutoSize, "Use callback to test data", ui.Fixed) + + ui.CreateLabel(view, ui.AutoSize, ui.AutoSize, "Correct credentials", ui.Fixed) + + frmCreds := ui.CreateFrame(view, 1, 1, ui.BorderNone, ui.Fixed) + frmCreds.SetPack(ui.Horizontal) + frmCreds.SetGaps(1, 0) + ui.CreateLabel(frmCreds, ui.AutoSize, ui.AutoSize, "Username", ui.Fixed) + edUser := ui.CreateEditField(frmCreds, 8, "", 1) + ui.CreateLabel(frmCreds, ui.AutoSize, ui.AutoSize, "Password", ui.Fixed) + edPass := ui.CreateEditField(frmCreds, 8, "", 1) + + lbRes := ui.CreateLabel(view, ui.AutoSize, ui.AutoSize, "Result:", ui.Fixed) + + frmBtns := ui.CreateFrame(view, 1, 1, ui.BorderNone, ui.Fixed) + frmBtns.SetPack(ui.Horizontal) + btnDlg := ui.CreateButton(frmBtns, ui.AutoSize, 4, "Login", ui.Fixed) + btnQuit := ui.CreateButton(frmBtns, ui.AutoSize, 4, "Quit", ui.Fixed) + ui.CreateFrame(frmBtns, 1, 1, ui.BorderNone, 1) + + ui.ActivateControl(view, edUser) + + btnDlg.OnClick(func(ev ui.Event) { + dlg := ui.CreateLoginDialog( + "Enter credentials", + edUser.Title(), + ) + + if cbCheck.State() == 1 { + dlg.OnCheck(func(u, p string) bool { + return u == edUser.Title() && p == edPass.Title() + }) + } else { + dlg.OnCheck(nil) + } + + dlg.OnClose(func() { + if dlg.Action == ui.LoginCanceled { + lbRes.SetTitle("Result:\nDialog canceled") + return + } + + if dlg.Action == ui.LoginInvalid { + lbRes.SetTitle("Result:\nInvalid username or password") + return + } + + if dlg.Action == ui.LoginOk { + if cbCheck.State() == 1 { + lbRes.SetTitle("Result:\nLogged in successfully") + } else { + lbRes.SetTitle("Result:\nEntered [" + dlg.Username + ":" + dlg.Password + "]") + } + return + } + }) + }) + + btnQuit.OnClick(func(ev ui.Event) { + go ui.Stop() + }) +} + +func mainLoop() { + // Every application must create a single Composer and + // call its intialize method + ui.InitLibrary() + defer ui.DeinitLibrary() + + createView() + + // start event processing loop - the main core of the library + ui.MainLoop() +} + +func main() { + mainLoop() +} diff --git a/logindlg.go b/logindlg.go new file mode 100644 index 0000000..e74ee55 --- /dev/null +++ b/logindlg.go @@ -0,0 +1,159 @@ +package clui + +const ( + LoginOk = iota + LoginCanceled + LoginInvalid +) + +// LoginDialog is a login dialog with fields to enter user name and password +// Public properties: +// * Username - login entered by a user +// * Password - password entered by a user +// * Action - how the dialog was closed: +// - LoginOk - button "OK" was clicked +// - LoginCanceled - button "Cancel" was clicked or dialog was dismissed +// - LoginInvalid - invalid credentials were entered. This value appears +// only in case of callback is used and button "OK" is clicked +// while entered username or password is incorrect +type LoginDialog struct { + View *Window + Username string + Password string + Action int + + result int + onClose func() + onCheck func(string, string) bool +} + +// LoginDialog creates a new login dialog +// * title - custom dialog title +// * userName - initial username. Maybe useful if you want to implement +// a feature "remember me" +// The active control depends on userName: if it is empty then the cursor is +// in Username field, and in Password field otherwise. +// By default the dialog is closed when button "OK" is clicked. But if you set +// OnCheck callback the dialog closes only if callback returns true or +// button "Cancel" is clicked. This is helpful if you do not want to recreate +// the dialog after every incorrect credentials. So, you define a callback +// that checks whether pair of Usename and Password is correct and then the +// button "OK" closed the dialog only if the callback returns true. If the +// credentials are not valid, then the dialog shows a warning. The warning +// automatically disappears when a user starts typing in Password or Username +// field. +func CreateLoginDialog(title, userName string) *LoginDialog { + dlg := new(LoginDialog) + + dlg.View = AddWindow(15, 8, 10, 4, title) + WindowManager().BeginUpdate() + defer WindowManager().EndUpdate() + + dlg.View.SetModal(true) + dlg.View.SetPack(Vertical) + + userfrm := CreateFrame(dlg.View, 1, 1, BorderNone, Fixed) + userfrm.SetPaddings(1, 1) + userfrm.SetPack(Horizontal) + userfrm.SetGaps(1, 0) + CreateLabel(userfrm, AutoSize, AutoSize, "User name", Fixed) + edUser := CreateEditField(userfrm, 20, userName, 1) + + passfrm := CreateFrame(dlg.View, 1, 1, BorderNone, 1) + passfrm.SetPaddings(1, 1) + passfrm.SetPack(Horizontal) + passfrm.SetGaps(1, 0) + CreateLabel(passfrm, AutoSize, AutoSize, "Password", Fixed) + edPass := CreateEditField(passfrm, 20, "", 1) + edPass.SetPasswordMode(true) + + filler := CreateFrame(dlg.View, 1, 1, BorderNone, 1) + filler.SetPack(Horizontal) + lbRes := CreateLabel(filler, AutoSize, AutoSize, "", 1) + + blist := CreateFrame(dlg.View, 1, 1, BorderNone, Fixed) + blist.SetPack(Horizontal) + blist.SetPaddings(1, 1) + btnOk := CreateButton(blist, 10, 4, "OK", Fixed) + btnCancel := CreateButton(blist, 10, 4, "Cancel", Fixed) + + btnCancel.OnClick(func(ev Event) { + WindowManager().DestroyWindow(dlg.View) + WindowManager().BeginUpdate() + dlg.Action = LoginCanceled + closeFunc := dlg.onClose + WindowManager().EndUpdate() + if closeFunc != nil { + closeFunc() + } + }) + + btnOk.OnClick(func(ev Event) { + if dlg.onCheck != nil && !dlg.onCheck(edUser.Title(), edPass.Title()) { + lbRes.SetTitle("Invalid username or password") + dlg.Action = LoginInvalid + return + } + + dlg.Action = LoginOk + if dlg.onCheck == nil { + dlg.Username = edUser.Title() + dlg.Password = edPass.Title() + } + + WindowManager().DestroyWindow(dlg.View) + WindowManager().BeginUpdate() + + closeFunc := dlg.onClose + WindowManager().EndUpdate() + if closeFunc != nil { + closeFunc() + } + }) + + dlg.View.OnClose(func(ev Event) bool { + if dlg.result == DialogAlive { + dlg.result = DialogClosed + if ev.X != 1 { + WindowManager().DestroyWindow(dlg.View) + } + if dlg.onClose != nil { + dlg.onClose() + } + } + return true + }) + + edUser.OnChange(func(ev Event) { + lbRes.SetTitle("") + }) + edPass.OnChange(func(ev Event) { + lbRes.SetTitle("") + }) + + if userName == "" { + ActivateControl(dlg.View, edUser) + } else { + ActivateControl(dlg.View, edPass) + } + return dlg +} + +// OnClose sets the callback that is called when the +// dialog is closed +func (d *LoginDialog) OnClose(fn func()) { + WindowManager().BeginUpdate() + defer WindowManager().EndUpdate() + d.onClose = fn +} + +// OnCheck sets the callback that is called when the +// button "OK" is clicked. The dialog sends to the callback two arguments: +// username and password. The callback validates the arguments and if +// the credentials are valid it returns true. That means the dialog can be +// closed. If the callback returns false then the dialog remains on the screen. +func (d *LoginDialog) OnCheck(fn func(string, string) bool) { + WindowManager().BeginUpdate() + defer WindowManager().EndUpdate() + d.onCheck = fn +}