package shlvdialogs import ( "fmt" "strings" "git.brettb.xyz/goinv/client/internal/api" "git.brettb.xyz/goinv/client/internal/types" "git.brettb.xyz/goinv/client/internal/ui/dialogs" "git.brettb.xyz/goinv/client/internal/ui/style" "git.brettb.xyz/goinv/client/internal/ui/utils" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "go.uber.org/zap" ) const ( shelfCreateDialogMaxWidth = 100 shelfCreateDialogHeight = 17 ) const ( createShelfFormFocus = 0 + iota createCategoriesFocus createCategoryPagesFocus createShelfNameFieldFocus createShelfRoomFieldFocus createShelfBuildingFieldFocus createShelfDescriptionFieldFocus ) const ( generalPageIndex = 0 + iota locationPageIndex commentPageIndex ) type ShelfCreateDialog struct { *tview.Box layout *tview.Flex createCategoryLabels []string categories *tview.TextView categoryPages *tview.Pages generalInfoPage *tview.Flex locationPage *tview.Flex commentsPage *tview.Flex form *tview.Form display bool activePageIndex int focusElement int logger *zap.Logger client *api.APIClient shelf *types.ShelfLocation buildingList []*types.Building shelfNameField *tview.InputField shelfRoomField *tview.InputField shelfDescriptionArea *tview.TextArea shelfBuildingField *tview.DropDown focusMap map[int]tview.Primitive cancelHandler func() createHandler func() } func NewShelfCreateDialog(logger *zap.Logger, client *api.APIClient) *ShelfCreateDialog { createDialog := ShelfCreateDialog{ Box: tview.NewBox(), layout: tview.NewFlex(), createCategoryLabels: []string{ "General", "Location", "Comments", }, categories: tview.NewTextView(), categoryPages: tview.NewPages(), generalInfoPage: tview.NewFlex(), locationPage: tview.NewFlex(), commentsPage: tview.NewFlex(), form: tview.NewForm(), display: false, activePageIndex: 0, logger: logger, client: client, shelfNameField: tview.NewInputField(), shelfRoomField: tview.NewInputField(), shelfBuildingField: tview.NewDropDown(), shelfDescriptionArea: tview.NewTextArea(), } createDialog.focusMap = map[int]tview.Primitive{ createShelfNameFieldFocus: createDialog.shelfNameField, createShelfRoomFieldFocus: createDialog.shelfRoomField, createShelfBuildingFieldFocus: createDialog.shelfBuildingField, createShelfDescriptionFieldFocus: createDialog.shelfDescriptionArea, } createDialog.setupLayout() createDialog.setActiveCategory(0) createDialog.initCustomInputHandlers() return &createDialog } func (d *ShelfCreateDialog) Display() { d.display = true d.initData() d.focusElement = createCategoryPagesFocus } func (d *ShelfCreateDialog) IsDisplay() bool { return d.display } func (d *ShelfCreateDialog) Hide() { d.display = false } func (d *ShelfCreateDialog) HasFocus() bool { return utils.CheckFocus(d.categories, d.categoryPages, d.Box, d.form) } func (d *ShelfCreateDialog) Focus(delegate func(tview.Primitive)) { switch d.focusElement { case createShelfFormFocus: button := d.form.GetButton(d.form.GetButtonCount() - 1) button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == utils.SwitchFocusKey.EventKey() { d.focusElement = createCategoriesFocus d.Focus(delegate) d.form.SetFocus(0) return nil } if event.Key() == tcell.KeyEnter { return nil } return event }) delegate(d.form) case createCategoriesFocus: d.categories.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == utils.SwitchFocusKey.EventKey() { d.focusElement = createCategoryPagesFocus d.Focus(delegate) return nil } if event.Key() == tcell.KeyBacktab { d.focusElement = createShelfFormFocus d.Focus(delegate) return nil } event = utils.ParseKeyEventKey(d.logger, event) if event.Key() == tcell.KeyDown { d.nextCategory() } if event.Key() == tcell.KeyUp { d.previousCategory() } return event }) delegate(d.categories) case createCategoryPagesFocus: delegate(d.categoryPages) default: delegate(d.focusMap[d.focusElement]) } } func (d *ShelfCreateDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { d.logger.Sugar().Debugf("shelf create dialog event %v received", event) if event.Key() == utils.CloseDialogKey.EventKey() && !d.dropdownHasFocus() { d.cancelHandler() return } if d.generalInfoPage.HasFocus() { if handler := d.generalInfoPage.InputHandler(); handler != nil { if event.Key() == utils.SwitchFocusKey.EventKey() { d.setGeneralInfoNextFocus() } if event.Key() == tcell.KeyBacktab { d.setGeneralInfoPrevFocus() } handler(event, setFocus) return } } if d.locationPage.HasFocus() { if handler := d.locationPage.InputHandler(); handler != nil { if event.Key() == utils.SwitchFocusKey.EventKey() { d.setLocationNextFocus() } if event.Key() == tcell.KeyBacktab { d.setLocationPrevFocus() } handler(event, setFocus) return } } if d.commentsPage.HasFocus() { if handler := d.commentsPage.InputHandler(); handler != nil { if event.Key() == utils.SwitchFocusKey.EventKey() { d.setCommentsNextFocus() } if event.Key() == tcell.KeyBacktab { d.setCommentsPrevFocus() } handler(event, setFocus) return } } if d.categories.HasFocus() { if categoryHandler := d.categories.InputHandler(); categoryHandler != nil { categoryHandler(event, setFocus) return } } if d.form.HasFocus() { if formHandler := d.form.InputHandler(); formHandler != nil { if event.Key() == tcell.KeyEnter { enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) if enterButton.HasFocus() { d.createHandler() } } formHandler(event, setFocus) return } } }) } func (d *ShelfCreateDialog) SetRect(x, y, width, height int) { if width > shelfCreateDialogMaxWidth { emptySpace := (width - shelfCreateDialogMaxWidth) / 2 x += emptySpace width = shelfCreateDialogMaxWidth } if height > shelfCreateDialogHeight { emptySpace := (height - shelfCreateDialogHeight) / 2 y += emptySpace height = shelfCreateDialogHeight } d.Box.SetRect(x, y, width, height) } func (d *ShelfCreateDialog) Draw(screen tcell.Screen) { if !d.display { return } d.Box.DrawForSubclass(screen, d) x, y, width, height := d.Box.GetInnerRect() d.layout.SetRect(x, y, width, height) d.layout.Draw(screen) } func (d *ShelfCreateDialog) SetCreateFunc(f func()) *ShelfCreateDialog { d.createHandler = f enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) enterButton.SetSelectedFunc(f) return d } func (d *ShelfCreateDialog) SetCancelFunc(f func()) *ShelfCreateDialog { d.cancelHandler = f cancelButton := d.form.GetButton(d.form.GetButtonCount() - 2) cancelButton.SetSelectedFunc(f) return d } func (d *ShelfCreateDialog) dropdownHasFocus() bool { return utils.CheckFocus(d.shelfBuildingField) } func (d *ShelfCreateDialog) SetShelf(a *types.ShelfLocation) { d.shelf = a } func (d *ShelfCreateDialog) initData() { // Get available buildings buildingList, _ := d.client.RetrieveAllBuildings() d.buildingList = buildingList buildingOptions := []string{""} for _, building := range d.buildingList { buildingOptions = append(buildingOptions, building.Name) // In case we want to do custom formatting later } d.setActiveCategory(0) // General category if d.shelf == nil { d.shelfNameField.SetText("") // Location category d.shelfBuildingField.SetOptions(buildingOptions, nil) d.shelfRoomField.SetText("") // Comments category d.shelfDescriptionArea.SetText("", true) } else { d.shelfNameField.SetText(d.shelf.Name) // Location category d.shelfBuildingField.SetOptions(buildingOptions, nil) if d.shelf.Building != nil { d.shelfBuildingField.SetCurrentOption(findOption(buildingOptions, d.shelf.Building.Name)) } d.shelfRoomField.SetText("") // Comments category d.shelfDescriptionArea.SetText("", true) } } func (d *ShelfCreateDialog) setupLayout() { bgColor := style.DialogBgColor fgColor := style.DialogFgColor d.categories.SetDynamicColors(true). SetWrap(true). SetTextAlign(tview.AlignLeft) d.categories.SetBackgroundColor(fgColor) d.categories.SetTextColor(bgColor) d.categories.SetBorder(true) d.categories.SetBorderColor(style.DialogSubBoxBorderColor) d.categoryPages.SetBackgroundColor(bgColor) d.categoryPages.SetBorder(true) d.categoryPages.SetBorderColor(style.DialogSubBoxBorderColor) d.setupGeneralInfoPageUI() d.setupLocationPageUI() d.setupCommentsPageUI() activatedStyle := tcell.StyleDefault. Background(style.ButtonSelectedBgColor). Foreground(style.ButtonSelectedFgColor) d.form.SetBackgroundColor(bgColor) d.form.AddButton("Cancel", nil) d.form.AddButton("Create", nil) d.form.SetButtonsAlign(tview.AlignRight) d.form.SetButtonBackgroundColor(style.ButtonBgColor) d.form.SetButtonTextColor(style.ButtonFgColor) d.form.SetButtonActivatedStyle(activatedStyle) d.categoryPages.AddPage(d.createCategoryLabels[generalPageIndex], d.generalInfoPage, true, true) d.categoryPages.AddPage(d.createCategoryLabels[locationPageIndex], d.locationPage, true, true) d.categoryPages.AddPage(d.createCategoryLabels[commentPageIndex], d.commentsPage, true, true) d.layout.SetBackgroundColor(bgColor) d.layout.SetBorder(true) d.layout.SetBorderColor(style.DialogBorderColor) d.layout.SetTitle("SHELF CREATE") d.layout.SetTitleColor(style.DialogFgColor) _, layoutWidth := utils.AlignStringListWidth(d.createCategoryLabels) layout := tview.NewFlex().SetDirection(tview.FlexColumn) layout.AddItem(d.categories, layoutWidth+6, 0, true) layout.AddItem(d.categoryPages, 0, 1, true) layout.SetBackgroundColor(bgColor) d.layout.SetDirection(tview.FlexRow) d.layout.AddItem(layout, 0, 1, true) d.layout.AddItem(d.form, dialogs.DialogFormHeight, 0, true) } func (d *ShelfCreateDialog) setupGeneralInfoPageUI() { bgColor := style.DialogBgColor inputFieldBgColor := style.InputFieldBgColor pageLabelWidth := 14 d.shelfNameField.SetLabel("name:") d.shelfNameField.SetLabelWidth(pageLabelWidth) d.shelfNameField.SetLabelColor(bgColor) d.shelfNameField.SetBackgroundColor(style.DialogFgColor) d.shelfNameField.SetFieldBackgroundColor(inputFieldBgColor) d.generalInfoPage.SetDirection(tview.FlexRow) d.generalInfoPage.AddItem(d.shelfNameField, 1, 0, true) d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) } func (d *ShelfCreateDialog) setupLocationPageUI() { bgColor := style.DialogBgColor inputFieldBgColor := style.InputFieldBgColor pageLabelWidth := 12 ddUnselectedStyle := style.DropdownUnselected ddSelectedStyle := style.DropdownSelected d.shelfRoomField.SetLabel("room:") d.shelfRoomField.SetLabelWidth(pageLabelWidth) d.shelfRoomField.SetLabelColor(bgColor) d.shelfRoomField.SetBackgroundColor(style.DialogFgColor) d.shelfRoomField.SetFieldBackgroundColor(inputFieldBgColor) d.shelfBuildingField.SetLabel("building:") d.shelfBuildingField.SetLabelWidth(pageLabelWidth) d.shelfBuildingField.SetBackgroundColor(bgColor) d.shelfBuildingField.SetLabelColor(style.DialogFgColor) d.shelfBuildingField.SetListStyles(ddUnselectedStyle, ddSelectedStyle) d.shelfBuildingField.SetFieldBackgroundColor(inputFieldBgColor) d.locationPage.SetDirection(tview.FlexRow) d.locationPage.AddItem(d.shelfRoomField, 1, 0, true) d.locationPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) d.locationPage.AddItem(d.shelfBuildingField, 1, 0, true) d.locationPage.SetBackgroundColor(bgColor) } func (d *ShelfCreateDialog) setupCommentsPageUI() { bgColor := style.DialogBgColor inputFieldBgColor := style.InputFieldBgColor pageLabelWidth := 12 d.shelfDescriptionArea.SetLabel("comments:") d.shelfDescriptionArea.SetLabelWidth(pageLabelWidth) d.shelfDescriptionArea.SetBackgroundColor(inputFieldBgColor) d.shelfDescriptionArea.SetLabelStyle(tcell.StyleDefault.Foreground(style.DialogFgColor).Background(bgColor)) d.shelfDescriptionArea.SetWrap(true) d.commentsPage.AddItem(d.shelfDescriptionArea, 0, 1, true) d.commentsPage.SetBackgroundColor(bgColor) } func (d *ShelfCreateDialog) setActiveCategory(idx int) { fgColor := style.ButtonSelectedFgColor bgBolor := style.ButtonSelectedBgColor ctgTextColor := style.GetColorHex(fgColor) ctgBgColor := style.GetColorHex(bgBolor) d.activePageIndex = idx d.categories.Clear() alignedList, _ := utils.AlignStringListWidth(d.createCategoryLabels) var ctgList []string for i, lbl := range alignedList { if i == idx { ctgList = append(ctgList, fmt.Sprintf("[%s:%s:b]-> %s ", ctgTextColor, ctgBgColor, lbl)) continue } ctgList = append(ctgList, fmt.Sprintf("[-:-:-] %s ", lbl)) } d.categories.SetText(strings.Join(ctgList, "\n")) d.categoryPages.SwitchToPage(d.createCategoryLabels[idx]) } func (d *ShelfCreateDialog) nextCategory() { activePage := d.activePageIndex if d.activePageIndex < len(d.createCategoryLabels)-1 { activePage++ d.setActiveCategory(activePage) return } d.setActiveCategory(0) } func (d *ShelfCreateDialog) previousCategory() { activePage := d.activePageIndex if d.activePageIndex > 0 { activePage-- d.setActiveCategory(activePage) return } d.setActiveCategory(len(d.createCategoryLabels) - 1) } func (d *ShelfCreateDialog) initCustomInputHandlers() { d.shelfBuildingField.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { event = utils.ParseKeyEventKey(d.logger, event) return event }) } func (d *ShelfCreateDialog) setGeneralInfoNextFocus() { // Name -> Quantity -> Length -> Manufacturer -> Model -> Price if d.shelfNameField.HasFocus() { d.focusElement = createShelfFormFocus return } d.focusElement = createShelfFormFocus } func (d *ShelfCreateDialog) setGeneralInfoPrevFocus() { if d.shelfNameField.HasFocus() { d.focusElement = createCategoriesFocus return } d.focusElement = createShelfNameFieldFocus } func (d *ShelfCreateDialog) setLocationNextFocus() { // Category -> Shelf if d.shelfRoomField.HasFocus() { d.focusElement = createShelfBuildingFieldFocus return } d.focusElement = createShelfFormFocus } func (d *ShelfCreateDialog) setLocationPrevFocus() { if d.shelfRoomField.HasFocus() { d.focusElement = createCategoriesFocus return } if d.shelfBuildingField.HasFocus() { d.focusElement = createShelfRoomFieldFocus return } d.focusElement = createShelfBuildingFieldFocus } func (d *ShelfCreateDialog) setCommentsNextFocus() { d.focusElement = createShelfFormFocus } func (d *ShelfCreateDialog) setCommentsPrevFocus() { d.focusElement = createCategoriesFocus } func (d *ShelfCreateDialog) CreateShelfOptions() types.CreateShelfRequest { var ( buildingID *uint64 ) selectedBuildingIndex, _ := d.shelfBuildingField.GetCurrentOption() if len(d.buildingList) > 0 && selectedBuildingIndex > 0 { buildingID = &d.buildingList[selectedBuildingIndex-1].ID } req := types.CreateShelfRequest{ Name: d.shelfNameField.GetText(), RoomNumber: d.shelfRoomField.GetText(), Description: d.shelfDescriptionArea.GetText(), BuildingID: buildingID, } return req }