Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
54 KB
Referenced Files
None
Subscribers
None
diff --git a/docs/ScriptAPI.md b/docs/ScriptAPI.md
index 995f245..493f06d 100644
--- a/docs/ScriptAPI.md
+++ b/docs/ScriptAPI.md
@@ -1,776 +1,776 @@
Me and My Shadow Script API Reference
=====================================
(draft)
The script language is Lua 5.3.
Always check `ScriptAPI.cpp` for the newest API changed unmentioned in this document.
How to edit script
==================
To edit the script of a block, right click the block and select "Scripting".
To edit the script of the level, right click the empty space of the level and select "Scripting".
You can also edit the `<level_file_name>.lua` file,
if this file is present it will be executed before the "onCreate" event of level.
Currently the scenery block doesn't support scripting.
Available event types of block:
Event type | Description
----------------------|--------------
"playerWalkOn" | Fired once when the player walks on. (For example this is used in fragile block.)
"playerIsOn" | Fired every frame when the player is on.
"playerLeave" | Fired once when the player leaves.
"onCreate" | Fired when object creates.
"onEnterFrame" | Fired every frame.
"onPlayerInteraction" | Fired when the player press DOWN key. Currently this event only fires when the block type is TYPE_SWITCH.
"onToggle" | Fired when the block receives "toggle" from a switch/button. NOTE: The switch/button itself will also receive this event. This is used in an old example found on my old computer.
"onSwitchOn" | Fired when the block receives "switch on" from a switch/button. NOTE: The switch/button itself will also receive this event. This is used in an old example found on my old computer.
"onSwitchOff" | Fired when the block receives "switch off" from a switch/button. NOTE: The switch/button itself will also receive this event. This is used in an old example found on my old computer.
NOTE: During the event execution the global variable `this` temporarily points to current block. (Ad-hoc workaround!)
When the event execution ends the global variable `this` is reset to its previous value.
The block event may return an integer value (default is 0) to alter the game logic:
Return value | Description
-------------|--------------
0 | Skip the default game logic for this event.
1 | Do the default game logic for this event.
Available event types of level:
Event type | Description
-----------|--------------
"onCreate" | Fired when the level is created or the game is reset. This happens **after** all the blocks are created and their `onCreate` is called.
"onSave" | Fired when the game is saved.
"onLoad" | Fired when the game is loaded.
For the newest lists of event types, see `init()` function in `Functions.cpp`.
NOTE: the following methods to specify scripts can be used:
* Specify scripts for each events in the block script editing dialog.
* Only specify `onCreate` script in the block script editing dialog,
and use `setEventHandler()` function in script to specify scripts for other events dynamically.
* Only specify `onCreate` script in the level script editing dialog
(you can put this script to the `<level_file_name>.lua` file),
and use `setEventHandler()` function in script to specify scripts for other events for level/blocks dynamically.
Script API reference
====================
The "block" library
-------------------
### Static functions:
* getBlockById(id)
Returns the first block with specified id. If not found, returns `nil`.
Example:
~~~lua
local b=block.getBlockById("1")
local x,y=b:getLocation()
print(x..","..y)
~~~
* getBlocksById(id)
Returns the list of all blocks with specified id.
Example:
~~~lua
local l=block.getBlocksById("1")
for i,b in ipairs(l) do
local x,y=b:getLocation()
print(x..","..y)
end
~~~
* removeAll()
Remove all blocks.
* addBlock(string,[x],[y],[w],[h])
Add a new block (optionally give it a new position and size) and return the newly created block.
The `string` is the text representation of a block which is used in `.map` file.
The new block can have scripts and the `onCreate` script will be executed immediately.
Example:
~~~lua
-- Assume we have moving blocks of id 1,
-- then this newly added switch can operate existing moving blocks.
block.addBlock([[
tile(Switch,0,0,50,50){
behaviour=toggle
id=1
script(onCreate){
script="print('Hello world from onCreate of dynamically added block')"
}
}]],250,300)
~~~
Another example:
~~~lua
local b=block.addBlock('tile(MovingBlock,0,0)')
b:setBaseLocation(
math.random()*level.getWidth(),
math.random()*level.getHeight())
local bx,by=b:getBaseLocation()
for i=1,10 do
b:addMovingPos({
math.random()*level.getWidth()-bx,
math.random()*level.getHeight()-by,
math.random()*80+40
})
end
b:addMovingPos({0,0,math.random()*80+40})
~~~
* addBlocks(string,[positions]) / addBlocks(string,offsetX,offsetY)
Add new blocks (optionally give them new positions and sizes) and return an array the newly created blocks.
The `string` is the text representation of blocks which is used in `.map` file.
In the first form,
If `string` contains only one block, then it will be created repeatedly using the specified positions.
If it contains more than one block, then each block will use corresponding position in `positions`.
The `positions` is an array of new positions whose entry is of format `{[x],[y],[w],[h]}`.
In the second form,
the `string` can contain one or more blocks,
and the position of each block will be offset by two numbers `offsetX` and `offsetY`.
The new blocks can have scripts and the `onCreate` script will be executed immediately.
### Member functions:
* isValid() -- check the object is valid (i.e. not deleted, etc.)
* moveTo(x,y)
Move the block to the new position, update the velocity of block according to the position changed.
Example:
~~~lua
local b=block.getBlockById("1")
local x,y=b:getLocation()
b:moveTo(x+1,y)
~~~
* getLocation()
Returns the position of the block.
Example: see the example for moveTo().
* setLocation(x,y)
Move the block to the new position without updating the velocity of block.
Example: omitted since it's almost the same as moveTo().
* getBaseLocation() / setBaseLocation(x,y)
Get or set the base position of the block. Mainly used for moving blocks.
* growTo(w,h)
Resize the block, update the velocity of block according to the size changed.
NOTE: I don't think the velocity need to be updated when resizing block, so don't use this function.
Example: omitted since it's almost the same as setSize().
* getSize()
Returns the size of the block.
Example:
~~~lua
local b=block.getBlockById("1")
local w,h=b:getSize()
print(w..","..h)
~~~
* setSize(w,h)
Resize the block without updating the velocity of block.
Example:
~~~lua
local b=block.getBlockById("1")
local w,h=b:getSize()
b:setSize(w+1,h)
~~~
* getBaseSize() / setBaseSize(x,y)
Get or set the base size of the block. Mainly used for moving blocks.
* getType()
Returns the type of the block (which is a string).
Example:
~~~lua
local b=block.getBlockById("1")
local s=b:getType()
print(s)
~~~
* changeThemeState(new_state)
Change the state of the block to new_state (which is a string).
Example:
~~~lua
local b=block.getBlockById("1")
b:changeThemeState("activated")
~~~
* setVisible(b)
Set the visibility the block.
NOTE: The default value is `true`. If set to `false` the block is hidden completely,
the animation is stopped, can't receive any event, can't execute any scripts (except for `onCreate`),
can't be used as a portal destination,
doesn't participate in collision check and game logic, etc...
NOTE: This is a newly added feature.
If you find any bugs (e.g. if an invisible block still affects the game logic)
please report the bugs to GitHub issue tracker.
Example:
~~~lua
local b=block.getBlockById("1")
if b:isVisible() then
b:setVisible(false)
else
b:setVisible(true)
end
~~~
* isVisible()
Returns whether the block is visible.
Example: see the example for setVisible().
* getEventHandler(event_type)
Returns the event handler of event_type (which is a string).
Example:
~~~lua
local b=block.getBlockById("1")
local f=b:getEventHandler("onSwitchOn")
b:setEventHandler("onSwitchOff",f)
~~~
* setEventHandler(event_type,handler)
Set the handler of event_type (which is a string). The handler should be a function or `nil`.
Returns the previous event handler.
Example:
~~~lua
local b=block.getBlockById("1")
b:setEventHandler("onSwitchOff",function()
print("I am switched off.")
end)
~~~
* onEvent(eventType)
Fire an event to specified block.
NOTE: The event will be processed immediately.
Example:
~~~lua
local b=block.getBlockById("1")
b:onEvent("onToggle")
~~~
NOTE: Be careful not to write infinite recursive code! Bad example:
~~~lua
-- onToggle event of a moving block
this:onEvent("onToggle")
~~~
* isActivated() / setActivated(bool)
Get/set a boolean indicates if the block is activated.
The block should be one of TYPE_MOVING_BLOCK, TYPE_MOVING_SHADOW_BLOCK, TYPE_MOVING_SPIKES,
TYPE_CONVEYOR_BELT, TYPE_SHADOW_CONVEYOR_BELT.
* isAutomatic() / setAutomatic(bool)
Get/set a boolean indicates if the portal is automatic.
The block should be TYPE_PORTAL.
* getBehavior() / setBehavior(str)
Get/set a string (must be "on", "off" or "toggle"),
representing the behavior of the block.
The block should be TYPE_BUTTON, TYPE_SWITCH.
* getState() / setState(num)
Get/set a number (must be 0,1,2 or 3),
representing the state of a fragile block.
The block should be TYPE_FRAGILE.
* isPlayerOn()
Get a boolean indicates if the player is on.
Currently only works for TYPE_BUTTON.
* getPathMaxTime()
Get the total time of the path of a moving block.
* getPathTime() / setPathTime(num)
Get/set the current time of the path of a moving block.
* isLooping() / setLooping(bool)
Get/set the looping property of a moving block.
* getSpeed() / setSpeed(num)
Get/set the speed of a conveyor belt.
NOTE: 1 Speed = 0.08 block/s = 0.1 pixel/frame.
* getAppearance() / setAppearance(str)
Get/set the custom appearance of a block.
The `str` is the name of the custom appearance, either `"<blockName>_Scenery"` or name of a scenery block.
Empty string or nil means the default appearance.
* getId() / setId(str)
Get/set the id of a block.
* getDestination() / setDestination(str)
Get/set the destination of a portal.
The block should be TYPE_PORTAL.
* getMessage() / setMessage(str)
Get/set the message of a notification block or a portal or a switch.
The block should be TYPE_NOTIFICATION_BLOCK, TYPE_PORTAL or TYPE_SWITCH.
NOTE: The block message will be always passed to gettext() before showing on the screen.
Therefore it's a good idea to wrap the message with `__()`.
DON'T wrap it with `_()`!
Example:
~~~lua
local b=block.getBlockById("1")
b:setMessage(__("do something"))
-- if the block is TYPE_NOTIFICATION_BLOCK, it will show (a localized version of) "do something"
-- if the block is TYPE_PORTAL or TYPE_SWITCH, it will show "Press DOWN key to do something."
~~~
* getMovingPosCount()
Get the number of moving positions of a moving block.
* getMovingPos() / getMovingPos(index) / getMovingPos(start, length)
Get the array of moving positions or the moving position at specified index
(the array index starts with 1 in Lua).
The individual point is of format `{x,y,t}`.
* setMovingPos(array) / setMovingPos(index, point) / setMovingPos(start, length, array)
Set the array of moving positions or modify the moving position at specified index
(the array index starts with 1 in Lua).
NOTE: the last two forms won't change the number of points,
while the first form will overwrite the list of points completely.
* addMovingPos(p) / addMovingPos(index, p)
Insert points to the array of moving positions at the end or at the specified index.
The `p` can be one point or a list of points.
* removeMovingPos() / removeMovingPos(index) / removeMovingPos(listOfIndices) / removeMovingPos(start, length)
Remove points in the array of moving positions: remove all points,
or remove a point at specified index, or remove points at specified indices,
or remove points in given range.
* clone([x],[y],[w],[h])
Create a clone of current block, optionally give it a new position and size.
The new block can have scripts and the `onCreate` script will be executed immediately.
Returns the newly created block.
* cloneMultiple(number) / cloneMultiple(positions)
Create multiple clones of current block, optionally give them new positions and sizes.
The `number` is the number of clones to made, whose positions are the same as the source block,
whereas `positions` is an array of new positions whose entry is of format `{[x],[y],[w],[h]}`.
The new blocks can have scripts and the `onCreate` script will be executed immediately.
Returns an array of newly created blocks.
* remove()
Remove current block.
The "playershadow" library
--------------------------
### Global constants:
* player
The player object.
* shadow
The shadow object.
### Member functions:
* getLocation()
Returns the location of player/shadow.
Example:
~~~lua
local x,y=player:getLocation()
print("player: "..x..","..y)
x,y=shadow:getLocation()
print("shadow: "..x..","..y)
~~~
* setLocation(x,y)
Set the location of player/shadow.
Example:
~~~lua
local x,y=player:getLocation()
player:setLocation(x+1,y)
~~~
* jump([strength=13])
Let the player/shadow jump if it's allowed.
strength: Jump strength.
Example:
~~~lua
player:jump(20)
~~~
* isShadow()
Returns whether the current object is shadow.
Example:
~~~lua
print(player:isShadow())
print(shadow:isShadow())
~~~
* getCurrentStand()
Returns the block on which the player/shadow is standing on. Can be `nil`.
Example:
~~~lua
local b=player:getCurrentStand()
if b then
print(b:getType())
else
print("The player is not standing on any blocks")
end
~~~
* isInAir() -- returns a boolean indicating if the player is in air
* canMove() -- returns a boolean indicating if the player can move (i.e. not standing on shadow)
* isDead() -- returns a boolean indicating if the player is dead
* isHoldingOther() -- returns a boolean indicating if the player is holding other
The "level" library
-------------------
### Static functions:
* getSize() -- get the level size
* getRect() / setRect(x,y,w,h) -- get or set the level rect (left,top,width,height)
* getWidth() -- get the level width
* getHeight() -- get the level height
* getName() -- get the level name
* getEventHandler(event_type) -- get the event handler
* setEventHandler(event_type,handler) -- set the event handler, return the old handler
* win() -- win the game
* getTime() -- get the game time (in frames)
* getRecordings() -- get the number of recordings
* getCollectables() -- get the number of currently obtained collectibles
* getTotalCollectables() -- get the number of total collectibles in the level
* broadcastObjectEvent(eventType,[objectType=nil],[id=nil],[target=nil])
Broadcast the event to blocks satisfying the specified condition.
NOTE: The event will be processed in next frame.
Argument name | Description
--------------|-------------
eventType | string.
objectType | string or nil. If this is set then the event is only received by the block with specified type.
id | string or nil. If this is set then the event is only received by the block with specified id.
target | block or nil. If this is set then the event is only received by the specified block.
Example:
~~~lua
level.broadcastObjectEvent("onToggle",nil,"1")
~~~
The "delayExecution" library
----------------------------
### Static functions:
* schedule(func,time,[repeatCount=1],[repeatInterval],[enabled=true],[arguments...])
Schedule a delay execution of a given function after the given time.
Argument name | Description
---------------|-------------
func | A function to be executed.
time | Time, given in frames (NOTE: 40 frames = 1 second). NOTE: If <=0 it is the same as =1.
repeatCount | The number of times the function will be executed. After such number of times executed, the delay execution will be removed from the list and get deleted. If =0 the delay execution object will be deleted soon. If <0 the function will be executed indefinitely.
repeatInterval | The repeat interval. If it is `nil` then the `time` argument will be used instead. NOTE: If <=0 the repeat execution will be disabled at all and the repeatCount will be set to 1.
enabled | Enabled.
arguments | Optional arguments passed to the function.
Return value: the delayExecution object.
NOTE: If you want to update time/repeatCount during the function execution,
notice that the time/repeatCount is updated BEFORE the function execution.
NOTE: During the execution the global variable `this`
temporarily points to current delay execution object. (Ad-hoc workaround!)
When the execution ends the global variable `this` is reset to its previous value.
Example:
~~~lua
local f=function()
local a
a=0
return(function(b)
shadow:jump()
print('obj1 '..this:getExecutionTime()..' '..a..' '..tostring(b))
a=a+2
end)
end
local obj1=delayExecution.schedule(f(),40*2,5,nil,nil,100)
local obj2=delayExecution.schedule(
function(o)
print('obj2 '..tostring(o:isValid()))
if not o:isValid() then
this:setFunc(f())
end
end,40*1,-1,nil,nil,obj1)
local obj3=delayExecution.schedule(
function(o)
o:cancel()
end,40*30,1,nil,nil,obj2)
~~~
### Member functions:
* isValid() -- Check if it's valid, i.e. not removed from list.
* cancel() -- Cancels a delay execution. The canceled delay execution will be removed from the list and can not be restored.
* isEnabled()/setEnabled(bool) -- get/set enabled of a delay execution. A disabled one will not count down its timer.
* getTime()/setTime(integer) -- get/set the remaining time until the next execution. NOTE: If <=0 it is the same as =1.
* getRepeatCount()/setRepeatCount(integer) -- get/set the remaining repeat count. If =0 the object will get deleted soon. If <0 the function will be executed indefinitely.
* getRepeatInterval()/setRepeatInterval(integer) -- get/set the repeat interval. NOTE: If <=0 then nothing happens.
* getFunc()/setFunc(func) -- get/set the function to be executed. NOTE: The setFunc will return the original function.
* getArguments()/setArguments(args...) -- get/set the arguments
* getExecutionTime()/setExecutionTime(integer) -- get/set the number of times the function being executed. NOTE: this execution time doesn't affect the default logic.
The "camera" library
--------------------
### Static functions:
* setMode(mode) -- set the camera mode, which is "player" or "shadow"
* lookAt(x,y) -- set the camera mode to "custom" and set the new center of camera
The "audio" library
-------------------
NOTE: the following functions are not going to work if the sound/music volume is 0.
### Static functions:
* playSound(name[,concurrent=-1[,force=false[,fade=-1]]])
Play a sound effect.
Argument name | Description
--------------|-------------
name | The name of the sound effect. Currently available: "jump", "hit", "checkpoint", "swap", "toggle", "error", "collect", "achievement".
concurrent | The number of times the same sfx can be played at once, -1 is unlimited. NOTE: there are 64 channels.
force | If the sound must be played even if all channels are used. In this case the sound effect in the first channel will be stopped.
fade | A factor to temporarily turn the music volume down (0-128). -1 means don't use this feature.
Return value: The channel of the sfx. -1 means failed (channel is full, invalid sfx name, sfx volume is 0, etc.)
* playMusic(name[,fade=true])
Play a music.
Argument name | Description
--------------|-------------
name | The name of the song, e.g. "default/neverending" or "menu".
fade | Boolean if it should fade the current one out or not.
* pickMusic() - pick a song from the current music list.
-* getMusicList()/setMusicList(name_of_the_music_list) - get/set the music list. Example: "default".
+* getMusicList()/setMusicList(name_of_the_music_list) -- get/set the music list. Example: "default".
* currentMusic() - get the current music.
The "gettext" library
--------------------
This library is used for translation support.
NOTE: Currently this library only uses the dictionary for current level pack.
This means it doesn't work for individual level which doesn't contain in a level pack,
and it can't make use of the translations of the core game.
### Global functions:
* `_(msgid)` -- translate the string using default context
* `__(msgid)` -- does nothing, just outputs the original string. However, it will be scanned by `xgettext`.
Mainly used in block:setMessage() since the block message will always be passed to gettext().
Also used in construction of an array of strings, which will be translated dynamically.
### Static functions:
* gettext(msgid) -- translate the string using default context
* pgettext(msgctxt,msgid) -- translate the string using specified context
* ngettext(msgid,msgid_plural,n) -- translate the string using default context, taking plural form into consideration
* npgettext(msgctxt,msgid,msgid_plural,n) -- translate the string using specified context, taking plural form into consideration
The "prng" library
--------------------
The Mersenne Twister 19937 pseudo-random number generator.
The random seed is recreated each time the game starts, and is saved to the record file,
which ensures the reproducibility of the replay.
### Static functions:
* random() / random(n) / random(m,n)
These functions have the same arguments as the Lua built-in function `math.random()`. More precisely:
When called without arguments, returns a pseudo-random float with uniform distribution in the range `[0,1)`.
When called with two integers `m` and `n`, returns a pseudo-random integer with uniform distribution in the range `[m, n]`.
The `m` cannot be larger than `n`
(othewise it will return a pseudo-random integer with uniform distribution in the union of `[m, 2^63-1]` and `[-2^63, n]`)
and must fit in two Lua integers.
The call `random(n)` is equivalent to `random(1,n)`.
* getSeed() / setSeed(string)
Get or set the random seed, which is a string.
This is mainly used when you want the pseudo-random number to be reproducible even between each plays.
NOTE: This should only contains alphanumeric characters. Other characters will be filtered out.
diff --git a/src/HelpManager.cpp b/src/HelpManager.cpp
index ea360bc..82d4ab8 100644
--- a/src/HelpManager.cpp
+++ b/src/HelpManager.cpp
@@ -1,723 +1,961 @@
/*
* Copyright (C) 2019 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "HelpManager.h"
#include "Functions.h"
#include "GUIWindow.h"
#include "GUIListBox.h"
#include "GUITextArea.h"
#include "WordWrapper.h"
#include "ThemeManager.h"
#include <iostream>
#include <fstream>
+#include <sstream>
#include <algorithm>
+#include <stdio.h>
#include "SDL_ttf_fontfallback.h"
class Chunk {
public:
int cachedMaxWidth, cachedWidth, cachedNumberOfLines;
public:
Chunk() : cachedMaxWidth(-1), cachedWidth(-1), cachedNumberOfLines(-1) {}
virtual ~Chunk() {}
void updateSize(int maxWidth) {
if (maxWidth != cachedMaxWidth) updateSizeForced(maxWidth);
}
virtual void updateSizeForced(int maxWidth) = 0;
virtual void createSurfaces(std::vector<SurfacePtr>& surfaces) = 0;
};
class ParagraphChunk : public Chunk {
public:
std::string text;
std::vector<std::string> cachedLines;
virtual void updateSizeForced(int maxWidth) override {
WordWrapper wrapper;
wrapper.font = fontText;
wrapper.wordWrap = true;
wrapper.maxWidth = maxWidth;
wrapper.hyphen = "-";
wrapper.hyphenatorLanguage = "en";
//TODO: verbatim support
cachedLines.clear();
cachedMaxWidth = maxWidth;
cachedWidth = wrapper.addString(cachedLines, text);
cachedNumberOfLines = cachedLines.size();
}
virtual void createSurfaces(std::vector<SurfacePtr>& surfaces) override {
SDL_Color fg = objThemes.getTextColor(true);
for (const std::string& s : cachedLines) {
surfaces.emplace_back(TTF_RenderUTF8_Blended(fontText, s.c_str(), fg));
}
}
};
class ItemizeChunk : public Chunk {
public:
std::vector<Chunk*> items;
virtual ~ItemizeChunk() {
for (auto item : items) {
delete item;
}
}
virtual void updateSizeForced(int maxWidth) override {
cachedMaxWidth = maxWidth;
cachedWidth = 0;
cachedNumberOfLines = 0;
for (auto item : items) {
if (item) {
item->updateSize(maxWidth - 16);
cachedWidth = std::max(item->cachedWidth, cachedWidth);
cachedNumberOfLines += item->cachedNumberOfLines;
}
}
cachedWidth += 16;
}
virtual void createSurfaces(std::vector<SurfacePtr>& surfaces) override {
auto bmGUI = getImageManager().loadImage(getDataPath() + "gfx/gui.png");
for (auto item : items) {
if (item) {
std::vector<SurfacePtr> tempSurfaces;
item->createSurfaces(tempSurfaces);
for (int i = 0, m = tempSurfaces.size(); i < m; i++) {
SDL_Surface *src = tempSurfaces[i].get();
SurfacePtr surface = createSurface(src->w + 16, src->h);
SDL_Rect srcrect = { 0, 0, src->w, src->h };
SDL_Rect dstrect = { 16, 0, src->w, src->h };
SDL_BlitSurface(src, &srcrect, surface.get(), &dstrect);
if (i == 0) {
srcrect = SDL_Rect{ 80, 64, 16, 16 };
dstrect = SDL_Rect{ 0, (src->h - 16) / 2, 16, 16 };
SDL_BlitSurface(bmGUI, &srcrect, surface.get(), &dstrect);
}
surfaces.push_back(std::move(surface));
}
}
}
}
};
class TableChunk : public Chunk {
public:
int columns, rows;
std::vector<Chunk*> items;
std::vector<int> cachedRowWidth;
static const int lineWidth = 16;
TableChunk() : columns(0), rows(0) {}
virtual ~TableChunk() {
for (auto item : items) {
delete item;
}
}
virtual void updateSizeForced(int maxWidth) override {
cachedMaxWidth = maxWidth;
cachedWidth = 0;
cachedNumberOfLines = 0;
cachedRowWidth.clear();
for (int i = 0; i < columns; i++) {
int mw = (i < columns - 1) ? 0x40000000 : (maxWidth - lineWidth * 2 - cachedWidth);
int w = 0;
for (int j = 0; j < rows; j++) {
auto item = items[j * columns + i];
if (item) {
item->updateSize(mw);
w = std::max(item->cachedWidth, w);
}
}
cachedRowWidth.push_back(w);
cachedWidth += w + lineWidth;
}
cachedWidth += lineWidth;
for (int j = 0; j < rows; j++) {
int h = 0;
for (int i = 0; i < columns; i++) {
auto item = items[j * columns + i];
if (item) {
h = std::max(item->cachedNumberOfLines, h);
}
}
cachedNumberOfLines += h;
}
cachedNumberOfLines += 3;
}
SurfacePtr createSurfaceWithGridLine(int height, bool drawHorizontal, int verticalTop, int verticalBottom) {
SurfacePtr surface = createSurface(cachedWidth, height);
SDL_Color fg = objThemes.getTextColor(true);
Uint32 color = SDL_MapRGBA(surface->format, fg.r, fg.g, fg.b, 255);
if (drawHorizontal) {
SDL_Rect r = { lineWidth / 2, height / 2, cachedWidth - lineWidth, 1 };
SDL_FillRect(surface.get(), &r, color);
}
if (verticalBottom > verticalTop) {
SDL_Rect r = { lineWidth / 2, verticalTop, 1, verticalBottom - verticalTop };
SDL_FillRect(surface.get(), &r, color);
for (int w : cachedRowWidth) {
r.x += w + lineWidth;
SDL_FillRect(surface.get(), &r, color);
}
}
return surface;
}
virtual void createSurfaces(std::vector<SurfacePtr>& surfaces) override {
int height = TTF_FontHeight(fontText) + 1;
for (int j = 0; j < rows; j++) {
if (j == 0) surfaces.push_back(createSurfaceWithGridLine(height, true, height / 2, height));
std::vector<std::vector<SurfacePtr> > tempSurfaces(columns);
int h = 0;
for (int i = 0; i < columns; i++) {
auto item = items[j * columns + i];
if (item) {
item->createSurfaces(tempSurfaces[i]);
h = std::max<int>(tempSurfaces[i].size(), h);
}
}
for (int y = 0; y < h; y++) {
SurfacePtr surface = createSurfaceWithGridLine(height, false, 0, height);
int x = lineWidth;
for (int i = 0; i < columns; i++) {
if (y < (int)tempSurfaces[i].size()) {
auto src = tempSurfaces[i][y].get();
SDL_Rect srcrect = { 0, 0, src->w, src->h };
SDL_Rect dstrect = { x, 0, src->w, src->h };
SDL_BlitSurface(src, &srcrect, surface.get(), &dstrect);
}
x += cachedRowWidth[i] + lineWidth;
}
surfaces.push_back(std::move(surface));
}
if (j == 0) surfaces.push_back(createSurfaceWithGridLine(height, true, 0, height));
}
surfaces.push_back(createSurfaceWithGridLine(height, true, 0, height / 2));
}
};
class CodeChunk : public Chunk {
public:
std::vector<std::string> lines;
virtual void updateSizeForced(int maxWidth) override {
int width = 0;
for (const std::string& s : lines) {
int w = 0;
TTF_SizeUTF8(fontMono, s.c_str(), &w, NULL);
width = std::max(width, w);
}
cachedMaxWidth = maxWidth;
cachedWidth = width;
cachedNumberOfLines = lines.size() + 1;
}
virtual void createSurfaces(std::vector<SurfacePtr>& surfaces) override {
SDL_Color fg = objThemes.getTextColor(true);
surfaces.emplace_back(TTF_RenderUTF8_Blended(fontText, "Copy code", fg));
for (const std::string& s : lines) {
surfaces.emplace_back(TTF_RenderUTF8_Blended(fontMono, s.c_str(), fg));
}
}
};
class HeaderChunk : public Chunk {
public:
int level;
Chunk *child;
HeaderChunk() : level(0), child(NULL) {}
virtual ~HeaderChunk() {
delete child;
}
int getAdditionalWidth() const {
return (level <= 1) ? 0 : (level == 2) ? 24 : 20;
}
int getAdditionalHeight() const {
return (level <= 1) ? 1 : 0;
}
virtual void updateSizeForced(int maxWidth) override {
const int additionalWidth = getAdditionalWidth();
const int additionalHeight = getAdditionalHeight();
cachedMaxWidth = maxWidth;
child->updateSize(maxWidth - additionalWidth);
cachedWidth = child->cachedWidth + additionalWidth;
cachedNumberOfLines = child->cachedNumberOfLines + additionalHeight;
}
virtual void createSurfaces(std::vector<SurfacePtr>& surfaces) override {
if (level <= 1) {
child->createSurfaces(surfaces);
int h = TTF_FontHeight(fontText);
SurfacePtr surface = createSurface(cachedWidth, h);
SDL_Color fg = objThemes.getTextColor(true);
Uint32 color = SDL_MapRGBA(surface->format, fg.r, fg.g, fg.b, 255);
if (level <= 0) {
SDL_Rect r[2] = {
{ 0, h / 2 - 3, cachedWidth, 2 },
{ 0, h / 2 + 1, cachedWidth, 2 },
};
SDL_FillRects(surface.get(), r, 2, color);
} else {
SDL_Rect r = { 0, h / 2 - 1, cachedWidth, 2 };
SDL_FillRect(surface.get(), &r, color);
}
surfaces.push_back(std::move(surface));
} else {
auto bmGUI = getImageManager().loadImage(getDataPath() + "gfx/gui.png");
std::vector<SurfacePtr> tempSurfaces;
child->createSurfaces(tempSurfaces);
const int additionalWidth = getAdditionalWidth();
for (int i = 0, m = tempSurfaces.size(); i < m; i++) {
SDL_Surface *src = tempSurfaces[i].get();
SurfacePtr surface = createSurface(src->w + additionalWidth, src->h);
SDL_Rect srcrect = { 0, 0, src->w, src->h };
SDL_Rect dstrect = { additionalWidth, 0, src->w, src->h };
SDL_BlitSurface(src, &srcrect, surface.get(), &dstrect);
if (i == 0) {
if (level == 2) {
srcrect = SDL_Rect{ 64, 0, 16, 16 };
} else {
srcrect = SDL_Rect{ 96, 16, 16, 16 };
}
dstrect = SDL_Rect{ (additionalWidth - 16) / 2, (src->h - 16) / 2, 16, 16 };
SDL_BlitSurface(bmGUI, &srcrect, surface.get(), &dstrect);
}
surfaces.push_back(std::move(surface));
}
}
}
};
class HelpPage {
public:
int level;
std::string title;
std::vector<Chunk*> chunks;
HelpPage() : level(0) {}
~HelpPage() {
for (auto chunk : chunks) {
delete chunk;
}
}
void show(SDL_Renderer& renderer, GUITextArea *textArea) {
SDL_Color fg = objThemes.getTextColor(true);
std::vector<SurfacePtr> surfaces;
for (auto chunk : chunks) {
if (chunk) {
chunk->updateSize(textArea->width - 16);
chunk->createSurfaces(surfaces);
surfaces.emplace_back(TTF_RenderUTF8_Blended(fontText, " ", fg));
}
}
textArea->setStringArray(renderer, surfaces);
}
};
void parseText(const std::vector<std::string>& lines, size_t start, size_t end, size_t startOfNonSpaces, std::string& output) {
bool init = false;
for (size_t i = start; i < end; i++) {
size_t lp = (i == start) ? startOfNonSpaces : 0;
const std::string& line = lines[i];
bool isSpace = true, isVerbatim = false;
for (; lp < line.size(); lp++) {
char c = line[lp];
if (c == ' ' || c == '\t') {
if (isVerbatim) output.push_back(' ');
isSpace = true;
} else {
if (!isVerbatim && init && isSpace) output.push_back(' ');
if (c == '`') isVerbatim = !isVerbatim;
output.push_back(c);
init = true;
isSpace = false;
}
}
}
}
ParagraphChunk* parseParagraph(const std::vector<std::string>& lines, size_t start, size_t end, size_t startOfNonSpaces) {
auto ret = new ParagraphChunk;
parseText(lines, start, end, startOfNonSpaces, ret->text);
return ret;
}
CodeChunk* parseCode(const std::vector<std::string>& lines, size_t start, size_t end) {
auto ret = new CodeChunk;
for (size_t i = start; i < end; i++) {
ret->lines.push_back(lines[i]);
}
return ret;
}
ItemizeChunk* parseItemize(const std::vector<std::string>& lines, size_t start, size_t end, char marker) {
auto ret = new ItemizeChunk;
size_t last = start, startOfNonSpaces = 0;
for (size_t i = start; i <= end; i++) {
bool isNewItem = false;
size_t lp = 0;
if (i < end) {
lp = lines[i].find_first_not_of(" \t");
if (lp != std::string::npos && lp + 1 < (int)lines[i].size() && lines[i][lp] == marker) {
char c = lines[i][lp + 1];
isNewItem = (c == ' ' || c == '\t');
}
} else {
isNewItem = true;
}
if (isNewItem) {
if (last != i) {
ret->items.push_back(parseParagraph(lines, last, i, startOfNonSpaces));
}
last = i;
startOfNonSpaces = lp + 1;
}
}
return ret;
}
void parseRow(const std::string& text, std::vector<std::string>& output) {
output.clear();
size_t lps = text.find_first_not_of(" \t");
if (lps == std::string::npos) return;
if (text[lps] == '|') lps++;
size_t lpe = text.find_last_not_of(" \t");
if (lpe < lps) return;
if (text[lpe] != '|') lpe++;
std::string item;
for (size_t lp = lps; lp <= lpe; lp++) {
char c = (lp < lpe) ? text[lp] : '|';
if (c == '|') {
output.push_back(item);
item.clear();
} else {
item.push_back(c);
}
}
}
TableChunk* parseTable(const std::vector<std::string>& lines, size_t start, size_t end) {
auto ret = new TableChunk;
std::vector<std::string> row;
//determine the number of columns
parseRow(lines[start], row);
int columns = row.size(), rows = 0;
if (columns > 0) {
ret->columns = columns;
ret->rows = rows = end - start - 1;
ret->items.resize(columns * rows, NULL);
for (int i = 0; i < columns; i++) {
ret->items[i] = parseParagraph(row, i, i + 1, 0);
}
for (int j = 1; j < rows; j++) {
parseRow(lines[start + j + 1], row);
for (int i = 0, m = std::min<int>(row.size(), columns); i < m; i++) {
ret->items[j * columns + i] = parseParagraph(row, i, i + 1, 0);
}
}
}
return ret;
}
HeaderChunk* parseHeader(const std::vector<std::string>& lines, size_t start, size_t end, size_t startOfNonSpaces, int level) {
auto ret = new HeaderChunk;
ret->level = level;
ret->child = parseParagraph(lines, start, end, startOfNonSpaces);
return ret;
}
Chunk* parseChunk(const std::vector<std::string>& lines, size_t& start) {
size_t end, startOfNonSpaces;
// skip empty lines
for (; start < lines.size(); start++) {
startOfNonSpaces = lines[start].find_first_not_of(" \t");
if (startOfNonSpaces != std::string::npos) break;
}
if (start >= lines.size()) return NULL;
// check if it's code
if (lines[start].find("~~~") == 0) {
start++;
if (start >= lines.size()) return NULL;
for (end = start; end < lines.size(); end++) {
if (lines[end].find("~~~") == 0) break;
}
auto ret = parseCode(lines, start, end);
start = end + 1;
return ret;
} else {
for (end = start; end < lines.size(); end++) {
if (lines[end].find_first_not_of(" \t") == std::string::npos) break;
}
}
if (end == start) return NULL;
// check if it's itemize
if (startOfNonSpaces + 1 < (int)lines[start].size()) {
char c = lines[start][startOfNonSpaces];
char c2 = lines[start][startOfNonSpaces + 1];
if ((c == '*' || c == '+' || c == '-') && (c2 == ' ' || c2 == '\t')) {
auto ret = parseItemize(lines, start, end, c);
start = end;
return ret;
}
}
// check if it's table
if (end - start >= 3 && lines[start].find_first_of('|') != std::string::npos && lines[start + 1].find_first_not_of(" \t-|") == std::string::npos) {
auto ret = parseTable(lines, start, end);
start = end;
return ret;
}
// check if it's header of syntax "### title"
if (lines[start][startOfNonSpaces] == '#') {
int level = 0;
for (; level < 5; level++) {
if ((size_t)(startOfNonSpaces + level + 1) >= lines[start].size()) {
level = -1;
break;
}
char c = lines[start][startOfNonSpaces + level + 1];
if (c == ' ' || c == '\t') break;
else if (c != '#') {
level = -1;
break;
}
}
if (level >= 0) {
auto ret = parseHeader(lines, start, end, startOfNonSpaces + level + 1, level);
start = end;
return ret;
}
}
// check if it's header of syntax "title\n==="
if (end - start >= 2) {
int level = -1;
if (lines[end - 1].find_first_not_of(" \t=") == std::string::npos) level = 0;
else if (lines[end - 1].find_first_not_of(" \t-") == std::string::npos) level = 1;
if (level >= 0) {
auto ret = parseHeader(lines, start, end - 1, startOfNonSpaces, level);
start = end;
return ret;
}
}
// now assume it's a normal paragraph
auto ret = parseParagraph(lines, start, end, startOfNonSpaces);
start = end;
return ret;
}
+class HelpWindow : public GUIWindow {
+public:
+ int currentPage;
+ std::vector<int> history;
+ int currentHistory;
+
+public:
+ HelpWindow(ImageManager& imageManager, SDL_Renderer& renderer, int left = 0, int top = 0, int width = 0, int height = 0,
+ bool enabled = true, bool visible = true, const char* caption = NULL)
+ : GUIWindow(imageManager, renderer, left, top, width, height, enabled, visible, caption)
+ , currentPage(0)
+ , currentHistory(0)
+ {
+ }
+};
+
HelpManager::HelpManager(GUIEventCallback *parent)
: parent(parent)
{
std::vector<std::string> lines;
{
std::ifstream fin((getDataPath() + "../docs/ScriptAPI.md").c_str());
if (fin) {
//Loop the lines of the file.
std::string line;
char c;
while (fin.get(c)) {
if (c == '\r') {
} else if (c == '\n') {
lines.push_back(line);
line.clear();
} else {
line.push_back(c);
}
}
lines.push_back(line);
} else {
std::cerr << "ERROR: Unable to open the ScriptAPI.md file." << std::endl;
lines.push_back("ERROR: Unable to open the ScriptAPI.md file.");
}
}
- //TEST ONLY! TODO:
- auto page = new HelpPage;
- pages.push_back(page);
+ //TODO: add hyperlinks of subpages to each page
+
+ {
+ bool treatItemizeAsTitle = false;
+ HelpPage *page = new HelpPage;
+
+ size_t start = 0;
+ while (auto chunk = parseChunk(lines, start)) {
+ if (HeaderChunk *header = dynamic_cast<HeaderChunk*>(chunk)) {
+ //We parse a new header so add a new page.
+ if (!page->chunks.empty()) {
+ pages.push_back(page);
+ page = new HelpPage;
+ }
+
+ page->level = header->level;
+
+ //Get the title.
+ if (auto par = dynamic_cast<ParagraphChunk*>(header->child)) {
+ page->title = par->text;
+
+ //Check if we should treat itemize as title. (FIXME: ad-hoc)
+ if (page->title.find("library") != std::string::npos) {
+ treatItemizeAsTitle = true;
+ }
+ }
+ }
+
+ if (ItemizeChunk *itemize = treatItemizeAsTitle ? dynamic_cast<ItemizeChunk*>(chunk) : NULL) {
+ //We parse a new header so add a new page.
+ if (!page->chunks.empty()) {
+ pages.push_back(page);
+ page = new HelpPage;
+ }
+
+ page->level = 3; // ???
+
+ //Get the title.
+ if (ParagraphChunk *par = itemize->items.empty() ? NULL : dynamic_cast<ParagraphChunk*>(itemize->items[0])) {
+ page->title = par->text;
+ }
+ }
+
+ page->chunks.push_back(chunk);
+ }
+
+ if (page->chunks.empty()) delete page;
+ else pages.push_back(page);
+ }
+
+ //Normalize the title of each page
+ for (auto page : pages) {
+ if (page->title.find_first_of("(/") != std::string::npos || page->title.find("--") != std::string::npos) {
+ std::string tmp = page->title;
+ size_t lp = tmp.find("--");
+ if (lp != std::string::npos) tmp = tmp.substr(0, lp);
+
+ std::vector<std::string> names;
+
+ std::istringstream stream(tmp);
+ std::string line;
+ while (std::getline(stream, line, '/')) {
+ lp = line.find_first_of('(');
+ if (lp != std::string::npos) line = line.substr(0, lp);
+ lp = line.find_first_not_of(" \t");
+ if (lp != std::string::npos) {
+ line = line.substr(lp, line.find_last_not_of(" \t") - lp + 1);
+ if (std::find(names.begin(), names.end(), line) == names.end()) {
+ names.push_back(line);
+ }
+ }
+ }
+
+ tmp.clear();
+ for (const std::string& s : names) {
+ if (!tmp.empty()) tmp.push_back('/');
+ tmp += s;
+ }
+
+ page->title = tmp;
+ }
- size_t start = 0;
- while (auto chunk = parseChunk(lines, start)) {
- page->chunks.push_back(chunk);
+ //Also normize the contents if necessary.
+ if (ItemizeChunk *itemize = page->chunks.empty() ? NULL : dynamic_cast<ItemizeChunk*>(page->chunks[0])) {
+ if (ParagraphChunk *par = itemize->items.empty() ? NULL : dynamic_cast<ParagraphChunk*>(itemize->items[0])) {
+ size_t lp = par->text.find("--");
+ if (lp != std::string::npos) {
+ std::vector<std::string> temp;
+ temp.push_back(par->text.substr(lp + 2));
+ page->chunks.push_back(parseParagraph(temp, 0, 1, 0));
+
+ lp = par->text.find_last_not_of(" \t", lp);
+ if (lp != std::string::npos) par->text = par->text.substr(0, lp);
+ }
+ }
+ }
}
}
HelpManager::~HelpManager() {
for (auto page : pages) {
delete page;
}
}
GUIWindow* HelpManager::newWindow(ImageManager& imageManager, SDL_Renderer& renderer, int pageIndex) {
//Create the GUI.
- GUIWindow* root = new GUIWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 500) / 2, 600, 500, true, true, _("Scripting Help"));
+ HelpWindow* root = new HelpWindow(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 500) / 2, 600, 500, true, true, _("Scripting Help"));
root->minWidth = root->width; root->minHeight = root->height;
root->name = "scriptingHelpWindow";
root->eventCallback = this;
GUIButton* btn;
const int BUTTON_SPACE = 70;
//Some navigation buttons
btn = new GUIButton(imageManager, renderer, root->width / 2 - BUTTON_SPACE * 3, 60, -1, 36, _("Homepage"), 0, true, true, GUIGravityCenter);
btn->gravityLeft = btn->gravityRight = GUIGravityCenter;
btn->name = "Homepage";
btn->smallFont = true;
btn->eventCallback = root;
root->addChild(btn);
- btn = new GUIButton(imageManager, renderer, root->width / 2 - BUTTON_SPACE, 60, -1, 36, _("Back"), 0, false, true, GUIGravityCenter);
+ btn = new GUIButton(imageManager, renderer, root->width / 2 - BUTTON_SPACE, 60, -1, 36, _("Back"), 0, true, true, GUIGravityCenter);
btn->gravityLeft = btn->gravityRight = GUIGravityCenter;
btn->name = "Back";
btn->smallFont = true;
btn->eventCallback = root;
root->addChild(btn);
- btn = new GUIButton(imageManager, renderer, root->width / 2 + BUTTON_SPACE, 60, -1, 36, _("Forward"), 0, false, true, GUIGravityCenter);
+ btn = new GUIButton(imageManager, renderer, root->width / 2 + BUTTON_SPACE, 60, -1, 36, _("Forward"), 0, true, true, GUIGravityCenter);
btn->gravityLeft = btn->gravityRight = GUIGravityCenter;
btn->name = "Forward";
btn->smallFont = true;
btn->eventCallback = root;
root->addChild(btn);
btn = new GUIButton(imageManager, renderer, root->width / 2 + BUTTON_SPACE * 3, 60, -1, 36, _("Search"), 0, true, true, GUIGravityCenter);
btn->gravityLeft = btn->gravityRight = GUIGravityCenter;
btn->name = "Search";
btn->smallFont = true;
btn->eventCallback = root;
root->addChild(btn);
- //Add a text area.
- GUITextArea* text = new GUITextArea(imageManager, renderer, 50, 100, 500, 340);
- text->gravityRight = text->gravityBottom = GUIGravityRight;
- text->name = "Text";
- text->editable = false;
- root->addChild(text);
+ //Some buttons for search mode
+ btn = new GUIButton(imageManager, renderer, root->width / 2 - BUTTON_SPACE * 3, 60, -1, 36, _("Back"), 0, true, false, GUIGravityCenter);
+ btn->gravityLeft = btn->gravityRight = GUIGravityCenter;
+ btn->name = "Back2";
+ btn->smallFont = true;
+ btn->eventCallback = root;
+ root->addChild(btn);
+ btn = new GUIButton(imageManager, renderer, root->width / 2 + BUTTON_SPACE * 3, 60, -1, 36, _("Goto"), 0, true, false, GUIGravityCenter);
+ btn->gravityLeft = btn->gravityRight = GUIGravityCenter;
+ btn->name = "Goto";
+ btn->smallFont = true;
+ btn->eventCallback = root;
+ root->addChild(btn);
+ //Add a text area.
+ GUITextArea *textArea = new GUITextArea(imageManager, renderer, 25, 100, 550, 340);
+ textArea->gravityRight = textArea->gravityBottom = GUIGravityRight;
+ textArea->name = "TextArea";
+ textArea->editable = false;
+ textArea->eventCallback = root;
+ root->addChild(textArea);
+
+ //Some widgets for search mode
+ GUITextBox *textBox = new GUITextBox(imageManager, renderer, 25, 100, 550, 36, NULL, 0, true, false);
+ textBox->gravityRight = GUIGravityRight;
+ textBox->name = "TextSearch";
+ textBox->eventCallback = root;
+ root->addChild(textBox);
+ GUIListBox *listBox = new GUIListBox(imageManager, renderer, 25, 140, 550, 300, true, false);
+ listBox->gravityRight = listBox->gravityBottom = GUIGravityRight;
+ listBox->name = "List";
+ root->addChild(listBox);
+
+ //The close button
btn = new GUIButton(imageManager, renderer, int(root->width*0.5f), 500 - 44, -1, 36, _("Close"), 0, true, true, GUIGravityCenter);
btn->gravityLeft = btn->gravityRight = GUIGravityCenter;
btn->gravityTop = btn->gravityBottom = GUIGravityRight;
btn->name = "cfgCancel";
btn->eventCallback = root;
root->addChild(btn);
//Show contents.
if (pageIndex < 0 || pageIndex >= (int)pages.size()) pageIndex = 0;
- if (pageIndex < (int)pages.size() && pages[pageIndex]) {
- pages[pageIndex]->show(renderer, text);
- }
+ pages[pageIndex]->show(renderer, textArea);
+ root->currentPage = pageIndex;
+ root->history.push_back(pageIndex);
return root;
}
-GUIWindow* HelpManager::newSearchWindow(ImageManager& imageManager, SDL_Renderer& renderer) {
- //TODO:
- return NULL;
-}
-
void HelpManager::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name, GUIObject* obj, int eventType) {
+ HelpWindow *window = dynamic_cast<HelpWindow*>(obj);
+
if (name == "Homepage") {
- //TODO:
+ auto textArea = dynamic_cast<GUITextArea*>(obj->getChild("TextArea"));
+ if (textArea && window->currentPage != 0) {
+ window->currentHistory++;
+ window->history.resize(window->currentHistory);
+ window->history.push_back(0);
+ window->currentPage = 0;
+ pages[0]->show(renderer, textArea);
+ }
return;
} else if (name == "Back") {
- //TODO:
+ auto textArea = dynamic_cast<GUITextArea*>(obj->getChild("TextArea"));
+ if (textArea && window->currentHistory > 0) {
+ window->currentHistory--;
+ int index = window->currentPage = window->history[window->currentHistory];
+ pages[index]->show(renderer, textArea);
+ }
return;
} else if (name == "Forward") {
- //TODO:
+ auto textArea = dynamic_cast<GUITextArea*>(obj->getChild("TextArea"));
+ if (textArea && window->currentHistory < (int)window->history.size() - 1) {
+ window->currentHistory++;
+ int index = window->currentPage = window->history[window->currentHistory];
+ pages[index]->show(renderer, textArea);
+ }
+ return;
+ } else if (name == "Search" || name == "Back2") {
+ bool isSearch = name == "Search";
+
+ if (auto o = obj->getChild("Homepage")) o->visible = !isSearch;
+ if (auto o = obj->getChild("Back")) o->visible = !isSearch;
+ if (auto o = obj->getChild("Forward")) o->visible = !isSearch;
+ if (auto o = obj->getChild("Search")) o->visible = !isSearch;
+ if (auto o = obj->getChild("TextArea")) o->visible = !isSearch;
+ if (auto o = obj->getChild("Back2")) o->visible = isSearch;
+ if (auto o = obj->getChild("Goto")) o->visible = isSearch;
+
+ auto textBox = obj->getChild("TextSearch");
+ auto listBox = dynamic_cast<GUIListBox*>(obj->getChild("List"));
+ if (textBox && listBox) {
+ textBox->visible = isSearch;
+ listBox->visible = isSearch;
+ if (isSearch && listBox->item.empty()) {
+ updateListBox(imageManager, renderer, listBox, textBox->caption);
+ }
+ }
+
+ return;
+ } else if (name == "Goto") {
+ auto listBox = dynamic_cast<GUIListBox*>(obj->getChild("List"));
+ auto textArea = dynamic_cast<GUITextArea*>(obj->getChild("TextArea"));
+ if (listBox && textArea && listBox->value >= 0 && listBox->value < (int)listBox->item.size()) {
+ const std::string& s = listBox->item[listBox->value];
+ int index = atoi(s.c_str());
+ if (index >= 0 && index < (int)pages.size()) {
+ window->currentHistory++;
+ window->history.resize(window->currentHistory);
+ window->history.push_back(index);
+ window->currentPage = index;
+ pages[index]->show(renderer, textArea);
+ GUIEventQueue.push_back(GUIEvent{ this, "Back2", obj, GUIEventClick });
+ }
+ }
return;
- } else if (name == "Search") {
+ } else if (name == "TextArea") {
//TODO:
return;
+ } else if (name == "TextSearch") {
+ auto textBox = obj->getChild("TextSearch");
+ auto listBox = dynamic_cast<GUIListBox*>(obj->getChild("List"));
+ if (textBox && listBox) {
+ updateListBox(imageManager, renderer, listBox, textBox->caption);
+ }
+ return;
}
//Do the default.
parent->GUIEventCallback_OnEvent(imageManager, renderer, name, obj, eventType);
}
+
+void HelpManager::updateListBox(ImageManager& imageManager, SDL_Renderer& renderer, GUIListBox *listBox, const std::string& keyword) {
+ std::vector<std::string> keywords;
+
+ std::string line;
+ for (char c : keyword) {
+ if (c == ' ' || c == '\t') {
+ if (!line.empty()) keywords.push_back(line);
+ line.clear();
+ } else {
+ line.push_back(tolower((unsigned char)c));
+ }
+ }
+ if (!line.empty()) keywords.push_back(line);
+
+ int backup = listBox->value;
+
+ listBox->clearItems();
+
+ for (size_t i = 0; i < pages.size(); i++) {
+ HelpPage *page = pages[i];
+
+ bool match = true;
+ std::string tmp;
+ for (char c : page->title) {
+ tmp.push_back(tolower((unsigned char)c));
+ }
+
+ for (const std::string& s : keywords) {
+ if (tmp.find(s) == std::string::npos) {
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ char s[32];
+ sprintf(s, "%d", i);
+ listBox->addItem(renderer, s, textureFromTextShared(renderer, *fontText,
+ (std::string(page->level, ' ') + page->title).c_str(),
+ objThemes.getTextColor(true)));
+ }
+ }
+
+ listBox->value = (backup >= 0 && backup < (int)listBox->item.size()) ? backup : -1;
+}
diff --git a/src/HelpManager.h b/src/HelpManager.h
index d48d4fd..a8f5a64 100644
--- a/src/HelpManager.h
+++ b/src/HelpManager.h
@@ -1,44 +1,45 @@
/*
* Copyright (C) 2019 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef HELPMANAGER_H
#define HELPMANAGER_H
#include "GUIObject.h"
class GUIWindow;
+class GUIListBox;
class HelpPage;
class HelpManager : public GUIEventCallback {
private:
GUIEventCallback *parent;
std::vector<HelpPage*> pages;
+ void updateListBox(ImageManager& imageManager, SDL_Renderer& renderer, GUIListBox *listBox, const std::string& keyword);
+
public:
HelpManager(GUIEventCallback *parent);
~HelpManager();
GUIWindow* newWindow(ImageManager& imageManager, SDL_Renderer& renderer, int pageIndex = 0);
- GUIWindow* newSearchWindow(ImageManager& imageManager, SDL_Renderer& renderer);
-
void GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name, GUIObject* obj, int eventType) override;
};
#endif

File Metadata

Mime Type
text/x-diff
Expires
Sat, May 9, 12:25 AM (1 w, 19 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
62731
Default Alt Text
(54 KB)

Event Timeline