feat: show full path bar in file browser (#1411)
 ## Summary Adds a full path display at the bottom of the file browser with a separator line matching the header style. Path uses the small font, left-truncates to always show the deepest folder when path is too long. Toggleable via Settings > System > Show Full Path (default on). ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_ --------- Co-authored-by: Patryk Radtke <patryk@Patryks-MacBook-Pro.local>
This commit is contained in:
@@ -164,7 +164,8 @@ void FileBrowserActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
const int pathReserved = renderer.getLineHeight(SMALL_FONT_ID) + UITheme::getInstance().getMetrics().verticalSpacing;
|
||||||
|
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false, pathReserved);
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (files.empty()) return;
|
if (files.empty()) return;
|
||||||
@@ -294,8 +295,11 @@ void FileBrowserActivity::render(RenderLock&&) {
|
|||||||
std::string folderName = (basepath == "/") ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1);
|
std::string folderName = (basepath == "/") ? tr(STR_SD_CARD) : basepath.substr(basepath.rfind('/') + 1);
|
||||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName.c_str());
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName.c_str());
|
||||||
|
|
||||||
|
const int pathLineHeight = renderer.getLineHeight(SMALL_FONT_ID);
|
||||||
|
const int pathReserved = pathLineHeight + metrics.verticalSpacing;
|
||||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
const int contentHeight =
|
||||||
|
pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing - pathReserved;
|
||||||
if (files.empty()) {
|
if (files.empty()) {
|
||||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_FILES_FOUND));
|
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, tr(STR_NO_FILES_FOUND));
|
||||||
} else {
|
} else {
|
||||||
@@ -306,6 +310,33 @@ void FileBrowserActivity::render(RenderLock&&) {
|
|||||||
[this](int index) { return getFileExtension(files[index]); }, false);
|
[this](int index) { return getFileExtension(files[index]); }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Full path display
|
||||||
|
{
|
||||||
|
const int pathY = pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - pathLineHeight;
|
||||||
|
const int separatorY = pathY - metrics.verticalSpacing / 2;
|
||||||
|
renderer.drawLine(0, separatorY, pageWidth - 1, separatorY, 3, true);
|
||||||
|
const int pathMaxWidth = pageWidth - metrics.contentSidePadding * 2;
|
||||||
|
// Left-truncate so the deepest directory is always visible
|
||||||
|
const char* pathStr = basepath.c_str();
|
||||||
|
const char* pathDisplay = pathStr;
|
||||||
|
char leftTruncBuf[256];
|
||||||
|
if (renderer.getTextWidth(SMALL_FONT_ID, pathStr) > pathMaxWidth) {
|
||||||
|
const char ellipsis[] = "\xe2\x80\xa6"; // UTF-8 ellipsis (…)
|
||||||
|
const int ellipsisWidth = renderer.getTextWidth(SMALL_FONT_ID, ellipsis);
|
||||||
|
const int available = pathMaxWidth - ellipsisWidth;
|
||||||
|
// Walk forward from the start until the suffix fits, skipping UTF-8 continuation bytes
|
||||||
|
const char* p = pathStr;
|
||||||
|
while (*p) {
|
||||||
|
if (renderer.getTextWidth(SMALL_FONT_ID, p) <= available) break;
|
||||||
|
++p;
|
||||||
|
while (*p && (static_cast<unsigned char>(*p) & 0xC0) == 0x80) ++p;
|
||||||
|
}
|
||||||
|
snprintf(leftTruncBuf, sizeof(leftTruncBuf), "%s%s", ellipsis, p);
|
||||||
|
pathDisplay = leftTruncBuf;
|
||||||
|
}
|
||||||
|
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, pathY, pathDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
// Help text
|
// Help text
|
||||||
const auto labels =
|
const auto labels =
|
||||||
mappedInput.mapLabels(basepath == "/" ? tr(STR_HOME) : tr(STR_BACK), files.empty() ? "" : tr(STR_OPEN),
|
mappedInput.mapLabels(basepath == "/" ? tr(STR_HOME) : tr(STR_BACK), files.empty() ? "" : tr(STR_OPEN),
|
||||||
@@ -319,4 +350,4 @@ size_t FileBrowserActivity::findEntry(const std::string& name) const {
|
|||||||
for (size_t i = 0; i < files.size(); i++)
|
for (size_t i = 0; i < files.size(); i++)
|
||||||
if (files[i] == name) return i;
|
if (files[i] == name) return i;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
||||||
bool hasSubtitle) {
|
bool hasSubtitle, int extraReservedHeight) {
|
||||||
const ThemeMetrics& metrics = UITheme::getInstance().getMetrics();
|
const ThemeMetrics& metrics = UITheme::getInstance().getMetrics();
|
||||||
int reservedHeight = metrics.topPadding;
|
int reservedHeight = metrics.topPadding;
|
||||||
if (hasHeader) {
|
if (hasHeader) {
|
||||||
@@ -61,7 +61,7 @@ int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader
|
|||||||
if (hasButtonHints) {
|
if (hasButtonHints) {
|
||||||
reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight;
|
reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight;
|
||||||
}
|
}
|
||||||
const int availableHeight = renderer.getScreenHeight() - reservedHeight;
|
const int availableHeight = renderer.getScreenHeight() - reservedHeight - extraReservedHeight;
|
||||||
int rowHeight = hasSubtitle ? metrics.listWithSubtitleRowHeight : metrics.listRowHeight;
|
int rowHeight = hasSubtitle ? metrics.listWithSubtitleRowHeight : metrics.listRowHeight;
|
||||||
return availableHeight / rowHeight;
|
return availableHeight / rowHeight;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class UITheme {
|
|||||||
void reload();
|
void reload();
|
||||||
void setTheme(CrossPointSettings::UI_THEME type);
|
void setTheme(CrossPointSettings::UI_THEME type);
|
||||||
static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
||||||
bool hasSubtitle);
|
bool hasSubtitle, int extraReservedHeight = 0);
|
||||||
static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight);
|
static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight);
|
||||||
static UIIcon getFileIcon(const std::string& filename);
|
static UIIcon getFileIcon(const std::string& filename);
|
||||||
static int getStatusBarHeight();
|
static int getStatusBarHeight();
|
||||||
|
|||||||
Reference in New Issue
Block a user