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 | |
51 | namespace app { |
52 | |
53 | using namespace app::skin; |
54 | using namespace ui; |
55 | |
56 | namespace { |
57 | |
58 | const char* kConfigSection = "FileSelector" ; |
59 | |
60 | template<class Container> |
61 | class NullableIterator { |
62 | public: |
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 | |
87 | private: |
88 | bool m_isNull; |
89 | iterator m_iterator; |
90 | }; |
91 | |
92 | // Variables used only to maintain the history of navigation. |
93 | FileItemList navigation_history; // Set of FileItems navigated |
94 | NullableIterator<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. |
102 | std::map<std::string, base::paths> preferred_open_extensions; |
103 | |
104 | void 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 | |
144 | std::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 | |
157 | class FileSelector::CustomFileNameEntry : public ComboBox { |
158 | public: |
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 | |
169 | protected: |
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 | |
205 | private: |
206 | FileList* m_fileList; |
207 | }; |
208 | |
209 | class FileSelector::CustomFileNameItem : public ListItem { |
210 | public: |
211 | CustomFileNameItem(const char* text, IFileItem* fileItem) |
212 | : ListItem(text) |
213 | , m_fileItem(fileItem) |
214 | { |
215 | } |
216 | |
217 | IFileItem* getFileItem() { return m_fileItem; } |
218 | |
219 | private: |
220 | IFileItem* m_fileItem; |
221 | }; |
222 | |
223 | class FileSelector::CustomFolderNameItem : public ListItem { |
224 | public: |
225 | CustomFolderNameItem(const char* text) |
226 | : ListItem(text) |
227 | { |
228 | } |
229 | }; |
230 | |
231 | class FileSelector::CustomFileExtensionItem : public ListItem { |
232 | public: |
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; } |
240 | private: |
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. |
251 | class FileSelector::ArrowNavigator : public Widget { |
252 | public: |
253 | ArrowNavigator(FileSelector* filesel) |
254 | : Widget(kGenericWidget) |
255 | , m_filesel(filesel) { |
256 | setVisible(false); |
257 | } |
258 | |
259 | protected: |
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 | |
313 | private: |
314 | FileSelector* m_filesel; |
315 | }; |
316 | |
317 | FileSelector::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 | |
359 | void FileSelector::setDefaultExtension(const std::string& extension) |
360 | { |
361 | m_defExtension = extension; |
362 | } |
363 | |
364 | FileSelector::~FileSelector() |
365 | { |
366 | } |
367 | |
368 | void FileSelector::goBack() |
369 | { |
370 | onGoBack(); |
371 | } |
372 | |
373 | void FileSelector::goForward() |
374 | { |
375 | onGoForward(); |
376 | } |
377 | |
378 | void FileSelector::goUp() |
379 | { |
380 | onGoUp(); |
381 | } |
382 | |
383 | void FileSelector::goInsideFolder() |
384 | { |
385 | if (m_fileList->selectedFileItem() && |
386 | m_fileList->selectedFileItem()->isBrowsable()) { |
387 | m_fileList->setCurrentFolder( |
388 | m_fileList->selectedFileItem()); |
389 | } |
390 | } |
391 | |
392 | void FileSelector::refreshCurrentFolder() |
393 | { |
394 | onRefreshFolder(); |
395 | } |
396 | |
397 | bool 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? |
530 | again: |
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 | |
713 | bool 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. |
725 | void 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 | |
787 | void 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 | |
810 | void 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 | |
832 | void 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 | |
858 | void 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 | |
887 | void FileSelector::onGoUp() |
888 | { |
889 | m_fileList->goUp(); |
890 | } |
891 | |
892 | void FileSelector::onRefreshFolder() |
893 | { |
894 | auto fs = FileSystemModule::instance(); |
895 | fs->refresh(); |
896 | |
897 | m_fileList->setCurrentFolder(m_fileList->currentFolder()); |
898 | } |
899 | |
900 | void 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 | |
929 | void 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 |
941 | void 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 |
970 | void 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 | |
1001 | void 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 | |
1016 | void FileSelector::onFileListFileAccepted() |
1017 | { |
1018 | closeWindow(m_fileList); |
1019 | } |
1020 | |
1021 | void 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 | |
1033 | std::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 | |