mirror of https://github.com/aseprite/aseprite.git
996 lines
29 KiB
C++
996 lines
29 KiB
C++
// Aseprite
|
|
// Copyright (C) 2019-2024 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_selector.h"
|
|
|
|
#include "app/app.h"
|
|
#include "app/console.h"
|
|
#include "app/file/file.h"
|
|
#include "app/i18n/strings.h"
|
|
#include "app/modules/gfx.h"
|
|
#include "app/modules/gui.h"
|
|
#include "app/pref/preferences.h"
|
|
#include "app/recent_files.h"
|
|
#include "app/ui/file_list.h"
|
|
#include "app/ui/file_list_view.h"
|
|
#include "app/ui/separator_in_view.h"
|
|
#include "app/ui/skin/skin_theme.h"
|
|
#include "app/widget_loader.h"
|
|
#include "base/convert_to.h"
|
|
#include "base/fs.h"
|
|
#include "base/paths.h"
|
|
#include "base/string.h"
|
|
#include "ui/ui.h"
|
|
|
|
#include "new_folder_window.xml.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cerrno>
|
|
#include <iterator>
|
|
#include <list>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#ifndef MAX_PATH
|
|
#define MAX_PATH 4096 // TODO this is needed for Linux, is it correct?
|
|
#endif
|
|
|
|
#define FILESEL_TRACE(...) // TRACE
|
|
|
|
namespace app {
|
|
|
|
using namespace app::skin;
|
|
using namespace ui;
|
|
|
|
namespace {
|
|
|
|
const char* kConfigSection = "FileSelector";
|
|
|
|
template<class Container>
|
|
class NullableIterator {
|
|
public:
|
|
typedef typename Container::iterator iterator;
|
|
|
|
NullableIterator() : m_isNull(true) {}
|
|
|
|
bool is_null() const { return m_isNull; }
|
|
bool is_valid() const { return !m_isNull; }
|
|
bool exists() const { return (is_valid() && (*m_iterator)->isExistent()); }
|
|
|
|
iterator get()
|
|
{
|
|
ASSERT(!m_isNull);
|
|
return m_iterator;
|
|
}
|
|
|
|
void reset() { m_isNull = true; }
|
|
|
|
void set(const iterator& it)
|
|
{
|
|
m_isNull = false;
|
|
m_iterator = it;
|
|
}
|
|
|
|
private:
|
|
bool m_isNull;
|
|
iterator m_iterator;
|
|
};
|
|
|
|
// Variables used only to maintain the history of navigation.
|
|
FileItemList navigation_history; // Set of FileItems navigated
|
|
NullableIterator<FileItemList> navigation_position; // Current position in the navigation history
|
|
|
|
// This map acts like a temporal customization by the user when he/she
|
|
// wants to open files. The key (first) is the real "allExtensions"
|
|
// parameter given to the FileSelector::show() function where each
|
|
// extension is concatenated with each other in one string separated
|
|
// by ','. The value (second) is the selected/preferred extension by
|
|
// the user. It's used only in FileSelector::Open type of dialogs.
|
|
std::map<std::string, base::paths> preferred_open_extensions;
|
|
|
|
void adjust_navigation_history(IFileItem* item)
|
|
{
|
|
auto it = navigation_history.begin();
|
|
const bool valid = navigation_position.is_valid();
|
|
int pos = (valid ? int(navigation_position.get() - it) : 0);
|
|
|
|
FILESEL_TRACE("FILESEL: Removed item '%s' detected (%p)\n", item->fileName().c_str(), item);
|
|
if (valid) {
|
|
FILESEL_TRACE("FILESEL: Old navigation pos [%d] = %s\n",
|
|
pos,
|
|
(*navigation_position.get())->fileName().c_str());
|
|
}
|
|
|
|
while (true) {
|
|
it = std::find(it, navigation_history.end(), item);
|
|
if (it == navigation_history.end())
|
|
break;
|
|
|
|
FILESEL_TRACE("FILESEL: Erase navigation pos [%d] = %s\n", pos, (*it)->fileName().c_str());
|
|
|
|
if (pos >= it - navigation_history.begin())
|
|
--pos;
|
|
|
|
it = navigation_history.erase(it);
|
|
}
|
|
|
|
if (valid && !navigation_history.empty()) {
|
|
pos = std::clamp(pos, 0, (int)navigation_history.size() - 1);
|
|
navigation_position.set(navigation_history.begin() + pos);
|
|
|
|
FILESEL_TRACE("FILESEL: New navigation pos [%d] = %s\n",
|
|
pos,
|
|
(*navigation_position.get())->fileName().c_str());
|
|
}
|
|
else {
|
|
navigation_position.reset();
|
|
FILESEL_TRACE("FILESEL: Without new navigation pos\n");
|
|
}
|
|
}
|
|
|
|
std::string merge_paths(const base::paths& paths)
|
|
{
|
|
std::string k;
|
|
for (const auto& p : paths) {
|
|
if (!k.empty())
|
|
k.push_back(',');
|
|
k += p;
|
|
}
|
|
return k;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
class FileSelector::CustomFileNameEntry : public ComboBox {
|
|
public:
|
|
CustomFileNameEntry() : m_fileList(nullptr)
|
|
{
|
|
setEditable(true);
|
|
getEntryWidget()->Change.connect(&CustomFileNameEntry::onEntryChange, this);
|
|
}
|
|
|
|
void setAssociatedFileList(FileList* fileList) { m_fileList = fileList; }
|
|
|
|
protected:
|
|
void onEntryChange()
|
|
{
|
|
// Deselect multiple-selection
|
|
if (m_fileList->multipleSelection())
|
|
m_fileList->deselectedFileItems();
|
|
|
|
deleteAllItems();
|
|
|
|
// String to be autocompleted
|
|
std::string left_part = getEntryWidget()->text();
|
|
closeListBox();
|
|
|
|
if (left_part.empty())
|
|
return;
|
|
|
|
for (const IFileItem* child : m_fileList->fileList()) {
|
|
std::string child_name = child->displayName();
|
|
std::string::const_iterator it1, it2;
|
|
|
|
for (it1 = child_name.begin(), it2 = left_part.begin();
|
|
it1 != child_name.end() && it2 != left_part.end();
|
|
++it1, ++it2) {
|
|
if (std::tolower(*it1) != std::tolower(*it2))
|
|
break;
|
|
}
|
|
|
|
// Is the pattern (left_part) in the child_name's beginning?
|
|
if (it1 != child_name.end() && it2 == left_part.end())
|
|
addItem(child_name);
|
|
}
|
|
|
|
if (getItemCount() > 0)
|
|
openListBox();
|
|
}
|
|
|
|
private:
|
|
FileList* m_fileList;
|
|
};
|
|
|
|
class FileSelector::CustomFileNameItem : public ListItem {
|
|
public:
|
|
CustomFileNameItem(const char* text, IFileItem* fileItem) : ListItem(text), m_fileItem(fileItem)
|
|
{
|
|
}
|
|
|
|
IFileItem* getFileItem() { return m_fileItem; }
|
|
|
|
private:
|
|
IFileItem* m_fileItem;
|
|
};
|
|
|
|
class FileSelector::CustomFolderNameItem : public ListItem {
|
|
public:
|
|
CustomFolderNameItem(const char* text) : ListItem(text) {}
|
|
};
|
|
|
|
class FileSelector::CustomFileExtensionItem : public ListItem {
|
|
public:
|
|
CustomFileExtensionItem(const std::string& text, const base::paths& exts)
|
|
: ListItem(text)
|
|
, m_exts(exts)
|
|
{
|
|
}
|
|
const base::paths& extensions() const { return m_exts; }
|
|
|
|
private:
|
|
base::paths m_exts;
|
|
};
|
|
|
|
// We have this dummy/hidden widget only to handle special navigation
|
|
// with arrow keys. In the past this code was in the same FileSelector
|
|
// itself, but there were problems adding that window as a message
|
|
// filter. Mainly there is a special combination of widgets
|
|
// (comboboxes) that need to filter Esc key (e.g. to close the
|
|
// combobox popup). And we cannot pre-add a filter that send that key
|
|
// to the Manager before it's processed by the combobox filter.
|
|
class FileSelector::ArrowNavigator : public Widget {
|
|
public:
|
|
ArrowNavigator(FileSelector* filesel) : Widget(kGenericWidget), m_filesel(filesel)
|
|
{
|
|
setVisible(false);
|
|
}
|
|
|
|
protected:
|
|
bool onProcessMessage(ui::Message* msg) override
|
|
{
|
|
switch (msg->type()) {
|
|
case kOpenMessage: manager()->addMessageFilter(kKeyDownMessage, this); break;
|
|
case kCloseMessage: manager()->removeMessageFilter(kKeyDownMessage, this); break;
|
|
case kKeyDownMessage: {
|
|
KeyMessage* keyMsg = static_cast<KeyMessage*>(msg);
|
|
KeyScancode scancode = keyMsg->scancode();
|
|
|
|
#ifdef __APPLE__
|
|
int unicode = keyMsg->unicodeChar();
|
|
bool up = (msg->cmdPressed() && scancode == kKeyUp);
|
|
bool enter = (msg->cmdPressed() && scancode == kKeyDown);
|
|
bool back = (msg->cmdPressed() && (unicode == '[' || scancode == kKeyOpenbrace));
|
|
bool forward = (msg->cmdPressed() && (unicode == ']' || scancode == kKeyClosebrace));
|
|
#else
|
|
bool up = (msg->altPressed() && scancode == kKeyUp);
|
|
bool enter = (msg->altPressed() && scancode == kKeyDown);
|
|
bool back = (msg->altPressed() && scancode == kKeyLeft);
|
|
bool forward = (msg->altPressed() && scancode == kKeyRight);
|
|
#endif
|
|
bool refresh = (scancode == kKeyF5 || (msg->ctrlPressed() && scancode == kKeyR) ||
|
|
(msg->cmdPressed() && scancode == kKeyR));
|
|
|
|
if (up) {
|
|
m_filesel->goUp();
|
|
return true;
|
|
}
|
|
if (enter) {
|
|
m_filesel->goInsideFolder();
|
|
return true;
|
|
}
|
|
if (back) {
|
|
m_filesel->goBack();
|
|
return true;
|
|
}
|
|
if (forward) {
|
|
m_filesel->goForward();
|
|
return true;
|
|
}
|
|
if (refresh) {
|
|
m_filesel->refreshCurrentFolder();
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
return Widget::onProcessMessage(msg);
|
|
}
|
|
|
|
private:
|
|
FileSelector* m_filesel;
|
|
};
|
|
|
|
FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLocked(false)
|
|
{
|
|
addChild(new ArrowNavigator(this));
|
|
|
|
m_fileName = new CustomFileNameEntry;
|
|
m_fileName->setFocusMagnet(true);
|
|
fileNamePlaceholder()->addChild(m_fileName);
|
|
|
|
goBackButton()->setFocusStop(false);
|
|
goForwardButton()->setFocusStop(false);
|
|
goUpButton()->setFocusStop(false);
|
|
refreshButton()->setFocusStop(false);
|
|
newFolderButton()->setFocusStop(false);
|
|
viewType()->setFocusStop(false);
|
|
for (auto child : viewType()->children())
|
|
child->setFocusStop(false);
|
|
|
|
showHiddenCheck()->setSelected(Preferences::instance().fileSelector.showHidden());
|
|
m_fileList = new FileList();
|
|
m_fileList->setId("fileview");
|
|
m_fileName->setAssociatedFileList(m_fileList);
|
|
m_fileList->setZoom(Preferences::instance().fileSelector.zoom());
|
|
|
|
m_fileView = new FileListView();
|
|
m_fileView->attachToView(m_fileList);
|
|
m_fileView->setExpansive(true);
|
|
fileViewPlaceholder()->addChild(m_fileView);
|
|
|
|
goBackButton()->Click.connect([this] { onGoBack(); });
|
|
goForwardButton()->Click.connect([this] { onGoForward(); });
|
|
goUpButton()->Click.connect([this] { onGoUp(); });
|
|
refreshButton()->Click.connect([this] { onRefreshFolder(); });
|
|
newFolderButton()->Click.connect([this] { onNewFolder(); });
|
|
viewType()->ItemChange.connect([this] { onChangeViewType(); });
|
|
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
|
|
fileType()->Change.connect([this] { onFileTypeChange(); });
|
|
showHiddenCheck()->Click.connect([this] {
|
|
Preferences::instance().fileSelector.showHidden(showHiddenCheck()->isSelected());
|
|
m_fileList->setShowHidden(showHiddenCheck()->isSelected());
|
|
});
|
|
m_fileList->FileSelected.connect([this] { onFileListFileSelected(); });
|
|
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
|
|
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });
|
|
}
|
|
|
|
void FileSelector::setDefaultExtension(const std::string& extension)
|
|
{
|
|
m_defExtension = extension;
|
|
}
|
|
|
|
FileSelector::~FileSelector()
|
|
{
|
|
}
|
|
|
|
void FileSelector::goBack()
|
|
{
|
|
onGoBack();
|
|
}
|
|
|
|
void FileSelector::goForward()
|
|
{
|
|
onGoForward();
|
|
}
|
|
|
|
void FileSelector::goUp()
|
|
{
|
|
onGoUp();
|
|
}
|
|
|
|
void FileSelector::goInsideFolder()
|
|
{
|
|
if (m_fileList->selectedFileItem() && m_fileList->selectedFileItem()->isBrowsable()) {
|
|
m_fileList->setCurrentFolder(m_fileList->selectedFileItem());
|
|
}
|
|
}
|
|
|
|
void FileSelector::refreshCurrentFolder()
|
|
{
|
|
onRefreshFolder();
|
|
}
|
|
|
|
bool FileSelector::show(const std::string& title,
|
|
const std::string& initialPath,
|
|
const base::paths& allExtensions,
|
|
base::paths& output)
|
|
{
|
|
FileSystemModule* fs = FileSystemModule::instance();
|
|
LockFS lock(fs);
|
|
|
|
// Connection used to remove items from the navigation history that
|
|
// are not found in the file system anymore.
|
|
obs::scoped_connection conn = fs->ItemRemoved.connect(&adjust_navigation_history);
|
|
|
|
fs->refresh();
|
|
|
|
// We have to find where the user should begin to browse files
|
|
std::string start_folder_path = base::get_file_path(
|
|
get_initial_path_to_select_filename(initialPath));
|
|
|
|
IFileItem* start_folder = fs->getFileItemFromPath(start_folder_path);
|
|
if (!start_folder) {
|
|
// If the directory doesn't exist anymore, get the default FS item
|
|
// (root item, or user folder).
|
|
start_folder = fs->getFileItemFromPath(std::string());
|
|
}
|
|
FILESEL_TRACE("FILESEL: Start folder '%s' (%p)\n", start_folder_path.c_str(), start_folder);
|
|
|
|
{
|
|
const gfx::Size workareaSize = ui::Manager::getDefault()->display()->workareaSizeUIScale();
|
|
setMinSize(workareaSize * 9 / 10);
|
|
}
|
|
|
|
remapWindow();
|
|
centerWindow();
|
|
// The minimum size is large, so do not limit the minimum loaded size
|
|
load_window_pos(this, kConfigSection, false);
|
|
|
|
// Change the file formats/extensions to be shown
|
|
std::string initialExtension = base::get_file_extension(initialPath);
|
|
base::paths exts;
|
|
if (m_type == FileSelectorType::Open || m_type == FileSelectorType::OpenMultiple) {
|
|
std::string k = merge_paths(allExtensions);
|
|
auto it = preferred_open_extensions.find(k);
|
|
if (it == preferred_open_extensions.end())
|
|
exts = allExtensions;
|
|
else
|
|
exts = preferred_open_extensions[k];
|
|
}
|
|
else {
|
|
ASSERT(m_type == FileSelectorType::Save);
|
|
if (!initialExtension.empty())
|
|
exts = base::paths{ initialExtension };
|
|
else
|
|
exts = allExtensions;
|
|
}
|
|
m_fileList->setMultipleSelection(m_type == FileSelectorType::OpenMultiple);
|
|
m_fileList->setExtensions(exts);
|
|
if (start_folder)
|
|
m_fileList->setCurrentFolder(start_folder);
|
|
|
|
// current location
|
|
navigation_position.reset();
|
|
addInNavigationHistory(m_fileList->currentFolder());
|
|
|
|
// fill the location combo-box
|
|
updateLocation();
|
|
updateNavigationButtons();
|
|
|
|
// fill file-type combo-box
|
|
fileType()->deleteAllItems();
|
|
|
|
// Get the default extension from the given initial file name
|
|
if (m_defExtension.empty())
|
|
m_defExtension = initialExtension;
|
|
|
|
// File type for all formats
|
|
fileType()->addItem(
|
|
new CustomFileExtensionItem(Strings::file_selector_all_formats(), allExtensions));
|
|
|
|
// One file type for each supported image format
|
|
for (const auto& e : allExtensions) {
|
|
// If the default extension is empty, use the first filter
|
|
if (m_defExtension.empty())
|
|
m_defExtension = e;
|
|
|
|
fileType()->addItem(new CustomFileExtensionItem(e + " files", base::paths{ e }));
|
|
}
|
|
// All files
|
|
fileType()->addItem(new CustomFileExtensionItem(Strings::file_selector_all_files(),
|
|
base::paths())); // Empty extensions means "*.*"
|
|
|
|
// file name entry field
|
|
m_fileName->setValue(base::get_file_name(initialPath).c_str());
|
|
m_fileName->getEntryWidget()->selectText(0, -1);
|
|
|
|
for (Widget* wItem : *fileType()) {
|
|
auto item = dynamic_cast<CustomFileExtensionItem*>(wItem);
|
|
ASSERT(item);
|
|
if (item && item->extensions() == exts) {
|
|
fileType()->setSelectedItem(item);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// setup the title of the window
|
|
setText(title.c_str());
|
|
|
|
// get the ok-button
|
|
Widget* ok = this->findChild("ok");
|
|
|
|
// update the view
|
|
View::getView(m_fileList)->updateView();
|
|
|
|
// TODO this loop needs a complete refactor
|
|
// Open the window and run... the user press ok?
|
|
again:
|
|
openWindowInForeground();
|
|
if (closer() == ok || closer() == m_fileList) {
|
|
IFileItem* folder = m_fileList->currentFolder();
|
|
ASSERT(folder);
|
|
|
|
// File name in the text entry field/combobox
|
|
std::string fn = m_fileName->getValue();
|
|
std::string buf;
|
|
IFileItem* enter_folder = nullptr;
|
|
|
|
// up a level?
|
|
if (fn == "..") {
|
|
enter_folder = folder->parent();
|
|
if (!enter_folder)
|
|
enter_folder = folder;
|
|
}
|
|
else if (fn.empty()) {
|
|
IFileItem* selected = m_fileList->selectedFileItem();
|
|
if (selected && selected->isBrowsable())
|
|
enter_folder = selected;
|
|
else if (m_type != FileSelectorType::OpenMultiple ||
|
|
m_fileList->selectedFileItems().empty()) {
|
|
// Show the window again
|
|
setVisible(true);
|
|
goto again;
|
|
}
|
|
}
|
|
else {
|
|
// check if the user specified in "fn" a item of "fileview"
|
|
const FileItemList& children = m_fileList->fileList();
|
|
|
|
std::string fn2 = fn;
|
|
#ifdef _WIN32
|
|
fn2 = base::string_to_lower(fn2);
|
|
#endif
|
|
|
|
for (IFileItem* child : children) {
|
|
std::string child_name = child->displayName();
|
|
#ifdef _WIN32
|
|
child_name = base::string_to_lower(child_name);
|
|
#endif
|
|
if (child_name == fn2) {
|
|
enter_folder = child;
|
|
buf = enter_folder->fileName();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!enter_folder) {
|
|
// does the file-name entry have separators?
|
|
if (base::is_path_separator(*fn.begin())) { // absolute path (UNIX style)
|
|
#ifdef _WIN32
|
|
// get the drive of the current folder
|
|
std::string drive = folder->fileName();
|
|
if (drive.size() >= 2 && drive[1] == ':') {
|
|
buf += drive[0];
|
|
buf += ':';
|
|
buf += fn;
|
|
}
|
|
else
|
|
buf = base::join_path("C:", fn);
|
|
#else
|
|
buf = fn;
|
|
#endif
|
|
}
|
|
#ifdef _WIN32
|
|
// does the file-name entry have colon?
|
|
else if (fn.find(':') != std::string::npos) { // absolute path on Windows
|
|
if (fn.size() == 2 && fn[1] == ':') {
|
|
buf = base::join_path(fn, "");
|
|
}
|
|
else {
|
|
buf = fn;
|
|
}
|
|
}
|
|
#endif
|
|
else {
|
|
buf = folder->fileName();
|
|
buf = base::join_path(buf, fn);
|
|
}
|
|
buf = base::fix_path_separators(buf);
|
|
|
|
// we can check if 'buf' is a folder, so we have to enter in it
|
|
enter_folder = fs->getFileItemFromPath(buf);
|
|
}
|
|
}
|
|
|
|
// did we find a folder to enter?
|
|
if (enter_folder && enter_folder->isFolder() && enter_folder->isBrowsable()) {
|
|
// enter in the folder that was specified in the 'm_fileName'
|
|
m_fileList->setCurrentFolder(enter_folder);
|
|
|
|
// clear the text of the entry widget
|
|
m_fileName->setValue("");
|
|
|
|
// show the window again
|
|
setVisible(true);
|
|
goto again;
|
|
}
|
|
// else file-name specified in the entry is really a file to open...
|
|
|
|
#ifdef _WIN32
|
|
// Check that the filename doesn't contain ilegal characters.
|
|
// Linux allows all kind of characters, only '/' is disallowed,
|
|
// but in that case we consider that a full path was entered in
|
|
// the filename and we can enter to the full path folder.
|
|
if (!enter_folder) {
|
|
const bool has_invalid_char =
|
|
(fn.find(':') != std::string::npos || fn.find('*') != std::string::npos ||
|
|
fn.find('?') != std::string::npos || fn.find('\"') != std::string::npos ||
|
|
fn.find('<') != std::string::npos || fn.find('>') != std::string::npos ||
|
|
fn.find('|') != std::string::npos);
|
|
if (has_invalid_char) {
|
|
const char* invalid_chars = ": * ? \" < > |";
|
|
|
|
ui::Alert::show(Strings::alerts_invalid_chars_in_filename(invalid_chars));
|
|
|
|
// show the window again
|
|
setVisible(true);
|
|
goto again;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Does it not have extension? ...we should add the extension
|
|
// selected in the filetype combo-box
|
|
if (m_type == FileSelectorType::Save && !buf.empty() && base::get_file_extension(buf).empty()) {
|
|
buf += '.';
|
|
buf += getSelectedExtension();
|
|
}
|
|
|
|
if (m_type == FileSelectorType::Save && base::is_file(buf)) {
|
|
int ret = Alert::show(Strings::alerts_overwrite_existent_file(base::get_file_name(buf)));
|
|
if (ret == 2) {
|
|
setVisible(true);
|
|
goto again;
|
|
}
|
|
else if (ret == 1) {
|
|
// Check for read-only attribute
|
|
if (base::has_readonly_attr(buf)) {
|
|
ui::Alert::show(Strings::alerts_cannot_save_in_read_only_file());
|
|
|
|
setVisible(true);
|
|
goto again;
|
|
}
|
|
}
|
|
// Cancel
|
|
else if (ret != 1) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Put in output the selected filenames
|
|
if (!buf.empty())
|
|
output.push_back(buf);
|
|
else if (m_type == FileSelectorType::OpenMultiple) {
|
|
for (IFileItem* fi : m_fileList->selectedFileItems())
|
|
output.push_back(fi->fileName());
|
|
}
|
|
|
|
// save the path in the configuration file
|
|
std::string lastpath = folder->keyName();
|
|
set_current_dir_for_file_selector(lastpath);
|
|
}
|
|
Preferences::instance().fileSelector.zoom(m_fileList->zoom());
|
|
|
|
return (!output.empty());
|
|
}
|
|
|
|
void FileSelector::onOpen(Event& ev)
|
|
{
|
|
app::gen::FileSelector::onOpen(ev);
|
|
onRefreshFolder();
|
|
}
|
|
|
|
bool FileSelector::onProcessMessage(ui::Message* msg)
|
|
{
|
|
switch (msg->type()) {
|
|
case kCloseMessage: save_window_pos(this, kConfigSection); break;
|
|
}
|
|
return app::gen::FileSelector::onProcessMessage(msg);
|
|
}
|
|
|
|
// Updates the content of the combo-box that shows the current
|
|
// location in the file-system.
|
|
void FileSelector::updateLocation()
|
|
{
|
|
IFileItem* currentFolder = m_fileList->currentFolder();
|
|
IFileItem* fileItem = currentFolder;
|
|
std::list<IFileItem*> locations;
|
|
int selected_index = -1;
|
|
|
|
while (fileItem) {
|
|
locations.push_front(fileItem);
|
|
fileItem = fileItem->parent();
|
|
}
|
|
|
|
// Clear all the items from the combo-box
|
|
location()->deleteAllItems();
|
|
|
|
// Add item by item (from root to the specific current folder)
|
|
int level = 0;
|
|
for (auto it = locations.begin(), end = locations.end(); it != end; ++it) {
|
|
fileItem = *it;
|
|
|
|
// Indentation
|
|
std::string buf;
|
|
for (int c = 0; c < level; ++c)
|
|
buf += " ";
|
|
|
|
// Location name
|
|
buf += fileItem->displayName();
|
|
|
|
// Add the new location to the combo-box
|
|
location()->addItem(new CustomFileNameItem(buf.c_str(), fileItem));
|
|
|
|
if (fileItem == currentFolder)
|
|
selected_index = level;
|
|
|
|
level++;
|
|
}
|
|
|
|
// Add paths from recent files list
|
|
auto recent = App::instance()->recentFiles();
|
|
if (!recent->pinnedFolders().empty()) {
|
|
auto sep = new SeparatorInView(Strings::file_selector_pinned_folders(), HORIZONTAL);
|
|
sep->setMinSize(gfx::Size(0, sep->sizeHint().h * 2));
|
|
location()->addItem(sep);
|
|
for (const auto& fn : recent->pinnedFolders())
|
|
location()->addItem(new CustomFolderNameItem(fn.c_str()));
|
|
}
|
|
if (!recent->recentFolders().empty()) {
|
|
auto sep = new SeparatorInView(Strings::file_selector_recent_folders(), HORIZONTAL);
|
|
sep->setMinSize(gfx::Size(0, sep->sizeHint().h * 2));
|
|
location()->addItem(sep);
|
|
for (const auto& fn : recent->recentFolders())
|
|
location()->addItem(new CustomFolderNameItem(fn.c_str()));
|
|
}
|
|
|
|
// Select the location
|
|
{
|
|
location()->setSelectedItemIndex(selected_index);
|
|
location()->getEntryWidget()->setText(currentFolder->displayName().c_str());
|
|
location()->getEntryWidget()->deselectText();
|
|
}
|
|
}
|
|
|
|
void FileSelector::updateNavigationButtons()
|
|
{
|
|
// Update the state of the go back button: if the navigation-history
|
|
// has two elements and the navigation-position isn't the first one.
|
|
goBackButton()->setEnabled(
|
|
navigation_history.size() > 1 &&
|
|
(navigation_position.is_null() || navigation_position.get() != navigation_history.begin()));
|
|
|
|
// Update the state of the go forward button: if the
|
|
// navigation-history has two elements and the navigation-position
|
|
// isn't the last one.
|
|
goForwardButton()->setEnabled(navigation_history.size() > 1 && navigation_position.is_valid() &&
|
|
navigation_position.get() != navigation_history.end() - 1);
|
|
|
|
// Update the state of the go up button: if the current-folder isn't
|
|
// the root-item.
|
|
IFileItem* currentFolder = m_fileList->currentFolder();
|
|
goUpButton()->setEnabled(currentFolder != FileSystemModule::instance()->getRootFileItem());
|
|
}
|
|
|
|
void FileSelector::addInNavigationHistory(IFileItem* folder)
|
|
{
|
|
ASSERT(folder);
|
|
ASSERT(folder->isFolder());
|
|
|
|
// Remove the history from the current position
|
|
if (navigation_position.is_valid()) {
|
|
navigation_history.erase(navigation_position.get() + 1, navigation_history.end());
|
|
navigation_position.reset();
|
|
}
|
|
|
|
// If the history is empty or if the last item isn't the folder that
|
|
// we are visiting...
|
|
if (navigation_history.empty() || navigation_history.back() != folder) {
|
|
// We can add the location in the history
|
|
navigation_history.push_back(folder);
|
|
navigation_position.set(navigation_history.end() - 1);
|
|
}
|
|
}
|
|
|
|
void FileSelector::onGoBack()
|
|
{
|
|
if (navigation_history.size() > 1) {
|
|
// The default navigation position is at the end of the history
|
|
if (navigation_position.is_null())
|
|
navigation_position.set(navigation_history.end() - 1);
|
|
|
|
if (navigation_position.get() != navigation_history.begin()) {
|
|
// Go back to the first existent element
|
|
do {
|
|
navigation_position.set(navigation_position.get() - 1);
|
|
} while (!navigation_position.exists() &&
|
|
navigation_position.get() != navigation_history.begin());
|
|
|
|
if (navigation_position.exists()) {
|
|
m_navigationLocked = true;
|
|
m_fileList->setCurrentFolder(*navigation_position.get());
|
|
m_navigationLocked = false;
|
|
}
|
|
else {
|
|
navigation_position.reset();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FileSelector::onGoForward()
|
|
{
|
|
if (navigation_history.size() > 1) {
|
|
// This should not happen, because the forward button should be
|
|
// disabled when the navigation position is null.
|
|
if (navigation_position.is_null()) {
|
|
ASSERT(false);
|
|
navigation_position.set(navigation_history.end() - 1);
|
|
}
|
|
|
|
if (navigation_position.get() != navigation_history.end() - 1) {
|
|
// Go forward to the first existent element
|
|
do {
|
|
navigation_position.set(navigation_position.get() + 1);
|
|
} while (!navigation_position.exists() &&
|
|
navigation_position.get() != navigation_history.end() - 1);
|
|
|
|
if (navigation_position.exists()) {
|
|
m_navigationLocked = true;
|
|
m_fileList->setCurrentFolder(*navigation_position.get());
|
|
m_navigationLocked = false;
|
|
}
|
|
else {
|
|
navigation_position.reset();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FileSelector::onGoUp()
|
|
{
|
|
m_fileList->goUp();
|
|
}
|
|
|
|
void FileSelector::onRefreshFolder()
|
|
{
|
|
auto fs = FileSystemModule::instance();
|
|
fs->refresh();
|
|
|
|
m_fileList->setCurrentFolder(m_fileList->currentFolder());
|
|
}
|
|
|
|
void FileSelector::onNewFolder()
|
|
{
|
|
app::gen::NewFolderWindow window;
|
|
|
|
window.openWindowInForeground();
|
|
if (window.closer() == window.ok()) {
|
|
IFileItem* currentFolder = m_fileList->currentFolder();
|
|
if (currentFolder) {
|
|
std::string dirname = window.name()->text();
|
|
|
|
// Create the new directory
|
|
try {
|
|
currentFolder->createDirectory(dirname);
|
|
|
|
// Enter in the new folder
|
|
for (auto child : currentFolder->children()) {
|
|
if (child->displayName() == dirname) {
|
|
m_fileList->setCurrentFolder(child);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (const std::exception& e) {
|
|
Console::showException(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FileSelector::onChangeViewType()
|
|
{
|
|
double newZoom = m_fileList->zoom();
|
|
switch (viewType()->selectedItem()) {
|
|
case 0: newZoom = 1.0; break;
|
|
case 1: newZoom = 2.0; break;
|
|
case 2: newZoom = 8.0; break;
|
|
}
|
|
m_fileList->animateToZoom(newZoom);
|
|
}
|
|
|
|
// Hook for the 'location' combo-box
|
|
void FileSelector::onLocationCloseListBox()
|
|
{
|
|
// When the user change the location we have to set the
|
|
// current-folder in the 'fileview' widget
|
|
CustomFileNameItem* comboFileItem = dynamic_cast<CustomFileNameItem*>(
|
|
location()->getSelectedItem());
|
|
IFileItem* fileItem = (comboFileItem ? comboFileItem->getFileItem() : nullptr);
|
|
|
|
// Maybe the user selected a recent file path
|
|
if (fileItem == nullptr) {
|
|
CustomFolderNameItem* comboFolderItem = dynamic_cast<CustomFolderNameItem*>(
|
|
location()->getSelectedItem());
|
|
|
|
if (comboFolderItem) {
|
|
std::string path = comboFolderItem->text();
|
|
fileItem = FileSystemModule::instance()->getFileItemFromPath(path);
|
|
}
|
|
}
|
|
|
|
if (fileItem) {
|
|
m_fileList->setCurrentFolder(fileItem);
|
|
|
|
// Refocus the 'fileview' (the focus in that widget is more
|
|
// useful for the user)
|
|
m_fileList->requestFocus();
|
|
}
|
|
}
|
|
|
|
// When the user selects a new file-type (extension), we have to
|
|
// change the file-extension in the 'filename' entry widget
|
|
void FileSelector::onFileTypeChange()
|
|
{
|
|
base::paths exts;
|
|
auto* selExtItem = dynamic_cast<CustomFileExtensionItem*>(fileType()->getSelectedItem());
|
|
if (selExtItem)
|
|
exts = selExtItem->extensions();
|
|
|
|
if (exts != m_fileList->extensions()) {
|
|
m_navigationLocked = true;
|
|
m_fileList->setExtensions(exts);
|
|
m_navigationLocked = false;
|
|
|
|
if (m_type == FileSelectorType::Open || m_type == FileSelectorType::OpenMultiple) {
|
|
const base::paths& allExtensions =
|
|
dynamic_cast<CustomFileExtensionItem*>(fileType()->getItem(0))->extensions();
|
|
std::string k = merge_paths(allExtensions);
|
|
preferred_open_extensions[k] = exts;
|
|
}
|
|
}
|
|
|
|
if (m_type == FileSelectorType::Save) {
|
|
std::string newExtension = getSelectedExtension();
|
|
std::string fileName = m_fileName->getValue();
|
|
std::string currentExtension = base::get_file_extension(fileName);
|
|
|
|
if (!currentExtension.empty())
|
|
m_fileName->setValue(
|
|
(fileName.substr(0, fileName.size() - currentExtension.size()) + newExtension).c_str());
|
|
}
|
|
}
|
|
|
|
void FileSelector::onFileListFileSelected()
|
|
{
|
|
IFileItem* fileitem = m_fileList->selectedFileItem();
|
|
|
|
if (fileitem && !fileitem->isFolder()) {
|
|
std::string filename = base::get_file_name(fileitem->fileName());
|
|
|
|
if (m_type != FileSelectorType::OpenMultiple || m_fileList->selectedFileItems().size() == 1)
|
|
m_fileName->setValue(filename.c_str());
|
|
else
|
|
m_fileName->setValue(std::string());
|
|
}
|
|
}
|
|
|
|
void FileSelector::onFileListFileAccepted()
|
|
{
|
|
closeWindow(m_fileList);
|
|
}
|
|
|
|
void FileSelector::onFileListCurrentFolderChanged()
|
|
{
|
|
if (!m_navigationLocked)
|
|
addInNavigationHistory(m_fileList->currentFolder());
|
|
|
|
updateLocation();
|
|
updateNavigationButtons();
|
|
|
|
// Close the autocomplete popup just in case it's open.
|
|
m_fileName->closeListBox();
|
|
}
|
|
|
|
std::string FileSelector::getSelectedExtension() const
|
|
{
|
|
auto selExtItem = dynamic_cast<CustomFileExtensionItem*>(fileType()->getSelectedItem());
|
|
if (selExtItem && selExtItem->extensions().size() == 1)
|
|
return selExtItem->extensions().front();
|
|
else
|
|
return m_defExtension;
|
|
}
|
|
|
|
} // namespace app
|