// Aseprite // Copyright (C) 2019-2022 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "app/ui/file_list.h" #include "app/modules/gfx.h" #include "app/thumbnail_generator.h" #include "app/ui/skin/skin_theme.h" #include "base/clamp.h" #include "base/string.h" #include "base/time.h" #include "os/font.h" #include "os/surface.h" #include "ui/ui.h" #include #include #include #define ISEARCH_KEYPRESS_INTERVAL_MSECS 500 namespace app { using namespace app::skin; using namespace gfx; using namespace ui; FileList::FileList() : Widget(kGenericWidget) , m_currentFolder(FileSystemModule::instance()->getRootFileItem()) , m_req_valid(false) , m_selected(nullptr) , m_isearchClock(0) , m_generateThumbnailTimer(200, this) , m_monitoringTimer(50, this) , m_itemToGenerateThumbnail(nullptr) , m_multiselect(false) , m_zoom(1.0) , m_itemsPerRow(0) { setFocusStop(true); setDoubleBuffered(true); m_generateThumbnailTimer.Tick.connect(&FileList::onGenerateThumbnailTick, this); m_monitoringTimer.Tick.connect(&FileList::onMonitoringTick, this); m_monitoringTimer.start(); regenerateList(); } FileList::~FileList() { // Stop timers. m_generateThumbnailTimer.stop(); m_monitoringTimer.stop(); // Stop workers creating thumbnails. ThumbnailGenerator::instance()->stopAllWorkers(); } void FileList::setExtensions(const base::paths& extensions) { m_exts = extensions; // Refresh if (isVisible()) setCurrentFolder(m_currentFolder); } void FileList::setCurrentFolder(IFileItem* folder) { ASSERT(folder != NULL); ASSERT(folder->isBrowsable()); m_currentFolder = folder; m_req_valid = false; m_selected = nullptr; regenerateList(); // As now we are in other folder, we can stop the generation of all // thumbnails. ThumbnailGenerator::instance()->stopAllWorkers(); // select first folder if (!m_list.empty() && m_list.front()->isBrowsable()) selectIndex(0); // Emit "CurrentFolderChanged" event. onCurrentFolderChanged(); invalidate(); View::getView(this)->updateView(); } FileItemList FileList::selectedFileItems() const { ASSERT(m_selectedItems.size() == m_list.size()); FileItemList result; for (int i=0; iparent(); if (parent) { setCurrentFolder(parent); m_selected = folder; deselectedFileItems(); // Make the selected item visible. makeSelectedFileitemVisible(); } } void FileList::setZoom(const double zoom) { m_zoom = base::clamp(zoom, 1.0, 8.0); m_req_valid = false; // if (auto view = View::getView(this)) recalcAllFileItemInfo(); } void FileList::animateToZoom(const double zoom) { m_fromZoom = m_zoom; m_toZoom = zoom; startAnimation(ANI_ZOOM, 10); } bool FileList::onProcessMessage(Message* msg) { switch (msg->type()) { case kMouseDownMessage: captureMouse(); case kMouseMoveMessage: if (hasCapture()) { MouseMessage* mouseMsg = static_cast(msg); IFileItem* oldSelected = m_selected; m_selected = nullptr; // Mouse position in client coordinates gfx::Point mousePos = mouseMsg->position() - bounds().origin(); // rows int i = 0; for (auto begin=m_list.begin(), end=m_list.end(), it=begin; it!=end; ++it, ++i) { IFileItem* fi = *it; ItemInfo info = getFileItemInfo(i); if ((info.bounds.contains(mousePos)) || (isListView() && ((it == begin && mousePos.y < info.bounds.y) || (it == end-1 && mousePos.y >= info.bounds.y2())))) { m_selected = fi; if (m_multiselect && oldSelected != fi && m_selectedItems.size() == m_list.size()) { if (msg->shiftPressed() || msg->ctrlPressed() || msg->cmdPressed()) { m_selectedItems[i] = !m_selectedItems[i]; } else { std::fill(m_selectedItems.begin(), m_selectedItems.end(), false); m_selectedItems[i] = true; } invalidate(); } makeSelectedFileitemVisible(); break; } } if (oldSelected != m_selected) { if (hasThumbnailsPerItem()) generateThumbnailForFileItem(m_selected); else delayThumbnailGenerationForSelectedItem(); invalidate(); // Emit "FileSelected" event. onFileSelected(); } } break; case kMouseUpMessage: if (hasCapture()) { releaseMouse(); } break; case kKeyDownMessage: if (hasFocus()) { KeyMessage* keyMsg = static_cast(msg); KeyScancode scancode = keyMsg->scancode(); int unicodeChar = keyMsg->unicodeChar(); int select = selectedIndex(); View* view = View::getView(this); int bottom = m_list.size(); switch (scancode) { case kKeyUp: if (select >= 0) select -= m_itemsPerRow; else select = 0; break; case kKeyDown: if (select >= 0) select += m_itemsPerRow; else select = 0; break; case kKeyHome: select = 0; break; case kKeyEnd: select = bottom-1; break; case kKeyPageUp: case kKeyPageDown: { int sgn = (scancode == kKeyPageUp) ? -1: 1; gfx::Rect vp = view->viewportBounds(); if (select < 0) select = 0; select += sgn * vp.h / (textHeight()+4*guiscale()); break; } case kKeyLeft: case kKeyRight: { const int delta = (scancode == kKeyLeft ? -1: 1); if (isIconView()) { if (select >= 0) select += delta; else select = 0; } else if (select >= 0) { gfx::Rect vp = view->viewportBounds(); gfx::Point scroll = view->viewScroll(); scroll.x += vp.w/2*delta; view->setViewScroll(scroll); } break; } case kKeyEnter: case kKeyEnterPad: if (m_selected) { if (m_selected->isBrowsable()) { setCurrentFolder(m_selected); return true; } if (m_selected->isFolder()) { // Do nothing (is a folder but not browseable). return true; } else { // Emit "FileAccepted" event. onFileAccepted(); return true; } } else return Widget::onProcessMessage(msg); case kKeyBackspace: goUp(); return true; default: if (unicodeChar == ' ' || (std::tolower(unicodeChar) >= 'a' && std::tolower(unicodeChar) <= 'z') || (unicodeChar >= '0' && unicodeChar <= '9')) { if ((base::current_tick() - m_isearchClock) > ISEARCH_KEYPRESS_INTERVAL_MSECS) m_isearch.clear(); m_isearch.push_back(unicodeChar); int i, chrs = m_isearch.size(); FileItemList::iterator link = m_list.begin() + ((select >= 0) ? select: 0); for (i=std::max(select, 0); idisplayName(), m_isearch, chrs) == 0) { select = i; break; } } m_isearchClock = base::current_tick(); // Go to selectIndex... } else return Widget::onProcessMessage(msg); } if (bottom > 0) selectIndex(base::clamp(select, 0, bottom-1)); return true; } break; case kMouseWheelMessage: { View* view = View::getView(this); if (view) { // Zoom if (msg->ctrlPressed() || // TODO configurable msg->cmdPressed()) { gfx::Point delta = static_cast(msg)->wheelDelta(); const bool precise = static_cast(msg)->preciseWheel(); double dz = delta.x + delta.y; if (precise) { dz /= 1.5; if (dz < -1.0) dz = -1.0; else if (dz > 1.0) dz = 1.0; } setZoom(zoom() - dz); break; } else { gfx::Point scroll = view->viewScroll(); if (static_cast(msg)->preciseWheel()) scroll += static_cast(msg)->wheelDelta(); else scroll += static_cast(msg)->wheelDelta() * 3*(textHeight()+4*guiscale()); view->setViewScroll(scroll); } } break; } case kDoubleClickMessage: if (m_selected) { if (m_selected->isBrowsable()) { setCurrentFolder(m_selected); return true; } else { onFileAccepted(); // Emit "FileAccepted" event. return true; } } break; case kTouchMagnifyMessage: { setZoom(zoom() + 4.0*static_cast(msg)->magnification()); break; } } return Widget::onProcessMessage(msg); } void FileList::onPaint(ui::PaintEvent& ev) { Graphics* g = ev.graphics(); SkinTheme* theme = static_cast(this->theme()); gfx::Rect bounds = clientBounds(); g->fillRect(theme->colors.background(), bounds); // g->fillRect(bgcolor, gfx::Rect(bounds.x, y, bounds.w, itemSize.h)); int i = 0, selectedIndex = -1; for (IFileItem* fi : m_list) { if (m_selected != fi) { paintItem(g, fi, i); } else { selectedIndex = i; } ++i; } // Paint main selected index (so if the filename label is bigger it // will appear over other items). if (m_selected) { ASSERT(selectedIndex >= 0); if (selectedIndex >= 0) paintItem(g, m_selected, selectedIndex); else { // Strange run-time state where the "m_selected" is not in the // list. The previous assert should fail on Debug so this is // here only for Release mode. return; } } // Draw main thumbnail for the selected item when there are no // thumbnails per item. if (isListView() && m_selected && m_selected->getThumbnail()) { gfx::Rect tbounds = mainThumbnailBounds(); tbounds.enlarge(1); g->drawRect(gfx::rgba(0, 0, 0, 64), tbounds); tbounds.shrink(1); os::SurfaceRef thumbnail = m_selected->getThumbnail(); g->drawRgbaSurface(thumbnail.get(), gfx::Rect(0, 0, thumbnail->width(), thumbnail->height()), tbounds); } } void FileList::paintItem(ui::Graphics* g, IFileItem* fi, const int i) { ItemInfo info = getFileItemInfo(i); if ((g->getClipBounds() & info.bounds).isEmpty()) return; SkinTheme* theme = static_cast(this->theme()); const bool evenRow = ((i & 1) == 0); gfx::Rect tbounds = info.thumbnail; gfx::Color bgcolor; gfx::Color fgcolor; if ((!m_multiselect && fi == m_selected) || (m_multiselect && m_selectedItems.size() == m_list.size() && m_selectedItems[i])) { fgcolor = theme->colors.filelistSelectedRowText(); bgcolor = theme->colors.filelistSelectedRowFace(); } else { bgcolor = evenRow ? theme->colors.filelistEvenRowFace(): theme->colors.filelistOddRowFace(); if (fi->isFolder() && !fi->isBrowsable()) fgcolor = theme->colors.filelistDisabledRowText(); else fgcolor = evenRow ? theme->colors.filelistEvenRowText(): theme->colors.filelistOddRowText(); } // Item background g->fillRect(bgcolor, info.bounds); gfx::Rect textBounds = info.text; // Folder icon or thumbnail os::SurfaceRef thumbnail = nullptr; if (fi->isFolder()) { if (isListView()) { thumbnail = theme->parts.folderIconSmall()->bitmapRef(0); tbounds = textBounds; tbounds.x += 2*guiscale(); tbounds.w = tbounds.h; textBounds.x += tbounds.x2(); } else { thumbnail = (m_zoom < 4.0 ? theme->parts.folderIconMedium()->bitmapRef(0): theme->parts.folderIconBig()->bitmapRef(0)); } } else { thumbnail = fi->getThumbnail(); } // item name if (isIconView() && textBounds.w > info.bounds.w) { g->drawAlignedUIText( fi->displayName().c_str(), fgcolor, bgcolor, (textBounds & gfx::Rect(info.bounds).shrink(2*guiscale())), ui::CENTER | ui::TOP | ui::CHARWRAP); } else { g->drawText( fi->displayName().c_str(), fgcolor, bgcolor, gfx::Point(textBounds.x+2*guiscale(), textBounds.y+2*guiscale())); } // Draw thumbnail progress bar double progress = fi->getThumbnailProgress(); if (isIconView() && !thumbnail) { if (progress == 0.0) generateThumbnailForFileItem(fi); } if (!tbounds.isEmpty()) { if (thumbnail) { tbounds = gfx::Rect(0, 0, thumbnail->width(), thumbnail->height()) .fitIn(tbounds); if (!fi->isFolder()) { g->drawRect(gfx::rgba(0, 0, 0, 64), tbounds); tbounds.shrink(1); } g->drawRgbaSurface(thumbnail.get(), gfx::Rect(0, 0, thumbnail->width(), thumbnail->height()), tbounds); } else { tbounds = gfx::Rect(0, 0, 20*guiscale(), 2+4*(8.0-m_zoom)/8.0*guiscale()) .fitIn(tbounds); // Start thumbnail generation for this item generateThumbnailForFileItem(fi); theme->paintProgressBar(g, tbounds, progress); } } } gfx::Rect FileList::mainThumbnailBounds() { gfx::Rect result; if (!m_selected) return result; os::SurfaceRef thumbnail = m_selected->getThumbnail(); if (!thumbnail) return result; ItemInfo info = getFileItemInfo(selectedIndex()); if (!info.thumbnail.isEmpty()) // There is thumbnail per item return result; View* view = View::getView(this); gfx::Rect vp = view->viewportBounds(); int x = vp.x+vp.w - 2*guiscale() - thumbnail->width(); int y = info.bounds.center().y - thumbnail->height()/2 + bounds().y; y = base::clamp(y, vp.y+2*guiscale(), vp.y+vp.h-3*guiscale()-thumbnail->height()); x -= bounds().x; y -= bounds().y; return gfx::Rect(x, y, thumbnail->width(), thumbnail->height()); } void FileList::onSizeHint(SizeHintEvent& ev) { if (!m_req_valid) { gfx::Rect req; // rows for (int i=0; igenerateThumbnail(fi); } if (ThumbnailGenerator::instance()->checkWorkers()) invalidate(); } void FileList::onGenerateThumbnailTick() { m_generateThumbnailTimer.stop(); auto fi = m_itemToGenerateThumbnail; if (fi) generateThumbnailForFileItem(fi); } void FileList::recalcAllFileItemInfo() { View* view = View::getView(this); if (view) view->setScrollableSize(gfx::Size(1, view->bounds().h), false); m_info.resize(m_list.size()); if (m_info.empty()) { m_itemsPerRow = 0; return; } for (int i=0; iupdateView(); if (!view->verticalBar()->isVisible()) { gfx::Rect vp = view->viewportBounds(); for (auto& info : m_info) { info.bounds.w = vp.w; } } } } // Redistribute items in X axis else if (isIconView()) { int maxWidth = 0; int maxTextWidth = 0; for (const auto& info : m_info) { int w = std::min(info.bounds.w, info.thumbnail.w*2); maxWidth = std::max(maxWidth, w); maxTextWidth = std::max(maxTextWidth, info.text.w); } if (maxWidth == 0) return; gfx::Size vp = (view ? view->viewportBounds().size(): size()); int itemsPerRow = vp.w / maxWidth; if (itemsPerRow < 3) itemsPerRow = 3; int itemWidth = vp.w / itemsPerRow; int i = 0; for (int y=0; i itemWidth) { info.bounds.h += info.text.h; info.text.h += info.text.h; } info.text.x = info.bounds.x + info.bounds.w/2 - info.text.w/2; info.text.y += deltay; info.thumbnail.x = info.bounds.x + info.bounds.w/2 - info.thumbnail.w/2; info.thumbnail.y += deltay; h = info.bounds.h; } if (h) y += h; else break; } m_itemsPerRow = itemsPerRow; } invalidate(); if (view) { view->updateView(); makeSelectedFileitemVisible(); } } FileList::ItemInfo FileList::calcFileItemInfo(int i) const { ASSERT(i >= 0 && i < int(m_list.size())); const bool withThumbnails = hasThumbnailsPerItem(); IFileItem* fi = m_list[i]; int len = 0; if (fi->isFolder() && isListView()) { SkinTheme* theme = static_cast(this->theme()); len += theme->parts.folderIconSmall()->bitmap(0)->width() + 2*guiscale(); } len += font()->textLength(fi->displayName().c_str()); int textHeight = this->textHeight() + 4*guiscale(); int rowHeight = textHeight + (withThumbnails ? 8*m_zoom+2*guiscale(): 0); ItemInfo info; info.text = gfx::Rect(0, 0+rowHeight*i, len+4*guiscale(), textHeight); if (withThumbnails) { info.thumbnail = gfx::Rect(0, info.text.y, 8*m_zoom*guiscale(), 8*m_zoom*guiscale()); info.text.y += info.thumbnail.h + 2*guiscale(); } info.bounds = info.text | info.thumbnail; if (withThumbnails) { info.text.x = info.bounds.x + info.bounds.w/2 - info.text.w/2; info.thumbnail.x = info.bounds.x + info.bounds.w/2 - info.thumbnail.w/2; } else { info.bounds.x = 0; if (View* view = View::getView(this)) info.bounds.w = view->viewportBounds().w; else info.bounds.w = bounds().w; } return info; } FileList::ItemInfo FileList::getFileItemInfo(int i) const { ASSERT(i >= 0 && i < int(m_info.size())); if (i >= 0 && i < int(m_info.size())) return m_info[i]; else return ItemInfo(); } void FileList::makeSelectedFileitemVisible() { int i = selectedIndex(); if (i < 0) return; View* view = View::getView(this); gfx::Rect vp = view->viewportBounds(); gfx::Point scroll = view->viewScroll(); ItemInfo info = getFileItemInfo(i); if (info.bounds.x+bounds().x <= vp.x) scroll.x = info.bounds.x; else if (info.bounds.x+bounds().x > vp.x2() - info.bounds.w) scroll.x = info.bounds.x - vp.w + info.bounds.w; if (info.bounds.y+bounds().y < vp.y) scroll.y = info.bounds.y; else if (info.bounds.y+bounds().y > vp.y2() - info.bounds.h) scroll.y = info.bounds.y - vp.h + info.bounds.h; view->setViewScroll(scroll); } void FileList::regenerateList() { // get the children of the current folder m_list = m_currentFolder->children(); // filter the list by the available extensions if (!m_exts.empty()) { for (FileItemList::iterator it=m_list.begin(); it!=m_list.end(); ) { IFileItem* fileitem = *it; if (fileitem->isHidden()) it = m_list.erase(it); else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) { it = m_list.erase(it); } else ++it; } } recalcAllFileItemInfo(); if (m_multiselect && !m_list.empty()) { m_selectedItems.resize(m_list.size()); deselectedFileItems(); } else m_selectedItems.clear(); } int FileList::selectedIndex() const { for (auto it = m_list.begin(), end = m_list.end(); it != end; ++it) { if (*it == m_selected) return it - m_list.begin(); } return -1; } void FileList::selectIndex(int index) { IFileItem* oldSelected = m_selected; m_selected = m_list.at(index); deselectedFileItems(); if (oldSelected != m_selected) { makeSelectedFileitemVisible(); invalidate(); // Emit "FileSelected" event. onFileSelected(); } delayThumbnailGenerationForSelectedItem(); } void FileList::generateThumbnailForFileItem(IFileItem* fi) { if (fi && animation() == ANI_NONE) { auto it = std::find(m_generateThumbnailsForTheseItems.begin(), m_generateThumbnailsForTheseItems.end(), fi); if (it != m_generateThumbnailsForTheseItems.end()) m_generateThumbnailsForTheseItems.erase(it); m_generateThumbnailsForTheseItems.push_front(fi); } } void FileList::delayThumbnailGenerationForSelectedItem() { if (m_selected && !m_selected->isFolder() && !m_selected->getThumbnail()) { m_itemToGenerateThumbnail = m_selected; m_generateThumbnailTimer.start(); } } void FileList::onAnimationStop(int animation) { if (animation == ANI_ZOOM) setZoom(m_toZoom); } void FileList::onAnimationFrame() { if (animation() == ANI_ZOOM) setZoom(inbetween(m_fromZoom, m_toZoom, animationTime())); } } // namespace app