1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/file_selector.h"
13
14#include "app/app.h"
15#include "app/console.h"
16#include "app/file/file.h"
17#include "app/i18n/strings.h"
18#include "app/modules/gfx.h"
19#include "app/modules/gui.h"
20#include "app/pref/preferences.h"
21#include "app/recent_files.h"
22#include "app/ui/file_list.h"
23#include "app/ui/file_list_view.h"
24#include "app/ui/separator_in_view.h"
25#include "app/ui/skin/skin_theme.h"
26#include "app/widget_loader.h"
27#include "base/convert_to.h"
28#include "base/fs.h"
29#include "base/paths.h"
30#include "base/string.h"
31#include "fmt/format.h"
32#include "ui/ui.h"
33
34#include "new_folder_window.xml.h"
35
36#include <algorithm>
37#include <cctype>
38#include <cerrno>
39#include <iterator>
40#include <list>
41#include <set>
42#include <string>
43#include <vector>
44
45#ifndef MAX_PATH
46# define MAX_PATH 4096 // TODO this is needed for Linux, is it correct?
47#endif
48
49#define FILESEL_TRACE(...) // TRACE
50
51namespace app {
52
53using namespace app::skin;
54using namespace ui;
55
56namespace {
57
58const char* kConfigSection = "FileSelector";
59
60template<class Container>
61class NullableIterator {
62public:
63 typedef typename Container::iterator iterator;
64
65 NullableIterator() : m_isNull(true) { }
66
67 bool is_null() const { return m_isNull; }
68 bool is_valid() const { return !m_isNull; }
69 bool exists() const {
70 return (is_valid() && (*m_iterator)->isExistent());
71 }
72
73 iterator get() {
74 ASSERT(!m_isNull);
75 return m_iterator;
76 }
77
78 void reset() {
79 m_isNull = true;
80 }
81
82 void set(const iterator& it) {
83 m_isNull = false;
84 m_iterator = it;
85 }
86
87private:
88 bool m_isNull;
89 iterator m_iterator;
90};
91
92// Variables used only to maintain the history of navigation.
93FileItemList navigation_history; // Set of FileItems navigated
94NullableIterator<FileItemList> navigation_position; // Current position in the navigation history
95
96// This map acts like a temporal customization by the user when he/she
97// wants to open files. The key (first) is the real "allExtensions"
98// parameter given to the FileSelector::show() function where each
99// extension is concatenated with each other in one string separated
100// by ','. The value (second) is the selected/preferred extension by
101// the user. It's used only in FileSelector::Open type of dialogs.
102std::map<std::string, base::paths> preferred_open_extensions;
103
104void adjust_navigation_history(IFileItem* item)
105{
106 auto it = navigation_history.begin();
107 const bool valid = navigation_position.is_valid();
108 int pos = (valid ? int(navigation_position.get() - it): 0);
109
110 FILESEL_TRACE("FILESEL: Removed item '%s' detected (%p)\n",
111 item->fileName().c_str(), item);
112 if (valid) {
113 FILESEL_TRACE("FILESEL: Old navigation pos [%d] = %s\n",
114 pos, (*navigation_position.get())->fileName().c_str());
115 }
116
117 while (true) {
118 it = std::find(it, navigation_history.end(), item);
119 if (it == navigation_history.end())
120 break;
121
122 FILESEL_TRACE("FILESEL: Erase navigation pos [%d] = %s\n", pos,
123 (*it)->fileName().c_str());
124
125 if (pos >= it-navigation_history.begin())
126 --pos;
127
128 it = navigation_history.erase(it);
129 }
130
131 if (valid && !navigation_history.empty()) {
132 pos = std::clamp(pos, 0, (int)navigation_history.size()-1);
133 navigation_position.set(navigation_history.begin() + pos);
134
135 FILESEL_TRACE("FILESEL: New navigation pos [%d] = %s\n",
136 pos, (*navigation_position.get())->fileName().c_str());
137 }
138 else {
139 navigation_position.reset();
140 FILESEL_TRACE("FILESEL: Without new navigation pos\n");
141 }
142}
143
144std::string merge_paths(const base::paths& paths)
145{
146 std::string k;
147 for (const auto& p : paths) {
148 if (!k.empty())
149 k.push_back(',');
150 k += p;
151 }
152 return k;
153}
154
155} // anonymous namespace
156
157class FileSelector::CustomFileNameEntry : public ComboBox {
158public:
159 CustomFileNameEntry()
160 : m_fileList(nullptr) {
161 setEditable(true);
162 getEntryWidget()->Change.connect(&CustomFileNameEntry::onEntryChange, this);
163 }
164
165 void setAssociatedFileList(FileList* fileList) {
166 m_fileList = fileList;
167 }
168
169protected:
170
171 void onEntryChange() {
172 // Deselect multiple-selection
173 if (m_fileList->multipleSelection())
174 m_fileList->deselectedFileItems();
175
176 deleteAllItems();
177
178 // String to be autocompleted
179 std::string left_part = getEntryWidget()->text();
180 closeListBox();
181
182 if (left_part.empty())
183 return;
184
185 for (const IFileItem* child : m_fileList->fileList()) {
186 std::string child_name = child->displayName();
187 std::string::const_iterator it1, it2;
188
189 for (it1 = child_name.begin(), it2 = left_part.begin();
190 it1 != child_name.end() && it2 != left_part.end();
191 ++it1, ++it2) {
192 if (std::tolower(*it1) != std::tolower(*it2))
193 break;
194 }
195
196 // Is the pattern (left_part) in the child_name's beginning?
197 if (it1 != child_name.end() && it2 == left_part.end())
198 addItem(child_name);
199 }
200
201 if (getItemCount() > 0)
202 openListBox();
203 }
204
205private:
206 FileList* m_fileList;
207};
208
209class FileSelector::CustomFileNameItem : public ListItem {
210public:
211 CustomFileNameItem(const char* text, IFileItem* fileItem)
212 : ListItem(text)
213 , m_fileItem(fileItem)
214 {
215 }
216
217 IFileItem* getFileItem() { return m_fileItem; }
218
219private:
220 IFileItem* m_fileItem;
221};
222
223class FileSelector::CustomFolderNameItem : public ListItem {
224public:
225 CustomFolderNameItem(const char* text)
226 : ListItem(text)
227 {
228 }
229};
230
231class FileSelector::CustomFileExtensionItem : public ListItem {
232public:
233 CustomFileExtensionItem(const std::string& text,
234 const base::paths& exts)
235 : ListItem(text)
236 , m_exts(exts)
237 {
238 }
239 const base::paths& extensions() const { return m_exts; }
240private:
241 base::paths m_exts;
242};
243
244// We have this dummy/hidden widget only to handle special navigation
245// with arrow keys. In the past this code was in the same FileSelector
246// itself, but there were problems adding that window as a message
247// filter. Mainly there is a special combination of widgets
248// (comboboxes) that need to filter Esc key (e.g. to close the
249// combobox popup). And we cannot pre-add a filter that send that key
250// to the Manager before it's processed by the combobox filter.
251class FileSelector::ArrowNavigator : public Widget {
252public:
253 ArrowNavigator(FileSelector* filesel)
254 : Widget(kGenericWidget)
255 , m_filesel(filesel) {
256 setVisible(false);
257 }
258
259protected:
260 bool onProcessMessage(ui::Message* msg) override {
261 switch (msg->type()) {
262 case kOpenMessage:
263 manager()->addMessageFilter(kKeyDownMessage, this);
264 break;
265 case kCloseMessage:
266 manager()->removeMessageFilter(kKeyDownMessage, this);
267 break;
268 case kKeyDownMessage: {
269 KeyMessage* keyMsg = static_cast<KeyMessage*>(msg);
270 KeyScancode scancode = keyMsg->scancode();
271
272#ifdef __APPLE__
273 int unicode = keyMsg->unicodeChar();
274 bool up = (msg->cmdPressed() && scancode == kKeyUp);
275 bool enter = (msg->cmdPressed() && scancode == kKeyDown);
276 bool back = (msg->cmdPressed() && (unicode == '[' || scancode == kKeyOpenbrace));
277 bool forward = (msg->cmdPressed() && (unicode == ']' || scancode == kKeyClosebrace));
278#else
279 bool up = (msg->altPressed() && scancode == kKeyUp);
280 bool enter = (msg->altPressed() && scancode == kKeyDown);
281 bool back = (msg->altPressed() && scancode == kKeyLeft);
282 bool forward = (msg->altPressed() && scancode == kKeyRight);
283#endif
284 bool refresh = (scancode == kKeyF5 ||
285 (msg->ctrlPressed() && scancode == kKeyR) ||
286 (msg->cmdPressed() && scancode == kKeyR));
287
288 if (up) {
289 m_filesel->goUp();
290 return true;
291 }
292 if (enter) {
293 m_filesel->goInsideFolder();
294 return true;
295 }
296 if (back) {
297 m_filesel->goBack();
298 return true;
299 }
300 if (forward) {
301 m_filesel->goForward();
302 return true;
303 }
304 if (refresh) {
305 m_filesel->refreshCurrentFolder();
306 }
307 return false;
308 }
309 }
310 return Widget::onProcessMessage(msg);
311 }
312
313private:
314 FileSelector* m_filesel;
315};
316
317FileSelector::FileSelector(FileSelectorType type)
318 : m_type(type)
319 , m_navigationLocked(false)
320{
321 addChild(new ArrowNavigator(this));
322
323 m_fileName = new CustomFileNameEntry;
324 m_fileName->setFocusMagnet(true);
325 fileNamePlaceholder()->addChild(m_fileName);
326
327 goBackButton()->setFocusStop(false);
328 goForwardButton()->setFocusStop(false);
329 goUpButton()->setFocusStop(false);
330 refreshButton()->setFocusStop(false);
331 newFolderButton()->setFocusStop(false);
332 viewType()->setFocusStop(false);
333 for (auto child : viewType()->children())
334 child->setFocusStop(false);
335
336 m_fileList = new FileList();
337 m_fileList->setId("fileview");
338 m_fileName->setAssociatedFileList(m_fileList);
339 m_fileList->setZoom(Preferences::instance().fileSelector.zoom());
340
341 m_fileView = new FileListView();
342 m_fileView->attachToView(m_fileList);
343 m_fileView->setExpansive(true);
344 fileViewPlaceholder()->addChild(m_fileView);
345
346 goBackButton()->Click.connect([this]{ onGoBack(); });
347 goForwardButton()->Click.connect([this]{ onGoForward(); });
348 goUpButton()->Click.connect([this]{ onGoUp(); });
349 refreshButton()->Click.connect([this] { onRefreshFolder(); });
350 newFolderButton()->Click.connect([this]{ onNewFolder(); });
351 viewType()->ItemChange.connect([this]{ onChangeViewType(); });
352 location()->CloseListBox.connect([this]{ onLocationCloseListBox(); });
353 fileType()->Change.connect([this]{ onFileTypeChange(); });
354 m_fileList->FileSelected.connect([this]{ onFileListFileSelected(); });
355 m_fileList->FileAccepted.connect([this]{ onFileListFileAccepted(); });
356 m_fileList->CurrentFolderChanged.connect([this]{ onFileListCurrentFolderChanged(); });
357}
358
359void FileSelector::setDefaultExtension(const std::string& extension)
360{
361 m_defExtension = extension;
362}
363
364FileSelector::~FileSelector()
365{
366}
367
368void FileSelector::goBack()
369{
370 onGoBack();
371}
372
373void FileSelector::goForward()
374{
375 onGoForward();
376}
377
378void FileSelector::goUp()
379{
380 onGoUp();
381}
382
383void FileSelector::goInsideFolder()
384{
385 if (m_fileList->selectedFileItem() &&
386 m_fileList->selectedFileItem()->isBrowsable()) {
387 m_fileList->setCurrentFolder(
388 m_fileList->selectedFileItem());
389 }
390}
391
392void FileSelector::refreshCurrentFolder()
393{
394 onRefreshFolder();
395}
396
397bool FileSelector::show(
398 const std::string& title,
399 const std::string& initialPath,
400 const base::paths& allExtensions,
401 base::paths& output)
402{
403 FileSystemModule* fs = FileSystemModule::instance();
404 LockFS lock(fs);
405
406 // Connection used to remove items from the navigation history that
407 // are not found in the file system anymore.
408 obs::scoped_connection conn =
409 fs->ItemRemoved.connect(&adjust_navigation_history);
410
411 fs->refresh();
412
413 // we have to find where the user should begin to browse files (start_folder)
414 std::string start_folder_path;
415 IFileItem* start_folder = nullptr;
416
417 // If initialPath doesn't contain a path.
418 if (base::get_file_path(initialPath).empty()) {
419 // Get the saved `path' in the configuration file.
420 std::string path = Preferences::instance().fileSelector.currentFolder();
421 if (path == "<empty>") {
422 start_folder_path = base::get_user_docs_folder();
423 path = base::join_path(start_folder_path, initialPath);
424 }
425 start_folder = fs->getFileItemFromPath(path);
426 }
427 else {
428 // Remove the filename.
429 start_folder_path = base::join_path(base::get_file_path(initialPath), "");
430 }
431 start_folder_path = base::fix_path_separators(start_folder_path);
432
433 if (!start_folder)
434 start_folder = fs->getFileItemFromPath(start_folder_path);
435
436 FILESEL_TRACE("FILESEL: Start folder '%s' (%p)\n", start_folder_path.c_str(), start_folder);
437
438 {
439 const gfx::Size workareaSize = ui::Manager::getDefault()->display()->workareaSizeUIScale();
440 setMinSize(workareaSize*9/10);
441 }
442
443 remapWindow();
444 centerWindow();
445 load_window_pos(this, kConfigSection);
446
447 // Change the file formats/extensions to be shown
448 std::string initialExtension = base::get_file_extension(initialPath);
449 base::paths exts;
450 if (m_type == FileSelectorType::Open ||
451 m_type == FileSelectorType::OpenMultiple) {
452 std::string k = merge_paths(allExtensions);
453 auto it = preferred_open_extensions.find(k);
454 if (it == preferred_open_extensions.end())
455 exts = allExtensions;
456 else
457 exts = preferred_open_extensions[k];
458 }
459 else {
460 ASSERT(m_type == FileSelectorType::Save);
461 if (!initialExtension.empty())
462 exts = base::paths{ initialExtension };
463 else
464 exts = allExtensions;
465 }
466 m_fileList->setMultipleSelection(m_type == FileSelectorType::OpenMultiple);
467 m_fileList->setExtensions(exts);
468 if (start_folder)
469 m_fileList->setCurrentFolder(start_folder);
470
471 // current location
472 navigation_position.reset();
473 addInNavigationHistory(m_fileList->currentFolder());
474
475 // fill the location combo-box
476 updateLocation();
477 updateNavigationButtons();
478
479 // fill file-type combo-box
480 fileType()->deleteAllItems();
481
482 // Get the default extension from the given initial file name
483 if (m_defExtension.empty())
484 m_defExtension = initialExtension;
485
486 // File type for all formats
487 fileType()->addItem(
488 new CustomFileExtensionItem(Strings::file_selector_all_formats(),
489 allExtensions));
490
491 // One file type for each supported image format
492 for (const auto& e : allExtensions) {
493 // If the default extension is empty, use the first filter
494 if (m_defExtension.empty())
495 m_defExtension = e;
496
497 fileType()->addItem(
498 new CustomFileExtensionItem(e + " files",
499 base::paths{ e }));
500 }
501 // All files
502 fileType()->addItem(
503 new CustomFileExtensionItem(Strings::file_selector_all_files(),
504 base::paths())); // Empty extensions means "*.*"
505
506 // file name entry field
507 m_fileName->setValue(base::get_file_name(initialPath).c_str());
508 m_fileName->getEntryWidget()->selectText(0, -1);
509
510 for (Widget* wItem : *fileType()) {
511 auto item = dynamic_cast<CustomFileExtensionItem*>(wItem);
512 ASSERT(item);
513 if (item && item->extensions() == exts) {
514 fileType()->setSelectedItem(item);
515 break;
516 }
517 }
518
519 // setup the title of the window
520 setText(title.c_str());
521
522 // get the ok-button
523 Widget* ok = this->findChild("ok");
524
525 // update the view
526 View::getView(m_fileList)->updateView();
527
528 // TODO this loop needs a complete refactor
529 // Open the window and run... the user press ok?
530again:
531 openWindowInForeground();
532 if (closer() == ok ||
533 closer() == m_fileList) {
534 IFileItem* folder = m_fileList->currentFolder();
535 ASSERT(folder);
536
537 // File name in the text entry field/combobox
538 std::string fn = m_fileName->getValue();
539 std::string buf;
540 IFileItem* enter_folder = nullptr;
541
542 // up a level?
543 if (fn == "..") {
544 enter_folder = folder->parent();
545 if (!enter_folder)
546 enter_folder = folder;
547 }
548 else if (fn.empty()) {
549 IFileItem* selected = m_fileList->selectedFileItem();
550 if (selected && selected->isBrowsable())
551 enter_folder = selected;
552 else if (m_type != FileSelectorType::OpenMultiple ||
553 m_fileList->selectedFileItems().empty()) {
554 // Show the window again
555 setVisible(true);
556 goto again;
557 }
558 }
559 else {
560 // check if the user specified in "fn" a item of "fileview"
561 const FileItemList& children = m_fileList->fileList();
562
563 std::string fn2 = fn;
564#ifdef _WIN32
565 fn2 = base::string_to_lower(fn2);
566#endif
567
568 for (IFileItem* child : children) {
569 std::string child_name = child->displayName();
570#ifdef _WIN32
571 child_name = base::string_to_lower(child_name);
572#endif
573 if (child_name == fn2) {
574 enter_folder = child;
575 buf = enter_folder->fileName();
576 break;
577 }
578 }
579
580 if (!enter_folder) {
581 // does the file-name entry have separators?
582 if (base::is_path_separator(*fn.begin())) { // absolute path (UNIX style)
583#ifdef _WIN32
584 // get the drive of the current folder
585 std::string drive = folder->fileName();
586 if (drive.size() >= 2 && drive[1] == ':') {
587 buf += drive[0];
588 buf += ':';
589 buf += fn;
590 }
591 else
592 buf = base::join_path("C:", fn);
593#else
594 buf = fn;
595#endif
596 }
597#ifdef _WIN32
598 // does the file-name entry have colon?
599 else if (fn.find(':') != std::string::npos) { // absolute path on Windows
600 if (fn.size() == 2 && fn[1] == ':') {
601 buf = base::join_path(fn, "");
602 }
603 else {
604 buf = fn;
605 }
606 }
607#endif
608 else {
609 buf = folder->fileName();
610 buf = base::join_path(buf, fn);
611 }
612 buf = base::fix_path_separators(buf);
613
614 // we can check if 'buf' is a folder, so we have to enter in it
615 enter_folder = fs->getFileItemFromPath(buf);
616 }
617 }
618
619 // did we find a folder to enter?
620 if (enter_folder &&
621 enter_folder->isFolder() &&
622 enter_folder->isBrowsable()) {
623 // enter in the folder that was specified in the 'm_fileName'
624 m_fileList->setCurrentFolder(enter_folder);
625
626 // clear the text of the entry widget
627 m_fileName->setValue("");
628
629 // show the window again
630 setVisible(true);
631 goto again;
632 }
633 // else file-name specified in the entry is really a file to open...
634
635#ifdef _WIN32
636 // Check that the filename doesn't contain ilegal characters.
637 // Linux allows all kind of characters, only '/' is disallowed,
638 // but in that case we consider that a full path was entered in
639 // the filename and we can enter to the full path folder.
640 if (!enter_folder) {
641 const bool has_invalid_char =
642 (fn.find(':') != std::string::npos ||
643 fn.find('*') != std::string::npos ||
644 fn.find('?') != std::string::npos ||
645 fn.find('\"') != std::string::npos ||
646 fn.find('<') != std::string::npos ||
647 fn.find('>') != std::string::npos ||
648 fn.find('|') != std::string::npos);
649 if (has_invalid_char) {
650 const char* invalid_chars = ": * ? \" < > |";
651
652 ui::Alert::show(
653 fmt::format(
654 Strings::alerts_invalid_chars_in_filename(),
655 invalid_chars));
656
657 // show the window again
658 setVisible(true);
659 goto again;
660 }
661 }
662#endif
663
664 // Does it not have extension? ...we should add the extension
665 // selected in the filetype combo-box
666 if (m_type == FileSelectorType::Save &&
667 !buf.empty() && base::get_file_extension(buf).empty()) {
668 buf += '.';
669 buf += getSelectedExtension();
670 }
671
672 if (m_type == FileSelectorType::Save && base::is_file(buf)) {
673 int ret = Alert::show(
674 fmt::format(
675 Strings::alerts_overwrite_existent_file(),
676 base::get_file_name(buf)));
677 if (ret == 2) {
678 setVisible(true);
679 goto again;
680 }
681 else if (ret == 1) {
682 // Check for read-only attribute
683 if (base::has_readonly_attr(buf)) {
684 ui::Alert::show(Strings::alerts_cannot_save_in_read_only_file());
685
686 setVisible(true);
687 goto again;
688 }
689 }
690 // Cancel
691 else if (ret != 1) {
692 return false;
693 }
694 }
695
696 // Put in output the selected filenames
697 if (!buf.empty())
698 output.push_back(buf);
699 else if (m_type == FileSelectorType::OpenMultiple) {
700 for (IFileItem* fi : m_fileList->selectedFileItems())
701 output.push_back(fi->fileName());
702 }
703
704 // save the path in the configuration file
705 std::string lastpath = folder->keyName();
706 Preferences::instance().fileSelector.currentFolder(lastpath);
707 }
708 Preferences::instance().fileSelector.zoom(m_fileList->zoom());
709
710 return (!output.empty());
711}
712
713bool FileSelector::onProcessMessage(ui::Message* msg)
714{
715 switch (msg->type()) {
716 case kCloseMessage:
717 save_window_pos(this, kConfigSection);
718 break;
719 }
720 return app::gen::FileSelector::onProcessMessage(msg);
721}
722
723// Updates the content of the combo-box that shows the current
724// location in the file-system.
725void FileSelector::updateLocation()
726{
727 IFileItem* currentFolder = m_fileList->currentFolder();
728 IFileItem* fileItem = currentFolder;
729 std::list<IFileItem*> locations;
730 int selected_index = -1;
731
732 while (fileItem) {
733 locations.push_front(fileItem);
734 fileItem = fileItem->parent();
735 }
736
737 // Clear all the items from the combo-box
738 location()->deleteAllItems();
739
740 // Add item by item (from root to the specific current folder)
741 int level = 0;
742 for (auto it=locations.begin(), end=locations.end(); it!=end; ++it) {
743 fileItem = *it;
744
745 // Indentation
746 std::string buf;
747 for (int c=0; c<level; ++c)
748 buf += " ";
749
750 // Location name
751 buf += fileItem->displayName();
752
753 // Add the new location to the combo-box
754 location()->addItem(new CustomFileNameItem(buf.c_str(), fileItem));
755
756 if (fileItem == currentFolder)
757 selected_index = level;
758
759 level++;
760 }
761
762 // Add paths from recent files list
763 auto recent = App::instance()->recentFiles();
764 if (!recent->pinnedFolders().empty()) {
765 auto sep = new SeparatorInView(Strings::file_selector_pinned_folders(), HORIZONTAL);
766 sep->setMinSize(gfx::Size(0, sep->sizeHint().h*2));
767 location()->addItem(sep);
768 for (const auto& fn : recent->pinnedFolders())
769 location()->addItem(new CustomFolderNameItem(fn.c_str()));
770 }
771 if (!recent->recentFolders().empty()) {
772 auto sep = new SeparatorInView(Strings::file_selector_recent_folders(), HORIZONTAL);
773 sep->setMinSize(gfx::Size(0, sep->sizeHint().h*2));
774 location()->addItem(sep);
775 for (const auto& fn : recent->recentFolders())
776 location()->addItem(new CustomFolderNameItem(fn.c_str()));
777 }
778
779 // Select the location
780 {
781 location()->setSelectedItemIndex(selected_index);
782 location()->getEntryWidget()->setText(currentFolder->displayName().c_str());
783 location()->getEntryWidget()->deselectText();
784 }
785}
786
787void FileSelector::updateNavigationButtons()
788{
789 // Update the state of the go back button: if the navigation-history
790 // has two elements and the navigation-position isn't the first one.
791 goBackButton()->setEnabled(
792 navigation_history.size() > 1 &&
793 (navigation_position.is_null() ||
794 navigation_position.get() != navigation_history.begin()));
795
796 // Update the state of the go forward button: if the
797 // navigation-history has two elements and the navigation-position
798 // isn't the last one.
799 goForwardButton()->setEnabled(
800 navigation_history.size() > 1 &&
801 navigation_position.is_valid() &&
802 navigation_position.get() != navigation_history.end()-1);
803
804 // Update the state of the go up button: if the current-folder isn't
805 // the root-item.
806 IFileItem* currentFolder = m_fileList->currentFolder();
807 goUpButton()->setEnabled(currentFolder != FileSystemModule::instance()->getRootFileItem());
808}
809
810void FileSelector::addInNavigationHistory(IFileItem* folder)
811{
812 ASSERT(folder);
813 ASSERT(folder->isFolder());
814
815 // Remove the history from the current position
816 if (navigation_position.is_valid()) {
817 navigation_history.erase(navigation_position.get()+1,
818 navigation_history.end());
819 navigation_position.reset();
820 }
821
822 // If the history is empty or if the last item isn't the folder that
823 // we are visiting...
824 if (navigation_history.empty() ||
825 navigation_history.back() != folder) {
826 // We can add the location in the history
827 navigation_history.push_back(folder);
828 navigation_position.set(navigation_history.end()-1);
829 }
830}
831
832void FileSelector::onGoBack()
833{
834 if (navigation_history.size() > 1) {
835 // The default navigation position is at the end of the history
836 if (navigation_position.is_null())
837 navigation_position.set(navigation_history.end()-1);
838
839 if (navigation_position.get() != navigation_history.begin()) {
840 // Go back to the first existent element
841 do {
842 navigation_position.set(navigation_position.get()-1);
843 } while (!navigation_position.exists() &&
844 navigation_position.get() != navigation_history.begin());
845
846 if (navigation_position.exists()) {
847 m_navigationLocked = true;
848 m_fileList->setCurrentFolder(*navigation_position.get());
849 m_navigationLocked = false;
850 }
851 else {
852 navigation_position.reset();
853 }
854 }
855 }
856}
857
858void FileSelector::onGoForward()
859{
860 if (navigation_history.size() > 1) {
861 // This should not happen, because the forward button should be
862 // disabled when the navigation position is null.
863 if (navigation_position.is_null()) {
864 ASSERT(false);
865 navigation_position.set(navigation_history.end()-1);
866 }
867
868 if (navigation_position.get() != navigation_history.end()-1) {
869 // Go forward to the first existent element
870 do {
871 navigation_position.set(navigation_position.get()+1);
872 } while (!navigation_position.exists() &&
873 navigation_position.get() != navigation_history.end()-1);
874
875 if (navigation_position.exists()) {
876 m_navigationLocked = true;
877 m_fileList->setCurrentFolder(*navigation_position.get());
878 m_navigationLocked = false;
879 }
880 else {
881 navigation_position.reset();
882 }
883 }
884 }
885}
886
887void FileSelector::onGoUp()
888{
889 m_fileList->goUp();
890}
891
892void FileSelector::onRefreshFolder()
893{
894 auto fs = FileSystemModule::instance();
895 fs->refresh();
896
897 m_fileList->setCurrentFolder(m_fileList->currentFolder());
898}
899
900void FileSelector::onNewFolder()
901{
902 app::gen::NewFolderWindow window;
903
904 window.openWindowInForeground();
905 if (window.closer() == window.ok()) {
906 IFileItem* currentFolder = m_fileList->currentFolder();
907 if (currentFolder) {
908 std::string dirname = window.name()->text();
909
910 // Create the new directory
911 try {
912 currentFolder->createDirectory(dirname);
913
914 // Enter in the new folder
915 for (auto child : currentFolder->children()) {
916 if (child->displayName() == dirname) {
917 m_fileList->setCurrentFolder(child);
918 break;
919 }
920 }
921 }
922 catch (const std::exception& e) {
923 Console::showException(e);
924 }
925 }
926 }
927}
928
929void FileSelector::onChangeViewType()
930{
931 double newZoom = m_fileList->zoom();
932 switch (viewType()->selectedItem()) {
933 case 0: newZoom = 1.0; break;
934 case 1: newZoom = 2.0; break;
935 case 2: newZoom = 8.0; break;
936 }
937 m_fileList->animateToZoom(newZoom);
938}
939
940// Hook for the 'location' combo-box
941void FileSelector::onLocationCloseListBox()
942{
943 // When the user change the location we have to set the
944 // current-folder in the 'fileview' widget
945 CustomFileNameItem* comboFileItem = dynamic_cast<CustomFileNameItem*>(location()->getSelectedItem());
946 IFileItem* fileItem = (comboFileItem ? comboFileItem->getFileItem(): nullptr);
947
948 // Maybe the user selected a recent file path
949 if (fileItem == nullptr) {
950 CustomFolderNameItem* comboFolderItem =
951 dynamic_cast<CustomFolderNameItem*>(location()->getSelectedItem());
952
953 if (comboFolderItem) {
954 std::string path = comboFolderItem->text();
955 fileItem = FileSystemModule::instance()->getFileItemFromPath(path);
956 }
957 }
958
959 if (fileItem) {
960 m_fileList->setCurrentFolder(fileItem);
961
962 // Refocus the 'fileview' (the focus in that widget is more
963 // useful for the user)
964 m_fileList->requestFocus();
965 }
966}
967
968// When the user selects a new file-type (extension), we have to
969// change the file-extension in the 'filename' entry widget
970void FileSelector::onFileTypeChange()
971{
972 base::paths exts;
973 auto* selExtItem = dynamic_cast<CustomFileExtensionItem*>(fileType()->getSelectedItem());
974 if (selExtItem)
975 exts = selExtItem->extensions();
976
977 if (exts != m_fileList->extensions()) {
978 m_navigationLocked = true;
979 m_fileList->setExtensions(exts);
980 m_navigationLocked = false;
981
982 if (m_type == FileSelectorType::Open ||
983 m_type == FileSelectorType::OpenMultiple) {
984 const base::paths& allExtensions =
985 dynamic_cast<CustomFileExtensionItem*>(fileType()->getItem(0))->extensions();
986 std::string k = merge_paths(allExtensions);
987 preferred_open_extensions[k] = exts;
988 }
989 }
990
991 if (m_type == FileSelectorType::Save) {
992 std::string newExtension = getSelectedExtension();
993 std::string fileName = m_fileName->getValue();
994 std::string currentExtension = base::get_file_extension(fileName);
995
996 if (!currentExtension.empty())
997 m_fileName->setValue((fileName.substr(0, fileName.size()-currentExtension.size())+newExtension).c_str());
998 }
999}
1000
1001void FileSelector::onFileListFileSelected()
1002{
1003 IFileItem* fileitem = m_fileList->selectedFileItem();
1004
1005 if (fileitem && !fileitem->isFolder()) {
1006 std::string filename = base::get_file_name(fileitem->fileName());
1007
1008 if (m_type != FileSelectorType::OpenMultiple ||
1009 m_fileList->selectedFileItems().size() == 1)
1010 m_fileName->setValue(filename.c_str());
1011 else
1012 m_fileName->setValue(std::string());
1013 }
1014}
1015
1016void FileSelector::onFileListFileAccepted()
1017{
1018 closeWindow(m_fileList);
1019}
1020
1021void FileSelector::onFileListCurrentFolderChanged()
1022{
1023 if (!m_navigationLocked)
1024 addInNavigationHistory(m_fileList->currentFolder());
1025
1026 updateLocation();
1027 updateNavigationButtons();
1028
1029 // Close the autocomplete popup just in case it's open.
1030 m_fileName->closeListBox();
1031}
1032
1033std::string FileSelector::getSelectedExtension() const
1034{
1035 auto selExtItem = dynamic_cast<CustomFileExtensionItem*>(fileType()->getSelectedItem());
1036 if (selExtItem && selExtItem->extensions().size() == 1)
1037 return selExtItem->extensions().front();
1038 else
1039 return m_defExtension;
1040}
1041
1042} // namespace app
1043