/* 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 . */ #include 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; #ifndef TW_NO_HAPTICS DataManager::Vibrate("tw_button_vibrate"); #endif 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* origText, std::vector* origColor, size_t* lastCount, std::vector* rText, std::vector* 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; }