Page Menu
Home
Phabricator (Chris)
Search
Configure Global Search
Log In
Files
F116767
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
70 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/GUITextArea.cpp b/src/GUITextArea.cpp
index b582eb6..0110d9c 100644
--- a/src/GUITextArea.cpp
+++ b/src/GUITextArea.cpp
@@ -1,1077 +1,1101 @@
/*
* Copyright (C) 2011-2012 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 "Functions.h"
#include "UTF8Functions.h"
#include "GUITextArea.h"
#include "ThemeManager.h"
#include "WordWrapper.h"
#include <cmath>
#include <algorithm>
#include <assert.h>
#include <ctype.h>
#include <SDL_ttf_fontfallback.h>
using namespace std;
#define SPACE_PER_TAB 2
GUITextArea::GUITextArea(ImageManager& imageManager, SDL_Renderer& renderer,int left,int top,int width,int height,bool enabled,bool visible):
GUIObject(imageManager,renderer,left,top,width,height,NULL,-1,enabled,visible),editable(true){
//Set some default values.
state=0;
setFont(fontText);
highlightLineStart=highlightLineEnd=0;
highlightStartX=highlightEndX=0;
highlightStart=highlightEnd=0;
//Add empty text.
lines.push_back("");
linesCache.push_back(nullptr);
//Create scrollbar widget.
scrollBar=new GUIScrollBar(imageManager,renderer,width-16,0,16,height,1,0,0,0);
childControls.push_back(scrollBar);
scrollBarH=new GUIScrollBar(imageManager,renderer,0,height-16,width-16,16,0,0,0,0,100,500,true,false);
childControls.push_back(scrollBarH);
}
void GUITextArea::setFont(TTF_Font* font){
//NOTE: This fuction shouldn't be called after adding items, so no need to update the whole cache.
widgetFont=font;
fontHeight=TTF_FontHeight(font)+1;
}
void GUITextArea::inputText(SDL_Renderer &renderer, const char* s) {
if (s && s[0]) {
//Split into lines.
vector<string> newLines;
newLines.push_back(std::string());
for (int i = 0; s[i]; i++) {
if (s[i] == '\r') continue;
if (s[i] == '\n') {
newLines.push_back(std::string());
continue;
}
if (s[i] == '\t') {
// Replace tabs by spaces.
newLines.back() += std::string(SPACE_PER_TAB, ' ');
continue;
}
newLines.back().push_back(s[i]);
}
const int m = newLines.size();
if (m == 1 && newLines[0].empty()) return;
//Remove selected text.
removeHighlight(renderer);
//Calculate the width of the last line.
int advance = 0;
{
const char* lastLine = newLines[m - 1].c_str();
for (int i = 0;;) {
int a = 0;
int ch = utf8ReadForward(lastLine, i);
if (ch <= 0) break;
TTF_GlyphMetrics(widgetFont, ch, NULL, NULL, NULL, NULL, &a);
advance += a;
}
}
if (m > 1) {
//Multiple lines.
highlightEnd = newLines[m - 1].size();
highlightStartX = highlightEndX = advance;
newLines[m - 1] += lines[highlightLineStart].substr(highlightStart);
lines[highlightLineStart] = lines[highlightLineStart].substr(0, highlightStart) + newLines[0];
lines.insert(lines.begin() + (highlightLineStart + 1), newLines.begin() + 1, newLines.end());
for (int i = 0; i < m - 1; i++) {
linesCache.insert(linesCache.begin() + (highlightLineStart + 1), nullptr);
}
highlightStart = highlightEnd;
} else {
//Single line.
highlightEnd = highlightStart + newLines[0].size();
lines[highlightLineStart].insert(highlightStart, newLines[0]);
highlightStart = highlightEnd;
highlightStartX = highlightEndX = highlightStartX + advance;
}
//Update cache.
highlightLineEnd = highlightLineStart + m - 1;
for (int i = highlightLineStart; i <= highlightLineEnd; i++) {
linesCache[i] = textureFromText(renderer, *widgetFont, lines[i].c_str(), objThemes.getTextColor(true));
}
highlightLineStart = highlightLineEnd;
//Update view if needed.
adjustView();
//If there is an event callback then call it.
if (eventCallback){
GUIEvent e = { eventCallback, name, this, GUIEventChange };
GUIEventQueue.push_back(e);
}
}
}
void GUITextArea::scrollScrollbar(int dx, int dy) {
if (dx && scrollBarH->visible){
scrollBarH->value = clamp(scrollBarH->value + dx, 0, scrollBarH->maxValue);
}
if (dy) {
scrollBar->value = clamp(scrollBar->value + dy, 0, scrollBar->maxValue);
}
}
bool GUITextArea::handleEvents(SDL_Renderer& renderer,int x,int y,bool enabled,bool visible,bool processed){
//Boolean if the event is processed.
bool b=processed;
//The GUIObject is only enabled when he and his parent are enabled.
enabled=enabled && this->enabled;
//The GUIObject is only enabled when he and his parent are enabled.
visible=visible && this->visible;
//Get the absolute position.
x+=left;
y+=top;
//Update the scrollbars.
b = b || scrollBar->handleEvents(renderer, x, y, enabled, visible, b);
b = b || scrollBarH->handleEvents(renderer, x, y, enabled, visible, b);
//NOTE: We don't reset the state to have a "focus" effect.
//Only check for events when the object is both enabled and visible.
if(enabled&&visible){
//Check if there's a key press and the event hasn't been already processed.
if(state==2 && event.type==SDL_KEYDOWN && !b && editable){
if ((event.key.keysym.mod & KMOD_CTRL) == 0) {
//Check if the key is supported.
if (event.key.keysym.sym == SDLK_BACKSPACE){
//Delete one character direct to prevent a lag.
backspaceChar(renderer);
} else if (event.key.keysym.sym == SDLK_DELETE){
//Delete one character direct to prevent a lag.
deleteChar(renderer);
} else if (event.key.keysym.sym == SDLK_RETURN){
removeHighlight(renderer);
//Split the current line and update.
string str2 = lines.at(highlightLineEnd).substr(highlightStart);
lines.at(highlightLineStart) = lines.at(highlightLineStart).substr(0, highlightStart);
linesCache.at(highlightLineStart) =
textureFromText(renderer, *widgetFont, lines.at(highlightLineStart).c_str(), objThemes.getTextColor(true));
//Calculate indentation.
int indent = 0;
for (int i = 0; i < (int)lines.at(highlightLineStart).length(); i++){
if (isspace(lines.at(highlightLineStart)[i]))
indent++;
else
break;
}
str2.insert(0, indent, ' ');
//Add the rest in a new line.
highlightLineStart++;
highlightStart = indent;
highlightEnd = highlightStart;
highlightLineEnd++;
highlightStartX = 0;
for (int i = 0; i < indent; i++){
int advance;
TTF_GlyphMetrics(widgetFont, str2.at(i), NULL, NULL, NULL, NULL, &advance);
highlightStartX += advance;
}
highlightEndX = highlightStartX;
lines.insert(lines.begin() + highlightLineStart, str2);
auto tex = textureFromText(renderer, *widgetFont, str2.c_str(), objThemes.getTextColor(true));
linesCache.insert(linesCache.begin() + highlightLineStart, std::move(tex));
adjustView();
//If there is an event callback then call it.
if (eventCallback){
GUIEvent e = { eventCallback, name, this, GUIEventChange };
GUIEventQueue.push_back(e);
}
} else if (event.key.keysym.sym == SDLK_TAB){
//Calculate the width of a space.
int advance;
TTF_GlyphMetrics(widgetFont, ' ', NULL, NULL, NULL, NULL, &advance);
int start = highlightLineStart, end = highlightLineEnd;
if (start > end) std::swap(start, end);
for (int line = start; line <= end; line++) {
int count = 0;
std::string &s = lines[line];
if (event.key.keysym.mod & KMOD_SHIFT) {
// remove spaces
for (; count < SPACE_PER_TAB; count++) {
if (s.c_str()[count] != ' ') break;
}
if (count > 0) {
s.erase(0, count);
count = -count;
}
} else {
// add spaces
count = SPACE_PER_TAB;
s.insert(0, count, ' ');
}
//Update cache.
if (count) {
linesCache.at(line) = textureFromText(renderer, *widgetFont, s.c_str(), objThemes.getTextColor(true));
}
//Update selection.
if (line == highlightLineStart) {
highlightStart += count;
highlightStartX += count*advance;
if (highlightStart <= 0) {
highlightStart = 0;
highlightStartX = 0;
}
}
if (line == highlightLineEnd) {
highlightEnd += count;
highlightEndX += count*advance;
if (highlightEnd <= 0) {
highlightEnd = 0;
highlightEndX = 0;
}
}
}
adjustView();
} else if (event.key.keysym.sym == SDLK_RIGHT){
//Move the carrot once to prevent a lag.
moveCarrotRight();
} else if (event.key.keysym.sym == SDLK_LEFT){
//Move the carrot once to prevent a lag.
moveCarrotLeft();
} else if (event.key.keysym.sym == SDLK_DOWN){
//Move the carrot once to prevent a lag.
moveCarrotDown();
} else if (event.key.keysym.sym == SDLK_UP){
//Move the carrot once to prevent a lag.
moveCarrotUp();
}
} else {
//Check hotkey.
if (event.key.keysym.sym == SDLK_a) {
//Select all.
highlightLineStart = 0;
highlightStart = 0;
highlightStartX = 0;
highlightLineEnd = lines.size() - 1;
highlightEnd = lines.back().size();
highlightEndX = 0;
if (highlightEnd > 0) {
TTF_SizeUTF8(widgetFont, lines.back().c_str(), &highlightEndX, NULL);
}
} else if (event.key.keysym.sym == SDLK_x || event.key.keysym.sym == SDLK_c) {
//Cut or copy.
int startLine = highlightLineStart, endLine = highlightLineEnd;
int start = highlightStart, end = highlightEnd;
if (startLine > endLine || (startLine == endLine && start > end)) {
std::swap(startLine, endLine);
std::swap(start, end);
}
std::string s;
if (startLine < endLine) {
//Multiple lines.
s = lines[startLine].substr(start);
s.push_back('\n');
for (int i = startLine + 1; i < endLine; i++) {
s += lines[i];
s.push_back('\n');
}
s += lines[endLine].substr(0, end);
} else {
//Single line.
s = lines[startLine].substr(start, end - start);
}
if (!s.empty()) {
SDL_SetClipboardText(s.c_str());
if (event.key.keysym.sym == SDLK_x) {
//Cut.
removeHighlight(renderer);
//If there is an event callback then call it.
if (eventCallback){
GUIEvent e = { eventCallback, name, this, GUIEventChange };
GUIEventQueue.push_back(e);
}
}
}
} else if (event.key.keysym.sym == SDLK_v) {
//Paste.
if (SDL_HasClipboardText()) {
char *s = SDL_GetClipboardText();
inputText(renderer, s);
SDL_free(s);
}
}
}
//The event has been processed.
b=true;
} else if (state == 2 && event.type == SDL_TEXTINPUT && !b && editable){
inputText(renderer, event.text.text);
} else if (state == 2 && event.type == SDL_TEXTEDITING && !b && editable){
// TODO: process SDL_TEXTEDITING event
}
if (event.type == SDL_MOUSEMOTION || event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEWHEEL) {
//The mouse location (x=i, y=j) and the mouse button (k).
int i, j, k;
k = SDL_GetMouseState(&i, &j);
//Check if the mouse is inside the GUIObject.
if (i >= x && i < x + width && j >= y && j < y + height && !b){
//We can only increase our state. (nothing->hover->focus).
if (state != 2){
state = 1;
}
//Check for mouse wheel scrolling.
//Scroll horizontally if mouse is over the horizontal scrollbar.
//Otherwise scroll vertically.
if (event.type == SDL_MOUSEWHEEL && event.wheel.y) {
if (j >= y + height - 16 && scrollBarH->visible){
scrollScrollbar(event.wheel.y < 0 ? 20 : -20, 0);
} else{
scrollScrollbar(0, event.wheel.y < 0 ? 1 : -1);
}
}
//When mouse is not over the scrollbar.
if (i < x + width - 16 && j < (scrollBarH->visible ? y + height - 16 : y + height)){
if (editable) {
//Update the cursor type.
currentCursor = CURSOR_CARROT;
if (((event.type == SDL_MOUSEBUTTONUP || event.type == SDL_MOUSEBUTTONDOWN) && event.button.button == 1)
|| (event.type == SDL_MOUSEMOTION && (k & SDL_BUTTON(1))))
{
//Move carrot to the place clicked.
const int mouseLine = clamp((int)floor(float(j - y) / float(fontHeight)) + scrollBar->value, 0, lines.size() - 1);
string* str = &lines.at(mouseLine);
value = str->length();
const int clickX = i - x + scrollBarH->value;
int finalX = 0;
int finalPos = str->length();
for (int i = 0;;){
int advance = 0;
int i0 = i;
int ch = utf8ReadForward(str->c_str(), i);
if (ch <= 0) break;
TTF_GlyphMetrics(widgetFont, ch, NULL, NULL, NULL, NULL, &advance);
finalX += advance;
if (clickX < finalX - advance / 2){
finalPos = i0;
finalX -= advance;
break;
}
}
if (event.type == SDL_MOUSEBUTTONUP){
state = 2;
highlightEnd = finalPos;
highlightEndX = finalX;
highlightLineEnd = mouseLine;
} else if (event.type == SDL_MOUSEBUTTONDOWN){
state = 2;
highlightStart = highlightEnd = finalPos;
highlightStartX = highlightEndX = finalX;
highlightLineStart = highlightLineEnd = mouseLine;
} else if (event.type == SDL_MOUSEMOTION){
state = 2;
highlightEnd = finalPos;
highlightEndX = finalX;
highlightLineEnd = mouseLine;
}
}
} else {
const int mouseLine = (int)floor(float(j - y) / float(fontHeight)) + scrollBar->value;
if (mouseLine >= 0 && mouseLine < (int)hyperlinks.size()) {
const int clickX = i - x + scrollBarH->value;
for (const Hyperlink& lnk : hyperlinks[mouseLine]) {
if (clickX >= lnk.startX && clickX < lnk.endX) {
currentCursor = CURSOR_POINTING_HAND;
if (event.type == SDL_MOUSEBUTTONDOWN && event.button.button == 1) {
- openWebsite(lnk.url);
+ if (lnk.url.size() >= 8 && (lnk.url.substr(0, 7) == "http://" || lnk.url.substr(0, 8) == "https://")) {
+ openWebsite(lnk.url);
+ } else if (eventCallback) {
+ clickedHyperlink = lnk.url;
+ GUIEvent e = { eventCallback, name, this, GUIEventClick };
+ GUIEventQueue.push_back(e);
+ }
}
break;
}
}
}
}
}
//Event has been processed as long as this is a mouse event and the mouse is inside the widget.
b = true;
} else{
//The mouse is outside the TextBox.
//If we don't have focus but only hover we lose it.
if (state == 1){
state = 0;
}
//If it's a click event outside the textbox then we blur.
if (event.type == SDL_MOUSEBUTTONUP && event.button.button == SDL_BUTTON_LEFT){
//Set state to 0.
state = 0;
}
}
}
}
if(!editable)
highlightLineStart=scrollBar->value;
//Process child controls event except for the scrollbars.
//That's why i ends at 2.
for (int i = childControls.size() - 1; i >= 2; i--) {
bool b1 = childControls[i]->handleEvents(renderer, x, y, enabled, visible, b);
//The event is processed when either our or the childs is true (or both).
b=b||b1;
}
return b;
}
void GUITextArea::removeHighlight(SDL_Renderer& renderer){
if (highlightLineStart==highlightLineEnd) {
if (highlightStart == highlightEnd) return;
int start=highlightStart, end=highlightEnd, startx=highlightStartX;
if(highlightStart>highlightEnd){
start=highlightEnd;
end=highlightStart;
startx=highlightEndX;
}
std::string& str=lines.at(highlightLineStart);
str.erase(start,end-start);
highlightStart=highlightEnd=start;
highlightStartX=highlightEndX=startx;
// Update cache.
linesCache.at(highlightLineStart) = textureFromText(renderer,*widgetFont,str.c_str(),objThemes.getTextColor(true));
}else{
int startLine=highlightLineStart, endLine=highlightLineEnd,
start=highlightStart, end=highlightEnd, startx=highlightStartX;
if(startLine>endLine){
startLine=highlightLineEnd;
endLine=highlightLineStart;
start=highlightEnd;
end=highlightStart;
startx=highlightEndX;
}
lines[startLine] = lines[startLine].substr(0, start) + lines[endLine].substr(end);
lines.erase(lines.begin() + startLine + 1, lines.begin() + endLine + 1);
linesCache.erase(linesCache.begin() + startLine + 1, linesCache.begin() + endLine + 1);
highlightLineStart=highlightLineEnd=startLine;
highlightStart=highlightEnd=start;
highlightStartX=highlightEndX=startx;
// Update cache.
linesCache.at(startLine) = textureFromText(renderer, *widgetFont, lines[startLine].c_str(), objThemes.getTextColor(true));
}
adjustView();
}
void GUITextArea::deleteChar(SDL_Renderer& renderer){
if (highlightLineStart==highlightLineEnd && highlightStart==highlightEnd){
if(highlightEnd>=(int)lines.at(highlightLineEnd).length()){
if(highlightLineEnd<(int)lines.size()-1){
highlightLineEnd++;
highlightEnd=0;
}
} else {
utf8ReadForward(lines.at(highlightLineEnd).c_str(), highlightEnd);
}
}
removeHighlight(renderer);
//If there is an event callback.
if(eventCallback){
GUIEvent e={eventCallback,name,this,GUIEventChange};
GUIEventQueue.push_back(e);
}
}
void GUITextArea::backspaceChar(SDL_Renderer& renderer){
if(highlightLineStart==highlightLineEnd && highlightStart==highlightEnd){
if(highlightStart<=0){
if(highlightLineStart==0){
highlightStart=0;
}else{
highlightLineStart--;
highlightStart=lines.at(highlightLineStart).length();
highlightStartX=0;
if (highlightStart > 0) {
TexturePtr& t = linesCache.at(highlightLineStart);
if (t) highlightStartX = textureWidth(*t);
}
}
}else{
int advance = 0;
int ch = utf8ReadBackward(lines.at(highlightLineStart).c_str(), highlightStart);
if (ch > 0) TTF_GlyphMetrics(widgetFont, ch, NULL, NULL, NULL, NULL, &advance);
highlightStartX -= advance;
}
}
removeHighlight(renderer);
//If there is an event callback.
if(eventCallback){
GUIEvent e={eventCallback,name,this,GUIEventChange};
GUIEventQueue.push_back(e);
}
}
void GUITextArea::moveCarrotRight(){
if (highlightEnd>=(int)lines.at(highlightLineEnd).length()){
if (highlightLineEnd<(int)lines.size()-1){
highlightEnd=0;
highlightEndX=0;
highlightLineEnd++;
}
}else{
int advance = 0;
int ch = utf8ReadForward(lines.at(highlightLineEnd).c_str(), highlightEnd);
if (ch > 0) TTF_GlyphMetrics(widgetFont, ch, NULL, NULL, NULL, NULL, &advance);
highlightEndX += advance;
}
if((SDL_GetModState()&KMOD_SHIFT)==0){
highlightLineStart=highlightLineEnd;
highlightStart=highlightEnd;
highlightStartX=highlightEndX;
}
adjustView();
}
void GUITextArea::moveCarrotLeft(){
if (highlightEnd<=0){
if (highlightLineEnd==0){
highlightEnd=0;
}else{
highlightLineEnd--;
highlightEnd=lines.at(highlightLineEnd).length();
highlightEndX=0;
if (highlightEnd > 0) {
TexturePtr& t = linesCache.at(highlightLineEnd);
if (t) highlightEndX = textureWidth(*t);
}
}
}else{
int advance = 0;
int ch = utf8ReadBackward(lines.at(highlightLineEnd).c_str(), highlightEnd);
if (ch > 0) TTF_GlyphMetrics(widgetFont, ch, NULL, NULL, NULL, NULL, &advance);
highlightEndX -= advance;
}
if((SDL_GetModState()&KMOD_SHIFT)==0){
highlightLineStart=highlightLineEnd;
highlightStart=highlightEnd;
highlightStartX=highlightEndX;
}
adjustView();
}
void GUITextArea::moveCarrotUp(){
if(highlightLineEnd==0){
highlightEnd=0;
highlightEndX=0;
}else{
highlightLineEnd--;
const std::string& str=lines.at(highlightLineEnd);
//Find out closest match.
int xPos=0;
int i=0;
for (;;){
int advance = 0;
int i0 = i;
int ch = utf8ReadForward(str.c_str(), i);
if (ch <= 0) break;
TTF_GlyphMetrics(widgetFont, ch, NULL, NULL, NULL, NULL, &advance);
xPos += advance;
if(highlightEndX<xPos-advance/2){
highlightEnd=i=i0;
highlightEndX=xPos-advance;
break;
}
}
if (i == 0) {
highlightEnd = highlightEndX = 0;
} else if (i == str.length()){
highlightEnd=str.length();
highlightEndX=0;
if (highlightEnd > 0) {
TexturePtr& t = linesCache.at(highlightLineEnd);
if (t) highlightEndX = textureWidth(*t);
}
}
}
if((SDL_GetModState()&KMOD_SHIFT)==0){
highlightStart=highlightEnd;
highlightStartX=highlightEndX;
highlightLineStart=highlightLineEnd;
}
adjustView();
}
void GUITextArea::moveCarrotDown(){
if(highlightLineEnd==lines.size()-1){
highlightEnd=lines.at(highlightLineEnd).length();
highlightEndX=0;
if (highlightEnd > 0) {
TexturePtr& t = linesCache.at(highlightLineEnd);
if (t) highlightEndX = textureWidth(*t);
}
}else{
highlightLineEnd++;
string* str=&lines.at(highlightLineEnd);
//Find out closest match.
int xPos=0;
int i = 0;
for (;;){
int advance = 0;
int i0 = i;
int ch = utf8ReadForward(str->c_str(), i);
if (ch <= 0) break;
TTF_GlyphMetrics(widgetFont, ch, NULL, NULL, NULL, NULL, &advance);
xPos += advance;
if (highlightEndX<xPos - advance / 2){
highlightEnd = i = i0;
highlightEndX = xPos - advance;
break;
}
}
if (i == 0) {
highlightEnd = highlightEndX = 0;
} else if (i == str->length()){
highlightEnd=str->length();
highlightEndX=0;
TexturePtr& t = linesCache.at(highlightLineEnd);
if(t) highlightEndX=textureWidth(*t);
}
}
if((SDL_GetModState()&KMOD_SHIFT)==0){
highlightStart=highlightEnd;
highlightStartX=highlightEndX;
highlightLineStart=highlightLineEnd;
}
adjustView();
}
void GUITextArea::adjustView(){
//Adjust view to current line.
if(fontHeight*(highlightLineEnd-scrollBar->value)+4>height-4)
scrollBar->value=highlightLineEnd-3;
else if(highlightLineEnd-scrollBar->value<0)
scrollBar->value=highlightLineEnd;
//Find out the lenght of the longest line.
int maxWidth=0;
for(const TexturePtr& tex: linesCache){
if(tex) {
const int texWidth = textureWidth(*tex.get());
if(texWidth>width-16&&texWidth>maxWidth)
maxWidth=texWidth;
}
}
//We need the horizontal scrollbar if any line is too long.
if(maxWidth>0){
scrollBar->height=height-16;
scrollBarH->visible=true;
scrollBarH->maxValue=maxWidth-width+24;
}else{
scrollBar->height=height;
scrollBarH->visible=false;
scrollBarH->value=0;
scrollBarH->maxValue=0;
}
//Adjust the horizontal view.
int carrotX=0;
for(int n=0;n<highlightEnd;){
int advance = 0;
int ch = utf8ReadForward(lines.at(highlightLineEnd).c_str(), n);
if (ch <= 0) break;
TTF_GlyphMetrics(widgetFont, ch, NULL, NULL, NULL, NULL, &advance);
carrotX += advance;
}
if(carrotX>width-24)
scrollBarH->value=scrollBarH->maxValue;
else
scrollBarH->value=0;
//Update vertical scrollbar.
int rh=height-(scrollBarH->visible?16:0);
int m=lines.size(),n=(int)floor((float)rh/(float)fontHeight);
if(m>n){
scrollBar->maxValue=m-n;
scrollBar->smallChange=1;
scrollBar->largeChange=n;
}else{
scrollBar->value=0;
scrollBar->maxValue=0;
}
}
void GUITextArea::drawHighlight(SDL_Renderer& renderer, int x,int y,SDL_Rect r,SDL_Color color){
if(r.x<x) {
int tmp_w = r.w - x + r.x;
if(tmp_w<=0) return;
r.w = tmp_w;
r.x = x;
}
if(r.x+r.w > x+width){
int tmp_w=width-r.x+x;
if(tmp_w<=0) return;
r.w=tmp_w;
}
if(r.y<y){
int tmp_h=r.h - y + r.y;
if(tmp_h<=0) return;
r.h=tmp_h;
r.y = y;
}
if(r.y+r.h > y+height){
int tmp_h=height-r.y+y;
if(tmp_h<=0) return;
r.h=tmp_h;
}
SDL_SetRenderDrawColor(&renderer, color.r, color.g, color.b, color.a);
SDL_RenderFillRect(&renderer, &r);
}
void GUITextArea::render(SDL_Renderer& renderer, int x,int y,bool draw){
//There's no need drawing the GUIObject when it's invisible.
if(!visible||!draw)
return;
//Get the absolute x and y location.
x+=left;
y+=top;
{
//Draw the box.
const Uint32 color=0xFFFFFFFF;
drawGUIBox(x,y,width,height,renderer,color);
}
//Place the highlighted area.
SDL_Rect r;
const SDL_Color color{128,128,128,255};
if (editable) {
if (highlightLineStart == highlightLineEnd){
r.x = x - scrollBarH->value;
r.y = y + ((highlightLineStart - scrollBar->value)*fontHeight);
r.h = fontHeight;
if (highlightStart < highlightEnd){
r.x += highlightStartX;
r.w = highlightEndX - highlightStartX;
} else{
r.x += highlightEndX;
r.w = highlightStartX - highlightEndX;
}
drawHighlight(renderer, x, y, r, color);
} else if (highlightLineStart < highlightLineEnd){
int lnc = highlightLineEnd - highlightLineStart;
for (int i = 0; i <= lnc; i++){
r.x = x - scrollBarH->value;
r.y = y + ((i + highlightLineStart - scrollBar->value)*fontHeight);
r.w = width + scrollBarH->maxValue;
r.h = fontHeight;
if (i == 0){
r.x += highlightStartX;
r.w -= highlightStartX;
} else if (i == lnc){
r.w = highlightEndX;
}
if (lines.at(i + highlightLineStart).empty()){
r.w = fontHeight / 4;
}
drawHighlight(renderer, x, y, r, color);
}
} else{
int lnc = highlightLineStart - highlightLineEnd;
for (int i = 0; i <= lnc; i++){
r.x = x - scrollBarH->value;
r.y = y + ((i + highlightLineEnd - scrollBar->value)*fontHeight);
r.w = width + scrollBarH->maxValue;
r.h = fontHeight;
if (i == 0){
r.x += highlightEndX;
r.w -= highlightEndX;
} else if (i == lnc){
r.w = highlightStartX;
}
if (lines.at(i + highlightLineEnd).empty()){
r.w = fontHeight / 4;
}
drawHighlight(renderer, x, y, r, color);
}
}
}
const int underlineOffset = TTF_FontAscent(widgetFont) + int(float(0.5f - 0.25f * TTF_FontDescent(widgetFont)));
//Draw text.
int lineY=0;
for(int line=scrollBar->value;line<(int)linesCache.size();line++){
TexturePtr& it = linesCache[line];
if(it){
if(lineY<height){
SDL_Rect r = { scrollBarH->value, 0, std::min(width - 17, textureWidth(*it.get()) - scrollBarH->value), textureHeight(*it.get()) };
int over=-height+lineY+fontHeight;
if(over>0) r.h-=over;
const SDL_Rect dstRect={x+1,y+1+lineY,r.w,r.h};
if(r.w>0 && r.h>0) SDL_RenderCopy(&renderer,it.get(),&r,&dstRect);
// draw hyperlinks
if (!editable && line<(int)hyperlinks.size()) {
r.y = lineY + underlineOffset;
if (r.y < height){
r.y += y + 1;
r.h = 1;
for (const Hyperlink& lnk : hyperlinks[line]) {
r.x = clamp(lnk.startX - scrollBarH->value, 0, width - 17);
r.w = clamp(lnk.endX - scrollBarH->value, 0, width - 17);
if (r.w > r.x) {
r.w -= r.x;
r.x += x + 1;
SDL_SetRenderDrawColor(&renderer, 0, 0, 0, 255);
SDL_RenderFillRect(&renderer, &r);
}
}
}
}
}else{
break;
}
}
lineY+=fontHeight;
}
//Only draw the carrot when focus.
if(state==2&&editable){
r.x=x-scrollBarH->value+highlightEndX;
r.y=y+4+fontHeight*(highlightLineEnd-scrollBar->value);
r.w=2;
r.h=fontHeight-4;
//Make sure that the carrot is inside the textbox.
if((r.y<y+height-4)&&(r.y>y)&&(r.x>x-1)&&(r.x<x+width-16)){
drawHighlight(renderer,x,y,r,SDL_Color{0,0,0,127});
}
}
//We now need to draw all the children of the GUIObject.
for(unsigned int i=0;i<childControls.size();i++){
childControls[i]->render(renderer,x,y,draw);
}
}
void GUITextArea::setString(SDL_Renderer& renderer, const std::string& input, bool wordWrap) {
WordWrapper wrapper;
wrapper.wordWrap = wordWrap;
setString(renderer, input, wrapper);
}
void GUITextArea::setString(SDL_Renderer& renderer, const std::string& input, WordWrapper& wrapper) {
//Clear previous content if any.
//Delete every line.
lines.clear();
linesCache.clear();
//Copy values.
wrapper.maxWidth = width - 16;
wrapper.font = widgetFont;
wrapper.addString(lines, input);
//Render and cache text.
for (const std::string& s : lines) {
linesCache.push_back(textureFromText(renderer, *widgetFont, s.c_str(), objThemes.getTextColor(true)));
}
adjustView();
}
void GUITextArea::setStringArray(SDL_Renderer& renderer, const std::vector<std::string>& input, bool wordWrap) {
WordWrapper wrapper;
wrapper.wordWrap = wordWrap;
setStringArray(renderer, input, wrapper);
}
void GUITextArea::setStringArray(SDL_Renderer& renderer, const std::vector<std::string>& input, WordWrapper& wrapper) {
//Free cached images.
linesCache.clear();
lines.clear();
//Copy values.
wrapper.maxWidth = width - 16;
wrapper.font = widgetFont;
wrapper.addLines(lines, input);
//Render and cache text.
for(const std::string& s: lines) {
linesCache.push_back(textureFromText(renderer,*widgetFont,s.c_str(),objThemes.getTextColor(true)));
}
adjustView();
}
void GUITextArea::setStringArray(SDL_Renderer &renderer, std::vector<SurfacePtr>& surfaces) {
//Free cached images.
linesCache.clear();
lines.clear();
//Copy values.
lines.resize(surfaces.size());
for (SurfacePtr& surface : surfaces) {
linesCache.emplace_back(SDL_CreateTextureFromSurface(&renderer, surface.get()));
}
adjustView();
}
void GUITextArea::extractHyperlinks() {
const int lm = lines.size();
hyperlinks.clear();
if (lm <= 0) return;
hyperlinks.resize(lm);
for (int l = 0; l < lm; l++) {
const char* s = lines[l].c_str();
for (int i = 0, m = lines[l].size(); i < m; i++) {
const int lps = i;
std::string url;
// we only support http or https
if ((s[i] == 'H' || s[i] == 'h')
&& (s[i + 1] == 'T' || s[i + 1] == 't')
&& (s[i + 2] == 'T' || s[i + 2] == 't')
&& (s[i + 3] == 'P' || s[i + 3] == 'p'))
{
if (s[i + 4] == ':' && s[i + 5] == '/' && s[i + 6] == '/') {
// http
i += 7;
url = "http://";
} else if ((s[i + 4] == 'S' || s[i + 4] == 's') && s[i + 5] == ':' && s[i + 6] == '/' && s[i + 7] == '/') {
// https
i += 8;
url = "https://";
} else {
continue;
}
for (; i < m; i++) {
char c = s[i];
// url ends with following character
if (c == '\0' || c == ' ' || c == ')' || c == ']' || c == '}' || c == '>' || c == '\r' || c == '\n' || c == '\t') {
break;
}
url.push_back(c);
}
} else {
continue;
}
const int lpe = i;
Hyperlink hyperlink = {};
TTF_SizeUTF8(widgetFont, lines[l].substr(0, lps).c_str(), &hyperlink.startX, NULL);
TTF_SizeUTF8(widgetFont, lines[l].substr(0, lpe).c_str(), &hyperlink.endX, NULL);
hyperlink.url = lines[l].substr(lps, lpe - lps);
hyperlinks[l].push_back(hyperlink);
}
}
}
+void GUITextArea::setHyperlinks(const std::vector<Hyperlink2>& links) {
+ hyperlinks.clear();
+ addHyperlinks(links);
+}
+
+void GUITextArea::addHyperlinks(const std::vector<Hyperlink2>& links) {
+ const int lm = lines.size();
+
+ if (lm <= 0) return;
+ hyperlinks.resize(lm);
+
+ for (const Hyperlink2& link : links) {
+ if (link.line >= 0 && link.line < lm) {
+ hyperlinks[link.line].push_back(Hyperlink{ link.startX, link.endX, link.url });
+ }
+ }
+}
+
string GUITextArea::getString(){
string tmp;
for(vector<string>::iterator it=lines.begin();it!=lines.end();++it){
//Append a newline only if not the first line.
if(it!=lines.begin())
tmp.append(1,'\n');
//Append the line.
tmp.append(*it);
}
return tmp;
}
void GUITextArea::onResize(){
scrollBar->left=width-16;
scrollBar->height=height;
if(scrollBarH->visible)
scrollBar->height-=16;
scrollBarH->top=height-16;
scrollBarH->width=width-16;
adjustView();
}
diff --git a/src/GUITextArea.h b/src/GUITextArea.h
index 85fd293..a0174a8 100644
--- a/src/GUITextArea.h
+++ b/src/GUITextArea.h
@@ -1,141 +1,159 @@
/*
* Copyright (C) 2011-2012 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 GUITEXTAREA_H
#define GUITEXTAREA_H
#include "GUIObject.h"
#include "GUIScrollBar.h"
class WordWrapper;
//Widget for multiline text input.
class GUITextArea:public GUIObject{
private:
//Method that will remove the last character of the text.
void backspaceChar(SDL_Renderer &renderer);
void deleteChar(SDL_Renderer &renderer);
//Methods to move the carrot by one character/line.
void moveCarrotLeft();
void moveCarrotRight();
void moveCarrotUp();
void moveCarrotDown();
// Remove all highlighted text.
void removeHighlight(SDL_Renderer &renderer);
// Input new text.
void inputText(SDL_Renderer &renderer, const char* s);
//Method to adjust view so carrot stays visible.
void adjustView();
//Pointer to the font used in the widget.
TTF_Font* widgetFont;
//Widget's text.
//One line per vector element.
std::vector<std::string> lines;
//Cache for rendered lines.
//Will be updated alongside with variable text.
std::vector<TexturePtr> linesCache;
//Variable for carrot position.
int highlightLineStart;
int highlightLineEnd;
int highlightStart;
int highlightStartX;
int highlightEnd;
int highlightEndX;
//Height of the font.
int fontHeight;
//Scrollbar widget.
GUIScrollBar* scrollBar;
GUIScrollBar* scrollBarH;
//A struct to save hyperlink.
struct Hyperlink {
int startX, endX;
std::string url;
};
//Hyperlinks.
std::vector<std::vector<Hyperlink> > hyperlinks;
void drawHighlight(SDL_Renderer& renderer, int x, int y, SDL_Rect r, SDL_Color color);
public:
+ //Another struct to save hyperlink.
+ struct Hyperlink2 {
+ int line, startX, endX;
+ std::string url;
+ };
+
//Constructor.
//left: The relative x location of the GUITextArea.
//top: The relative y location of the GUITextArea.
//witdh: The width of the GUITextArea.
//height: The height of the GUITextArea.
//enabled: Boolean if the GUITextArea is enabled or not.
//visible: Boolean if the GUITextArea is visisble or not.
GUITextArea(ImageManager& imageManager, SDL_Renderer& renderer,int left=0,int top=0,int width=0,int height=0,bool enabled=true,bool visible=true);
//Method used to change the font.
//font: Pointer to the font
void setFont(TTF_Font* font);
//Method used to reposition scrollbars after a resize.
void onResize() override;
//Method used to get widget's text in a single string.
std::string getString();
//Method used to set widget's text.
//NOTE: wordWrap will actually change the text saved in the text area!
void setString(SDL_Renderer& renderer, const std::string& input, bool wordWrap = false);
void setStringArray(SDL_Renderer &renderer, const std::vector<std::string>& input, bool wordWrap = false);
void setString(SDL_Renderer& renderer, const std::string& input, WordWrapper& wrapper);
void setStringArray(SDL_Renderer &renderer, const std::vector<std::string>& input, WordWrapper& wrapper);
void setStringArray(SDL_Renderer &renderer, std::vector<SurfacePtr>& surfaces);
//Extract hyperlinks from text.
//Currently only http and https links are extracted.
void extractHyperlinks();
-
+
+ //Set hyperlinks from a list.
+ void setHyperlinks(const std::vector<Hyperlink2>& links);
+
+ //Add hyperlinks from a list.
+ void addHyperlinks(const std::vector<Hyperlink2>& links);
+
//Bool if user can edit text in the widget.
bool editable;
//Method used to handle mouse and/or key events.
//x: The x mouse location.
//y: The y mouse location.
//enabled: Boolean if the parent is enabled or not.
//visible: Boolean if the parent is visible or not.
//processed: Boolean if the event has been processed (by the parent) or not.
//Returns: Boolean if the event is processed by the child.
virtual bool handleEvents(SDL_Renderer&renderer, int x=0, int y=0, bool enabled=true, bool visible=true, bool processed=false);
//Method that will render the GUITextArea.
//x: The x location to draw the GUITextArea. (x+left)
//y: The y location to draw the GUITextArea. (y+top)
virtual void render(SDL_Renderer &renderer, int x=0, int y=0, bool draw=true);
//Scroll the scrollbar.
//dx: horizontal scroll (in pixels), typically multiple of 20
//dy: vertical scroll (in lines)
void scrollScrollbar(int dx, int dy);
+
+ //The URL of clicked hyperlink.
+ //This only makes sense when you received a GUIEventClick event.
+ //NOTE: the widgets will process URL begins with http:// or https:// (NOTE: case sensitive) automatically
+ //and you will not receive the event in these cases.
+ std::string clickedHyperlink;
};
#endif
diff --git a/src/HelpManager.cpp b/src/HelpManager.cpp
index 9d58e16..3ccbbdc 100644
--- a/src/HelpManager.cpp
+++ b/src/HelpManager.cpp
@@ -1,1005 +1,1051 @@
/*
* 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;
+ virtual void createSurfaces(std::vector<SurfacePtr>& surfaces, std::vector<GUITextArea::Hyperlink2>& links) = 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";
+ wrapper.reservedFragments.insert("/");
//TODO: verbatim support
cachedLines.clear();
cachedMaxWidth = maxWidth;
cachedWidth = wrapper.addString(cachedLines, text);
cachedNumberOfLines = cachedLines.size();
}
- virtual void createSurfaces(std::vector<SurfacePtr>& surfaces) override {
+ virtual void createSurfaces(std::vector<SurfacePtr>& surfaces, std::vector<GUITextArea::Hyperlink2>& links) override {
SDL_Color fg = objThemes.getTextColor(true);
for (const std::string& s : cachedLines) {
surfaces.emplace_back(TTF_RenderUTF8_Blended(fontText, s.c_str(), fg));
}
+
+ //TODO: extract hyperlinks
}
};
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 {
+ virtual void createSurfaces(std::vector<SurfacePtr>& surfaces, std::vector<GUITextArea::Hyperlink2>& links) override {
auto bmGUI = getImageManager().loadImage(getDataPath() + "gfx/gui.png");
for (auto item : items) {
if (item) {
std::vector<SurfacePtr> tempSurfaces;
- item->createSurfaces(tempSurfaces);
+ const size_t oldLineCount = surfaces.size();
+ const size_t oldLinkSize = links.size();
+ item->createSurfaces(tempSurfaces, links);
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));
}
+
+ for (size_t i = oldLinkSize; i < links.size(); i++) {
+ GUITextArea::Hyperlink2 &link = links[i];
+ link.line += oldLineCount;
+ link.startX += 16;
+ link.endX += 16;
+ }
}
}
}
};
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 {
+ virtual void createSurfaces(std::vector<SurfacePtr>& surfaces, std::vector<GUITextArea::Hyperlink2>& links) 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));
+ const size_t oldLineCount = surfaces.size();
+
std::vector<std::vector<SurfacePtr> > tempSurfaces(columns);
int h = 0;
+ int x = lineWidth;
for (int i = 0; i < columns; i++) {
auto item = items[j * columns + i];
if (item) {
- item->createSurfaces(tempSurfaces[i]);
+ const size_t oldLinkSize = links.size();
+ item->createSurfaces(tempSurfaces[i], links);
h = std::max<int>(tempSurfaces[i].size(), h);
+
+ for (size_t k = oldLinkSize; k < links.size(); k++) {
+ GUITextArea::Hyperlink2 &link = links[k];
+ link.line += oldLineCount;
+ link.startX += x;
+ link.endX += x;
+ }
}
+ x += cachedRowWidth[i] + lineWidth;
}
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 {
+ virtual void createSurfaces(std::vector<SurfacePtr>& surfaces, std::vector<GUITextArea::Hyperlink2>& links) override {
SDL_Color fg = objThemes.getTextColor(true);
surfaces.emplace_back(TTF_RenderUTF8_Blended(fontText, "Copy code", fg));
+
+ links.push_back(GUITextArea::Hyperlink2{ surfaces.size() - 1, 0, surfaces.back()->w, "code:" });
+
for (const std::string& s : lines) {
surfaces.emplace_back(TTF_RenderUTF8_Blended(fontMono, s.c_str(), fg));
+ links.back().url += s + "\n";
}
}
};
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 {
+ virtual void createSurfaces(std::vector<SurfacePtr>& surfaces, std::vector<GUITextArea::Hyperlink2>& links) override {
if (level <= 1) {
- child->createSurfaces(surfaces);
+ child->createSurfaces(surfaces, links);
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");
+ const size_t oldLineCount = surfaces.size();
+ const size_t oldLinkSize = links.size();
+
std::vector<SurfacePtr> tempSurfaces;
- child->createSurfaces(tempSurfaces);
+ child->createSurfaces(tempSurfaces, links);
const int additionalWidth = getAdditionalWidth();
+ for (size_t i = oldLinkSize; i < links.size(); i++) {
+ GUITextArea::Hyperlink2 &link = links[i];
+ link.line += oldLineCount;
+ link.startX += additionalWidth;
+ link.endX += additionalWidth;
+ }
+
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::string nameSpace;
std::vector<Chunk*> chunks;
static const int BIGGEST_LEVEL = 10;
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;
+ std::vector<GUITextArea::Hyperlink2> links;
for (auto chunk : chunks) {
if (chunk) {
chunk->updateSize(textArea->width - 16);
- chunk->createSurfaces(surfaces);
+ chunk->createSurfaces(surfaces, links);
surfaces.emplace_back(TTF_RenderUTF8_Blended(fontText, " ", fg));
}
}
textArea->setStringArray(renderer, surfaces);
+ textArea->addHyperlinks(links);
}
};
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.");
}
}
//TODO: add hyperlinks of subpages to each page
{
std::string library, nameSpace;
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;
//Get the library name.
if (page->title.find("library") != std::string::npos) {
library.clear();
nameSpace.clear();
size_t lps = page->title.find_first_of('\"');
if (lps != std::string::npos) {
size_t lpe = page->title.find_last_of('\"');
if (lpe > lps) {
library = page->title.substr(lps + 1, lpe - lps - 1);
}
}
}
//Get the namespace.
if (page->title.find("Global") != std::string::npos) {
nameSpace.clear();
} else if (page->title.find("Static") != std::string::npos) {
nameSpace = library + ".";
} else if (page->title.find("Member") != std::string::npos) {
nameSpace = library + ":";
}
}
}
if (ItemizeChunk *itemize = library.empty() ? NULL : dynamic_cast<ItemizeChunk*>(chunk)) {
//We parse a new header so add a new page.
if (!page->chunks.empty()) {
pages.push_back(page);
page = new HelpPage;
}
page->level = HelpPage::BIGGEST_LEVEL;
page->nameSpace = nameSpace;
//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;
}
//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.
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, 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, 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);
//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;
pages[pageIndex]->show(renderer, textArea);
root->currentPage = pageIndex;
root->history.push_back(pageIndex);
return root;
}
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") {
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") {
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") {
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 == "TextArea") {
- //TODO:
+ auto textArea = dynamic_cast<GUITextArea*>(obj->getChild("TextArea"));
+ if (textArea && textArea->clickedHyperlink.size() > 5) {
+ std::string s = textArea->clickedHyperlink.substr(0, 5);
+ if (s == "code:") {
+ SDL_SetClipboardText(textArea->clickedHyperlink.c_str() + 5);
+ }
+ }
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) {
SharedTexture tex;
SDL_Color fg = objThemes.getTextColor(true);
if (page->level == HelpPage::BIGGEST_LEVEL) {
SDL_Color fg2 = { Uint8((255 + fg.r) / 2), Uint8((255 + fg.g) / 2), Uint8((255 + fg.b) / 2), Uint8(255) };
SurfacePtr surf1(TTF_RenderUTF8_Blended(fontMono, (std::string(5, ' ') + page->nameSpace).c_str(), fg2));
SurfacePtr surf2(TTF_RenderUTF8_Blended(fontMono, page->title.empty() ? " " : page->title.c_str(), fg));
SurfacePtr surf = createSurface(surf1->w + surf2->w, std::max(surf1->h, surf2->h) + 4);
SDL_Rect srcrect = { 0, 0, surf1->w, surf1->h };
SDL_Rect dstrect = { 0, 2, surf1->w, surf1->h };
SDL_BlitSurface(surf1.get(), &srcrect, surf.get(), &dstrect);
srcrect = SDL_Rect{ 0, 0, surf2->w, surf2->h };
dstrect = SDL_Rect{ surf1->w, 2, surf2->w, surf2->h };
SDL_BlitSurface(surf2.get(), &srcrect, surf.get(), &dstrect);
tex = textureFromSurface(renderer, std::move(surf));
} else {
tex = textureFromTextShared(renderer, *fontText,
(std::string(page->level, ' ') + page->title).c_str(),
fg);
}
char s[32];
sprintf(s, "%d", i);
listBox->addItem(renderer, s, tex);
}
}
listBox->value = (backup >= 0 && backup < (int)listBox->item.size()) ? backup : -1;
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, May 8, 8:29 PM (1 w, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
62728
Default Alt Text
(70 KB)
Attached To
Mode
R79 meandmyshadow
Attached
Detach File
Event Timeline