Serenity Operating System
1/*
2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "DirectoryView.h"
9#include "FileUtils.h"
10#include <AK/LexicalPath.h>
11#include <AK/NumberFormat.h>
12#include <AK/StringBuilder.h>
13#include <LibConfig/Client.h>
14#include <LibCore/DeprecatedFile.h>
15#include <LibCore/MimeData.h>
16#include <LibCore/StandardPaths.h>
17#include <LibGUI/FileIconProvider.h>
18#include <LibGUI/InputBox.h>
19#include <LibGUI/Label.h>
20#include <LibGUI/MessageBox.h>
21#include <LibGUI/ModelEditingDelegate.h>
22#include <LibGUI/SortingProxyModel.h>
23#include <serenity.h>
24#include <spawn.h>
25#include <stdio.h>
26#include <unistd.h>
27
28namespace FileManager {
29
30void spawn_terminal(DeprecatedString const& directory)
31{
32 posix_spawn_file_actions_t spawn_actions;
33 posix_spawn_file_actions_init(&spawn_actions);
34 posix_spawn_file_actions_addchdir(&spawn_actions, directory.characters());
35
36 pid_t pid;
37 char const* argv[] = { "Terminal", nullptr };
38 if ((errno = posix_spawn(&pid, "/bin/Terminal", &spawn_actions, nullptr, const_cast<char**>(argv), environ))) {
39 perror("posix_spawn");
40 } else {
41 if (disown(pid) < 0)
42 perror("disown");
43 }
44 posix_spawn_file_actions_destroy(&spawn_actions);
45}
46
47NonnullRefPtr<GUI::Action> LauncherHandler::create_launch_action(Function<void(LauncherHandler const&)> launch_handler)
48{
49 auto icon = GUI::FileIconProvider::icon_for_executable(details().executable).bitmap_for_size(16);
50 return GUI::Action::create(details().name, move(icon), [this, launch_handler = move(launch_handler)](auto&) {
51 launch_handler(*this);
52 });
53}
54
55RefPtr<LauncherHandler> DirectoryView::get_default_launch_handler(Vector<NonnullRefPtr<LauncherHandler>> const& handlers)
56{
57 // If this is an application, pick it first
58 for (size_t i = 0; i < handlers.size(); i++) {
59 if (handlers[i]->details().launcher_type == Desktop::Launcher::LauncherType::Application)
60 return handlers[i];
61 }
62 // If there's a handler preferred by the user, pick this first
63 for (size_t i = 0; i < handlers.size(); i++) {
64 if (handlers[i]->details().launcher_type == Desktop::Launcher::LauncherType::UserPreferred)
65 return handlers[i];
66 }
67 // Otherwise, use the user's default, if available
68 for (size_t i = 0; i < handlers.size(); i++) {
69 if (handlers[i]->details().launcher_type == Desktop::Launcher::LauncherType::UserDefault)
70 return handlers[i];
71 }
72 // If still no match, use the first one we find
73 if (!handlers.is_empty()) {
74 return handlers[0];
75 }
76
77 return {};
78}
79
80Vector<NonnullRefPtr<LauncherHandler>> DirectoryView::get_launch_handlers(URL const& url)
81{
82 Vector<NonnullRefPtr<LauncherHandler>> handlers;
83 for (auto& h : Desktop::Launcher::get_handlers_with_details_for_url(url)) {
84 handlers.append(adopt_ref(*new LauncherHandler(h)));
85 }
86 return handlers;
87}
88
89Vector<NonnullRefPtr<LauncherHandler>> DirectoryView::get_launch_handlers(DeprecatedString const& path)
90{
91 return get_launch_handlers(URL::create_with_file_scheme(path));
92}
93
94void DirectoryView::handle_activation(GUI::ModelIndex const& index)
95{
96 if (!index.is_valid())
97 return;
98
99 auto& node = this->node(index);
100 auto path = node.full_path();
101
102 struct stat st;
103 if (stat(path.characters(), &st) < 0) {
104 perror("stat");
105 auto error_message = DeprecatedString::formatted("Could not stat {}: {}", path, strerror(errno));
106 GUI::MessageBox::show(window(), error_message, "File Manager"sv, GUI::MessageBox::Type::Error);
107 return;
108 }
109
110 if (S_ISDIR(st.st_mode)) {
111 if (is_desktop()) {
112 Desktop::Launcher::open(URL::create_with_file_scheme(path));
113 return;
114 }
115 open(path);
116 return;
117 }
118
119 auto url = URL::create_with_file_scheme(path);
120 auto launcher_handlers = get_launch_handlers(url);
121 auto default_launcher = get_default_launch_handler(launcher_handlers);
122
123 if (default_launcher) {
124 auto launch_origin_rect = current_view().to_widget_rect(current_view().content_rect(index)).translated(current_view().screen_relative_rect().location());
125 setenv("__libgui_launch_origin_rect", DeprecatedString::formatted("{},{},{},{}", launch_origin_rect.x(), launch_origin_rect.y(), launch_origin_rect.width(), launch_origin_rect.height()).characters(), 1);
126 launch(url, *default_launcher);
127 unsetenv("__libgui_launch_origin_rect");
128 } else {
129 auto error_message = DeprecatedString::formatted("Could not open {}", path);
130 GUI::MessageBox::show(window(), error_message, "File Manager"sv, GUI::MessageBox::Type::Error);
131 }
132}
133
134DirectoryView::DirectoryView(Mode mode)
135 : m_mode(mode)
136 , m_model(GUI::FileSystemModel::create({}))
137 , m_sorting_model(MUST(GUI::SortingProxyModel::create(m_model)))
138{
139 set_active_widget(nullptr);
140 set_grabbable_margins(2);
141
142 setup_actions();
143
144 m_error_label = add<GUI::Label>();
145 m_error_label->set_font(m_error_label->font().bold_variant());
146
147 setup_model();
148
149 setup_icon_view();
150 if (mode != Mode::Desktop) {
151 setup_columns_view();
152 setup_table_view();
153 }
154
155 set_view_mode(ViewMode::Icon);
156}
157
158GUI::FileSystemModel::Node const& DirectoryView::node(GUI::ModelIndex const& index) const
159{
160 return model().node(m_sorting_model->map_to_source(index));
161}
162
163void DirectoryView::setup_model()
164{
165 m_model->on_directory_change_error = [this](int, char const* error_string) {
166 auto failed_path = m_model->root_path();
167 auto error_message = DeprecatedString::formatted("Could not read {}:\n{}", failed_path, error_string);
168 m_error_label->set_text(error_message);
169 set_active_widget(m_error_label);
170
171 m_mkdir_action->set_enabled(false);
172 m_touch_action->set_enabled(false);
173
174 add_path_to_history(model().root_path());
175
176 if (on_path_change)
177 on_path_change(failed_path, false, false);
178 };
179
180 m_model->on_rename_error = [this](int, char const* error_string) {
181 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Unable to rename file: {}", error_string));
182 };
183
184 m_model->on_complete = [this] {
185 if (m_table_view)
186 m_table_view->selection().clear();
187 if (m_icon_view)
188 m_icon_view->selection().clear();
189
190 add_path_to_history(model().root_path());
191
192 bool can_write_in_path = access(model().root_path().characters(), W_OK) == 0;
193
194 m_mkdir_action->set_enabled(can_write_in_path);
195 m_touch_action->set_enabled(can_write_in_path);
196
197 if (on_path_change)
198 on_path_change(model().root_path(), true, can_write_in_path);
199 };
200
201 m_model->on_root_path_removed = [this] {
202 // Change model root to the first existing parent directory.
203 LexicalPath model_root(model().root_path());
204
205 while (model_root.string() != "/") {
206 model_root = model_root.parent();
207 if (Core::DeprecatedFile::is_directory(model_root.string()))
208 break;
209 }
210
211 open(model_root.string());
212 };
213
214 m_model->register_client(*this);
215
216 m_model->on_thumbnail_progress = [this](int done, int total) {
217 if (on_thumbnail_progress)
218 on_thumbnail_progress(done, total);
219 };
220
221 if (is_desktop())
222 m_model->set_root_path(Core::StandardPaths::desktop_directory());
223}
224
225void DirectoryView::setup_icon_view()
226{
227 m_icon_view = add<GUI::IconView>();
228 m_icon_view->set_should_hide_unnecessary_scrollbars(true);
229 m_icon_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
230 m_icon_view->set_editable(true);
231 m_icon_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
232 m_icon_view->aid_create_editing_delegate = [](auto&) {
233 return make<GUI::StringModelEditingDelegate>();
234 };
235
236 if (is_desktop()) {
237 m_icon_view->set_frame_shape(Gfx::FrameShape::NoFrame);
238 m_icon_view->set_frame_thickness(0);
239 m_icon_view->set_scrollbars_enabled(false);
240 m_icon_view->set_fill_with_background_color(false);
241 m_icon_view->set_draw_item_text_with_shadow(true);
242 m_icon_view->set_flow_direction(GUI::IconView::FlowDirection::TopToBottom);
243 m_icon_view->set_accepts_command_palette(false);
244 }
245
246 m_icon_view->set_model(m_sorting_model);
247 m_icon_view->set_model_column(GUI::FileSystemModel::Column::Name);
248 m_icon_view->on_activation = [&](auto& index) {
249 handle_activation(index);
250 };
251 m_icon_view->on_selection_change = [this] {
252 handle_selection_change();
253 };
254 m_icon_view->on_context_menu_request = [this](auto& index, auto& event) {
255 if (on_context_menu_request)
256 on_context_menu_request(index, event);
257 };
258 m_icon_view->on_drop = [this](auto& index, auto& event) {
259 handle_drop(index, event);
260 };
261}
262
263void DirectoryView::setup_columns_view()
264{
265 m_columns_view = add<GUI::ColumnsView>();
266 m_columns_view->set_should_hide_unnecessary_scrollbars(true);
267 m_columns_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
268 m_columns_view->set_editable(true);
269 m_columns_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
270 m_columns_view->aid_create_editing_delegate = [](auto&) {
271 return make<GUI::StringModelEditingDelegate>();
272 };
273
274 m_columns_view->set_model(m_sorting_model);
275 m_columns_view->set_model_column(GUI::FileSystemModel::Column::Name);
276
277 m_columns_view->on_activation = [&](auto& index) {
278 handle_activation(index);
279 };
280
281 m_columns_view->on_selection_change = [this] {
282 handle_selection_change();
283 };
284
285 m_columns_view->on_context_menu_request = [this](auto& index, auto& event) {
286 if (on_context_menu_request)
287 on_context_menu_request(index, event);
288 };
289
290 m_columns_view->on_drop = [this](auto& index, auto& event) {
291 handle_drop(index, event);
292 };
293}
294
295void DirectoryView::setup_table_view()
296{
297 m_table_view = add<GUI::TableView>();
298 m_table_view->set_should_hide_unnecessary_scrollbars(true);
299 m_table_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
300 m_table_view->set_editable(true);
301 m_table_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
302 m_table_view->aid_create_editing_delegate = [](auto&) {
303 return make<GUI::StringModelEditingDelegate>();
304 };
305
306 m_table_view->set_model(m_sorting_model);
307 m_table_view->set_key_column_and_sort_order(GUI::FileSystemModel::Column::Name, GUI::SortOrder::Ascending);
308
309 m_table_view->set_column_visible(GUI::FileSystemModel::Column::Inode, false);
310 m_table_view->set_column_visible(GUI::FileSystemModel::Column::SymlinkTarget, false);
311
312 m_table_view->on_activation = [&](auto& index) {
313 handle_activation(index);
314 };
315
316 m_table_view->on_selection_change = [this] {
317 handle_selection_change();
318 };
319
320 m_table_view->on_context_menu_request = [this](auto& index, auto& event) {
321 if (on_context_menu_request)
322 on_context_menu_request(index, event);
323 };
324
325 m_table_view->on_drop = [this](auto& index, auto& event) {
326 handle_drop(index, event);
327 };
328}
329
330DirectoryView::~DirectoryView()
331{
332 m_model->unregister_client(*this);
333}
334
335void DirectoryView::model_did_update(unsigned flags)
336{
337 if (flags & GUI::Model::UpdateFlag::InvalidateAllIndices) {
338 for_each_view_implementation([](auto& view) {
339 view.selection().clear();
340 });
341 }
342 update_statusbar();
343}
344
345void DirectoryView::set_view_mode_from_string(DeprecatedString const& mode)
346{
347 if (m_mode == Mode::Desktop)
348 return;
349
350 if (mode.contains("Table"sv)) {
351 set_view_mode(DirectoryView::ViewMode::Table);
352 m_view_as_table_action->set_checked(true);
353 } else if (mode.contains("Columns"sv)) {
354 set_view_mode(DirectoryView::ViewMode::Columns);
355 m_view_as_columns_action->set_checked(true);
356 } else {
357 set_view_mode(DirectoryView::ViewMode::Icon);
358 m_view_as_icons_action->set_checked(true);
359 }
360}
361
362void DirectoryView::config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value)
363{
364 if (domain != "FileManager" || group != "DirectoryView")
365 return;
366
367 if (key == "ViewMode") {
368 set_view_mode_from_string(value);
369 return;
370 }
371}
372
373void DirectoryView::set_view_mode(ViewMode mode)
374{
375 if (m_view_mode == mode)
376 return;
377 m_view_mode = mode;
378 update();
379 if (mode == ViewMode::Table) {
380 set_active_widget(m_table_view);
381 return;
382 }
383 if (mode == ViewMode::Columns) {
384 set_active_widget(m_columns_view);
385 return;
386 }
387 if (mode == ViewMode::Icon) {
388 set_active_widget(m_icon_view);
389 return;
390 }
391 VERIFY_NOT_REACHED();
392}
393
394void DirectoryView::add_path_to_history(DeprecatedString path)
395{
396 if (m_path_history.size() && m_path_history.at(m_path_history_position) == path)
397 return;
398
399 if (m_path_history_position < m_path_history.size())
400 m_path_history.resize(m_path_history_position + 1);
401
402 m_path_history.append(move(path));
403 m_path_history_position = m_path_history.size() - 1;
404}
405
406bool DirectoryView::open(DeprecatedString const& path)
407{
408 auto real_path = Core::DeprecatedFile::real_path_for(path);
409 if (real_path.is_null() || !Core::DeprecatedFile::is_directory(path))
410 return false;
411
412 if (chdir(real_path.characters()) < 0) {
413 perror("chdir");
414 }
415 if (model().root_path() == real_path) {
416 refresh();
417 } else {
418 set_active_widget(¤t_view());
419 model().set_root_path(real_path);
420 }
421 return true;
422}
423
424void DirectoryView::set_status_message(StringView message)
425{
426 if (on_status_message)
427 on_status_message(message);
428}
429
430void DirectoryView::open_parent_directory()
431{
432 open("..");
433}
434
435void DirectoryView::refresh()
436{
437 model().invalidate();
438}
439
440void DirectoryView::open_previous_directory()
441{
442 if (m_path_history_position > 0)
443 open(m_path_history[--m_path_history_position]);
444}
445void DirectoryView::open_next_directory()
446{
447 if (m_path_history_position < m_path_history.size() - 1)
448 open(m_path_history[++m_path_history_position]);
449}
450
451void DirectoryView::update_statusbar()
452{
453 // If we're triggered during widget construction, just ignore it.
454 if (m_view_mode == ViewMode::Invalid)
455 return;
456
457 StringBuilder builder;
458
459 if (current_view().selection().is_empty()) {
460 int total_item_count = model().row_count();
461 size_t total_size = model().node({}).total_size;
462 builder.appendff("{} item{} ({})", total_item_count, total_item_count != 1 ? "s" : "", human_readable_size(total_size));
463 set_status_message(builder.string_view());
464 return;
465 }
466
467 int selected_item_count = current_view().selection().size();
468 size_t selected_byte_count = 0;
469
470 current_view().selection().for_each_index([&](auto& index) {
471 auto const& node = this->node(index);
472 selected_byte_count += node.size;
473 });
474
475 builder.appendff("{} item{} selected ({})", selected_item_count, selected_item_count != 1 ? "s" : "", human_readable_size(selected_byte_count));
476
477 if (selected_item_count == 1) {
478 auto& node = this->node(current_view().selection().first());
479 if (!node.symlink_target.is_empty()) {
480 builder.append(" → "sv);
481 builder.append(node.symlink_target);
482 }
483 }
484
485 set_status_message(builder.string_view());
486}
487
488void DirectoryView::set_should_show_dotfiles(bool show_dotfiles)
489{
490 m_model->set_should_show_dotfiles(show_dotfiles);
491}
492
493void DirectoryView::launch(URL const&, LauncherHandler const& launcher_handler) const
494{
495 pid_t child;
496
497 posix_spawnattr_t spawn_attributes;
498 posix_spawnattr_init(&spawn_attributes);
499
500 posix_spawnattr_setpgroup(&spawn_attributes, getsid(0));
501
502 short current_flag;
503 posix_spawnattr_getflags(&spawn_attributes, ¤t_flag);
504 posix_spawnattr_setflags(&spawn_attributes, static_cast<short>(current_flag | POSIX_SPAWN_SETPGROUP));
505
506 if (launcher_handler.details().launcher_type == Desktop::Launcher::LauncherType::Application) {
507 posix_spawn_file_actions_t spawn_actions;
508 posix_spawn_file_actions_init(&spawn_actions);
509 posix_spawn_file_actions_addchdir(&spawn_actions, path().characters());
510
511 char const* argv[] = { launcher_handler.details().name.characters(), nullptr };
512 errno = posix_spawn(&child, launcher_handler.details().executable.characters(), &spawn_actions, &spawn_attributes, const_cast<char**>(argv), environ);
513 if (errno) {
514 perror("posix_spawn");
515 } else if (disown(child) < 0) {
516 perror("disown");
517 }
518 posix_spawn_file_actions_destroy(&spawn_actions);
519 } else {
520 for (auto& path : selected_file_paths()) {
521 char const* argv[] = { launcher_handler.details().name.characters(), path.characters(), nullptr };
522 if ((errno = posix_spawn(&child, launcher_handler.details().executable.characters(), nullptr, &spawn_attributes, const_cast<char**>(argv), environ)))
523 continue;
524 if (disown(child) < 0)
525 perror("disown");
526 }
527 }
528}
529
530Vector<DeprecatedString> DirectoryView::selected_file_paths() const
531{
532 Vector<DeprecatedString> paths;
533 auto& view = current_view();
534 auto& model = *view.model();
535 view.selection().for_each_index([&](GUI::ModelIndex const& index) {
536 auto parent_index = model.parent_index(index);
537 auto name_index = model.index(index.row(), GUI::FileSystemModel::Column::Name, parent_index);
538 auto path = name_index.data(GUI::ModelRole::Custom).to_deprecated_string();
539 paths.append(path);
540 });
541 return paths;
542}
543
544void DirectoryView::do_delete(bool should_confirm)
545{
546 auto paths = selected_file_paths();
547 VERIFY(!paths.is_empty());
548 delete_paths(paths, should_confirm, window());
549 current_view().selection().clear();
550}
551
552bool DirectoryView::can_modify_current_selection()
553{
554 auto selections = current_view().selection().indices();
555 // FIXME: remove once Clang formats this properly.
556 // clang-format off
557 return selections.first_matching([&](auto& index) {
558 return Core::DeprecatedFile::can_delete_or_move(node(index).full_path());
559 }).has_value();
560 // clang-format on
561}
562
563void DirectoryView::handle_selection_change()
564{
565 update_statusbar();
566
567 bool can_modify = can_modify_current_selection();
568 m_delete_action->set_enabled(can_modify);
569 m_force_delete_action->set_enabled(can_modify);
570 m_rename_action->set_enabled(can_modify);
571
572 if (on_selection_change)
573 on_selection_change(current_view());
574}
575
576void DirectoryView::setup_actions()
577{
578 m_mkdir_action = GUI::Action::create("&New Directory...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"sv).release_value_but_fixme_should_propagate_errors(), [&](GUI::Action const&) {
579 DeprecatedString value;
580 if (GUI::InputBox::show(window(), value, "Enter name:"sv, "New directory"sv, GUI::InputType::NonemptyText) == GUI::InputBox::ExecResult::OK) {
581 auto new_dir_path = LexicalPath::canonicalized_path(DeprecatedString::formatted("{}/{}", path(), value));
582 int rc = mkdir(new_dir_path.characters(), 0777);
583 if (rc < 0) {
584 auto saved_errno = errno;
585 GUI::MessageBox::show(window(), DeprecatedString::formatted("mkdir(\"{}\") failed: {}", new_dir_path, strerror(saved_errno)), "Error"sv, GUI::MessageBox::Type::Error);
586 }
587 }
588 });
589
590 m_touch_action = GUI::Action::create("New &File...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"sv).release_value_but_fixme_should_propagate_errors(), [&](GUI::Action const&) {
591 DeprecatedString value;
592 if (GUI::InputBox::show(window(), value, "Enter name:"sv, "New file"sv, GUI::InputType::NonemptyText) == GUI::InputBox::ExecResult::OK) {
593 auto new_file_path = LexicalPath::canonicalized_path(DeprecatedString::formatted("{}/{}", path(), value));
594 struct stat st;
595 int rc = stat(new_file_path.characters(), &st);
596 if ((rc < 0 && errno != ENOENT)) {
597 auto saved_errno = errno;
598 GUI::MessageBox::show(window(), DeprecatedString::formatted("stat(\"{}\") failed: {}", new_file_path, strerror(saved_errno)), "Error"sv, GUI::MessageBox::Type::Error);
599 return;
600 }
601 if (rc == 0) {
602 GUI::MessageBox::show(window(), DeprecatedString::formatted("{}: Already exists", new_file_path), "Error"sv, GUI::MessageBox::Type::Error);
603 return;
604 }
605 int fd = creat(new_file_path.characters(), 0666);
606 if (fd < 0) {
607 auto saved_errno = errno;
608 GUI::MessageBox::show(window(), DeprecatedString::formatted("creat(\"{}\") failed: {}", new_file_path, strerror(saved_errno)), "Error"sv, GUI::MessageBox::Type::Error);
609 return;
610 }
611 rc = close(fd);
612 VERIFY(rc >= 0);
613 }
614 });
615
616 m_open_terminal_action = GUI::Action::create("Open &Terminal Here", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) {
617 spawn_terminal(path());
618 });
619
620 m_delete_action = GUI::CommonActions::make_delete_action([this](auto&) { do_delete(true); }, window());
621 m_rename_action = GUI::CommonActions::make_rename_action([this](auto&) {
622 if (can_modify_current_selection())
623 current_view().begin_editing(current_view().cursor_index());
624 },
625 window());
626
627 m_force_delete_action = GUI::Action::create(
628 "Delete Without Confirmation", { Mod_Shift, Key_Delete },
629 [this](auto&) { do_delete(false); },
630 window());
631
632 m_view_as_icons_action = GUI::Action::create_checkable(
633 "View as &Icons", { Mod_Ctrl, KeyCode::Key_1 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/icon-view.png"sv).release_value_but_fixme_should_propagate_errors(), [&](GUI::Action const&) {
634 set_view_mode(DirectoryView::ViewMode::Icon);
635 Config::write_string("FileManager"sv, "DirectoryView"sv, "ViewMode"sv, "Icon"sv);
636 },
637 window());
638
639 m_view_as_table_action = GUI::Action::create_checkable(
640 "View as &Table", { Mod_Ctrl, KeyCode::Key_2 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/table-view.png"sv).release_value_but_fixme_should_propagate_errors(), [&](GUI::Action const&) {
641 set_view_mode(DirectoryView::ViewMode::Table);
642 Config::write_string("FileManager"sv, "DirectoryView"sv, "ViewMode"sv, "Table"sv);
643 },
644 window());
645
646 m_view_as_columns_action = GUI::Action::create_checkable(
647 "View as &Columns", { Mod_Ctrl, KeyCode::Key_3 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/columns-view.png"sv).release_value_but_fixme_should_propagate_errors(), [&](GUI::Action const&) {
648 set_view_mode(DirectoryView::ViewMode::Columns);
649 Config::write_string("FileManager"sv, "DirectoryView"sv, "ViewMode"sv, "Columns"sv);
650 },
651 window());
652
653 if (m_mode == Mode::Desktop) {
654 m_view_as_icons_action->set_enabled(false);
655 m_view_as_table_action->set_enabled(false);
656 m_view_as_columns_action->set_enabled(false);
657 }
658}
659
660void DirectoryView::handle_drop(GUI::ModelIndex const& index, GUI::DropEvent const& event)
661{
662 auto const& target_node = node(index);
663
664 bool const has_accepted_drop = ::FileManager::handle_drop(event, target_node.full_path(), window()).release_value_but_fixme_should_propagate_errors();
665
666 if (has_accepted_drop && on_accepted_drop)
667 on_accepted_drop();
668}
669
670}