Index: ps/trunk/binaries/data/mods/mod/gui/gui.rnc
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/gui.rnc (revision 24432)
+++ ps/trunk/binaries/data/mods/mod/gui/gui.rnc (revision 24433)
@@ -1,289 +1,291 @@
namespace a = "http://relaxng.org/ns/compatibility/annotations/1.0"
##
# NOTE: To modify this Relax NG grammar, edit the Relax NG Compact (.rnc) file
# and use a converter tool like trang to generate the Relax NG XML (.rng) file
##
start = object | objects | setup | sprites | styles
##
# Types #
##
# xsd:boolean could be used instead of this definition,
# though it considers "1" & "0" as valid values.
bool = "true" | "false"
align = "left" | "center" | "right"
valign = "top" | "center" | "bottom"
wrapmode = "repeat" | "mirrored_repeat" | "clamp_to_edge"
coord = xsd:string { pattern = "-?\d*\.?\d+%?([\+\-]\d*\.?\d+%?)*" }
clientarea = list { coord, coord, coord, coord }
# color can be a name or "R G B A" format string
rgba = list { xsd:integer { minInclusive = "0" maxInclusive = "255" },
xsd:integer { minInclusive = "0" maxInclusive = "255" },
xsd:integer { minInclusive = "0" maxInclusive = "255" },
xsd:integer { minInclusive = "0" maxInclusive = "255" }?}
ccolor = rgba | xsd:string { pattern = "[A-Za-z]+" }
size = list { xsd:decimal, xsd:decimal }
pos = list { xsd:decimal, xsd:decimal }
rect = list { xsd:decimal, xsd:decimal, xsd:decimal, xsd:decimal }
##
# Defines #
##
unique_settings =
attribute name { text }?,
[ a:defaultValue = "empty" ] attribute type { text }?,
attribute style { text }?
# This could probably be made more specific/strict
# with more information regarding the use/meaning
# of these attributes.
base_settings =
attribute absolute { bool }?&
attribute enable { bool }?&
attribute ghost { bool }?&
attribute hidden { bool }?&
attribute size { clientarea }?&
attribute z { xsd:decimal }?
# Defaults are not put in here, because it ruins the concept of styles.
ex_settings =
attribute anchor { valign }?&
attribute auto_scroll { bool }?&
attribute buffer_zone { xsd:decimal }?&
attribute buffer_width { xsd:decimal }?&
attribute button_width { xsd:decimal }?&
attribute checked { bool }?&
attribute clip { bool }?&
attribute dropdown_size { xsd:decimal }?&
attribute dropdown_buffer { xsd:decimal }?&
attribute minimum_visible_items { xsd:nonNegativeInteger }?&
attribute enabled { bool }?&
attribute font { text }?&
attribute format_x { text }?&
attribute format_y { text }?&
attribute fov_wedge_color { ccolor }?&
attribute hotkey { text }?&
attribute cell_id { xsd:integer }?&
attribute independent { bool }?&
attribute input_initvalue_destroyed_at_focus { bool }?&
attribute mask { bool }?&
attribute mask_char { xsd:string { minLength = "1" maxLength = "1" } }?&
attribute max_length { xsd:nonNegativeInteger }?&
attribute maxwidth { xsd:decimal }? &
attribute multiline { bool }?&
attribute offset { pos }?&
+ attribute placeholder_text { text }?&
+ attribute placeholder_color { ccolor }?&
attribute readonly { bool }?&
attribute scrollbar { bool }?&
attribute scrollbar_style { text }?&
attribute scroll_bottom { bool }?&
attribute scroll_top { bool }?&
attribute selected_column { text }?&
attribute selected_column_order { text }?&
attribute sortable { bool }?&
attribute sound_closed { text }?&
attribute sound_disabled { text }?&
attribute sound_enter { text }?&
attribute sound_leave { text }?&
attribute sound_opened { text }?&
attribute sound_pressed { text }?&
attribute sound_selected { text }?&
attribute sprite { text }?&
attribute sprite2 { text }?&
attribute sprite_asc { text }?&
attribute sprite_heading { text }?&
attribute sprite_bar { text }?&
attribute sprite_background { text }?&
attribute sprite_desc { text }?&
attribute sprite_disabled { text }?&
attribute sprite_list { text }?&
attribute sprite2_disabled { text }?&
attribute sprite_not_sorted { text }?&
attribute sprite_over { text }?&
attribute sprite2_over { text }?&
attribute sprite_pressed { text }?&
attribute sprite2_pressed { text }?&
attribute sprite_selectarea { text }?&
attribute square_side { xsd:decimal }?&
attribute textcolor { ccolor }?&
attribute textcolor_disabled { ccolor }?&
attribute textcolor_over { ccolor }?&
attribute textcolor_pressed { ccolor }?&
attribute textcolor_selected { ccolor }?&
attribute text_align { align }?&
attribute text_valign { valign }?&
attribute tooltip { text }?&
attribute tooltip_style { text }?
##
# Objects #
##
objects = element objects { (script | object)* }
script =
element script {
text &
attribute file { text }? &
attribute directory { text }?
}
object =
element object {
((object
| action
| \attribute
| column
| \include
| item
| repeat
| script
| translatableAttribute)*
| text),
unique_settings,
base_settings,
ex_settings
}
action =
element action {
text,
attribute on { text },
attribute file { text }?
}
\attribute =
element attribute {
(keep | translate)*,
attribute id { text }
}
column =
element column {
translatableAttribute?,
(
attribute id { text }&
attribute color { ccolor }?&
attribute heading { text }?&
attribute width { text }?&
attribute hidden { bool }?
)
}
\include =
element include {
attribute file { text }|
attribute directory { text }
}
item =
element item {
text,
attribute enabled { bool }?
}
keep = element keep { text }
repeat =
element repeat {
object+,
attribute count { xsd:nonNegativeInteger },
attribute var { text }?
}
translate = element translate { text }
translatableAttribute =
element translatableAttribute {
text,
(
attribute id { text }&
attribute comment { text }?&
attribute context { text }?
)
}
##
# Styles #
##
styles = element styles { style* }
style =
element style {
attribute name { text },
base_settings,
ex_settings
}
##
# Setup #
##
setup = element setup { (icon | scrollbar | tooltip | color)* }
scrollbar =
element scrollbar {
attribute name { text }&
attribute width { xsd:decimal }&
attribute alwaysshown { bool }?&
attribute maximum_bar_size { xsd:decimal }?&
attribute minimum_bar_size { xsd:decimal }?&
attribute scroll_wheel { bool }?&
attribute show_edge_buttons { bool }?&
attribute sprite_button_top { text }?&
attribute sprite_button_top_pressed { text }?&
attribute sprite_button_top_disabled { text }?&
attribute sprite_button_top_over { text }?&
attribute sprite_button_bottom { text }?&
attribute sprite_button_bottom_pressed { text }?&
attribute sprite_button_bottom_disabled { text }?&
attribute sprite_button_bottom_over { text }?&
attribute sprite_bar_vertical { text }?&
attribute sprite_bar_vertical_over { text }?&
attribute sprite_bar_vertical_pressed { text }?&
attribute sprite_back_vertical { text }?
}
icon =
element icon {
attribute name { text }&
attribute size { size }&
attribute sprite { text }&
attribute cell_id { text }?
}
tooltip =
element tooltip {
attribute name { text }&
attribute sprite { text }?&
attribute anchor { valign }?&
attribute axis_color { ccolor }?&
attribute axis_width { xsd:decimal { minInclusive = "0" } }?&
attribute buffer_zone { xsd:decimal }?&
attribute font { text }?&
attribute maxwidth { xsd:decimal }?&
attribute offset { pos }?&
attribute textcolor { ccolor }?&
attribute delay { xsd:integer }?&
attribute use_object { text }?&
attribute hide_object { bool }?
}
color =
element color {
rgba,
attribute name { text }
}
##
# Sprites #
##
sprites = element sprites { sprite* }
sprite =
element sprite {
(effect?, image+),
attribute name { text }
}
image =
element image {
effect?,
(
attribute texture { text }?&
attribute size { clientarea }?&
attribute texture_size { clientarea }?&
attribute real_texture_placement { rect }?&
attribute cell_size { size }?&
attribute backcolor { ccolor }?&
attribute bordercolor { ccolor }?&
attribute border { bool }?&
attribute z_level { xsd:float }?&
attribute fixed_h_aspect_ratio { xsd:decimal }?&
attribute round_coordinates { bool }?&
attribute wrap_mode { wrapmode }?
)
}
effect =
element effect {
attribute add_color { ccolor }?,
attribute grayscale { empty }?
}
Index: ps/trunk/binaries/data/mods/mod/gui/gui.rng
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/gui.rng (revision 24432)
+++ ps/trunk/binaries/data/mods/mod/gui/gui.rng (revision 24433)
@@ -1,883 +1,891 @@
truefalseleftcenterrighttopcenterbottomrepeatmirrored_repeatclamp_to_edge
-?\d*\.?\d+%?([\+\-]\d*\.?\d+%?)*
0
255
0
255
0
255
0
255
[A-Za-z]+
0
1
1
+
+
+
+
+
+
+
+
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js (revision 24432)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.js (revision 24433)
@@ -1,442 +1,440 @@
/**
* @file This GUI page displays all available mods and allows the player to enabled and launch a set of compatible mods.
*/
/**
* A mod is defined by a mod.json file, for example
* {
* "name": "0ad",
* "version": "0.0.24",
* "label": "0 A.D. - Empires Ascendant",
* "url": "https://wildfiregames.com/",
* "description": "A free, open-source, historical RTS game.",
* "dependencies": []
* }
*
* Or:
* {
* "name": "mod2",
* "label": "Mod 2",
* "version": "1.1",
* "description": "",
* "dependencies": ["0ad<=0.0.24", "rote"]
* }
*
* A mod is identified by the directory name.
* A mod must define the "name", "version", "label", "description" and "dependencies" property.
* The "url" property is optional.
*
* The property "name" can consist alphanumeric characters, underscore and dash.
* The name is used for version comparison of mod dependencies.
* The property "version" may only contain numbers and up to two periods.
* The property "label" is a human-readable name of the mod.
* The property "description" is a human-readable summary of the features of the mod.
* The property "url" is reference to a website about the mod.
* The property "dependencies" is an array of strings. Each string is either a modname or a mod version comparison.
* A mod version comparison is a modname, followed by an operator (=, <, >, <= or >=), followed by a mod version.
* This allows mods to express upwards and downwards compatibility.
*/
/**
* Mod definitions loaded from the files, including invalid mods.
*/
var g_Mods = {};
/**
* Folder names of all mods that are or can be launched.
*/
var g_ModsEnabled = [];
var g_ModsDisabled = [];
var g_ModsEnabledFiltered = [];
var g_ModsDisabledFiltered = [];
/**
* Name of the mods installed by the ModInstaller.
*/
var g_InstalledMods;
var g_ColorNoModSelected = "255 255 100";
var g_ColorDependenciesMet = "100 255 100";
var g_ColorDependenciesNotMet = "255 100 100";
function init(data, hotloadData)
{
g_InstalledMods = data && data.installedMods || hotloadData && hotloadData.installedMods || [];
initMods();
initGUIButtons(data);
}
function initMods()
{
loadMods();
loadEnabledMods();
validateMods();
initGUIFilters();
}
function getHotloadData()
{
return { "installedMods": g_InstalledMods };
}
function loadMods()
{
g_Mods = Engine.GetAvailableMods();
deepfreeze(g_Mods);
}
function loadEnabledMods()
{
g_ModsEnabled = Engine.ConfigDB_GetValue("user", "mod.enabledmods").split(/\s+/).filter(folder => !!g_Mods[folder]);
g_ModsDisabled = Object.keys(g_Mods).filter(folder => g_ModsEnabled.indexOf(folder) == -1);
g_ModsEnabledFiltered = g_ModsEnabled;
g_ModsDisabledFiltered = g_ModsDisabled;
}
function validateMods()
{
for (let folder in g_Mods)
validateMod(folder, g_Mods[folder], true);
}
function initGUIFilters()
{
Engine.GetGUIObjectByName("negateFilter").checked = false;
- Engine.GetGUIObjectByName("modGenericFilter").caption = translate("Filter");
displayModLists();
}
function initGUIButtons(data)
{
// Either get back to the previous page or quit if there is no previous page
let cancelButton = !data || data.cancelbutton;
Engine.GetGUIObjectByName("cancelButton").hidden = !cancelButton;
Engine.GetGUIObjectByName("quitButton").hidden = cancelButton;
Engine.GetGUIObjectByName("toggleModButton").caption = translateWithContext("mod activation", "Enable");
}
function saveMods()
{
sortEnabledMods();
Engine.ConfigDB_CreateValue("user", "mod.enabledmods", ["mod"].concat(g_ModsEnabled).join(" "));
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
}
function startMods()
{
sortEnabledMods();
Engine.SetMods(["mod"].concat(g_ModsEnabled));
Engine.RestartEngine();
}
function displayModLists()
{
g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled);
g_ModsDisabledFiltered = displayModList("modsDisabledList", g_ModsDisabled);
}
function displayModList(listObjectName, folders)
{
let listObject = Engine.GetGUIObjectByName(listObjectName);
if (listObjectName == "modsDisabledList")
{
let sortFolder = folder => String(g_Mods[folder][listObject.selected_column] || folder);
folders.sort((folder1, folder2) =>
listObject.selected_column_order *
sortFolder(folder1).localeCompare(sortFolder(folder2)));
}
folders = folders.filter(filterMod);
listObject.list_name = folders.map(folder => g_Mods[folder].name).map(name => g_InstalledMods.indexOf(name) == -1 ? name : coloredText(name, "green"));
listObject.list_folder = folders;
listObject.list_label = folders.map(folder => g_Mods[folder].label);
listObject.list_url = folders.map(folder => g_Mods[folder].url || "");
listObject.list_version = folders.map(folder => g_Mods[folder].version);
listObject.list_dependencies = folders.map(folder => g_Mods[folder].dependencies.join(" "));
listObject.list = folders;
return folders;
}
function reloadDisabledMods()
{
g_ModsDisabled = Object.keys(g_Mods).filter(folder => g_ModsEnabled.indexOf(folder) == -1);
}
function enableMod()
{
let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList");
let pos = modsDisabledList.selected;
if (pos == -1 || !areDependenciesMet(g_ModsDisabledFiltered[pos]))
return;
g_ModsEnabled.push(g_ModsDisabledFiltered.splice(pos, 1)[0]);
reloadDisabledMods();
if (pos >= g_ModsDisabledFiltered.length)
--pos;
displayModLists();
modsDisabledList.selected = pos;
}
function disableMod()
{
let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList");
let pos = modsEnabledList.selected;
if (pos == -1)
return;
// Find true position of disabled mod and remove it
let disabledMod = g_ModsEnabledFiltered[pos];
for (let i = 0; i < g_ModsEnabled.length; ++i)
if (g_ModsEnabled[i] == disabledMod)
{
g_ModsEnabled.splice(i, 1);
break;
}
g_ModsDisabled.push(disabledMod);
// Remove mods that required the removed mod and cascade
// Sort them, so we know which ones can depend on the removed mod
// TODO: Find position where the removed mod would have fit (for now assume idx 0)
sortEnabledMods();
for (let i = 0; i < g_ModsEnabled.length; ++i)
if (!areDependenciesMet(g_ModsEnabled[i]))
{
g_ModsDisabled.push(g_ModsEnabled.splice(i, 1)[0]);
--i;
}
displayModLists();
modsEnabledList.selected = Math.min(pos, g_ModsEnabledFiltered.length - 1);
}
function applyFilters()
{
// Save selected rows
let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList");
let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList");
let selectedDisabledFolder = modsDisabledList.list_folder[modsDisabledList.selected];
let selectedEnabledFolder = modsEnabledList.list_folder[modsEnabledList.selected];
// Remove selected rows to prevent a link to a non existing item
modsDisabledList.selected = -1;
modsEnabledList.selected = -1;
displayModLists();
// Restore previously selected rows
modsDisabledList.selected = modsDisabledList.list_folder.indexOf(selectedDisabledFolder);
modsEnabledList.selected = modsEnabledList.list_folder.indexOf(selectedEnabledFolder);
Engine.GetGUIObjectByName("globalModDescription").caption = "";
}
function filterMod(folder)
{
let mod = g_Mods[folder];
let negateFilter = Engine.GetGUIObjectByName("negateFilter").checked;
let searchText = Engine.GetGUIObjectByName("modGenericFilter").caption;
if (searchText &&
- searchText != translate("Filter") &&
folder.indexOf(searchText) == -1 &&
mod.name.indexOf(searchText) == -1 &&
mod.label.indexOf(searchText) == -1 &&
(mod.url || "").indexOf(searchText) == -1 &&
mod.version.indexOf(searchText) == -1 &&
mod.description.indexOf(searchText) == -1 &&
mod.dependencies.indexOf(searchText) == -1)
return negateFilter;
return !negateFilter;
}
function closePage()
{
Engine.SwitchGuiPage("page_pregame.xml", {});
}
function areFilters()
{
let searchText = Engine.GetGUIObjectByName("modGenericFilter").caption;
return searchText && searchText != translate("Filter");
}
/**
* Moves an item in the list up or down.
*/
function moveCurrItem(objectName, up)
{
// Prevent moving while filters are applied
// because we would need to map filtered positions
// to not filtered positions so changes will persist.
if (areFilters())
return;
let obj = Engine.GetGUIObjectByName(objectName);
let idx = obj.selected;
if (idx == -1)
return;
let num = obj.list.length;
let idx2 = idx + (up ? -1 : 1);
if (idx2 < 0 || idx2 >= num)
return;
let tmp = g_ModsEnabled[idx];
g_ModsEnabled[idx] = g_ModsEnabled[idx2];
g_ModsEnabled[idx2] = tmp;
g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled);
obj.selected = idx2;
}
function areDependenciesMet(folder)
{
let guiObject = Engine.GetGUIObjectByName("message");
for (let dependency of g_Mods[folder].dependencies)
{
if (isDependencyMet(dependency))
continue;
guiObject.caption = coloredText(
sprintf(translate('Dependency not met: %(dep)s'), { "dep": dependency }),
g_ColorDependenciesNotMet);
return false;
}
guiObject.caption = coloredText(translate('All dependencies met'), g_ColorDependenciesMet);
return true;
}
/**
* @param dependency is a mod name or a mod version comparison.
*/
function isDependencyMet(dependency)
{
let operator = dependency.match(g_RegExpComparisonOperator);
let [name, version] = operator ? dependency.split(operator[0]) : [dependency, undefined];
return g_ModsEnabled.some(folder =>
g_Mods[folder].name == name &&
(!operator || versionSatisfied(g_Mods[folder].version, operator[0], version)));
}
/**
* Compares the given versions using the given operator.
* '-' or '_' is ignored. Only numbers are supported.
* @note "5.3" < "5.3.0"
*/
function versionSatisfied(version1, operator, version2)
{
let versionList1 = version1.split(/[-_]/)[0].split(/\./g);
let versionList2 = version2.split(/[-_]/)[0].split(/\./g);
let eq = operator.indexOf("=") != -1;
let lt = operator.indexOf("<") != -1;
let gt = operator.indexOf(">") != -1;
for (let i = 0; i < Math.min(versionList1.length, versionList2.length); ++i)
{
let diff = +versionList1[i] - +versionList2[i];
if (gt && diff > 0 || lt && diff < 0)
return true;
if (gt && diff < 0 || lt && diff > 0 || eq && diff)
return false;
}
// common prefix matches
let ldiff = versionList1.length - versionList2.length;
if (!ldiff)
return eq;
// NB: 2.3 != 2.3.0
if (ldiff < 0)
return lt;
return gt;
}
function sortEnabledMods()
{
let dependencies = {};
for (let folder of g_ModsEnabled)
dependencies[folder] = g_Mods[folder].dependencies.map(d => d.split(g_RegExpComparisonOperator)[0]);
g_ModsEnabled.sort((folder1, folder2) =>
dependencies[folder1].indexOf(g_Mods[folder2].name) != -1 ? 1 :
dependencies[folder2].indexOf(g_Mods[folder1].name) != -1 ? -1 : 0);
g_ModsEnabledFiltered = displayModList("modsEnabledList", g_ModsEnabled);
}
function selectedMod(listObjectName)
{
let listObject = Engine.GetGUIObjectByName(listObjectName);
let otherListObject = Engine.GetGUIObjectByName(listObjectName == "modsDisabledList" ?
"modsEnabledList" : "modsDisabledList");
let toggleModButton = Engine.GetGUIObjectByName("toggleModButton");
let modSelected = listObject.selected != -1;
if (modSelected)
{
otherListObject.selected = -1;
toggleModButton.onPress = listObjectName == "modsDisabledList" ? enableMod : disableMod;
}
Engine.GetGUIObjectByName("visitWebButton").enabled = modSelected && !!getSelectedModUrl();
toggleModButton.caption = listObjectName == "modsDisabledList" ?
translateWithContext("mod activation", "Enable") :
translateWithContext("mod activation", "Disable");
toggleModButton.enabled = modSelected;
Engine.GetGUIObjectByName("enabledModUp").enabled = modSelected && listObjectName == "modsEnabledList" && !areFilters();
Engine.GetGUIObjectByName("enabledModDown").enabled = modSelected && listObjectName == "modsEnabledList" && !areFilters();
Engine.GetGUIObjectByName("globalModDescription").caption =
listObject.list[listObject.selected] ?
g_Mods[listObject.list[listObject.selected]].description :
'[color="' + g_ColorNoModSelected + '"]' + translate("No mod has been selected.") + '[/color]';
}
/**
* @returns {string} The url of the currently selected mod.
*/
function getSelectedModUrl()
{
let modsEnabledList = Engine.GetGUIObjectByName("modsEnabledList");
let modsDisabledList = Engine.GetGUIObjectByName("modsDisabledList");
let list = modsEnabledList.selected == -1 ? modsDisabledList : modsEnabledList;
let folder = list.list_folder[list.selected];
return folder && g_Mods[folder] && g_Mods[folder].url || undefined;
}
function visitModWebsite()
{
let url = getSelectedModUrl();
if (!url)
return;
if (!url.startsWith("http://") && !url.startsWith("https://"))
url = "http://" + url;
openURL(url);
}
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml (revision 24432)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml (revision 24433)
@@ -1,205 +1,207 @@
Index: ps/trunk/source/gui/ObjectTypes/CInput.cpp
===================================================================
--- ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 24432)
+++ ps/trunk/source/gui/ObjectTypes/CInput.cpp (revision 24433)
@@ -1,2074 +1,2105 @@
/* Copyright (C) 2020 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 "CInput.h"
#include "graphics/FontMetrics.h"
#include "graphics/ShaderManager.h"
#include "graphics/TextRenderer.h"
#include "gui/CGUI.h"
#include "gui/CGUIScrollBarVertical.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "ps/ConfigDB.h"
#include "ps/GameSetup/Config.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "renderer/Renderer.h"
#include
extern int g_yres;
const CStr CInput::EventNameTextEdit = "TextEdit";
const CStr CInput::EventNamePress = "Press";
const CStr CInput::EventNameTab = "Tab";
CInput::CInput(CGUI& pGUI)
:
IGUIObject(pGUI),
IGUIScrollBarOwner(*static_cast(this)),
m_iBufferPos(-1),
m_iBufferPos_Tail(-1),
m_SelectingText(),
m_HorizontalScroll(),
m_PrevTime(),
m_CursorVisState(true),
m_CursorBlinkRate(0.5),
m_ComposingText(),
+ m_GeneratedPlaceholderTextValid(false),
m_iComposedLength(),
m_iComposedPos(),
m_iInsertPos(),
m_BufferPosition(),
m_BufferZone(),
m_Caption(),
m_CellID(),
m_Font(),
m_MaskChar(),
m_Mask(),
m_MaxLength(),
m_MultiLine(),
m_Readonly(),
m_ScrollBar(),
m_ScrollBarStyle(),
m_Sprite(),
m_SpriteSelectArea(),
m_TextColor(),
m_TextColorSelected()
{
RegisterSetting("buffer_position", m_BufferPosition);
RegisterSetting("buffer_zone", m_BufferZone);
RegisterSetting("caption", m_Caption);
RegisterSetting("cell_id", m_CellID);
RegisterSetting("font", m_Font);
RegisterSetting("mask_char", m_MaskChar);
RegisterSetting("mask", m_Mask);
RegisterSetting("max_length", m_MaxLength);
RegisterSetting("multiline", m_MultiLine);
RegisterSetting("readonly", m_Readonly);
RegisterSetting("scrollbar", m_ScrollBar);
RegisterSetting("scrollbar_style", m_ScrollBarStyle);
RegisterSetting("sprite", m_Sprite);
RegisterSetting("sprite_selectarea", m_SpriteSelectArea);
RegisterSetting("textcolor", m_TextColor);
RegisterSetting("textcolor_selected", m_TextColorSelected);
+ RegisterSetting("placeholder_text", m_PlaceholderText);
+ RegisterSetting("placeholder_color", m_PlaceholderColor);
CFG_GET_VAL("gui.cursorblinkrate", m_CursorBlinkRate);
CGUIScrollBarVertical* bar = new CGUIScrollBarVertical(pGUI);
bar->SetRightAligned(true);
AddScrollBar(bar);
}
CInput::~CInput()
{
}
void CInput::UpdateBufferPositionSetting()
{
SetSetting("buffer_position", m_iBufferPos, false);
}
void CInput::ClearComposedText()
{
m_Caption.erase(m_iInsertPos, m_iComposedLength);
m_iBufferPos = m_iInsertPos;
UpdateBufferPositionSetting();
m_iComposedLength = 0;
m_iComposedPos = 0;
}
InReaction CInput::ManuallyHandleKeys(const SDL_Event_* ev)
{
ENSURE(m_iBufferPos != -1);
switch (ev->ev.type)
{
case SDL_HOTKEYDOWN:
{
if (m_ComposingText)
return IN_HANDLED;
return ManuallyHandleHotkeyEvent(ev);
}
// SDL2 has a new method of text input that better supports Unicode and CJK
// see https://wiki.libsdl.org/Tutorials/TextInput
case SDL_TEXTINPUT:
{
if (m_Readonly)
return IN_PASS;
// Text has been committed, either single key presses or through an IME
std::wstring text = wstring_from_utf8(ev->ev.text.text);
// Check max length
if (m_MaxLength != 0 && m_Caption.length() + text.length() > static_cast(m_MaxLength))
return IN_HANDLED;
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
if (m_ComposingText)
{
ClearComposedText();
m_ComposingText = false;
}
if (m_iBufferPos == static_cast(m_Caption.length()))
m_Caption.append(text);
else
m_Caption.insert(m_iBufferPos, text);
UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1);
m_iBufferPos += text.length();
UpdateBufferPositionSetting();
m_iBufferPos_Tail = -1;
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
return IN_HANDLED;
}
case SDL_TEXTEDITING:
{
if (m_Readonly)
return IN_PASS;
// Text is being composed with an IME
// TODO: indicate this by e.g. underlining the uncommitted text
const char* rawText = ev->ev.edit.text;
int rawLength = strlen(rawText);
std::wstring wtext = wstring_from_utf8(rawText);
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
// Remember cursor position when text composition begins
if (!m_ComposingText)
m_iInsertPos = m_iBufferPos;
else
{
// Composed text is replaced each time
ClearComposedText();
}
m_ComposingText = ev->ev.edit.start != 0 || rawLength != 0;
if (m_ComposingText)
{
m_Caption.insert(m_iInsertPos, wtext);
// The text buffer is limited to SDL_TEXTEDITINGEVENT_TEXT_SIZE bytes, yet start
// increases without limit, so don't let it advance beyond the composed text length
m_iComposedLength = wtext.length();
m_iComposedPos = ev->ev.edit.start < m_iComposedLength ? ev->ev.edit.start : m_iComposedLength;
m_iBufferPos = m_iInsertPos + m_iComposedPos;
// TODO: composed text selection - what does ev.edit.length do?
m_iBufferPos_Tail = -1;
}
UpdateBufferPositionSetting();
UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1);
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
return IN_HANDLED;
}
case SDL_KEYDOWN:
{
// Since the GUI framework doesn't handle to set settings
// in Unicode (CStrW), we'll simply retrieve the actual
// pointer and edit that.
SDL_Keycode keyCode = ev->ev.key.keysym.sym;
// Regular text input is handled in SDL_TEXTINPUT, however keydown events are still sent (and they come first).
// To make things work correctly, we pass through 'escape' which is a non-character key.
// TODO: there are probably other keys that we could ignore, but recognizing "non-glyph" keys isn't that trivial.
// Further, don't input text if modifiers other than shift are pressed (the user is presumably trying to perform a hotkey).
if (keyCode == SDLK_ESCAPE ||
g_keys[SDLK_LCTRL] || g_keys[SDLK_RCTRL] ||
g_keys[SDLK_LALT] || g_keys[SDLK_RALT] ||
g_keys[SDLK_LGUI] || g_keys[SDLK_RGUI])
return IN_PASS;
if (m_ComposingText)
return IN_HANDLED;
ManuallyImmutableHandleKeyDownEvent(keyCode);
ManuallyMutableHandleKeyDownEvent(keyCode);
UpdateBufferPositionSetting();
return IN_HANDLED;
}
default:
{
return IN_PASS;
}
}
}
void CInput::ManuallyMutableHandleKeyDownEvent(const SDL_Keycode keyCode)
{
if (m_Readonly)
return;
wchar_t cooked = 0;
switch (keyCode)
{
case SDLK_TAB:
{
SendEvent(GUIM_TAB, EventNameTab);
// Don't send a textedit event, because it should only
// be sent if the GUI control changes the text
break;
}
case SDLK_BACKSPACE:
{
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
else
{
m_iBufferPos_Tail = -1;
if (m_Caption.empty() || m_iBufferPos == 0)
break;
if (m_iBufferPos == static_cast(m_Caption.length()))
m_Caption = m_Caption.Left(static_cast(m_Caption.length()) - 1);
else
m_Caption =
m_Caption.Left(m_iBufferPos - 1) +
m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos);
--m_iBufferPos;
UpdateText(m_iBufferPos, m_iBufferPos + 1, m_iBufferPos);
}
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
break;
}
case SDLK_DELETE:
{
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
else
{
if (m_Caption.empty() || m_iBufferPos == static_cast(m_Caption.length()))
break;
m_Caption =
m_Caption.Left(m_iBufferPos) +
m_Caption.Right(static_cast(m_Caption.length()) - (m_iBufferPos + 1));
UpdateText(m_iBufferPos, m_iBufferPos + 1, m_iBufferPos);
}
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
break;
}
case SDLK_KP_ENTER:
case SDLK_RETURN:
{
// 'Return' should do a Press event for single liners (e.g. submitting forms)
// otherwise a '\n' character will be added.
if (!m_MultiLine)
{
SendEvent(GUIM_PRESSED, EventNamePress);
break;
}
cooked = '\n'; // Change to '\n' and do default:
FALLTHROUGH;
}
default: // Insert a character
{
// Regular input is handled via SDL_TEXTINPUT, so we should ignore it here.
if (cooked == 0)
return;
// Check max length
if (m_MaxLength != 0 && m_Caption.length() >= static_cast(m_MaxLength))
break;
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
m_iBufferPos_Tail = -1;
if (m_iBufferPos == static_cast(m_Caption.length()))
m_Caption += cooked;
else
m_Caption =
m_Caption.Left(m_iBufferPos) + cooked +
m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos);
UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos + 1);
++m_iBufferPos;
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
break;
}
}
}
void CInput::ManuallyImmutableHandleKeyDownEvent(const SDL_Keycode keyCode)
{
bool shiftKeyPressed = g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT];
switch (keyCode)
{
case SDLK_HOME:
{
// If there's not a selection, we should create one now
if (!shiftKeyPressed)
{
// Make sure a selection isn't created.
m_iBufferPos_Tail = -1;
}
else if (!SelectingText())
{
// Place tail at the current point:
m_iBufferPos_Tail = m_iBufferPos;
}
m_iBufferPos = 0;
m_WantedX = 0.0f;
UpdateAutoScroll();
break;
}
case SDLK_END:
{
// If there's not a selection, we should create one now
if (!shiftKeyPressed)
{
// Make sure a selection isn't created.
m_iBufferPos_Tail = -1;
}
else if (!SelectingText())
{
// Place tail at the current point:
m_iBufferPos_Tail = m_iBufferPos;
}
m_iBufferPos = static_cast(m_Caption.length());
m_WantedX = 0.0f;
UpdateAutoScroll();
break;
}
/**
* Conventions for Left/Right when text is selected:
*
* References:
*
* Visual Studio
* Visual Studio has the 'newer' approach, used by newer versions of
* things, and in newer applications. A left press will always place
* the pointer on the left edge of the selection, and then of course
* remove the selection. Right will do the exact same thing.
* If you have the pointer on the right edge and press right, it will
* in other words just remove the selection.
*
* Windows (eg. Notepad)
* A left press always takes the pointer a step to the left and
* removes the selection as if it were never there in the first place.
* Right of course does the same thing but to the right.
*
* I chose the Visual Studio convention. Used also in Word, gtk 2.0, MSN
* Messenger.
*/
case SDLK_LEFT:
{
m_WantedX = 0.f;
if (shiftKeyPressed || !SelectingText())
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
if (m_iBufferPos > 0)
--m_iBufferPos;
}
else
{
if (m_iBufferPos_Tail < m_iBufferPos)
m_iBufferPos = m_iBufferPos_Tail;
m_iBufferPos_Tail = -1;
}
UpdateAutoScroll();
break;
}
case SDLK_RIGHT:
{
m_WantedX = 0.0f;
if (shiftKeyPressed || !SelectingText())
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
if (m_iBufferPos < static_cast(m_Caption.length()))
++m_iBufferPos;
}
else
{
if (m_iBufferPos_Tail > m_iBufferPos)
m_iBufferPos = m_iBufferPos_Tail;
m_iBufferPos_Tail = -1;
}
UpdateAutoScroll();
break;
}
/**
* Conventions for Up/Down when text is selected:
*
* References:
*
* Visual Studio
* Visual Studio has a very strange approach, down takes you below the
* selection to the next row, and up to the one prior to the whole
* selection. The weird part is that it is always aligned as the
* 'pointer'. I decided this is to much work for something that is
* a bit arbitrary
*
* Windows (eg. Notepad)
* Just like with left/right, the selection is destroyed and it moves
* just as if there never were a selection.
*
* I chose the Notepad convention even though I use the VS convention with
* left/right.
*/
case SDLK_UP:
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
std::list::iterator current = m_CharacterPositions.begin();
while (current != m_CharacterPositions.end())
{
if (m_iBufferPos >= current->m_ListStart &&
m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size())
break;
++current;
}
float pos_x;
if (m_iBufferPos - current->m_ListStart == 0)
pos_x = 0.f;
else
pos_x = current->m_ListOfX[m_iBufferPos - current->m_ListStart - 1];
if (m_WantedX > pos_x)
pos_x = m_WantedX;
// Now change row:
if (current != m_CharacterPositions.begin())
{
--current;
// Find X-position:
m_iBufferPos = current->m_ListStart + GetXTextPosition(current, pos_x, m_WantedX);
}
// else we can't move up
UpdateAutoScroll();
break;
}
case SDLK_DOWN:
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
std::list::iterator current = m_CharacterPositions.begin();
while (current != m_CharacterPositions.end())
{
if (m_iBufferPos >= current->m_ListStart &&
m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size())
break;
++current;
}
float pos_x;
if (m_iBufferPos - current->m_ListStart == 0)
pos_x = 0.f;
else
pos_x = current->m_ListOfX[m_iBufferPos - current->m_ListStart - 1];
if (m_WantedX > pos_x)
pos_x = m_WantedX;
// Now change row:
// Add first, so we can check if it's .end()
++current;
if (current != m_CharacterPositions.end())
{
// Find X-position:
m_iBufferPos = current->m_ListStart + GetXTextPosition(current, pos_x, m_WantedX);
}
// else we can't move up
UpdateAutoScroll();
break;
}
case SDLK_PAGEUP:
{
GetScrollBar(0).ScrollMinusPlenty();
UpdateAutoScroll();
break;
}
case SDLK_PAGEDOWN:
{
GetScrollBar(0).ScrollPlusPlenty();
UpdateAutoScroll();
break;
}
default:
{
break;
}
}
}
+void CInput::SetupGeneratedPlaceholderText()
+{
+ m_GeneratedPlaceholderText = CGUIText(m_pGUI, m_PlaceholderText, m_Font, 0, m_BufferZone, this);
+ m_GeneratedPlaceholderTextValid = true;
+}
+
InReaction CInput::ManuallyHandleHotkeyEvent(const SDL_Event_* ev)
{
bool shiftKeyPressed = g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT];
std::string hotkey = static_cast(ev->ev.user.data1);
if (hotkey == "paste")
{
if (m_Readonly)
return IN_PASS;
m_WantedX = 0.0f;
char* utf8_text = SDL_GetClipboardText();
if (!utf8_text)
return IN_HANDLED;
std::wstring text = wstring_from_utf8(utf8_text);
SDL_free(utf8_text);
// Check max length
if (m_MaxLength != 0 && m_Caption.length() + text.length() > static_cast(m_MaxLength))
text = text.substr(0, static_cast(m_MaxLength) - m_Caption.length());
if (SelectingText())
DeleteCurSelection();
if (m_iBufferPos == static_cast(m_Caption.length()))
m_Caption += text;
else
m_Caption =
m_Caption.Left(m_iBufferPos) + text +
m_Caption.Right(static_cast(m_Caption.length()) - m_iBufferPos);
UpdateText(m_iBufferPos, m_iBufferPos, m_iBufferPos+1);
m_iBufferPos += static_cast(text.size());
UpdateAutoScroll();
UpdateBufferPositionSetting();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
return IN_HANDLED;
}
else if (hotkey == "copy" || hotkey == "cut")
{
if (m_Readonly && hotkey == "cut")
return IN_PASS;
m_WantedX = 0.0f;
if (SelectingText())
{
int virtualFrom;
int virtualTo;
if (m_iBufferPos_Tail >= m_iBufferPos)
{
virtualFrom = m_iBufferPos;
virtualTo = m_iBufferPos_Tail;
}
else
{
virtualFrom = m_iBufferPos_Tail;
virtualTo = m_iBufferPos;
}
CStrW text = m_Caption.Left(virtualTo).Right(virtualTo - virtualFrom);
SDL_SetClipboardText(text.ToUTF8().c_str());
if (hotkey == "cut")
{
DeleteCurSelection();
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
}
}
return IN_HANDLED;
}
else if (hotkey == "text.delete.left")
{
if (m_Readonly)
return IN_PASS;
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
if (!m_Caption.empty() && m_iBufferPos != 0)
{
m_iBufferPos_Tail = m_iBufferPos;
CStrW searchString = m_Caption.Left(m_iBufferPos);
// If we are starting in whitespace, adjust position until we get a non whitespace
while (m_iBufferPos > 0)
{
if (!iswspace(searchString[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
// If we end up on a punctuation char we just delete it (treat punct like a word)
if (iswpunct(searchString[m_iBufferPos - 1]))
m_iBufferPos--;
else
{
// Now we are on a non white space character, adjust position to char after next whitespace char is found
while (m_iBufferPos > 0)
{
if (iswspace(searchString[m_iBufferPos - 1]) || iswpunct(searchString[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
}
UpdateBufferPositionSetting();
DeleteCurSelection();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
}
UpdateAutoScroll();
return IN_HANDLED;
}
else if (hotkey == "text.delete.right")
{
if (m_Readonly)
return IN_PASS;
m_WantedX = 0.0f;
if (SelectingText())
DeleteCurSelection();
if (!m_Caption.empty() && m_iBufferPos < static_cast(m_Caption.length()))
{
// Delete the word to the right of the cursor
m_iBufferPos_Tail = m_iBufferPos;
// Delete chars to the right unit we hit whitespace
while (++m_iBufferPos < static_cast(m_Caption.length()))
{
if (iswspace(m_Caption[m_iBufferPos]) || iswpunct(m_Caption[m_iBufferPos]))
break;
}
// Eliminate any whitespace behind the word we just deleted
while (m_iBufferPos < static_cast(m_Caption.length()))
{
if (!iswspace(m_Caption[m_iBufferPos]))
break;
++m_iBufferPos;
}
UpdateBufferPositionSetting();
DeleteCurSelection();
}
UpdateAutoScroll();
SendEvent(GUIM_TEXTEDIT, EventNameTextEdit);
return IN_HANDLED;
}
else if (hotkey == "text.move.left")
{
m_WantedX = 0.0f;
if (shiftKeyPressed || !SelectingText())
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
if (!m_Caption.empty() && m_iBufferPos != 0)
{
CStrW searchString = m_Caption.Left(m_iBufferPos);
// If we are starting in whitespace, adjust position until we get a non whitespace
while (m_iBufferPos > 0)
{
if (!iswspace(searchString[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
// If we end up on a puctuation char we just select it (treat punct like a word)
if (iswpunct(searchString[m_iBufferPos - 1]))
m_iBufferPos--;
else
{
// Now we are on a non white space character, adjust position to char after next whitespace char is found
while (m_iBufferPos > 0)
{
if (iswspace(searchString[m_iBufferPos - 1]) || iswpunct(searchString[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
}
}
}
else
{
if (m_iBufferPos_Tail < m_iBufferPos)
m_iBufferPos = m_iBufferPos_Tail;
m_iBufferPos_Tail = -1;
}
UpdateBufferPositionSetting();
UpdateAutoScroll();
return IN_HANDLED;
}
else if (hotkey == "text.move.right")
{
m_WantedX = 0.0f;
if (shiftKeyPressed || !SelectingText())
{
if (!shiftKeyPressed)
m_iBufferPos_Tail = -1;
else if (!SelectingText())
m_iBufferPos_Tail = m_iBufferPos;
if (!m_Caption.empty() && m_iBufferPos < static_cast(m_Caption.length()))
{
// Select chars to the right until we hit whitespace
while (++m_iBufferPos < static_cast(m_Caption.length()))
{
if (iswspace(m_Caption[m_iBufferPos]) || iswpunct(m_Caption[m_iBufferPos]))
break;
}
// Also select any whitespace following the word we just selected
while (m_iBufferPos < static_cast(m_Caption.length()))
{
if (!iswspace(m_Caption[m_iBufferPos]))
break;
++m_iBufferPos;
}
}
}
else
{
if (m_iBufferPos_Tail > m_iBufferPos)
m_iBufferPos = m_iBufferPos_Tail;
m_iBufferPos_Tail = -1;
}
UpdateBufferPositionSetting();
UpdateAutoScroll();
return IN_HANDLED;
}
return IN_PASS;
}
void CInput::ResetStates()
{
IGUIObject::ResetStates();
IGUIScrollBarOwner::ResetStates();
}
void CInput::HandleMessage(SGUIMessage& Message)
{
IGUIObject::HandleMessage(Message);
IGUIScrollBarOwner::HandleMessage(Message);
switch (Message.type)
{
case GUIM_SETTINGS_UPDATED:
{
// Update scroll-bar
// TODO Gee: (2004-09-01) Is this really updated each time it should?
if (m_ScrollBar &&
(Message.value == "size" ||
Message.value == "z" ||
Message.value == "absolute"))
{
GetScrollBar(0).SetX(m_CachedActualSize.right);
GetScrollBar(0).SetY(m_CachedActualSize.top);
GetScrollBar(0).SetZ(GetBufferedZ());
GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top);
}
// Update scrollbar
if (Message.value == "scrollbar_style")
GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle);
if (Message.value == "buffer_position")
{
m_iBufferPos = m_MaxLength != 0 ? std::min(m_MaxLength, m_BufferPosition) : m_BufferPosition;
m_iBufferPos_Tail = -1; // position change resets selection
}
if (Message.value == "size" ||
Message.value == "z" ||
Message.value == "font" ||
Message.value == "absolute" ||
Message.value == "caption" ||
Message.value == "scrollbar" ||
Message.value == "scrollbar_style")
{
UpdateText();
}
if (Message.value == "multiline")
{
if (!m_MultiLine)
GetScrollBar(0).SetLength(0.f);
else
GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top);
UpdateText();
}
+ if (Message.value == "placeholder_text" ||
+ Message.value == "size" ||
+ Message.value == "font" ||
+ Message.value == "z" ||
+ Message.value == "text_valign")
+ {
+ m_GeneratedPlaceholderTextValid = false;
+ }
+
UpdateAutoScroll();
break;
}
case GUIM_MOUSE_PRESS_LEFT:
{
// Check if we're selecting the scrollbar
if (m_ScrollBar &&
m_MultiLine &&
GetScrollBar(0).GetStyle())
{
if (m_pGUI.GetMousePos().x > m_CachedActualSize.right - GetScrollBar(0).GetStyle()->m_Width)
break;
}
if (m_ComposingText)
break;
// Okay, this section is about pressing the mouse and
// choosing where the point should be placed. For
// instance, if we press between a and b, the point
// should of course be placed accordingly. Other
// special cases are handled like the input box norms.
if (g_keys[SDLK_RSHIFT] || g_keys[SDLK_LSHIFT])
m_iBufferPos = GetMouseHoveringTextPosition();
else
m_iBufferPos = m_iBufferPos_Tail = GetMouseHoveringTextPosition();
m_SelectingText = true;
UpdateAutoScroll();
// If we immediately release the button it will just be seen as a click
// for the user though.
break;
}
case GUIM_MOUSE_DBLCLICK_LEFT:
{
if (m_ComposingText)
break;
if (m_Caption.empty())
break;
m_iBufferPos = m_iBufferPos_Tail = GetMouseHoveringTextPosition();
if (m_iBufferPos >= (int)m_Caption.length())
m_iBufferPos = m_iBufferPos_Tail = m_Caption.length() - 1;
// See if we are clicking over whitespace
if (iswspace(m_Caption[m_iBufferPos]))
{
// see if we are in a section of whitespace greater than one character
if ((m_iBufferPos + 1 < (int) m_Caption.length() && iswspace(m_Caption[m_iBufferPos + 1])) ||
(m_iBufferPos - 1 > 0 && iswspace(m_Caption[m_iBufferPos - 1])))
{
//
// We are clicking in an area with more than one whitespace character
// so we select both the word to the left and then the word to the right
//
// [1] First the left
// skip the whitespace
while (m_iBufferPos > 0)
{
if (!iswspace(m_Caption[m_iBufferPos - 1]))
break;
m_iBufferPos--;
}
// now go until we hit white space or punctuation
while (m_iBufferPos > 0)
{
if (iswspace(m_Caption[m_iBufferPos - 1]))
break;
m_iBufferPos--;
if (iswpunct(m_Caption[m_iBufferPos]))
break;
}
// [2] Then the right
// go right until we are not in whitespace
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
{
if (!iswspace(m_Caption[m_iBufferPos_Tail]))
break;
}
if (m_iBufferPos_Tail == static_cast(m_Caption.length()))
break;
// now go to the right until we hit whitespace or punctuation
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
{
if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail]))
break;
}
}
else
{
// single whitespace so select word to the right
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
{
if (!iswspace(m_Caption[m_iBufferPos_Tail]))
break;
}
if (m_iBufferPos_Tail == static_cast(m_Caption.length()))
break;
// Don't include the leading whitespace
m_iBufferPos = m_iBufferPos_Tail;
// now go to the right until we hit whitespace or punctuation
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
{
if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail]))
break;
}
}
}
else
{
// clicked on non-whitespace so select current word
// go until we hit white space or punctuation
while (m_iBufferPos > 0)
{
if (iswspace(m_Caption[m_iBufferPos - 1]))
break;
m_iBufferPos--;
if (iswpunct(m_Caption[m_iBufferPos]))
break;
}
// go to the right until we hit whitespace or punctuation
while (++m_iBufferPos_Tail < static_cast(m_Caption.length()))
if (iswspace(m_Caption[m_iBufferPos_Tail]) || iswpunct(m_Caption[m_iBufferPos_Tail]))
break;
}
UpdateAutoScroll();
break;
}
case GUIM_MOUSE_RELEASE_LEFT:
{
if (m_SelectingText)
m_SelectingText = false;
break;
}
case GUIM_MOUSE_MOTION:
{
// If we just pressed down and started to move before releasing
// this is one way of selecting larger portions of text.
if (m_SelectingText)
{
// Actually, first we need to re-check that the mouse button is
// really pressed (it can be released while outside the control.
if (!g_mouse_buttons[SDL_BUTTON_LEFT])
m_SelectingText = false;
else
m_iBufferPos = GetMouseHoveringTextPosition();
UpdateAutoScroll();
}
break;
}
case GUIM_LOAD:
{
GetScrollBar(0).SetX(m_CachedActualSize.right);
GetScrollBar(0).SetY(m_CachedActualSize.top);
GetScrollBar(0).SetZ(GetBufferedZ());
GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top);
GetScrollBar(0).SetScrollBarStyle(m_ScrollBarStyle);
UpdateText();
UpdateAutoScroll();
break;
}
case GUIM_GOT_FOCUS:
{
m_iBufferPos = 0;
m_PrevTime = 0.0;
m_CursorVisState = false;
// Tell the IME where to draw the candidate list
SDL_Rect rect;
rect.h = m_CachedActualSize.GetSize().cy;
rect.w = m_CachedActualSize.GetSize().cx;
rect.x = m_CachedActualSize.TopLeft().x;
rect.y = m_CachedActualSize.TopLeft().y;
SDL_SetTextInputRect(&rect);
SDL_StartTextInput();
break;
}
case GUIM_LOST_FOCUS:
{
if (m_ComposingText)
{
// Simulate a final text editing event to clear the composition
SDL_Event_ evt;
evt.ev.type = SDL_TEXTEDITING;
evt.ev.edit.length = 0;
evt.ev.edit.start = 0;
evt.ev.edit.text[0] = 0;
ManuallyHandleKeys(&evt);
}
SDL_StopTextInput();
m_iBufferPos = -1;
m_iBufferPos_Tail = -1;
break;
}
default:
{
break;
}
}
UpdateBufferPositionSetting();
}
void CInput::UpdateCachedSize()
{
// If an ancestor's size changed, this will let us intercept the change and
// update our scrollbar positions
IGUIObject::UpdateCachedSize();
if (m_ScrollBar)
{
GetScrollBar(0).SetX(m_CachedActualSize.right);
GetScrollBar(0).SetY(m_CachedActualSize.top);
GetScrollBar(0).SetZ(GetBufferedZ());
GetScrollBar(0).SetLength(m_CachedActualSize.bottom - m_CachedActualSize.top);
}
+
+ m_GeneratedPlaceholderTextValid = false;
}
void CInput::Draw()
{
float bz = GetBufferedZ();
if (m_CursorBlinkRate > 0.0)
{
// check if the cursor visibility state needs to be changed
double currTime = timer_Time();
if (currTime - m_PrevTime >= m_CursorBlinkRate)
{
m_CursorVisState = !m_CursorVisState;
m_PrevTime = currTime;
}
}
else
// should always be visible
m_CursorVisState = true;
// First call draw on ScrollBarOwner
if (m_ScrollBar && m_MultiLine)
IGUIScrollBarOwner::Draw();
CStrIntern font_name(m_Font.ToUTF8());
wchar_t mask_char = L'*';
if (m_Mask && m_MaskChar.length() > 0)
mask_char = m_MaskChar[0];
m_pGUI.DrawSprite(m_Sprite, m_CellID, bz, m_CachedActualSize);
float scroll = 0.f;
if (m_ScrollBar && m_MultiLine)
scroll = GetScrollBar(0).GetPos();
CFontMetrics font(font_name);
// We'll have to setup clipping manually, since we're doing the rendering manually.
CRect cliparea(m_CachedActualSize);
// First we'll figure out the clipping area, which is the cached actual size
// substracted by an optional scrollbar
if (m_ScrollBar)
{
scroll = GetScrollBar(0).GetPos();
// substract scrollbar from cliparea
if (cliparea.right > GetScrollBar(0).GetOuterRect().left &&
cliparea.right <= GetScrollBar(0).GetOuterRect().right)
cliparea.right = GetScrollBar(0).GetOuterRect().left;
if (cliparea.left >= GetScrollBar(0).GetOuterRect().left &&
cliparea.left < GetScrollBar(0).GetOuterRect().right)
cliparea.left = GetScrollBar(0).GetOuterRect().right;
}
if (cliparea != CRect())
{
glEnable(GL_SCISSOR_TEST);
glScissor(
cliparea.left * g_GuiScale,
g_yres - cliparea.bottom * g_GuiScale,
cliparea.GetWidth() * g_GuiScale,
cliparea.GetHeight() * g_GuiScale);
}
// These are useful later.
int VirtualFrom, VirtualTo;
if (m_iBufferPos_Tail >= m_iBufferPos)
{
VirtualFrom = m_iBufferPos;
VirtualTo = m_iBufferPos_Tail;
}
else
{
VirtualFrom = m_iBufferPos_Tail;
VirtualTo = m_iBufferPos;
}
// Get the height of this font.
float h = (float)font.GetHeight();
float ls = (float)font.GetLineSpacing();
CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_gui_text);
CTextRenderer textRenderer(tech->GetShader());
textRenderer.Font(font_name);
// Set the Z to somewhat more, so we can draw a selected area between the
// the control and the text.
textRenderer.Translate(
(float)(int)(m_CachedActualSize.left) + m_BufferZone,
(float)(int)(m_CachedActualSize.top+h) + m_BufferZone,
bz+0.1f);
// U+FE33: PRESENTATION FORM FOR VERTICAL LOW LINE
// (sort of like a | which is aligned to the left of most characters)
float buffered_y = -scroll + m_BufferZone;
// When selecting larger areas, we need to draw a rectangle box
// around it, and this is to keep track of where the box
// started, because we need to follow the iteration until we
// reach the end, before we can actually draw it.
bool drawing_box = false;
float box_x = 0.f;
float x_pointer = 0.f;
// If we have a selecting box (i.e. when you have selected letters, not just when
// the pointer is between two letters) we need to process all letters once
// before we do it the second time and render all the text. We can't do it
// in the same loop because text will have been drawn, so it will disappear when
// drawn behind the text that has already been drawn. Confusing, well it's necessary
// (I think).
if (SelectingText())
{
// Now m_iBufferPos_Tail can be of both sides of m_iBufferPos,
// just like you can select from right to left, as you can
// left to right. Is there a difference? Yes, the pointer
// be placed accordingly, so that if you select shift and
// expand this selection, it will expand on appropriate side.
// Anyway, since the drawing procedure needs "To" to be
// greater than from, we need virtual values that might switch
// place.
int virtualFrom = 0;
int virtualTo = 0;
if (m_iBufferPos_Tail >= m_iBufferPos)
{
virtualFrom = m_iBufferPos;
virtualTo = m_iBufferPos_Tail;
}
else
{
virtualFrom = m_iBufferPos_Tail;
virtualTo = m_iBufferPos;
}
bool done = false;
for (std::list::const_iterator it = m_CharacterPositions.begin();
it != m_CharacterPositions.end();
++it, buffered_y += ls, x_pointer = 0.f)
{
if (m_MultiLine && buffered_y > m_CachedActualSize.GetHeight())
break;
// We might as well use 'i' here to iterate, because we need it
// (often compared against ints, so don't make it size_t)
for (int i = 0; i < (int)it->m_ListOfX.size()+2; ++i)
{
if (it->m_ListStart + i == virtualFrom)
{
// we won't actually draw it now, because we don't
// know the width of each glyph to that position.
// we need to go along with the iteration, and
// make a mark where the box started:
drawing_box = true; // will turn false when finally rendered.
// Get current x position
box_x = x_pointer;
}
const bool at_end = (i == (int)it->m_ListOfX.size()+1);
if (drawing_box && (it->m_ListStart + i == virtualTo || at_end))
{
// Depending on if it's just a row change, or if it's
// the end of the select box, do slightly different things.
if (at_end)
{
if (it->m_ListStart + i != virtualFrom)
// and actually add a white space! yes, this is done in any common input
x_pointer += font.GetCharacterWidth(L' ');
}
else
{
drawing_box = false;
done = true;
}
CRect rect;
// Set 'rect' depending on if it's a multiline control, or a one-line control
if (m_MultiLine)
{
rect = CRect(
m_CachedActualSize.left + box_x + m_BufferZone,
m_CachedActualSize.top + buffered_y + (h - ls) / 2,
m_CachedActualSize.left + x_pointer + m_BufferZone,
m_CachedActualSize.top + buffered_y + (h + ls) / 2);
if (rect.bottom < m_CachedActualSize.top)
continue;
if (rect.top < m_CachedActualSize.top)
rect.top = m_CachedActualSize.top;
if (rect.bottom > m_CachedActualSize.bottom)
rect.bottom = m_CachedActualSize.bottom;
}
else // if one-line
{
rect = CRect(
m_CachedActualSize.left + box_x + m_BufferZone - m_HorizontalScroll,
m_CachedActualSize.top + buffered_y + (h - ls) / 2,
m_CachedActualSize.left + x_pointer + m_BufferZone - m_HorizontalScroll,
m_CachedActualSize.top + buffered_y + (h + ls) / 2);
if (rect.left < m_CachedActualSize.left)
rect.left = m_CachedActualSize.left;
if (rect.right > m_CachedActualSize.right)
rect.right = m_CachedActualSize.right;
}
m_pGUI.DrawSprite(m_SpriteSelectArea, m_CellID, bz + 0.05f, rect);
}
if (i < (int)it->m_ListOfX.size())
{
if (!m_Mask)
x_pointer += font.GetCharacterWidth(m_Caption[it->m_ListStart + i]);
else
x_pointer += font.GetCharacterWidth(mask_char);
}
}
if (done)
break;
// If we're about to draw a box, and all of a sudden changes
// line, we need to draw that line's box, and then reset
// the box drawing to the beginning of the new line.
if (drawing_box)
box_x = 0.f;
}
}
// Reset some from previous run
buffered_y = -scroll;
// Setup initial color (then it might change and change back, when drawing selected area)
textRenderer.Color(m_TextColor);
tech->BeginPass();
bool using_selected_color = false;
for (std::list::const_iterator it = m_CharacterPositions.begin();
it != m_CharacterPositions.end();
++it, buffered_y += ls)
{
if (buffered_y + m_BufferZone >= -ls || !m_MultiLine)
{
if (m_MultiLine && buffered_y + m_BufferZone > m_CachedActualSize.GetHeight())
break;
CMatrix3D savedTransform = textRenderer.GetTransform();
// Text must always be drawn in integer values. So we have to convert scroll
if (m_MultiLine)
textRenderer.Translate(0.f, -(float)(int)scroll, 0.f);
else
textRenderer.Translate(-(float)(int)m_HorizontalScroll, 0.f, 0.f);
// We might as well use 'i' here, because we need it
// (often compared against ints, so don't make it size_t)
for (int i = 0; i < (int)it->m_ListOfX.size()+1; ++i)
{
if (!m_MultiLine && i < (int)it->m_ListOfX.size())
{
if (it->m_ListOfX[i] - m_HorizontalScroll < -m_BufferZone)
{
// We still need to translate the OpenGL matrix
if (i == 0)
textRenderer.Translate(it->m_ListOfX[i], 0.f, 0.f);
else
textRenderer.Translate(it->m_ListOfX[i] - it->m_ListOfX[i-1], 0.f, 0.f);
continue;
}
}
// End of selected area, change back color
if (SelectingText() && it->m_ListStart + i == VirtualTo)
{
using_selected_color = false;
textRenderer.Color(m_TextColor);
}
// selecting only one, then we need only to draw a cursor.
if (i != (int)it->m_ListOfX.size() && it->m_ListStart + i == m_iBufferPos && m_CursorVisState)
textRenderer.Put(0.0f, 0.0f, L"_");
// Drawing selected area
if (SelectingText() &&
it->m_ListStart + i >= VirtualFrom &&
it->m_ListStart + i < VirtualTo &&
!using_selected_color)
{
using_selected_color = true;
textRenderer.Color(m_TextColorSelected);
}
if (i != (int)it->m_ListOfX.size())
{
if (!m_Mask)
textRenderer.PrintfAdvance(L"%lc", m_Caption[it->m_ListStart + i]);
else
textRenderer.PrintfAdvance(L"%lc", mask_char);
}
// check it's now outside a one-liner, then we'll break
if (!m_MultiLine && i < (int)it->m_ListOfX.size() &&
it->m_ListOfX[i] - m_HorizontalScroll > m_CachedActualSize.GetWidth() - m_BufferZone)
break;
}
if (it->m_ListStart + (int)it->m_ListOfX.size() == m_iBufferPos)
{
textRenderer.Color(m_TextColor);
if (m_CursorVisState)
textRenderer.PutAdvance(L"_");
if (using_selected_color)
textRenderer.Color(m_TextColorSelected);
}
textRenderer.SetTransform(savedTransform);
}
textRenderer.Translate(0.f, ls, 0.f);
}
textRenderer.Render();
if (cliparea != CRect())
glDisable(GL_SCISSOR_TEST);
tech->EndPass();
+
+ if (m_Caption.empty() && !m_PlaceholderText.GetRawString().empty())
+ DrawPlaceholderText(bz, cliparea);
+}
+
+void CInput::DrawPlaceholderText(float z, const CRect& clipping)
+{
+ if (!m_GeneratedPlaceholderTextValid)
+ SetupGeneratedPlaceholderText();
+
+ m_GeneratedPlaceholderText.Draw(m_pGUI, m_PlaceholderColor, m_CachedActualSize.TopLeft(), z, clipping);
}
void CInput::UpdateText(int from, int to_before, int to_after)
{
if (m_MaxLength != 0 && m_Caption.length() > static_cast(m_MaxLength))
m_Caption = m_Caption.substr(0, m_MaxLength);
CStrIntern font_name(m_Font.ToUTF8());
wchar_t mask_char = L'*';
if (m_Mask && m_MaskChar.length() > 0)
mask_char = m_MaskChar[0];
// Ensure positions are valid after caption changes
m_iBufferPos = std::min(m_iBufferPos, static_cast(m_Caption.size()));
m_iBufferPos_Tail = std::min(m_iBufferPos_Tail, static_cast(m_Caption.size()));
UpdateBufferPositionSetting();
if (font_name.empty())
{
// Destroy everything stored, there's no font, so there can be no data.
m_CharacterPositions.clear();
return;
}
SRow row;
row.m_ListStart = 0;
int to = 0; // make sure it's initialized
if (to_before == -1)
to = static_cast(m_Caption.length());
CFontMetrics font(font_name);
std::list::iterator current_line;
// Used to ... TODO
int check_point_row_start = -1;
int check_point_row_end = -1;
// Reset
if (from == 0 && to_before == -1)
{
m_CharacterPositions.clear();
current_line = m_CharacterPositions.begin();
}
else
{
ENSURE(to_before != -1);
std::list::iterator destroy_row_from;
std::list::iterator destroy_row_to;
// Used to check if the above has been set to anything,
// previously a comparison like:
// destroy_row_from == std::list::iterator()
// ... was used, but it didn't work with GCC.
bool destroy_row_from_used = false;
bool destroy_row_to_used = false;
// Iterate, and remove everything between 'from' and 'to_before'
// actually remove the entire lines they are on, it'll all have
// to be redone. And when going along, we'll delete a row at a time
// when continuing to see how much more after 'to' we need to remake.
int i = 0;
for (std::list::iterator it = m_CharacterPositions.begin();
it != m_CharacterPositions.end();
++it, ++i)
{
if (!destroy_row_from_used && it->m_ListStart > from)
{
// Destroy the previous line, and all to 'to_before'
destroy_row_from = it;
--destroy_row_from;
destroy_row_from_used = true;
// For the rare case that we might remove characters to a word
// so that it suddenly fits on the previous row,
// we need to by standards re-do the whole previous line too
// (if one exists)
if (destroy_row_from != m_CharacterPositions.begin())
--destroy_row_from;
}
if (!destroy_row_to_used && it->m_ListStart > to_before)
{
destroy_row_to = it;
destroy_row_to_used = true;
// If it isn't the last row, we'll add another row to delete,
// just so we can see if the last restorted line is
// identical to what it was before. If it isn't, then we'll
// have to continue.
// 'check_point_row_start' is where we store how the that
// line looked.
if (destroy_row_to != m_CharacterPositions.end())
{
check_point_row_start = destroy_row_to->m_ListStart;
check_point_row_end = check_point_row_start + (int)destroy_row_to->m_ListOfX.size();
if (destroy_row_to->m_ListOfX.empty())
++check_point_row_end;
}
++destroy_row_to;
break;
}
}
if (!destroy_row_from_used)
{
destroy_row_from = m_CharacterPositions.end();
--destroy_row_from;
// As usual, let's destroy another row back
if (destroy_row_from != m_CharacterPositions.begin())
--destroy_row_from;
current_line = destroy_row_from;
}
if (!destroy_row_to_used)
{
destroy_row_to = m_CharacterPositions.end();
check_point_row_start = -1;
}
// set 'from' to the row we'll destroy from
// and 'to' to the row we'll destroy to
from = destroy_row_from->m_ListStart;
if (destroy_row_to != m_CharacterPositions.end())
to = destroy_row_to->m_ListStart; // notice it will iterate [from, to), so it will never reach to.
else
to = static_cast(m_Caption.length());
// Setup the first row
row.m_ListStart = destroy_row_from->m_ListStart;
std::list::iterator temp_it = destroy_row_to;
--temp_it;
current_line = m_CharacterPositions.erase(destroy_row_from, destroy_row_to);
// If there has been a change in number of characters
// we need to change all m_ListStart that comes after
// the interval we just destroyed. We'll change all
// values with the delta change of the string length.
int delta = to_after - to_before;
if (delta != 0)
{
for (std::list::iterator it = current_line;
it != m_CharacterPositions.end();
++it)
it->m_ListStart += delta;
// Update our check point too!
check_point_row_start += delta;
check_point_row_end += delta;
if (to != static_cast(m_Caption.length()))
to += delta;
}
}
int last_word_started = from;
float x_pos = 0.f;
//if (to_before != -1)
// return;
for (int i = from; i < to; ++i)
{
if (m_Caption[i] == L'\n' && m_MultiLine)
{
if (i == to-1 && to != static_cast(m_Caption.length()))
break; // it will be added outside
current_line = m_CharacterPositions.insert(current_line, row);
++current_line;
// Setup the next row:
row.m_ListOfX.clear();
row.m_ListStart = i+1;
x_pos = 0.f;
}
else
{
if (m_Caption[i] == L' '/* || TODO Gee (2004-10-13): the '-' disappears, fix.
m_Caption[i] == L'-'*/)
last_word_started = i+1;
if (!m_Mask)
x_pos += font.GetCharacterWidth(m_Caption[i]);
else
x_pos += font.GetCharacterWidth(mask_char);
if (x_pos >= GetTextAreaWidth() && m_MultiLine)
{
// The following decides whether it will word-wrap a word,
// or if it's only one word on the line, where it has to
// break the word apart.
if (last_word_started == row.m_ListStart)
{
last_word_started = i;
row.m_ListOfX.resize(row.m_ListOfX.size() - (i-last_word_started));
//row.m_ListOfX.push_back(x_pos);
//continue;
}
else
{
// regular word-wrap
row.m_ListOfX.resize(row.m_ListOfX.size() - (i-last_word_started+1));
}
// Now, create a new line:
// notice: when we enter a newline, you can stand with the cursor
// both before and after that character, being on different
// rows. With automatic word-wrapping, that is not possible. Which
// is intuitively correct.
current_line = m_CharacterPositions.insert(current_line, row);
++current_line;
// Setup the next row:
row.m_ListOfX.clear();
row.m_ListStart = last_word_started;
i = last_word_started-1;
x_pos = 0.f;
}
else
// Get width of this character:
row.m_ListOfX.push_back(x_pos);
}
// Check if it's the last iteration, and we're not revising the whole string
// because in that case, more word-wrapping might be needed.
// also check if the current line isn't the end
if (to_before != -1 && i == to-1 && current_line != m_CharacterPositions.end())
{
// check all rows and see if any existing
if (row.m_ListStart != check_point_row_start)
{
std::list::iterator destroy_row_from;
std::list::iterator destroy_row_to;
// Are used to check if the above has been set to anything,
// previously a comparison like:
// destroy_row_from == std::list::iterator()
// was used, but it didn't work with GCC.
bool destroy_row_from_used = false;
bool destroy_row_to_used = false;
// Iterate, and remove everything between 'from' and 'to_before'
// actually remove the entire lines they are on, it'll all have
// to be redone. And when going along, we'll delete a row at a time
// when continuing to see how much more after 'to' we need to remake.
for (std::list::iterator it = m_CharacterPositions.begin();
it != m_CharacterPositions.end();
++it)
{
if (!destroy_row_from_used && it->m_ListStart > check_point_row_start)
{
// Destroy the previous line, and all to 'to_before'
//if (i >= 2)
// destroy_row_from = it-2;
//else
// destroy_row_from = it-1;
destroy_row_from = it;
destroy_row_from_used = true;
//--destroy_row_from;
}
if (!destroy_row_to_used && it->m_ListStart > check_point_row_end)
{
destroy_row_to = it;
destroy_row_to_used = true;
// If it isn't the last row, we'll add another row to delete,
// just so we can see if the last restorted line is
// identical to what it was before. If it isn't, then we'll
// have to continue.
// 'check_point_row_start' is where we store how the that
// line looked.
if (destroy_row_to != m_CharacterPositions.end())
{
check_point_row_start = destroy_row_to->m_ListStart;
check_point_row_end = check_point_row_start + (int)destroy_row_to->m_ListOfX.size();
if (destroy_row_to->m_ListOfX.empty())
++check_point_row_end;
}
else
check_point_row_start = check_point_row_end = -1;
++destroy_row_to;
break;
}
}
if (!destroy_row_from_used)
{
destroy_row_from = m_CharacterPositions.end();
--destroy_row_from;
current_line = destroy_row_from;
}
if (!destroy_row_to_used)
{
destroy_row_to = m_CharacterPositions.end();
check_point_row_start = check_point_row_end = -1;
}
if (destroy_row_to != m_CharacterPositions.end())
to = destroy_row_to->m_ListStart; // notice it will iterate [from, to[, so it will never reach to.
else
to = static_cast(m_Caption.length());
// Set current line, new rows will be added before current_line, so
// we'll choose the destroy_row_to, because it won't be deleted
// in the coming erase.
current_line = destroy_row_to;
m_CharacterPositions.erase(destroy_row_from, destroy_row_to);
}
// else, the for loop will end naturally.
}
}
// This is kind of special, when we renew a some lines, then the last
// one will sometimes end with a space (' '), that really should
// be omitted when word-wrapping. So we'll check if the last row
// we'll add has got the same value as the next row.
if (current_line != m_CharacterPositions.end())
{
if (row.m_ListStart + (int)row.m_ListOfX.size() == current_line->m_ListStart)
row.m_ListOfX.resize(row.m_ListOfX.size()-1);
}
// add the final row (even if empty)
m_CharacterPositions.insert(current_line, row);
if (m_ScrollBar)
{
GetScrollBar(0).SetScrollRange(m_CharacterPositions.size() * font.GetLineSpacing() + m_BufferZone * 2.f);
GetScrollBar(0).SetScrollSpace(m_CachedActualSize.GetHeight());
}
}
int CInput::GetMouseHoveringTextPosition() const
{
if (m_CharacterPositions.empty())
return 0;
// Return position
int retPosition;
std::list::const_iterator current = m_CharacterPositions.begin();
CPos mouse = m_pGUI.GetMousePos();
if (m_MultiLine)
{
float scroll = 0.f;
if (m_ScrollBar)
scroll = GetScrollBarPos(0);
// Now get the height of the font.
// TODO: Get the real font
CFontMetrics font(CStrIntern(m_Font.ToUTF8()));
float spacing = (float)font.GetLineSpacing();
// Change mouse position relative to text.
mouse -= m_CachedActualSize.TopLeft();
mouse.x -= m_BufferZone;
mouse.y += scroll - m_BufferZone;
int row = (int)((mouse.y) / spacing);
if (row < 0)
row = 0;
if (row > (int)m_CharacterPositions.size()-1)
row = (int)m_CharacterPositions.size()-1;
// TODO Gee (2004-11-21): Okay, I need a 'std::list' for some reasons, but I would really like to
// be able to get the specific element here. This is hopefully a temporary hack.
for (int i = 0; i < row; ++i)
++current;
}
else
{
// current is already set to begin,
// but we'll change the mouse.x to fit our horizontal scrolling
mouse -= m_CachedActualSize.TopLeft();
mouse.x -= m_BufferZone - m_HorizontalScroll;
// mouse.y is moot
}
retPosition = current->m_ListStart;
// Okay, now loop through the glyphs to find the appropriate X position
float dummy;
retPosition += GetXTextPosition(current, mouse.x, dummy);
return retPosition;
}
// Does not process horizontal scrolling, 'x' must be modified before inputted.
int CInput::GetXTextPosition(const std::list::const_iterator& current, const float& x, float& wanted) const
{
int ret = 0;
float previous = 0.f;
int i = 0;
for (std::vector::const_iterator it = current->m_ListOfX.begin();
it != current->m_ListOfX.end();
++it, ++i)
{
if (*it >= x)
{
if (x - previous >= *it - x)
ret += i+1;
else
ret += i;
break;
}
previous = *it;
}
// If a position wasn't found, we will assume the last
// character of that line.
if (i == (int)current->m_ListOfX.size())
{
ret += i;
wanted = x;
}
else
wanted = 0.f;
return ret;
}
void CInput::DeleteCurSelection()
{
int virtualFrom;
int virtualTo;
if (m_iBufferPos_Tail >= m_iBufferPos)
{
virtualFrom = m_iBufferPos;
virtualTo = m_iBufferPos_Tail;
}
else
{
virtualFrom = m_iBufferPos_Tail;
virtualTo = m_iBufferPos;
}
m_Caption =
m_Caption.Left(virtualFrom) +
m_Caption.Right(static_cast(m_Caption.length()) - virtualTo);
UpdateText(virtualFrom, virtualTo, virtualFrom);
// Remove selection
m_iBufferPos_Tail = -1;
m_iBufferPos = virtualFrom;
UpdateBufferPositionSetting();
}
bool CInput::SelectingText() const
{
return m_iBufferPos_Tail != -1 &&
m_iBufferPos_Tail != m_iBufferPos;
}
float CInput::GetTextAreaWidth()
{
if (m_ScrollBar && GetScrollBar(0).GetStyle())
return m_CachedActualSize.GetWidth() - m_BufferZone * 2.f - GetScrollBar(0).GetStyle()->m_Width;
return m_CachedActualSize.GetWidth() - m_BufferZone * 2.f;
}
void CInput::UpdateAutoScroll()
{
// Autoscrolling up and down
if (m_MultiLine)
{
if (!m_ScrollBar)
return;
const float scroll = GetScrollBar(0).GetPos();
// Now get the height of the font.
// TODO: Get the real font
CFontMetrics font(CStrIntern(m_Font.ToUTF8()));
float spacing = (float)font.GetLineSpacing();
//float height = font.GetHeight();
// TODO Gee (2004-11-21): Okay, I need a 'std::list' for some reasons, but I would really like to
// be able to get the specific element here. This is hopefully a temporary hack.
std::list::iterator current = m_CharacterPositions.begin();
int row = 0;
while (current != m_CharacterPositions.end())
{
if (m_iBufferPos >= current->m_ListStart &&
m_iBufferPos <= current->m_ListStart + (int)current->m_ListOfX.size())
break;
++current;
++row;
}
// If scrolling down
if (-scroll + static_cast(row + 1) * spacing + m_BufferZone * 2.f > m_CachedActualSize.GetHeight())
{
// Scroll so the selected row is shown completely, also with m_BufferZone length to the edge.
GetScrollBar(0).SetPos(static_cast(row + 1) * spacing - m_CachedActualSize.GetHeight() + m_BufferZone * 2.f);
}
// If scrolling up
else if (-scroll + (float)row * spacing < 0.f)
{
// Scroll so the selected row is shown completely, also with m_BufferZone length to the edge.
GetScrollBar(0).SetPos((float)row * spacing);
}
}
else // autoscrolling left and right
{
// Get X position of position:
if (m_CharacterPositions.empty())
return;
float x_position = 0.f;
float x_total = 0.f;
if (!m_CharacterPositions.begin()->m_ListOfX.empty())
{
// Get position of m_iBufferPos
if ((int)m_CharacterPositions.begin()->m_ListOfX.size() >= m_iBufferPos &&
m_iBufferPos > 0)
x_position = m_CharacterPositions.begin()->m_ListOfX[m_iBufferPos-1];
// Get complete length:
x_total = m_CharacterPositions.begin()->m_ListOfX[m_CharacterPositions.begin()->m_ListOfX.size()-1];
}
// Check if outside to the right
if (x_position - m_HorizontalScroll + m_BufferZone * 2.f > m_CachedActualSize.GetWidth())
m_HorizontalScroll = x_position - m_CachedActualSize.GetWidth() + m_BufferZone * 2.f;
// Check if outside to the left
if (x_position - m_HorizontalScroll < 0.f)
m_HorizontalScroll = x_position;
// Check if the text doesn't even fill up to the right edge even though scrolling is done.
if (m_HorizontalScroll != 0.f &&
x_total - m_HorizontalScroll + m_BufferZone * 2.f < m_CachedActualSize.GetWidth())
m_HorizontalScroll = x_total - m_CachedActualSize.GetWidth() + m_BufferZone * 2.f;
// Now this is the fail-safe, if x_total isn't even the length of the control,
// remove all scrolling
if (x_total + m_BufferZone * 2.f < m_CachedActualSize.GetWidth())
m_HorizontalScroll = 0.f;
}
}
Index: ps/trunk/source/gui/ObjectTypes/CInput.h
===================================================================
--- ps/trunk/source/gui/ObjectTypes/CInput.h (revision 24432)
+++ ps/trunk/source/gui/ObjectTypes/CInput.h (revision 24433)
@@ -1,218 +1,240 @@
/* Copyright (C) 2020 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 .
*/
#ifndef INCLUDED_CINPUT
#define INCLUDED_CINPUT
#include "gui/CGUISprite.h"
#include "gui/ObjectBases/IGUIObject.h"
#include "gui/ObjectBases/IGUIScrollBarOwner.h"
+#include "gui/SettingTypes/CGUIString.h"
#include "lib/external_libraries/libsdl.h"
#include
#include
/**
* Text field where you can input and edit the text.
*
* It doesn't use IGUITextOwner, because we don't need
* any other features than word-wrapping, and we need to be
* able to rapidly change the string.
*/
class CInput : public IGUIObject, public IGUIScrollBarOwner
{
GUI_OBJECT(CInput)
protected: // forwards
struct SRow;
public:
CInput(CGUI& pGUI);
virtual ~CInput();
/**
* @see IGUIObject#ResetStates()
*/
virtual void ResetStates();
// Check where the mouse is hovering, and get the appropriate text position.
// return is the text-position index.
int GetMouseHoveringTextPosition() const;
// Same as above, but only on one row in X, and a given value, not the mouse's.
// wanted is filled with x if the row didn't extend as far as the mouse pos.
int GetXTextPosition(const std::list::const_iterator& c, const float& x, float& wanted) const;
protected:
+
+ void SetupGeneratedPlaceholderText();
+
/**
* @see IGUIObject#HandleMessage()
*/
virtual void HandleMessage(SGUIMessage& Message);
/**
* Handle events manually to catch keyboard inputting.
*/
virtual InReaction ManuallyHandleKeys(const SDL_Event_* ev);
/**
* Handle events manually to catch keys which change the text.
*/
virtual void ManuallyMutableHandleKeyDownEvent(const SDL_Keycode keyCode);
/**
* Handle events manually to catch keys which don't change the text.
*/
virtual void ManuallyImmutableHandleKeyDownEvent(const SDL_Keycode keyCode);
/**
* Handle hotkey events (called by ManuallyHandleKeys)
*/
virtual InReaction ManuallyHandleHotkeyEvent(const SDL_Event_* ev);
/**
* @see IGUIObject#UpdateCachedSize()
*/
virtual void UpdateCachedSize();
/**
* Draws the Text
*/
virtual void Draw();
/**
* Calculate m_CharacterPosition
* the main task for this function is to perfom word-wrapping
* You input from which character it has been changed, because
* if we add a character to the very last end, we don't want
* process everything all over again! Also notice you can
* specify a 'to' also, it will only be used though if a '\n'
* appears, because then the word-wrapping won't change after
* that.
*/
void UpdateText(int from = 0, int to_before = -1, int to_after = -1);
/**
+ * Draws the text generated for placeholder.
+ *
+ * @param z Z value
+ * @param clipping Clipping rectangle, don't even add a parameter
+ * to get no clipping.
+ */
+ virtual void DrawPlaceholderText(float z, const CRect& clipping = CRect());
+
+ /**
* Delete the current selection. Also places the pointer at the
* crack between the two segments kept.
*/
void DeleteCurSelection();
/**
* Is text selected? It can be denote two ways, m_iBufferPos_Tail
* being -1 or the same as m_iBufferPos. This makes for clearer
* code.
*/
bool SelectingText() const;
/// Get area of where text can be drawn.
float GetTextAreaWidth();
/// Called every time the auto-scrolling should be checked.
void UpdateAutoScroll();
/// Clear composed IME input when supported (SDL2 only).
void ClearComposedText();
/// Updates the buffer (cursor) position exposed to JS.
void UpdateBufferPositionSetting();
protected:
/// Cursor position
int m_iBufferPos;
/// Cursor position we started to select from. (-1 if not selecting)
/// (NB: Can be larger than m_iBufferPos if selecting from back to front.)
int m_iBufferPos_Tail;
/// If we're composing text with an IME
bool m_ComposingText;
/// The length and position of the current IME composition
int m_iComposedLength, m_iComposedPos;
/// The position to insert committed text
int m_iInsertPos;
// the outer vector is lines, and the inner is X positions
// in a row. So that we can determine where characters are
// placed. It's important because we need to know where the
// pointer should be placed when the input control is pressed.
struct SRow
{
// Where the Row starts
int m_ListStart;
// List of X values for each character.
std::vector m_ListOfX;
};
/**
* List of rows to ease changing its size, so iterators stay valid.
* For one-liners only one row is used.
*/
std::list m_CharacterPositions;
// *** Things for a multi-lined input control *** //
/**
* When you change row with up/down, and the row you jump to does
* not have anything at that X position, then it will keep the
* m_WantedX position in mind when switching to the next row.
* It will keep on being used until it reach a row which meets the
* requirements.
* 0.0f means not in use.
*/
float m_WantedX;
/**
* If we are in the process of selecting a larger selection of text
* using the mouse click (hold) and drag, this is true.
*/
bool m_SelectingText;
+ /**
+ * Whether the cached text is currently valid (if not then SetupText will be called by Draw)
+ */
+ bool m_GeneratedPlaceholderTextValid;
+
+ CGUIText m_GeneratedPlaceholderText;
+
// *** Things for one-line input control *** //
float m_HorizontalScroll;
/// Used to store the previous time for flashing the cursor.
double m_PrevTime;
/// Cursor blink rate in seconds, if greater than 0.0.
double m_CursorBlinkRate;
/// If the cursor should be drawn or not.
bool m_CursorVisState;
static const CStr EventNameTextEdit;
static const CStr EventNamePress;
static const CStr EventNameTab;
// Settings
i32 m_BufferPosition;
float m_BufferZone;
CStrW m_Caption;
+ CGUIString m_PlaceholderText;
i32 m_CellID;
CStrW m_Font;
CStrW m_MaskChar;
bool m_Mask;
i32 m_MaxLength;
bool m_MultiLine;
bool m_Readonly;
bool m_ScrollBar;
CStr m_ScrollBarStyle;
CGUISpriteInstance m_Sprite;
CGUISpriteInstance m_SpriteSelectArea;
CGUIColor m_TextColor;
CGUIColor m_TextColorSelected;
+ CGUIColor m_PlaceholderColor;
};
#endif // INCLUDED_CINPUT