/*
Copyright 2013 bigbiff/Dees_Troy TeamWin
This file is part of TWRP/TeamWin Recovery Project.
TWRP 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.
TWRP 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 TWRP. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string.h>
extern "C" {
#include "../twcommon.h"
}
#include "../minuitwrp/minui.h"
#include "rapidxml.hpp"
#include "objects.hpp"
#include "../data.hpp"
const float SCROLLING_SPEED_DECREMENT = 0.9; // friction
const int SCROLLING_FLOOR = 2; // minimum pixels for scrolling to stop
GUIScrollList::GUIScrollList(xml_node<>* node) : GUIObject(node)
{
xml_node<>* child;
firstDisplayedItem = mItemSpacing = mFontHeight = mSeparatorH = y_offset = scrollingSpeed = 0;
maxIconWidth = maxIconHeight = mHeaderIconHeight = mHeaderIconWidth = 0;
mHeaderSeparatorH = mHeaderH = actualItemHeight = 0;
mHeaderIsStatic = false;
mBackground = mHeaderIcon = NULL;
mFont = NULL;
mFastScrollW = mFastScrollLineW = mFastScrollRectW = mFastScrollRectH = 0;
mFastScrollRectCurrentY = mFastScrollRectCurrentH = mFastScrollRectTouchY = 0;
lastY = last2Y = fastScroll = 0;
mUpdate = 0;
touchDebounce = 6;
ConvertStrToColor("black", &mBackgroundColor);
ConvertStrToColor("black", &mHeaderBackgroundColor);
ConvertStrToColor("black", &mSeparatorColor);
ConvertStrToColor("black", &mHeaderSeparatorColor);
ConvertStrToColor("white", &mFontColor);
ConvertStrToColor("white", &mHeaderFontColor);
ConvertStrToColor("white", &mFastScrollLineColor);
ConvertStrToColor("white", &mFastScrollRectColor);
hasHighlightColor = false;
allowSelection = true;
selectedItem = NO_ITEM;
// Load header text
// note: node can be NULL for the emergency console
child = node ? node->first_node("text") : NULL;
if (child) mHeaderText = child->value();
// Simple way to check for static state
mLastHeaderValue = gui_parse_text(mHeaderText);
mHeaderIsStatic = (mLastHeaderValue == mHeaderText);
mHighlightColor = LoadAttrColor(FindNode(node, "highlight"), "color", &hasHighlightColor);
child = FindNode(node, "background");
if (child)
{
mBackground = LoadAttrImage(child, "resource");
mBackgroundColor = LoadAttrColor(child, "color");
}
// Load the placement
LoadPlacement(FindNode(node, "placement"), &mRenderX, &mRenderY, &mRenderW, &mRenderH);
SetActionPos(mRenderX, mRenderY, mRenderW, mRenderH);
// Load the font, and possibly override the color
child = FindNode(node, "font");
if (child)
{
mFont = LoadAttrFont(child, "resource");
mFontColor = LoadAttrColor(child, "color");
mFontHighlightColor = LoadAttrColor(child, "highlightcolor", mFontColor);
mItemSpacing = LoadAttrIntScaleY(child, "spacing");
}
// Load the separator if it exists
child = FindNode(node, "separator");
if (child)
{
mSeparatorColor = LoadAttrColor(child, "color");
mSeparatorH = LoadAttrIntScaleY(child, "height");
}
// Fast scroll
child = FindNode(node, "fastscroll");
if (child)
{
mFastScrollLineColor = LoadAttrColor(child, "linecolor");
mFastScrollRectColor = LoadAttrColor(child, "rectcolor");
mFastScrollW = LoadAttrIntScaleX(child, "w");
mFastScrollLineW = LoadAttrIntScaleX(child, "linew");
mFastScrollRectW = LoadAttrIntScaleX(child, "rectw");
mFastScrollRectH = LoadAttrIntScaleY(child, "recth");
}
// Retrieve the line height
mFontHeight = mFont->GetHeight();
actualItemHeight = mFontHeight + mItemSpacing + mSeparatorH;
// Load the header if it exists
child = FindNode(node, "header");
if (child)
{
mHeaderH = mFontHeight;
mHeaderIcon = LoadAttrImage(child, "icon");
mHeaderBackgroundColor = LoadAttrColor(child, "background", mBackgroundColor);
mHeaderFontColor = LoadAttrColor(child, "textcolor", mFontColor);
mHeaderSeparatorColor = LoadAttrColor(child, "separatorcolor", mSeparatorColor);
mHeaderSeparatorH = LoadAttrIntScaleY(child, "separatorheight", mSeparatorH);
if (mHeaderIcon && mHeaderIcon->GetResource())
{
mHeaderIconWidth = mHeaderIcon->GetWidth();
mHeaderIconHeight = mHeaderIcon->GetHeight();
if (mHeaderIconHeight > mHeaderH)
mHeaderH = mHeaderIconHeight;
if (mHeaderIconWidth > maxIconWidth)
maxIconWidth = mHeaderIconWidth;
}
mHeaderH += mItemSpacing + mHeaderSeparatorH;
if (mHeaderH < actualItemHeight)
mHeaderH = actualItemHeight;
}
if (actualItemHeight / 3 > 6)
touchDebounce = actualItemHeight / 3;
}
GUIScrollList::~GUIScrollList()
{
}
void GUIScrollList::SetMaxIconSize(int w, int h)
{
if (w > maxIconWidth)
maxIconWidth = w;
if (h > maxIconHeight)
maxIconHeight = h;
if (maxIconHeight > mFontHeight) {
actualItemHeight = maxIconHeight + mItemSpacing + mSeparatorH;
if (mHeaderH > 0 && actualItemHeight > mHeaderH)
mHeaderH = actualItemHeight;
}
}
void GUIScrollList::SetVisibleListLocation(size_t list_index)
{
// This will make sure that the item indicated by list_index is visible on the screen
size_t lines = GetDisplayItemCount();
if (list_index <= (unsigned)firstDisplayedItem) {
// list_index is above the currently displayed items, put the selected item at the very top
firstDisplayedItem = list_index;
y_offset = 0;
} else if (list_index >= firstDisplayedItem + lines) {
// list_index is below the currently displayed items, put the selected item at the very bottom
firstDisplayedItem = list_index - lines + 1;
if (GetDisplayRemainder() != 0) {
// There's a partial row displayed, set the scrolling offset so that the selected item really is at the very bottom
firstDisplayedItem--;
y_offset = GetDisplayRemainder() - actualItemHeight;
} else {
// There's no partial row so zero out the offset
y_offset = 0;
}
if (firstDisplayedItem < 0)
firstDisplayedItem = 0;
}
scrollingSpeed = 0; // stop kinetic scrolling on setting visible location
mUpdate = 1;
}
int GUIScrollList::Render(void)
{
if (!isConditionTrue())
return 0;
// First step, fill background
gr_color(mBackgroundColor.red, mBackgroundColor.green, mBackgroundColor.blue, mBackgroundColor.alpha);
gr_fill(mRenderX, mRenderY + mHeaderH, mRenderW, mRenderH - mHeaderH);
// don't paint outside of the box
gr_clip(mRenderX, mRenderY, mRenderW, mRenderH);
// Next, render the background resource (if it exists)
if (mBackground && mBackground->GetResource())
{
int BackgroundW = mBackground->GetWidth();
int BackgroundH = mBackground->GetHeight();
int BackgroundX = mRenderX + ((mRenderW - BackgroundW) / 2);
int BackgroundY = mRenderY + ((mRenderH - BackgroundH) / 2);
gr_blit(mBackground->GetResource(), 0, 0, BackgroundW, BackgroundH, BackgroundX, BackgroundY);
}
// This tells us how many full lines we can actually render
size_t lines = GetDisplayItemCount();
size_t listSize = GetItemCount();
int listW = mRenderW; // this is only used for the separators - the list items are rendered in the full width of the list
if (listSize <= lines) {
hasScroll = false;
scrollingSpeed = 0;
lines = listSize;
y_offset = 0;
} else {
hasScroll = true;
listW -= mFastScrollW; // space for fast scroll
lines++;
if (lines < listSize)
lines++;
}
int yPos = mRenderY + mHeaderH + y_offset;
// render all visible items
for (size_t line = 0; line < lines; line++)
{
size_t itemindex = line + firstDisplayedItem;
if (itemindex >= listSize)
break;
RenderItem(itemindex, yPos, itemindex == selectedItem);
// Add the separator
gr_color(mSeparatorColor.red, mSeparatorColor.green, mSeparatorColor.blue, mSeparatorColor.alpha);
gr_fill(mRenderX, yPos + actualItemHeight - mSeparatorH, listW, mSeparatorH);
// Move the yPos
yPos += actualItemHeight;
}
// Render the Header (last so that it overwrites the top most row for per pixel scrolling)
yPos = mRenderY;
if (mHeaderH > 0) {
// First step, fill background
gr_color(mHeaderBackgroundColor.red, mHeaderBackgroundColor.green, mHeaderBackgroundColor.blue, mHeaderBackgroundColor.alpha);
gr_fill(mRenderX, mRenderY, mRenderW, mHeaderH);
int IconOffsetX = 0;
// render the icon if it exists
if (mHeaderIcon && mHeaderIcon->GetResource())
{
gr_blit(mHeaderIcon->GetResource(), 0, 0, mHeaderIconWidth, mHeaderIconHeight, mRenderX + ((mHeaderIconWidth - maxIconWidth) / 2), (yPos + (int)((mHeaderH - mHeaderIconHeight) / 2)));
IconOffsetX = maxIconWidth;
}
// render the text
if (mFont && mFont->GetResource()) {
gr_color(mHeaderFontColor.red, mHeaderFontColor.green, mHeaderFontColor.blue, mHeaderFontColor.alpha);
gr_textEx_scaleW(mRenderX + IconOffsetX + 5, yPos + (int)(mHeaderH / 2), mLastHeaderValue.c_str(), mFont->GetResource(), mRenderW, TEXT_ONLY_RIGHT, 0);
}
// Add the separator
gr_color(mHeaderSeparatorColor.red, mHeaderSeparatorColor.green, mHeaderSeparatorColor.blue, mHeaderSeparatorColor.alpha);
gr_fill(mRenderX, yPos + mHeaderH - mHeaderSeparatorH, mRenderW, mHeaderSeparatorH);
}
// reset clipping
gr_noclip();
// render fast scroll
if (hasScroll) {
int fWidth = mRenderW - listW;
int fHeight = mRenderH - mHeaderH;
int centerX = listW + mRenderX + fWidth / 2;
// first determine the total list height and where we are in the list
int totalHeight = GetItemCount() * actualItemHeight; // total height of the full list in pixels
int topPos = firstDisplayedItem * actualItemHeight - y_offset;
// now scale it proportionally to the scrollbar height
int boxH = fHeight * fHeight / totalHeight; // proportional height of the displayed portion
boxH = std::max(boxH, mFastScrollRectH); // but keep a minimum height
int boxY = (fHeight - boxH) * topPos / (totalHeight - fHeight); // pixels relative to top of list
int boxW = mFastScrollRectW;
int x = centerX - boxW / 2;
int y = mRenderY + mHeaderH + boxY;
// line above and below box (needs to be split because box can be transparent)
gr_color(mFastScrollLineColor.red, mFastScrollLineColor.green, mFastScrollLineColor.blue, mFastScrollLineColor.alpha);
gr_fill(centerX - mFastScrollLineW / 2, mRenderY + mHeaderH, mFastScrollLineW, boxY);
gr_fill(centerX - mFastScrollLineW / 2, y + boxH, mFastScrollLineW, fHeight - boxY - boxH);
// box
gr_color(mFastScrollRectColor.red, mFastScrollRectColor.green, mFastScrollRectColor.blue, mFastScrollRectColor.alpha);
gr_fill(x, y, boxW, boxH);
mFastScrollRectCurrentY = boxY;
mFastScrollRectCurrentH = boxH;
}
mUpdate = 0;
return 0;
}
void GUIScrollList::RenderItem(size_t itemindex __unused, int yPos, bool selected)
{
RenderStdItem(yPos, selected, NULL, "implement RenderItem!");
}
void GUIScrollList::RenderStdItem(int yPos, bool selected, ImageResource* icon, const char* text, int iconAndTextH)
{
if (hasHighlightColor && selected) {
// Highlight the item background of the selected item
gr_color(mHighlightColor.red, mHighlightColor.green, mHighlightColor.blue, mHighlightColor.alpha);
gr_fill(mRenderX, yPos, mRenderW, actualItemHeight);
}
if (selected) {
// Use the highlight color for the font
gr_color(mFontHighlightColor.red, mFontHighlightColor.green, mFontHighlightColor.blue, mFontHighlightColor.alpha);
} else {
// Set the color for the font
gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, mFontColor.alpha);
}
if (!iconAndTextH)
iconAndTextH = actualItemHeight;
// render icon
if (icon && icon->GetResource()) {
int iconH = icon->GetHeight();
int iconW = icon->GetWidth();
int iconY = yPos + (iconAndTextH - iconH) / 2;
int iconX = mRenderX + (maxIconWidth - iconW) / 2;
gr_blit(icon->GetResource(), 0, 0, iconW, iconH, iconX, iconY);
}
// render label text
if (mFont && mFont->GetResource()) {
int textX = mRenderX + maxIconWidth + 5;
int textY = yPos + (iconAndTextH / 2);
gr_textEx_scaleW(textX, textY, text, mFont->GetResource(), mRenderW, TEXT_ONLY_RIGHT, 0);
}
}
int GUIScrollList::Update(void)
{
if (!isConditionTrue())
return 0;
if (!mHeaderIsStatic) {
std::string newValue = gui_parse_text(mHeaderText);
if (mLastHeaderValue != newValue) {
mLastHeaderValue = newValue;
mUpdate = 1;
}
}
// Handle kinetic scrolling
// maximum number of items to scroll per update
float maxItemsScrolledPerFrame = std::max(2.5, float(GetDisplayItemCount() / 4) + 0.5);
int maxScrollDistance = actualItemHeight * maxItemsScrolledPerFrame;
int oldScrollingSpeed = scrollingSpeed;
if (scrollingSpeed == 0) {
// Do nothing
return 0;
} else if (scrollingSpeed > 0) {
if (scrollingSpeed < maxScrollDistance)
y_offset += scrollingSpeed;
else
y_offset += maxScrollDistance;
scrollingSpeed *= SCROLLING_SPEED_DECREMENT;
if (scrollingSpeed == oldScrollingSpeed)
--scrollingSpeed;
} else if (scrollingSpeed < 0) {
if (abs(scrollingSpeed) < maxScrollDistance)
y_offset += scrollingSpeed;
else
y_offset -= maxScrollDistance;
scrollingSpeed *= SCROLLING_SPEED_DECREMENT;
if (scrollingSpeed == oldScrollingSpeed)
++scrollingSpeed;
}
if (abs(scrollingSpeed) < SCROLLING_FLOOR)
scrollingSpeed = 0;
HandleScrolling();
mUpdate = 1;
return 0;
}
size_t GUIScrollList::HitTestItem(int x __unused, int y)
{
// We only care about y position
if (y < mRenderY || y - mRenderY <= mHeaderH || y - mRenderY > mRenderH)
return NO_ITEM;
int startSelection = (y - mRenderY - mHeaderH);
// Locate the correct item
size_t actualSelection = firstDisplayedItem;
int selectY = y_offset;
while (selectY + actualItemHeight < startSelection) {
selectY += actualItemHeight;
actualSelection++;
}
if (actualSelection < GetItemCount())
return actualSelection;
return NO_ITEM;
}
int GUIScrollList::NotifyTouch(TOUCH_STATE state, int x, int y)
{
if (!isConditionTrue())
return -1;
switch (state)
{
case TOUCH_START:
if (hasScroll && x >= mRenderX + mRenderW - mFastScrollW) {
fastScroll = 1; // Initial touch is in the fast scroll region
int fastScrollBoxTop = mFastScrollRectCurrentY + mRenderY + mHeaderH;
int fastScrollBoxBottom = fastScrollBoxTop + mFastScrollRectCurrentH;
if (y >= fastScrollBoxTop && y < fastScrollBoxBottom)
// user grabbed the fastscroll bar
// try to keep the initially touched part of the scrollbar under the finger
mFastScrollRectTouchY = y - fastScrollBoxTop;
else
// user tapped outside the fastscroll bar
// center fastscroll rect on the initial touch position
mFastScrollRectTouchY = mFastScrollRectCurrentH / 2;
}
if (scrollingSpeed != 0) {
selectedItem = NO_ITEM; // this allows the user to tap the list to stop the scrolling without selecting the item they tap
scrollingSpeed = 0; // stop scrolling on a new touch
} else if (!fastScroll && allowSelection) {
// find out which item the user touched
selectedItem = HitTestItem(x, y);
}
if (selectedItem != NO_ITEM)
mUpdate = 1;
lastY = last2Y = y;
break;
case TOUCH_DRAG:
if (fastScroll)
{
int relY = y - mRenderY - mHeaderH; // touch position relative to window
int windowH = mRenderH - mHeaderH;
int totalHeight = GetItemCount() * actualItemHeight; // total height of the full list in pixels
// calculate new top position of the fastscroll bar relative to window
int newY = relY - mFastScrollRectTouchY;
// keep it fully inside the list
newY = std::min(std::max(newY, 0), windowH - mFastScrollRectCurrentH);
// now compute the new scroll position for the list
int newTopPos = newY * (totalHeight - windowH) / (windowH - mFastScrollRectCurrentH); // new top pixel of list
newTopPos = std::min(newTopPos, totalHeight - windowH); // account for rounding errors
firstDisplayedItem = newTopPos / actualItemHeight;
y_offset = - newTopPos % actualItemHeight;
selectedItem = NO_ITEM;
mUpdate = 1;
scrollingSpeed = 0; // prevent kinetic scrolling when using fast scroll
break;
}
// Provide some debounce on initial touches
if (selectedItem != NO_ITEM && abs(y - lastY) < touchDebounce) {
mUpdate = 1;
break;
}
selectedItem = NO_ITEM; // nothing is selected because we dragged too far
// Handle scrolling
if (hasScroll) {
y_offset += y - lastY; // adjust the scrolling offset based on the difference between the starting touch and the current touch
last2Y = lastY; // keep track of previous y locations so that we can tell how fast to scroll for kinetic scrolling
lastY = y; // update last touch to the current touch so we can tell how far and what direction we scroll for the next touch event
HandleScrolling();
} else
y_offset = 0;
mUpdate = 1;
break;
case TOUCH_RELEASE:
if (fastScroll)
mUpdate = 1; // get rid of touch effects on the fastscroll bar
fastScroll = 0;
if (selectedItem != NO_ITEM) {
// We've selected an item!
NotifySelect(selectedItem);
mUpdate = 1;
DataManager::Vibrate("tw_button_vibrate");
selectedItem = NO_ITEM;
} else {
// Start kinetic scrolling
scrollingSpeed = lastY - last2Y;
if (abs(scrollingSpeed) < touchDebounce)
scrollingSpeed = 0;
}
case TOUCH_REPEAT:
case TOUCH_HOLD:
break;
}
return 0;
}
void GUIScrollList::HandleScrolling()
{
// handle dragging downward, scrolling upward
// the offset should always be <= 0 and > -actualItemHeight, adjust the first display row and offset as needed
while (firstDisplayedItem && y_offset > 0) {
firstDisplayedItem--;
y_offset -= actualItemHeight;
}
if (firstDisplayedItem == 0 && y_offset > 0) {
y_offset = 0; // user kept dragging downward past the top of the list, so always reset the offset to 0 since we can't scroll any further in this direction
scrollingSpeed = 0; // stop kinetic scrolling
}
// handle dragging upward, scrolling downward
int totalSize = GetItemCount();
int lines = GetDisplayItemCount(); // number of full lines our list can display at once
int bottom_offset = GetDisplayRemainder() - actualItemHeight; // extra display area that can display a partial line for per pixel scrolling
// the offset should always be <= 0 and > -actualItemHeight, adjust the first display row and offset as needed
while (firstDisplayedItem + lines + (bottom_offset ? 1 : 0) < totalSize && abs(y_offset) > actualItemHeight) {
firstDisplayedItem++;
y_offset += actualItemHeight;
}
// Check if we dragged too far, set the list at the bottom and adjust offset as needed
if (bottom_offset != 0 && firstDisplayedItem + lines + 1 >= totalSize && y_offset <= bottom_offset) {
firstDisplayedItem = totalSize - lines - 1;
y_offset = bottom_offset;
scrollingSpeed = 0; // stop kinetic scrolling
} else if (firstDisplayedItem + lines >= totalSize && y_offset < 0) {
firstDisplayedItem = totalSize - lines;
y_offset = 0;
scrollingSpeed = 0; // stop kinetic scrolling
}
}
int GUIScrollList::GetDisplayItemCount()
{
return (mRenderH - mHeaderH) / (actualItemHeight);
}
int GUIScrollList::GetDisplayRemainder()
{
return (mRenderH - mHeaderH) % actualItemHeight;
}
int GUIScrollList::NotifyVarChange(const std::string& varName, const std::string& value)
{
GUIObject::NotifyVarChange(varName, value);
if (!isConditionTrue())
return 0;
if (!mHeaderIsStatic) {
std::string newValue = gui_parse_text(mHeaderText);
if (mLastHeaderValue != newValue) {
mLastHeaderValue = newValue;
firstDisplayedItem = 0;
y_offset = 0;
scrollingSpeed = 0; // stop kinetic scrolling on variable changes
mUpdate = 1;
}
}
return 0;
}
int GUIScrollList::SetRenderPos(int x, int y, int w /* = 0 */, int h /* = 0 */)
{
mRenderX = x;
mRenderY = y;
if (w || h)
{
mRenderW = w;
mRenderH = h;
}
SetActionPos(mRenderX, mRenderY, mRenderW, mRenderH);
mUpdate = 1;
return 0;
}
void GUIScrollList::SetPageFocus(int inFocus)
{
if (inFocus) {
NotifyVarChange("", ""); // This forces a check for the header text
scrollingSpeed = 0; // stop kinetic scrolling on page changes
mUpdate = 1;
}
}
bool GUIScrollList::AddLines(std::vector<std::string>* origText, std::vector<std::string>* origColor, size_t* lastCount, std::vector<std::string>* rText, std::vector<std::string>* rColor)
{
if (!mFont || !mFont->GetResource())
return false;
if (*lastCount == origText->size())
return false; // nothing to add
size_t prevCount = *lastCount;
*lastCount = origText->size();
// Due to word wrap, figure out what / how the newly added text needs to be added to the render vector that is word wrapped
// Note, that multiple consoles on different GUI pages may be different widths or use different fonts, so the word wrapping
// may different in different console windows
for (size_t i = prevCount; i < *lastCount; i++) {
string curr_line = origText->at(i);
string curr_color;
if (origColor)
curr_color = origColor->at(i);
for (;;) {
size_t line_char_width = gr_ttf_maxExW(curr_line.c_str(), mFont->GetResource(), mRenderW);
if (line_char_width < curr_line.size()) {
//string left = curr_line.substr(0, line_char_width);
size_t wrap_pos = curr_line.find_last_of(" ,./:-_;", line_char_width - 1);
if (wrap_pos == string::npos)
wrap_pos = line_char_width;
else if (wrap_pos < line_char_width - 1)
wrap_pos++;
rText->push_back(curr_line.substr(0, wrap_pos));
if (origColor)
rColor->push_back(curr_color);
curr_line = curr_line.substr(wrap_pos);
/* After word wrapping, delete any leading spaces. Note that the word wrapping is not smart enough to know not
* to wrap in the middle of something like ... so some of the ... could appear on the following line. */
curr_line.erase(0, curr_line.find_first_not_of(" "));
} else {
rText->push_back(curr_line);
if (origColor)
rColor->push_back(curr_color);
break;
}
}
}
return true;
}