/* 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 int SCROLLING_SPEED_DECREMENT = 6; // friction const int SCROLLING_FLOOR = 10; // minimum pixels for scrolling to start or stop const int SCROLLING_MULTIPLIER = 1; // initial speed of kinetic scrolling const float SCROLLING_SPEED_LIMIT = 2.5; // maximum number of items to scroll per update GUIScrollList::GUIScrollList(xml_node<>* node) : GUIObject(node) { xml_attribute<>* attr; 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; mBackgroundW = mBackgroundH = 0; mFastScrollW = mFastScrollLineW = mFastScrollRectW = mFastScrollRectH = 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; selectedItem = NO_ITEM; // Load header text child = node->first_node("text"); if (child) mHeaderText = child->value(); // Simple way to check for static state mLastHeaderValue = gui_parse_text(mHeaderText); mHeaderIsStatic = (mLastHeaderValue == mHeaderText); memset(&mHighlightColor, 0, sizeof(COLOR)); child = node->first_node("highlight"); if (child) { attr = child->first_attribute("color"); if (attr) { hasHighlightColor = true; std::string color = attr->value(); ConvertStrToColor(color, &mHighlightColor); } } child = node->first_node("background"); if (child) { mBackground = LoadAttrImage(child, "resource"); mBackgroundColor = LoadAttrColor(child, "color"); } // Load the placement LoadPlacement(node->first_node("placement"), &mRenderX, &mRenderY, &mRenderW, &mRenderH); SetActionPos(mRenderX, mRenderY, mRenderW, mRenderH); // Load the font, and possibly override the color child = node->first_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 = node->first_node("separator"); if (child) { mSeparatorColor = LoadAttrColor(child, "color"); mSeparatorH = LoadAttrIntScaleY(child, "height"); } // Fast scroll child = node->first_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 = node->first_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; if (mBackground && mBackground->GetResource()) { mBackgroundW = mBackground->GetWidth(); mBackgroundH = mBackground->GetHeight(); } } 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(), listSize = GetItemCount(); 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; } } 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 mBackgroundX = mRenderX + ((mRenderW - mBackgroundW) / 2); int mBackgroundY = mRenderY + ((mRenderH - mBackgroundH) / 2); gr_blit(mBackground->GetResource(), 0, 0, mBackgroundW, mBackgroundH, mBackgroundX, mBackgroundY); } // This tells us how many full lines we can actually render size_t lines = GetDisplayItemCount(); size_t listSize = GetItemCount(); int listW = mRenderW; 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++; } void* fontResource = NULL; if (mFont) fontResource = mFont->GetResource(); int yPos = mRenderY + mHeaderH + y_offset; int fontOffsetY = (int)((actualItemHeight - mFontHeight) / 2); // render all visible items for (size_t line = 0; line < lines; line++) { size_t itemindex = line + firstDisplayedItem; if (itemindex >= listSize) break; // get item data ImageResource* icon; std::string label; if (GetListItem(itemindex, icon, label)) break; if (hasHighlightColor && itemindex == selectedItem) { // 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 (itemindex == selectedItem) { // 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); } // render icon if (icon && icon->GetResource()) { int currentIconHeight = icon->GetHeight(); int currentIconWidth = icon->GetWidth(); int currentIconOffsetY = (actualItemHeight - currentIconHeight) / 2; int currentIconOffsetX = (maxIconWidth - currentIconWidth) / 2; int image_y = (yPos + currentIconOffsetY); gr_blit(icon->GetResource(), 0, 0, currentIconWidth, currentIconHeight, mRenderX + currentIconOffsetX, image_y); } // render label text gr_textEx(mRenderX + maxIconWidth + 5, yPos + fontOffsetY, label.c_str(), fontResource); // 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 mIconOffsetX = 0; // render the icon if it exists ImageResource* headerIcon = mHeaderIcon; if (headerIcon && headerIcon->GetResource()) { gr_blit(headerIcon->GetResource(), 0, 0, mHeaderIconWidth, mHeaderIconHeight, mRenderX + ((mHeaderIconWidth - maxIconWidth) / 2), (yPos + (int)((mHeaderH - mHeaderIconHeight) / 2))); mIconOffsetX = maxIconWidth; } // render the text gr_color(mHeaderFontColor.red, mHeaderFontColor.green, mHeaderFontColor.blue, mHeaderFontColor.alpha); gr_textEx(mRenderX + mIconOffsetX + 5, yPos + (int)((mHeaderH - mFontHeight) / 2), mLastHeaderValue.c_str(), fontResource); // Add the separator gr_color(mHeaderSeparatorColor.red, mHeaderSeparatorColor.green, mHeaderSeparatorColor.blue, mHeaderSeparatorColor.alpha); gr_fill(mRenderX, yPos + mHeaderH - mHeaderSeparatorH, mRenderW, mHeaderSeparatorH); } // render fast scroll lines = GetDisplayItemCount(); if (hasScroll) { int startX = listW + mRenderX; int fWidth = mRenderW - listW; int fHeight = mRenderH - mHeaderH; // line gr_color(mFastScrollLineColor.red, mFastScrollLineColor.green, mFastScrollLineColor.blue, mFastScrollLineColor.alpha); gr_fill(startX + fWidth/2, mRenderY + mHeaderH, mFastScrollLineW, mRenderH - mHeaderH); // rect int pct = 0; if (GetDisplayRemainder() != 0) { // Properly handle the percentage if a partial line is present int partial_line_size = actualItemHeight - GetDisplayRemainder(); pct = ((firstDisplayedItem*actualItemHeight - y_offset)*100)/(listSize*actualItemHeight-((lines + 1)*actualItemHeight) + partial_line_size); } else { pct = ((firstDisplayedItem*actualItemHeight - y_offset)*100)/(listSize*actualItemHeight-lines*actualItemHeight); } int mFastScrollRectX = startX + (fWidth - mFastScrollRectW)/2; int mFastScrollRectY = mRenderY+mHeaderH + ((fHeight - mFastScrollRectH)*pct)/100; gr_color(mFastScrollRectColor.red, mFastScrollRectColor.green, mFastScrollRectColor.blue, mFastScrollRectColor.alpha); gr_fill(mFastScrollRectX, mFastScrollRectY, mFastScrollRectW, mFastScrollRectH); } mUpdate = 0; // reset clipping gr_noclip(); return 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 int maxScrollDistance = actualItemHeight * SCROLLING_SPEED_LIMIT; 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; } else if (scrollingSpeed < 0) { if (abs(scrollingSpeed) < maxScrollDistance) y_offset += scrollingSpeed; else y_offset -= maxScrollDistance; scrollingSpeed += SCROLLING_SPEED_DECREMENT; } if (abs(scrollingSpeed) < SCROLLING_FLOOR) scrollingSpeed = 0; HandleScrolling(); mUpdate = 1; return 0; } size_t GUIScrollList::HitTestItem(int x, 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 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) { // 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 pct = ((y-mRenderY-mHeaderH)*100)/(mRenderH-mHeaderH); int totalSize = GetItemCount(); int lines = GetDisplayItemCount(); float l = float((totalSize-lines)*pct)/100; if(l + lines >= totalSize) { firstDisplayedItem = totalSize - lines; if (GetDisplayRemainder() != 0) { // There's a partial row displayed, set the scrolling offset so that the last 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; } } else { if (l < 0) l = 0; firstDisplayedItem = l; y_offset = -(l - int(l))*actualItemHeight; if (GetDisplayRemainder() != 0) { // There's a partial row displayed, make sure y_offset doesn't go past the max if (firstDisplayedItem == totalSize - lines - 1 && y_offset < GetDisplayRemainder() - actualItemHeight) y_offset = GetDisplayRemainder() - actualItemHeight; } else if (firstDisplayedItem == totalSize - lines) y_offset = 0; } 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: 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) > SCROLLING_FLOOR) scrollingSpeed *= SCROLLING_MULTIPLIER; else 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; } }