Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js (revision 25829) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js (revision 25830) @@ -1,47 +1,51 @@ PlayerSettingControls.PlayerColor = class PlayerColor extends GameSettingControlDropdown { constructor(...args) { super(...args); g_GameSettings.playerColor.watch(() => this.render(), ["values", "locked"]); this.render(); } setControl() { this.dropdown = Engine.GetGUIObjectByName("playerColor[" + this.playerIndex + "]"); this.playerBackgroundColor = Engine.GetGUIObjectByName("playerBackgroundColor[" + this.playerIndex + "]"); this.playerColorHeading = Engine.GetGUIObjectByName("playerColorHeading"); } render() { if (g_GameSettings.playerCount.nbPlayers < this.playerIndex + 1) return; - let hidden = !g_IsController || g_GameSettings.map.type == "scenario"; + const hidden = !g_IsController || g_GameSettings.map.type == "scenario"; this.dropdown.hidden = hidden; this.playerColorHeading.hidden = hidden; - let value = g_GameSettings.playerColor.get(this.playerIndex); - this.setSelectedValue(value); + const value = g_GameSettings.playerColor.get(this.playerIndex); this.playerBackgroundColor.sprite = "color:" + rgbToGuiColor(value, 100); this.values = g_GameSettings.playerColor.available; this.dropdown.list = this.values.map(color => coloredText(this.ColorIcon, rgbToGuiColor(color))); this.dropdown.list_data = this.values.map((color, i) => i); + this.setSelectedValue(this.values.map((color, i) => { + if (color.r === value.r && color.g === value.g && color.b === value.b) + return i; + return undefined; + }).filter(x => x !== undefined)?.[0] ?? -1); } onSelectionChange(itemIdx) { g_GameSettings.playerColor.setColor(this.playerIndex, this.values[itemIdx]); this.gameSettingsController.setNetworkInitAttributes(); } }; PlayerSettingControls.PlayerColor.prototype.Tooltip = translate("Pick a color."); PlayerSettingControls.PlayerColor.prototype.ColorIcon = "■"; Index: ps/trunk/source/gui/ObjectTypes/CDropDown.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CDropDown.cpp (revision 25829) +++ ps/trunk/source/gui/ObjectTypes/CDropDown.cpp (revision 25830) @@ -1,487 +1,490 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CDropDown.h" #include "gui/CGUI.h" #include "gui/IGUIScrollBar.h" #include "gui/SettingTypes/CGUIColor.h" #include "gui/SettingTypes/CGUIList.h" #include "lib/external_libraries/libsdl.h" #include "lib/timer.h" #include "ps/Profile.h" CDropDown::CDropDown(CGUI& pGUI) : CList(pGUI), m_Open(), m_HideScrollBar(), m_ElementHighlight(-1), m_ButtonWidth(this, "button_width"), m_DropDownSize(this, "dropdown_size"), m_DropDownBuffer(this, "dropdown_buffer"), m_MinimumVisibleItems(this, "minimum_visible_items"), m_SoundClosed(this, "sound_closed"), m_SoundEnter(this, "sound_enter"), m_SoundLeave(this, "sound_leave"), m_SoundOpened(this, "sound_opened"), // Setting "sprite" is registered by CList and used as the background m_SpriteDisabled(this, "sprite_disabled"), m_SpriteOverlayDisabled(this, "sprite_overlay_disabled"), m_SpriteList(this, "sprite_list"), // Background of the drop down list m_SpriteListOverlay(this, "sprite_list_overlay"), // Overlay above the drop down list m_Sprite2(this, "sprite2"), // Button that sits to the right m_Sprite2Over(this, "sprite2_over"), m_Sprite2Pressed(this, "sprite2_pressed"), m_Sprite2Disabled(this, "sprite2_disabled"), m_TextColorDisabled(this, "textcolor_disabled") // Add these in CList! And implement TODO //RegisterSetting("textcolor_over"); //RegisterSetting("textcolor_pressed"); { m_ScrollBar.Set(true, true); } CDropDown::~CDropDown() { } void CDropDown::SetupText() { SetupListRect(); CList::SetupText(); } void CDropDown::UpdateCachedSize() { CList::UpdateCachedSize(); SetupText(); } void CDropDown::HandleMessage(SGUIMessage& Message) { // CList::HandleMessage(Message); placed after the switch! switch (Message.type) { case GUIM_SETTINGS_UPDATED: { // Update cached list rect if (Message.value == "size" || Message.value == "absolute" || Message.value == "dropdown_size" || Message.value == "dropdown_buffer" || Message.value == "minimum_visible_items" || Message.value == "scrollbar_style" || Message.value == "button_width") { SetupListRect(); } break; } case GUIM_MOUSE_MOTION: { if (!m_Open) break; CVector2D mouse = m_pGUI.GetMousePos(); if (!GetListRect().PointInside(mouse)) break; const float scroll = m_ScrollBar ? GetScrollBar(0).GetPos() : 0.f; CRect rect = GetListRect(); mouse.Y += scroll; int set = -1; for (int i = 0; i < static_cast(m_List->m_Items.size()); ++i) { if (mouse.Y >= rect.top + m_ItemsYPositions[i] && mouse.Y < rect.top + m_ItemsYPositions[i+1] && // mouse is not over scroll-bar (m_HideScrollBar || mouse.X < GetScrollBar(0).GetOuterRect().left || mouse.X > GetScrollBar(0).GetOuterRect().right)) { set = i; } } if (set != -1) { m_ElementHighlight = set; //UpdateAutoScroll(); } break; } case GUIM_MOUSE_ENTER: { if (m_Enabled) PlaySound(m_SoundEnter); break; } case GUIM_MOUSE_LEAVE: { m_ElementHighlight = m_Selected; if (m_Enabled) PlaySound(m_SoundLeave); break; } // We can't inherent this routine from CList, because we need to include // a mouse click to open the dropdown, also the coordinates are changed. case GUIM_MOUSE_PRESS_LEFT: { if (!m_Enabled) { PlaySound(m_SoundDisabled); break; } if (!m_Open) { if (m_List->m_Items.empty()) return; m_Open = true; GetScrollBar(0).SetZ(GetBufferedZ()); m_ElementHighlight = m_Selected; // Start at the position of the selected item, if possible. - GetScrollBar(0).SetPos(m_ItemsYPositions.empty() ? 0 : m_ItemsYPositions[m_ElementHighlight] - 60); + if (m_ItemsYPositions.empty() || m_ElementHighlight < 0 || static_cast(m_ElementHighlight) >= m_ItemsYPositions.size()) + GetScrollBar(0).SetPos(0); + else + GetScrollBar(0).SetPos(m_ItemsYPositions[m_ElementHighlight] - 60); PlaySound(m_SoundOpened); return; // overshadow } else { const CVector2D& mouse = m_pGUI.GetMousePos(); // If the regular area is pressed, then abort, and close. if (m_CachedActualSize.PointInside(mouse)) { m_Open = false; GetScrollBar(0).SetZ(GetBufferedZ()); PlaySound(m_SoundClosed); return; // overshadow } if (m_HideScrollBar || mouse.X < GetScrollBar(0).GetOuterRect().left || mouse.X > GetScrollBar(0).GetOuterRect().right || mouse.Y < GetListRect().top) { m_Open = false; GetScrollBar(0).SetZ(GetBufferedZ()); } } break; } case GUIM_MOUSE_WHEEL_DOWN: { // Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar. if (m_Open || !m_Enabled) break; m_ElementHighlight = m_Selected; if (m_ElementHighlight + 1 >= (int)m_ItemsYPositions.size() - 1) break; ++m_ElementHighlight; m_Selected.Set(m_ElementHighlight, true); break; } case GUIM_MOUSE_WHEEL_UP: { // Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar. if (m_Open || !m_Enabled) break; m_ElementHighlight = m_Selected; if (m_ElementHighlight - 1 < 0) break; --m_ElementHighlight; m_Selected.Set(m_ElementHighlight, true); break; } case GUIM_LOST_FOCUS: { if (m_Open) PlaySound(m_SoundClosed); m_Open = false; break; } case GUIM_LOAD: SetupListRect(); break; default: break; } // Important that this is after, so that overshadowed implementations aren't processed CList::HandleMessage(Message); // As HandleMessage functions return void, we need to manually verify // whether the child list's items were modified. if (CList::GetModified()) SetupText(); } InReaction CDropDown::ManuallyHandleKeys(const SDL_Event_* ev) { InReaction result = IN_PASS; bool update_highlight = false; if (ev->ev.type == SDL_KEYDOWN) { int szChar = ev->ev.key.keysym.sym; switch (szChar) { case '\r': m_Open = false; result = IN_HANDLED; break; case SDLK_HOME: case SDLK_END: case SDLK_UP: case SDLK_DOWN: case SDLK_PAGEUP: case SDLK_PAGEDOWN: if (!m_Open) return IN_PASS; // Set current selected item to highlighted, before // then really processing these in CList::ManuallyHandleKeys() m_Selected.Set(m_ElementHighlight, true); update_highlight = true; break; default: // If we have typed a character try to get the closest element to it. // TODO: not too nice and doesn't deal with dashes. if (m_Open && ((szChar >= SDLK_a && szChar <= SDLK_z) || szChar == SDLK_SPACE || (szChar >= SDLK_0 && szChar <= SDLK_9) || (szChar >= SDLK_KP_1 && szChar <= SDLK_KP_0))) { // arbitrary 1 second limit to add to string or start fresh. // maximal amount of characters is 100, which imo is far more than enough. if (timer_Time() - m_TimeOfLastInput > 1.0 || m_InputBuffer.length() >= 100) m_InputBuffer = szChar; else m_InputBuffer += szChar; m_TimeOfLastInput = timer_Time(); // let's look for the closest element // basically it's alphabetic order and "as many letters as we can get". int closest = -1; int bestIndex = -1; int difference = 1250; for (int i = 0; i < static_cast(m_List->m_Items.size()); ++i) { int indexOfDifference = 0; int diff = 0; for (size_t j = 0; j < m_InputBuffer.length(); ++j) { diff = std::abs(static_cast(m_List->m_Items[i].GetRawString().LowerCase()[j]) - static_cast(m_InputBuffer[j])); if (diff == 0) indexOfDifference = j+1; else break; } if (indexOfDifference > bestIndex || (indexOfDifference >= bestIndex && diff < difference)) { bestIndex = indexOfDifference; closest = i; difference = diff; } } // let's select the closest element. There should basically always be one. if (closest != -1) { m_Selected.Set(closest, true); update_highlight = true; GetScrollBar(0).SetPos(m_ItemsYPositions[closest] - 60); } result = IN_HANDLED; } break; } } if (CList::ManuallyHandleKeys(ev) == IN_HANDLED) result = IN_HANDLED; if (update_highlight) m_ElementHighlight = m_Selected; return result; } void CDropDown::SetupListRect() { const CSize2D windowSize = m_pGUI.GetWindowSize(); if (m_ItemsYPositions.empty()) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer, m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize); m_HideScrollBar = false; } // Too many items so use a scrollbar else if (m_ItemsYPositions.back() > m_DropDownSize) { // Place items below if at least some items can be placed below if (m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize <= windowSize.Height) m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer, m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_DropDownSize); else if ((m_ItemsYPositions.size() > m_MinimumVisibleItems && windowSize.Height - m_CachedActualSize.bottom - m_DropDownBuffer >= m_ItemsYPositions[m_MinimumVisibleItems]) || m_CachedActualSize.top < windowSize.Height - m_CachedActualSize.bottom) m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer, m_CachedActualSize.right, windowSize.Height); // Not enough space below, thus place items above else m_CachedListRect = CRect(m_CachedActualSize.left, std::max(0.f, m_CachedActualSize.top - m_DropDownBuffer - m_DropDownSize), m_CachedActualSize.right, m_CachedActualSize.top - m_DropDownBuffer); m_HideScrollBar = false; } else { // Enough space below, no scrollbar needed if (m_CachedActualSize.bottom + m_DropDownBuffer + m_ItemsYPositions.back() <= windowSize.Height) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer, m_CachedActualSize.right, m_CachedActualSize.bottom + m_DropDownBuffer + m_ItemsYPositions.back()); m_HideScrollBar = true; } // Enough space below for some items, but not all, so place items below and use a scrollbar else if ((m_ItemsYPositions.size() > m_MinimumVisibleItems && windowSize.Height - m_CachedActualSize.bottom - m_DropDownBuffer >= m_ItemsYPositions[m_MinimumVisibleItems]) || m_CachedActualSize.top < windowSize.Height - m_CachedActualSize.bottom) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + m_DropDownBuffer, m_CachedActualSize.right, windowSize.Height); m_HideScrollBar = false; } // Not enough space below, thus place items above. Hide the scrollbar accordingly else { m_CachedListRect = CRect(m_CachedActualSize.left, std::max(0.f, m_CachedActualSize.top - m_DropDownBuffer - m_ItemsYPositions.back()), m_CachedActualSize.right, m_CachedActualSize.top - m_DropDownBuffer); m_HideScrollBar = m_CachedActualSize.top > m_ItemsYPositions.back() + m_DropDownBuffer; } } } CRect CDropDown::GetListRect() const { return m_CachedListRect; } bool CDropDown::IsMouseOver() const { if (m_Open) { CRect rect(m_CachedActualSize.left, std::min(m_CachedActualSize.top, GetListRect().top), m_CachedActualSize.right, std::max(m_CachedActualSize.bottom, GetListRect().bottom)); return rect.PointInside(m_pGUI.GetMousePos()); } else return m_CachedActualSize.PointInside(m_pGUI.GetMousePos()); } void CDropDown::Draw(CCanvas2D& canvas) { const CGUISpriteInstance& sprite = m_Enabled ? m_Sprite : m_SpriteDisabled; const CGUISpriteInstance& spriteOverlay = m_Enabled ? m_SpriteOverlay : m_SpriteOverlayDisabled; m_pGUI.DrawSprite(sprite, canvas, m_CachedActualSize); if (m_ButtonWidth > 0.f) { CRect rect(m_CachedActualSize.right - m_ButtonWidth, m_CachedActualSize.top, m_CachedActualSize.right, m_CachedActualSize.bottom); if (!m_Enabled) { m_pGUI.DrawSprite(*m_Sprite2Disabled ? m_Sprite2Disabled : m_Sprite2, canvas, rect); } else if (m_Open) { m_pGUI.DrawSprite(*m_Sprite2Pressed ? m_Sprite2Pressed : m_Sprite2, canvas, rect); } else if (m_MouseHovering) { m_pGUI.DrawSprite(*m_Sprite2Over ? m_Sprite2Over : m_Sprite2, canvas, rect); } else m_pGUI.DrawSprite(m_Sprite2, canvas, rect); } if (m_Selected != -1) // TODO: Maybe check validity completely? { CRect cliparea(m_CachedActualSize.left, m_CachedActualSize.top, m_CachedActualSize.right - m_ButtonWidth, m_CachedActualSize.bottom); CVector2D pos(m_CachedActualSize.left, m_CachedActualSize.top); DrawText(canvas, m_Selected, m_Enabled ? m_TextColorSelected : m_TextColorDisabled, pos, cliparea); } if (m_Open) { // Disable scrollbar during drawing without sending a setting-changed message const bool old = m_ScrollBar; // TODO: drawScrollbar as an argument of DrawList? if (m_HideScrollBar) m_ScrollBar.Set(false, false); DrawList(canvas, m_ElementHighlight, m_SpriteList, m_SpriteListOverlay, m_SpriteSelectArea, m_SpriteSelectAreaOverlay, m_TextColor); if (m_HideScrollBar) m_ScrollBar.Set(old, false); } m_pGUI.DrawSprite(spriteOverlay, canvas, m_CachedActualSize); } // When a dropdown list is opened, it needs to be visible above all the other // controls on the page. The only way I can think of to do this is to increase // its z value when opened, so that it's probably on top. float CDropDown::GetBufferedZ() const { float bz = CList::GetBufferedZ(); if (m_Open) return std::min(bz + 500.f, 1000.f); // TODO - don't use magic number for max z value else return bz; }