package d2player import ( "image/color" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2util" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui" ) const ( selectionBackgroundColor = 0x000000d0 defaultGameEvent = -1 keyBindingMenuWidth = 620 keyBindingMenuHeight = 375 keyBindingMenuX = 90 keyBindingMenuY = 75 keyBindingMenuPaddingX = 17 keyBindingSettingPaddingY = 19 keyBindingMenuHeaderHeight = 24 keyBindingMenuHeaderSpacer1 = 131 keyBindingMenuHeaderSpacer2 = 86 keyBindingMenuBindingSpacerBetween = 25 keyBindingMenuBindingSpacerLeft = 17 keyBindingMenuBindingDescWidth = 190 keyBindingMenuBindingDescHeight = 0 keyBindingMenuBindingPrimaryWidth = 190 keyBindingMenuBindingPrimaryHeight = 0 keyBindingMenuBindingSecondaryWidth = 90 keyBindingMenuBindingSecondaryHeight = 0 ) type bindingChange struct { target *KeyBinding primary d2enum.Key secondary d2enum.Key } // NewKeyBindingMenu generates a new instance of the "Configure Keys" // menu found in the options func NewKeyBindingMenu( asset *d2asset.AssetManager, renderer d2interface.Renderer, ui *d2ui.UIManager, guiManager *d2gui.GuiManager, keyMap *KeyMap, l d2util.LogLevel, escapeMenu *EscapeMenu, ) *KeyBindingMenu { mainLayout := d2gui.CreateLayout(renderer, d2gui.PositionTypeAbsolute, asset) contentLayout := mainLayout.AddLayout(d2gui.PositionTypeAbsolute) ret := &KeyBindingMenu{ keyMap: keyMap, asset: asset, ui: ui, guiManager: guiManager, renderer: renderer, mainLayout: mainLayout, contentLayout: contentLayout, bindingLayouts: []*bindingLayout{}, changesToBeSaved: make(map[d2enum.GameEvent]*bindingChange), escapeMenu: escapeMenu, } ret.Logger = d2util.NewLogger() ret.Logger.SetLevel(l) ret.Logger.SetPrefix(logPrefix) ret.Box = d2gui.NewBox( asset, renderer, ui, ret.mainLayout, keyBindingMenuWidth, keyBindingMenuHeight, keyBindingMenuX, keyBindingMenuY, l, "", ) ret.Box.SetPadding(keyBindingMenuPaddingX, keyBindingSettingPaddingY) ret.Box.SetOptions([]*d2gui.LabelButton{ d2gui.NewLabelButton(0, 0, "Cancel", d2util.Color(d2gui.ColorRed), d2util.LogLevelDefault, func() { if err := ret.onCancelClicked(); err != nil { ret.Errorf("error while clicking option Cancel: %v", err.Error()) } }), d2gui.NewLabelButton(0, 0, "Default", d2util.Color(d2gui.ColorBlue), d2util.LogLevelDefault, func() { if err := ret.onDefaultClicked(); err != nil { ret.Errorf("error while clicking option Default: %v", err) } }), d2gui.NewLabelButton(0, 0, "Accept", d2util.Color(d2gui.ColorGreen), d2util.LogLevelDefault, func() { if err := ret.onAcceptClicked(); err != nil { ret.Errorf("error while clicking option Accept: %v", err) } }), }) return ret } // KeyBindingMenu represents the menu to view/edit the // key bindings type KeyBindingMenu struct { *d2gui.Box asset *d2asset.AssetManager renderer d2interface.Renderer ui *d2ui.UIManager guiManager *d2gui.GuiManager keyMap *KeyMap escapeMenu *EscapeMenu mainLayout *d2gui.Layout contentLayout *d2gui.Layout scrollbar *d2gui.LayoutScrollbar bindingLayouts []*bindingLayout changesToBeSaved map[d2enum.GameEvent]*bindingChange isAwaitingKeyDown bool currentBindingModifierType KeyBindingType currentBindingModifier d2enum.GameEvent currentBindingLayout *bindingLayout lastBindingLayout *bindingLayout *d2util.Logger } // Close will disable the render of the menu and clear // the current selection func (menu *KeyBindingMenu) Close() error { menu.Box.Close() if err := menu.clearSelection(); err != nil { return err } return nil } // Load will setup the layouts of the menu func (menu *KeyBindingMenu) Load() error { if err := menu.Box.Load(); err != nil { return err } mainLayoutW, mainLayoutH := menu.mainLayout.GetSize() headerLayout := menu.contentLayout.AddLayout(d2gui.PositionTypeHorizontal) headerLayout.SetSize(mainLayoutW, keyBindingMenuHeaderHeight) if _, err := headerLayout.AddLabelWithColor( menu.asset.TranslateString("CfgFunction"), d2gui.FontStyleFormal11Units, d2util.Color(d2gui.ColorBrown), ); err != nil { return err } headerLayout.AddSpacerStatic(keyBindingMenuHeaderSpacer1, keyBindingMenuHeaderHeight) if _, err := headerLayout.AddLabelWithColor( menu.asset.TranslateString("CfgPrimaryKey"), d2gui.FontStyleFormal11Units, d2util.Color(d2gui.ColorBrown), ); err != nil { return err } headerLayout.AddSpacerStatic(keyBindingMenuHeaderSpacer2, 1) if _, err := headerLayout.AddLabelWithColor( menu.asset.TranslateString("CfgSecondaryKey"), d2gui.FontStyleFormal11Units, d2util.Color(d2gui.ColorBrown), ); err != nil { return err } headerLayout.SetVerticalAlign(d2gui.VerticalAlignMiddle) bindingWrapper := menu.contentLayout.AddLayout(d2gui.PositionTypeAbsolute) bindingWrapper.SetPosition(0, keyBindingMenuHeaderHeight) bindingWrapper.SetSize(mainLayoutW, mainLayoutH-keyBindingMenuHeaderHeight) bindingLayout := menu.generateLayout() menu.Box.GetLayout().AdjustEntryPlacement() menu.mainLayout.AdjustEntryPlacement() menu.contentLayout.AdjustEntryPlacement() menu.scrollbar = d2gui.NewLayoutScrollbar(bindingWrapper, bindingLayout) if err := menu.scrollbar.Load(menu.ui); err != nil { return err } bindingWrapper.AddLayoutFromSource(bindingLayout) bindingWrapper.AdjustEntryPlacement() return nil } type keyBindingSetting struct { label string gameEvent d2enum.GameEvent } func (menu *KeyBindingMenu) getBindingGroups() [][]keyBindingSetting { return [][]keyBindingSetting{ { {menu.asset.TranslateString("CfgCharacter"), d2enum.ToggleCharacterPanel}, {menu.asset.TranslateString("CfgInventory"), d2enum.ToggleInventoryPanel}, {menu.asset.TranslateString("CfgParty"), d2enum.TogglePartyPanel}, {menu.asset.TranslateString("Cfghireling"), d2enum.ToggleHirelingPanel}, {menu.asset.TranslateString("CfgMessageLog"), d2enum.ToggleMessageLog}, {menu.asset.TranslateString("CfgQuestLog"), d2enum.ToggleQuestLog}, {menu.asset.TranslateString("CfgHelp"), d2enum.ToggleHelpScreen}, }, { {menu.asset.TranslateString("CfgSkillTree"), d2enum.ToggleSkillTreePanel}, {menu.asset.TranslateString("CfgSkillPick"), d2enum.ToggleRightSkillSelector}, {menu.asset.TranslateString("CfgSkill1"), d2enum.UseSkill1}, {menu.asset.TranslateString("CfgSkill2"), d2enum.UseSkill2}, {menu.asset.TranslateString("CfgSkill3"), d2enum.UseSkill3}, {menu.asset.TranslateString("CfgSkill4"), d2enum.UseSkill4}, {menu.asset.TranslateString("CfgSkill5"), d2enum.UseSkill5}, {menu.asset.TranslateString("CfgSkill6"), d2enum.UseSkill6}, {menu.asset.TranslateString("CfgSkill7"), d2enum.UseSkill7}, {menu.asset.TranslateString("CfgSkill8"), d2enum.UseSkill8}, {menu.asset.TranslateString("CfgSkill9"), d2enum.UseSkill9}, {menu.asset.TranslateString("CfgSkill10"), d2enum.UseSkill10}, {menu.asset.TranslateString("CfgSkill11"), d2enum.UseSkill11}, {menu.asset.TranslateString("CfgSkill12"), d2enum.UseSkill12}, {menu.asset.TranslateString("CfgSkill13"), d2enum.UseSkill13}, {menu.asset.TranslateString("CfgSkill14"), d2enum.UseSkill14}, {menu.asset.TranslateString("CfgSkill15"), d2enum.UseSkill15}, {menu.asset.TranslateString("CfgSkill16"), d2enum.UseSkill16}, {menu.asset.TranslateString("Cfgskillup"), d2enum.SelectPreviousSkill}, {menu.asset.TranslateString("Cfgskilldown"), d2enum.SelectNextSkill}, }, { {menu.asset.TranslateString("CfgBeltShow"), d2enum.ToggleBelts}, {menu.asset.TranslateString("CfgBelt1"), d2enum.UseBeltSlot1}, {menu.asset.TranslateString("CfgBelt2"), d2enum.UseBeltSlot2}, {menu.asset.TranslateString("CfgBelt3"), d2enum.UseBeltSlot3}, {menu.asset.TranslateString("CfgBelt4"), d2enum.UseBeltSlot4}, {menu.asset.TranslateString("Cfgswapweapons"), d2enum.SwapWeapons}, }, { {menu.asset.TranslateString("CfgChat"), d2enum.ToggleChatBox}, {menu.asset.TranslateString("CfgRun"), d2enum.HoldRun}, {menu.asset.TranslateString("CfgRunLock"), d2enum.ToggleRunWalk}, {menu.asset.TranslateString("CfgStandStill"), d2enum.HoldStandStill}, {menu.asset.TranslateString("CfgShowItems"), d2enum.HoldShowGroundItems}, {menu.asset.TranslateString("CfgTogglePortraits"), d2enum.HoldShowPortraits}, }, { {menu.asset.TranslateString("CfgAutoMap"), d2enum.ToggleAutomap}, {menu.asset.TranslateString("CfgAutoMapCenter"), d2enum.CenterAutomap}, {menu.asset.TranslateString("CfgAutoMapParty"), d2enum.TogglePartyOnAutomap}, {menu.asset.TranslateString("CfgAutoMapNames"), d2enum.ToggleNamesOnAutomap}, {menu.asset.TranslateString("CfgToggleminimap"), d2enum.ToggleMiniMap}, }, { {menu.asset.TranslateString("CfgSay0"), d2enum.SayHelp}, {menu.asset.TranslateString("CfgSay1"), d2enum.SayFollowMe}, {menu.asset.TranslateString("CfgSay2"), d2enum.SayThisIsForYou}, {menu.asset.TranslateString("CfgSay3"), d2enum.SayThanks}, {menu.asset.TranslateString("CfgSay4"), d2enum.SaySorry}, {menu.asset.TranslateString("CfgSay5"), d2enum.SayBye}, {menu.asset.TranslateString("CfgSay6"), d2enum.SayNowYouDie}, {menu.asset.TranslateString("CfgSay7"), d2enum.SayNowYouDie}, }, { {menu.asset.TranslateString("CfgSnapshot"), d2enum.TakeScreenShot}, {menu.asset.TranslateString("CfgClearScreen"), d2enum.ClearScreen}, {menu.asset.TranslateString("Cfgcleartextmsg"), d2enum.ClearMessages}, }, } } func (menu *KeyBindingMenu) generateLayout() *d2gui.Layout { groups := menu.getBindingGroups() wrapper := d2gui.CreateLayout(menu.renderer, d2gui.PositionTypeAbsolute, menu.asset) layout := wrapper.AddLayout(d2gui.PositionTypeVertical) for i, settingsGroup := range groups { groupLayout := layout.AddLayout(d2gui.PositionTypeVertical) for _, setting := range settingsGroup { bl := bindingLayout{} settingLayout := groupLayout.AddLayout(d2gui.PositionTypeHorizontal) settingLayout.AddSpacerStatic(keyBindingMenuBindingSpacerLeft, 0) descLabelWrapper := settingLayout.AddLayout(d2gui.PositionTypeAbsolute) descLabelWrapper.SetSize(keyBindingMenuBindingDescWidth, keyBindingMenuBindingDescHeight) descLabel, _ := descLabelWrapper.AddLabel(setting.label, d2gui.FontStyleFormal11Units) descLabel.SetHoverColor(d2util.Color(d2gui.ColorBlue)) bl.wrapperLayout = settingLayout bl.descLabel = descLabel bl.descLayout = descLabelWrapper if binding := menu.keyMap.GetKeysForGameEvent(setting.gameEvent); binding != nil { primaryStr := menu.keyMap.KeyToString(binding.Primary) secondaryStr := menu.keyMap.KeyToString(binding.Secondary) primaryCol := menu.getKeyColor(binding.Primary) secondaryCol := menu.getKeyColor(binding.Secondary) if binding.IsEmpty() { primaryCol = d2util.Color(d2gui.ColorRed) secondaryCol = d2util.Color(d2gui.ColorRed) } primaryKeyLabelWrapper := settingLayout.AddLayout(d2gui.PositionTypeAbsolute) primaryKeyLabelWrapper.SetSize(keyBindingMenuBindingPrimaryWidth, keyBindingMenuBindingPrimaryHeight) primaryLabel, _ := primaryKeyLabelWrapper.AddLabelWithColor(primaryStr, d2gui.FontStyleFormal11Units, primaryCol) primaryLabel.SetHoverColor(d2util.Color(d2gui.ColorBlue)) bl.primaryLabel = primaryLabel bl.primaryLayout = primaryKeyLabelWrapper bl.gameEvent = setting.gameEvent secondaryKeyLabelWrapper := settingLayout.AddLayout(d2gui.PositionTypeAbsolute) secondaryKeyLabelWrapper.SetSize(keyBindingMenuBindingSecondaryWidth, keyBindingMenuBindingSecondaryHeight) secondaryLabel, _ := secondaryKeyLabelWrapper.AddLabelWithColor(secondaryStr, d2gui.FontStyleFormal11Units, secondaryCol) secondaryLabel.SetHoverColor(d2util.Color(d2gui.ColorBlue)) bl.secondaryLabel = secondaryLabel bl.secondaryLayout = secondaryKeyLabelWrapper bl.binding = binding } menu.bindingLayouts = append(menu.bindingLayouts, &bl) } if i < len(groups)-1 { layout.AddSpacerStatic(0, keyBindingMenuBindingSpacerBetween) } } return wrapper } func (menu *KeyBindingMenu) getKeyColor(key d2enum.Key) color.RGBA { switch key { case -1: return d2util.Color(d2gui.ColorGrey) default: return d2util.Color(d2gui.ColorBrown) } } func (menu *KeyBindingMenu) setSelection(bl *bindingLayout, bindingType KeyBindingType, gameEvent d2enum.GameEvent) error { if menu.currentBindingLayout != nil { menu.lastBindingLayout = menu.currentBindingLayout if err := menu.currentBindingLayout.Reset(); err != nil { return err } } menu.currentBindingModifier = gameEvent menu.currentBindingLayout = bl if bindingType == KeyBindingTypePrimary { menu.currentBindingLayout.primaryLabel.SetIsBlinking(true) } else if bindingType == KeyBindingTypeSecondary { menu.currentBindingLayout.secondaryLabel.SetIsBlinking(true) } menu.currentBindingModifierType = bindingType menu.isAwaitingKeyDown = true if err := bl.descLabel.SetIsHovered(true); err != nil { return err } if err := bl.primaryLabel.SetIsHovered(true); err != nil { return err } if err := bl.secondaryLabel.SetIsHovered(true); err != nil { return err } return nil } func (menu *KeyBindingMenu) onMouseButtonDown(event d2interface.MouseEvent) error { if !menu.IsOpen() { return nil } menu.Box.OnMouseButtonDown(event) if menu.scrollbar != nil { if menu.scrollbar.IsInSliderRect(event.X(), event.Y()) { menu.scrollbar.SetSliderClicked(true) menu.scrollbar.OnSliderMouseClick(event) return nil } if menu.scrollbar.IsInArrowUpRect(event.X(), event.Y()) { if !menu.scrollbar.IsArrowUpClicked() { menu.scrollbar.SetArrowUpClicked(true) } menu.scrollbar.OnArrowUpClick() return nil } if menu.scrollbar.IsInArrowDownRect(event.X(), event.Y()) { if !menu.scrollbar.IsArrowDownClicked() { menu.scrollbar.SetArrowDownClicked(true) } menu.scrollbar.OnArrowDownClick() return nil } } for _, bl := range menu.bindingLayouts { gameEvent, typ := bl.GetPointedLayoutAndLabel(event.X(), event.Y()) if gameEvent != -1 { if err := menu.setSelection(bl, typ, gameEvent); err != nil { return err } break } else if menu.currentBindingLayout != nil { if err := menu.clearSelection(); err != nil { return err } } } return nil } func (menu *KeyBindingMenu) onMouseMove(event d2interface.MouseMoveEvent) { if !menu.IsOpen() { return } if menu.scrollbar != nil && menu.scrollbar.IsSliderClicked() { menu.scrollbar.OnMouseMove(event) } } func (menu *KeyBindingMenu) onMouseButtonUp() { if !menu.IsOpen() { return } if menu.scrollbar != nil { menu.scrollbar.SetSliderClicked(false) menu.scrollbar.SetArrowDownClicked(false) menu.scrollbar.SetArrowUpClicked(false) } } func (menu *KeyBindingMenu) getPendingChangeByKey(key d2enum.Key) (*bindingChange, *KeyBinding, d2enum.GameEvent, KeyBindingType) { var ( existingBinding *KeyBinding gameEvent d2enum.GameEvent bindingType KeyBindingType ) for ge, existingChange := range menu.changesToBeSaved { if existingChange.primary == key { bindingType = KeyBindingTypePrimary } else if existingChange.secondary == key { bindingType = KeyBindingTypeSecondary } if bindingType != -1 { existingBinding = existingChange.target gameEvent = ge return existingChange, existingBinding, gameEvent, bindingType } } return nil, nil, -1, KeyBindingTypeNone } func (menu *KeyBindingMenu) saveKeyChange(key d2enum.Key) error { changeExisting, existingBinding, gameEvent, bindingType := menu.getPendingChangeByKey(key) if changeExisting == nil { existingBinding, gameEvent, bindingType = menu.keyMap.GetBindingByKey(key) } if existingBinding != nil && changeExisting == nil { changeExisting = &bindingChange{ target: existingBinding, primary: existingBinding.Primary, secondary: existingBinding.Secondary, } menu.changesToBeSaved[gameEvent] = changeExisting } changeCurrent := menu.changesToBeSaved[menu.currentBindingLayout.gameEvent] if changeCurrent == nil { changeCurrent = &bindingChange{ target: menu.currentBindingLayout.binding, primary: menu.currentBindingLayout.binding.Primary, secondary: menu.currentBindingLayout.binding.Secondary, } menu.changesToBeSaved[menu.currentBindingLayout.gameEvent] = changeCurrent } switch menu.currentBindingModifierType { case KeyBindingTypePrimary: changeCurrent.primary = key case KeyBindingTypeSecondary: changeCurrent.secondary = key } if changeExisting != nil { if bindingType == KeyBindingTypePrimary { changeExisting.primary = -1 } if bindingType == KeyBindingTypeSecondary { changeExisting.secondary = -1 } } if err := menu.setBindingLabels( changeCurrent.primary, changeCurrent.secondary, menu.currentBindingLayout, ); err != nil { return err } if changeExisting != nil { for _, bindingLayout := range menu.bindingLayouts { if bindingLayout.binding == changeExisting.target { if err := menu.setBindingLabels(changeExisting.primary, changeExisting.secondary, bindingLayout); err != nil { return err } } } } return nil } func (menu *KeyBindingMenu) setBindingLabels(primary, secondary d2enum.Key, bl *bindingLayout) error { noneStr := menu.keyMap.KeyToString(-1) if primary != -1 { if err := bl.SetPrimaryBindingTextAndColor(menu.keyMap.KeyToString(primary), d2util.Color(d2gui.ColorBrown)); err != nil { return err } } else { if err := bl.SetPrimaryBindingTextAndColor(noneStr, d2util.Color(d2gui.ColorGrey)); err != nil { return err } } if secondary != -1 { if err := bl.SetSecondaryBindingTextAndColor(menu.keyMap.KeyToString(secondary), d2util.Color(d2gui.ColorBrown)); err != nil { return err } } else { if err := bl.SetSecondaryBindingTextAndColor(noneStr, d2util.Color(d2gui.ColorGrey)); err != nil { return err } } if primary == -1 && secondary == -1 { if err := bl.primaryLabel.SetColor(d2util.Color(d2gui.ColorRed)); err != nil { return err } if err := bl.secondaryLabel.SetColor(d2util.Color(d2gui.ColorRed)); err != nil { return err } } return nil } func (menu *KeyBindingMenu) onCancelClicked() error { for gameEvent := range menu.changesToBeSaved { for _, bindingLayout := range menu.bindingLayouts { if bindingLayout.gameEvent == gameEvent { if err := menu.setBindingLabels(bindingLayout.binding.Primary, bindingLayout.binding.Secondary, bindingLayout); err != nil { return err } } } } menu.changesToBeSaved = make(map[d2enum.GameEvent]*bindingChange) if menu.currentBindingLayout != nil { if err := menu.clearSelection(); err != nil { return err } } if err := menu.Close(); err != nil { return err } menu.escapeMenu.showLayout(optionsLayoutID) return nil } func (menu *KeyBindingMenu) reload() error { for _, bl := range menu.bindingLayouts { if bl.binding != nil { if err := menu.setBindingLabels(bl.binding.Primary, bl.binding.Secondary, bl); err != nil { return err } } } return nil } func (menu *KeyBindingMenu) clearSelection() error { if menu.currentBindingLayout != nil { if err := menu.currentBindingLayout.Reset(); err != nil { return err } menu.lastBindingLayout = menu.currentBindingLayout menu.currentBindingLayout = nil menu.currentBindingModifier = -1 menu.currentBindingModifierType = -1 } return nil } func (menu *KeyBindingMenu) onDefaultClicked() error { menu.keyMap.ResetToDefault() if err := menu.reload(); err != nil { return err } menu.changesToBeSaved = make(map[d2enum.GameEvent]*bindingChange) return menu.clearSelection() } func (menu *KeyBindingMenu) onAcceptClicked() error { for gameEvent, change := range menu.changesToBeSaved { menu.keyMap.SetPrimaryBinding(gameEvent, change.primary) menu.keyMap.SetSecondaryBinding(gameEvent, change.secondary) } menu.changesToBeSaved = make(map[d2enum.GameEvent]*bindingChange) return menu.clearSelection() } // OnKeyDown will assign the new key to the selected binding if any func (menu *KeyBindingMenu) OnKeyDown(event d2interface.KeyEvent) error { if menu.isAwaitingKeyDown { key := event.Key() if key == d2enum.KeyEscape { if menu.currentBindingLayout != nil { menu.lastBindingLayout = menu.currentBindingLayout if err := menu.currentBindingLayout.Reset(); err != nil { return err } if err := menu.clearSelection(); err != nil { return err } } } else { if err := menu.saveKeyChange(key); err != nil { return err } } menu.isAwaitingKeyDown = false } return nil } // Advance computes the state of the elements of the menu overtime func (menu *KeyBindingMenu) Advance(elapsed float64) error { if menu.scrollbar != nil { if err := menu.scrollbar.Advance(elapsed); err != nil { return err } } return nil } // Render draws the different element of the menu on the target surface func (menu *KeyBindingMenu) Render(target d2interface.Surface) error { if menu.IsOpen() { if err := menu.Box.Render(target); err != nil { return err } if menu.scrollbar != nil { menu.scrollbar.Render(target) } if menu.currentBindingLayout != nil { x, y := menu.currentBindingLayout.wrapperLayout.Sx, menu.currentBindingLayout.wrapperLayout.Sy w, h := menu.currentBindingLayout.wrapperLayout.GetSize() target.PushTranslation(x, y) target.DrawRect(w, h, d2util.Color(selectionBackgroundColor)) target.Pop() } } return nil }