Files
scummvm-cursorfix/engines/ags/shared/gui/gui_listbox.cpp
2026-02-02 04:50:13 +01:00

454 lines
13 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/std/algorithm.h"
#include "ags/shared/gui/gui_listbox.h"
#include "ags/shared/ac/game_version.h"
#include "ags/shared/font/fonts.h"
#include "ags/shared/gui/gui_main.h"
#include "ags/shared/util/stream.h"
#include "ags/shared/util/string_utils.h"
namespace AGS3 {
namespace AGS {
namespace Shared {
GUIListBox::GUIListBox() {
ItemCount = 0;
SelectedItem = 0;
TopItem = 0;
RowHeight = 0;
VisibleItemCount = 0;
Font = 0;
TextColor = 0;
SelectedTextColor = 7;
ListBoxFlags = kListBox_DefFlags;
SelectedBgColor = 16;
TextAlignment = kHAlignLeft;
_scEventCount = 1;
_scEventNames[0] = "SelectionChanged";
_scEventArgs[0] = "GUIControl *control";
}
bool GUIListBox::HasAlphaChannel() const {
return is_font_antialiased(Font);
}
int GUIListBox::GetItemAt(int x, int y) const {
if (RowHeight <= 0 || IsInRightMargin(x))
return -1;
int index = y / RowHeight + TopItem;
if (index < 0 || index >= ItemCount)
return -1;
return index;
}
bool GUIListBox::AreArrowsShown() const {
return (ListBoxFlags & kListBox_ShowArrows) != 0;
}
bool GUIListBox::IsBorderShown() const {
return (ListBoxFlags & kListBox_ShowBorder) != 0;
}
bool GUIListBox::IsSvgIndex() const {
return (ListBoxFlags & kListBox_SvgIndex) != 0;
}
bool GUIListBox::IsInRightMargin(int x) const {
if (x >= (_width - get_fixed_pixel_size(6)) && IsBorderShown() && AreArrowsShown())
return 1;
return 0;
}
Rect GUIListBox::CalcGraphicRect(bool clipped) {
if (clipped)
return RectWH(0, 0, _width, _height);
// TODO: need to find a way to text position, or there'll be some repetition
// have to precache text and size on some events:
// - translation change
// - macro value change (score, overhotspot etc)
Rect rc = RectWH(0, 0, _width, _height);
UpdateMetrics();
const int width = _width - 1;
const int pixel_size = get_fixed_pixel_size(1);
int right_hand_edge = width - pixel_size - 1;
// calculate the scroll bar's width if necessary
if (ItemCount > VisibleItemCount &&IsBorderShown() && AreArrowsShown())
right_hand_edge -= get_fixed_pixel_size(7);
Line max_line;
for (int item = 0; (item < VisibleItemCount) && (item + TopItem < ItemCount); ++item) {
int at_y = pixel_size + item * RowHeight;
int item_index = item + TopItem;
PrepareTextToDraw(Items[item_index]);
Line lpos = GUI::CalcTextPositionHor(_textToDraw.GetCStr(), Font, 1 + pixel_size, right_hand_edge, at_y + 1,
(FrameAlignment)TextAlignment);
max_line.X2 = MAX(max_line.X2, lpos.X2);
}
int last_line_y = pixel_size + 1 + (VisibleItemCount - 1) * RowHeight;
// Include font fixes for the first and last text line,
// in case graphical height is different, and there's a VerticalOffset
Line vextent = GUI::CalcFontGraphicalVExtent(Font);
Rect text_rc = RectWH(0, vextent.Y1, max_line.X2 - max_line.X1 + 1, last_line_y + (vextent.Y2 - vextent.Y1));
return SumRects(rc, text_rc);
}
int GUIListBox::AddItem(const String &text) {
Items.push_back(text);
SavedGameIndex.push_back(-1);
ItemCount++;
MarkChanged();
return ItemCount - 1;
}
void GUIListBox::Clear() {
if (Items.empty())
return;
Items.clear();
SavedGameIndex.clear();
ItemCount = 0;
SelectedItem = 0;
TopItem = 0;
MarkChanged();
}
void GUIListBox::Draw(Bitmap *ds, int x, int y) {
const int width = _width - 1;
const int height = _height - 1;
const int pixel_size = get_fixed_pixel_size(1);
color_t text_color = ds->GetCompatibleColor(TextColor);
color_t draw_color = ds->GetCompatibleColor(TextColor);
if (IsBorderShown()) {
ds->DrawRect(Rect(x, y, x + width, y + height), draw_color);
if (pixel_size > 1)
ds->DrawRect(Rect(x + 1, y + 1, x + width - 1, y + height - 1), draw_color);
}
int right_hand_edge = (x + width) - pixel_size - 1;
// update the RowHeight and VisibleItemCount
// FIXME: find a way to update this whenever relevant things change in the engine
UpdateMetrics();
// draw the scroll bar in if necessary
bool scrollbar = (ItemCount > VisibleItemCount) && IsBorderShown() && AreArrowsShown();
if (scrollbar) {
int xstrt, ystrt;
ds->DrawRect(Rect(x + width - get_fixed_pixel_size(7), y, (x + (pixel_size - 1) + width) - get_fixed_pixel_size(7), y + height), draw_color);
ds->DrawRect(Rect(x + width - get_fixed_pixel_size(7), y + height / 2, x + width, y + height / 2 + (pixel_size - 1)), draw_color);
xstrt = (x + width - get_fixed_pixel_size(6)) + (pixel_size - 1);
ystrt = (y + height - 3) - get_fixed_pixel_size(5);
draw_color = ds->GetCompatibleColor(TextColor);
ds->DrawTriangle(Triangle(xstrt, ystrt, xstrt + get_fixed_pixel_size(4), ystrt,
xstrt + get_fixed_pixel_size(2),
ystrt + get_fixed_pixel_size(5)), draw_color);
ystrt = y + 3;
ds->DrawTriangle(Triangle(xstrt, ystrt + get_fixed_pixel_size(5),
xstrt + get_fixed_pixel_size(4),
ystrt + get_fixed_pixel_size(5),
xstrt + get_fixed_pixel_size(2), ystrt), draw_color);
right_hand_edge -= get_fixed_pixel_size(7);
}
Rect old_clip = ds->GetClip();
if (scrollbar && GUI::Options.ClipControls)
ds->SetClip(Rect(x, y, right_hand_edge + 1, y + _height - 1));
for (int item = 0; (item < VisibleItemCount) && (item + TopItem < ItemCount); ++item) {
int at_y = y + pixel_size + item * RowHeight;
if (item + TopItem == SelectedItem) {
text_color = ds->GetCompatibleColor(SelectedTextColor);
if (SelectedBgColor > 0) {
int stretch_to = (x + width) - pixel_size;
// draw the SelectedItem item bar (if colour not transparent)
draw_color = ds->GetCompatibleColor(SelectedBgColor);
if ((VisibleItemCount < ItemCount) && IsBorderShown() && AreArrowsShown())
stretch_to -= get_fixed_pixel_size(7);
ds->FillRect(Rect(x + pixel_size, at_y, stretch_to, at_y + RowHeight - pixel_size), draw_color);
}
} else
text_color = ds->GetCompatibleColor(TextColor);
int item_index = item + TopItem;
PrepareTextToDraw(Items[item_index]);
GUI::DrawTextAlignedHor(ds, _textToDraw.GetCStr(), Font, text_color, x + 1 + pixel_size, right_hand_edge, at_y + 1,
(FrameAlignment)TextAlignment);
}
ds->SetClip(old_clip);
}
int GUIListBox::InsertItem(int index, const String &text) {
if (index < 0 || index > ItemCount)
return -1;
Items.insert(Items.begin() + index, text);
SavedGameIndex.insert(SavedGameIndex.begin() + index, -1);
if (SelectedItem >= index)
SelectedItem++;
ItemCount++;
MarkChanged();
return ItemCount - 1;
}
void GUIListBox::RemoveItem(int index) {
if (index < 0 || index >= ItemCount)
return;
Items.erase(Items.begin() + index);
SavedGameIndex.erase(SavedGameIndex.begin() + index);
ItemCount--;
if (SelectedItem > index)
SelectedItem--;
if (SelectedItem >= ItemCount)
SelectedItem = -1;
MarkChanged();
}
void GUIListBox::SetShowArrows(bool on) {
if (on != ((ListBoxFlags & kListBox_ShowArrows) != 0))
MarkChanged();
if (on)
ListBoxFlags |= kListBox_ShowArrows;
else
ListBoxFlags &= ~kListBox_ShowArrows;
}
void GUIListBox::SetShowBorder(bool on) {
if (on != ((ListBoxFlags & kListBox_ShowBorder) != 0))
MarkChanged();
if (on)
ListBoxFlags |= kListBox_ShowBorder;
else
ListBoxFlags &= ~kListBox_ShowBorder;
}
void GUIListBox::SetSvgIndex(bool on) {
if (on)
ListBoxFlags |= kListBox_SvgIndex;
else
ListBoxFlags &= ~kListBox_SvgIndex;
}
void GUIListBox::SetFont(int font) {
if (Font == font)
return;
Font = font;
UpdateMetrics();
MarkChanged();
}
void GUIListBox::SetItemText(int index, const String &text) {
if ((index >= 0) && (index < ItemCount) && (text != Items[index])) {
Items[index] = text;
MarkChanged();
}
}
bool GUIListBox::OnMouseDown() {
if (IsInRightMargin(MousePos.X)) {
int top_item = TopItem;
if (MousePos.Y < _height / 2 && TopItem > 0)
top_item = TopItem - 1;
if (MousePos.Y >= _height / 2 && ItemCount > TopItem + VisibleItemCount)
top_item = TopItem + 1;
if (TopItem != top_item) {
TopItem = top_item;
MarkChanged();
}
return false;
}
int sel = GetItemAt(MousePos.X, MousePos.Y);
if (sel < 0)
return false;
if (sel != SelectedItem) {
SelectedItem = sel;
MarkChanged();
}
IsActivated = true;
return false;
}
void GUIListBox::OnMouseMove(int x_, int y_) {
MousePos.X = x_ - X;
MousePos.Y = y_ - Y;
}
void GUIListBox::OnResized() {
UpdateMetrics();
MarkChanged();
}
void GUIListBox::UpdateMetrics() {
int font_height = (_G(loaded_game_file_version) < kGameVersion_360_21) ?
get_font_height(Font) : get_font_height_outlined(Font);
RowHeight = font_height + get_fixed_pixel_size(2); // +1 top/bottom margin
VisibleItemCount = _height / RowHeight;
if (ItemCount <= VisibleItemCount)
TopItem = 0; // reset scroll if all items are visible
}
// TODO: replace string serialization with StrUtil::ReadString and WriteString
// methods in the future, to keep this organized.
void GUIListBox::WriteToFile(Stream *out) const {
GUIObject::WriteToFile(out);
out->WriteInt32(ItemCount);
out->WriteInt32(Font);
out->WriteInt32(TextColor);
out->WriteInt32(SelectedTextColor);
out->WriteInt32(ListBoxFlags);
out->WriteInt32(TextAlignment);
out->WriteInt32(SelectedBgColor);
for (int i = 0; i < ItemCount; ++i)
Items[i].Write(out);
}
void GUIListBox::ReadFromFile(Stream *in, GuiVersion gui_version) {
Clear();
GUIObject::ReadFromFile(in, gui_version);
ItemCount = in->ReadInt32();
if (gui_version < kGuiVersion_350) { // NOTE: reading into actual variables only for old savegame support
SelectedItem = in->ReadInt32();
TopItem = in->ReadInt32();
MousePos.X = in->ReadInt32();
MousePos.Y = in->ReadInt32();
RowHeight = in->ReadInt32();
VisibleItemCount = in->ReadInt32();
}
Font = in->ReadInt32();
TextColor = in->ReadInt32();
SelectedTextColor = in->ReadInt32();
ListBoxFlags = in->ReadInt32();
// reverse particular flags from older format
if (gui_version < kGuiVersion_350)
ListBoxFlags ^= kListBox_OldFmtXorMask;
if (gui_version >= kGuiVersion_272b) {
if (gui_version < kGuiVersion_350) {
TextAlignment = ConvertLegacyGUIAlignment((LegacyGUIAlignment)in->ReadInt32());
in->ReadInt32(); // reserved1
} else {
TextAlignment = (HorAlignment)in->ReadInt32();
}
} else {
TextAlignment = kHAlignLeft;
}
if (gui_version >= kGuiVersion_unkn_107) {
SelectedBgColor = in->ReadInt32();
} else {
SelectedBgColor = TextColor;
if (SelectedBgColor == 0)
SelectedBgColor = 16;
}
// NOTE: we leave items in game data format as a potential support for defining
// ListBox contents at design-time, although Editor does not support it as of 3.5.0.
Items.resize(ItemCount);
SavedGameIndex.resize(ItemCount, -1);
for (int i = 0; i < ItemCount; ++i) {
Items[i].Read(in);
}
if (gui_version >= kGuiVersion_272d && gui_version < kGuiVersion_350 &&
(ListBoxFlags & kListBox_SvgIndex)) { // NOTE: reading into actual variables only for old savegame support
for (int i = 0; i < ItemCount; ++i)
SavedGameIndex[i] = in->ReadInt16();
}
if (TextColor == 0)
TextColor = 16;
UpdateMetrics();
}
void GUIListBox::ReadFromSavegame(Stream *in, GuiSvgVersion svg_ver) {
GUIObject::ReadFromSavegame(in, svg_ver);
// Properties
ListBoxFlags = in->ReadInt32();
Font = in->ReadInt32();
if (svg_ver < kGuiSvgVersion_350) {
// reverse particular flags from older format
ListBoxFlags ^= kListBox_OldFmtXorMask;
} else {
SelectedBgColor = in->ReadInt32();
SelectedTextColor = in->ReadInt32();
TextAlignment = (HorAlignment)in->ReadInt32();
TextColor = in->ReadInt32();
}
// Items
ItemCount = in->ReadInt32();
Items.resize(ItemCount);
SavedGameIndex.resize(ItemCount);
for (int i = 0; i < ItemCount; ++i)
Items[i] = StrUtil::ReadString(in);
// TODO: investigate this, it might be unreasonable to save and read
// savegame index like that because list of savegames may easily change
// in between writing and restoring the game. Perhaps clearing and forcing
// this list to update on load somehow may make more sense.
if (ListBoxFlags & kListBox_SvgIndex)
for (int i = 0; i < ItemCount; ++i)
SavedGameIndex[i] = in->ReadInt16();
TopItem = in->ReadInt32();
SelectedItem = in->ReadInt32();
UpdateMetrics();
}
void GUIListBox::WriteToSavegame(Stream *out) const {
GUIObject::WriteToSavegame(out);
// Properties
out->WriteInt32(ListBoxFlags);
out->WriteInt32(Font);
out->WriteInt32(SelectedBgColor);
out->WriteInt32(SelectedTextColor);
out->WriteInt32(TextAlignment);
out->WriteInt32(TextColor);
// Items
out->WriteInt32(ItemCount);
for (int i = 0; i < ItemCount; ++i)
StrUtil::WriteString(Items[i], out);
if (ListBoxFlags & kListBox_SvgIndex)
for (int i = 0; i < ItemCount; ++i)
out->WriteInt16(SavedGameIndex[i]);
out->WriteInt32(TopItem);
out->WriteInt32(SelectedItem);
}
} // namespace Shared
} // namespace AGS
} // namespace AGS3