Serenity Operating System
at master 670 lines 24 kB view raw
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(&current_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, &current_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}