Serenity Operating System
at master 1298 lines 58 kB view raw
1/* 2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org> 4 * Copyright (c) 2021, Mustafa Quraish <mustafa@cs.toronto.edu> 5 * Copyright (c) 2022-2023, the SerenityOS developers. 6 * 7 * SPDX-License-Identifier: BSD-2-Clause 8 */ 9 10#include "DesktopWidget.h" 11#include "DirectoryView.h" 12#include "FileUtils.h" 13#include "PropertiesWindow.h" 14#include <AK/LexicalPath.h> 15#include <AK/StringBuilder.h> 16#include <AK/Try.h> 17#include <AK/URL.h> 18#include <Applications/FileManager/FileManagerWindowGML.h> 19#include <LibConfig/Client.h> 20#include <LibConfig/Listener.h> 21#include <LibCore/ArgsParser.h> 22#include <LibCore/DeprecatedFile.h> 23#include <LibCore/Process.h> 24#include <LibCore/StandardPaths.h> 25#include <LibCore/System.h> 26#include <LibDesktop/Launcher.h> 27#include <LibGUI/Action.h> 28#include <LibGUI/ActionGroup.h> 29#include <LibGUI/Application.h> 30#include <LibGUI/BoxLayout.h> 31#include <LibGUI/Clipboard.h> 32#include <LibGUI/Desktop.h> 33#include <LibGUI/FileIconProvider.h> 34#include <LibGUI/FileSystemModel.h> 35#include <LibGUI/InputBox.h> 36#include <LibGUI/Menu.h> 37#include <LibGUI/Menubar.h> 38#include <LibGUI/MessageBox.h> 39#include <LibGUI/Painter.h> 40#include <LibGUI/PathBreadcrumbbar.h> 41#include <LibGUI/Progressbar.h> 42#include <LibGUI/Splitter.h> 43#include <LibGUI/Statusbar.h> 44#include <LibGUI/Toolbar.h> 45#include <LibGUI/ToolbarContainer.h> 46#include <LibGUI/TreeView.h> 47#include <LibGUI/Widget.h> 48#include <LibGUI/Window.h> 49#include <LibGfx/Palette.h> 50#include <LibMain/Main.h> 51#include <pthread.h> 52#include <signal.h> 53#include <stdio.h> 54#include <string.h> 55#include <sys/wait.h> 56#include <unistd.h> 57 58using namespace FileManager; 59 60static ErrorOr<int> run_in_desktop_mode(); 61static ErrorOr<int> run_in_windowed_mode(DeprecatedString const& initial_location, DeprecatedString const& entry_focused_on_init); 62static void do_copy(Vector<DeprecatedString> const& selected_file_paths, FileOperation file_operation); 63static void do_paste(DeprecatedString const& target_directory, GUI::Window* window); 64static void do_create_link(Vector<DeprecatedString> const& selected_file_paths, GUI::Window* window); 65static void do_create_archive(Vector<DeprecatedString> const& selected_file_paths, GUI::Window* window); 66static void do_set_wallpaper(DeprecatedString const& file_path, GUI::Window* window); 67static void do_unzip_archive(Vector<DeprecatedString> const& selected_file_paths, GUI::Window* window); 68static void show_properties(DeprecatedString const& container_dir_path, DeprecatedString const& path, Vector<DeprecatedString> const& selected, GUI::Window* window); 69static bool add_launch_handler_actions_to_menu(RefPtr<GUI::Menu>& menu, DirectoryView const& directory_view, DeprecatedString const& full_path, RefPtr<GUI::Action>& default_action, Vector<NonnullRefPtr<LauncherHandler>>& current_file_launch_handlers); 70 71ErrorOr<int> serenity_main(Main::Arguments arguments) 72{ 73 TRY(Core::System::pledge("stdio thread recvfd sendfd unix cpath rpath wpath fattr proc exec sigaction")); 74 75 struct sigaction act = {}; 76 act.sa_flags = SA_NOCLDWAIT; 77 act.sa_handler = SIG_IGN; 78 TRY(Core::System::sigaction(SIGCHLD, &act, nullptr)); 79 80 Core::ArgsParser args_parser; 81 bool is_desktop_mode { false }; 82 bool is_selection_mode { false }; 83 bool ignore_path_resolution { false }; 84 DeprecatedString initial_location; 85 args_parser.add_option(is_desktop_mode, "Run in desktop mode", "desktop", 'd'); 86 args_parser.add_option(is_selection_mode, "Show entry in parent folder", "select", 's'); 87 args_parser.add_option(ignore_path_resolution, "Use raw path, do not resolve real path", "raw", 'r'); 88 args_parser.add_positional_argument(initial_location, "Path to open", "path", Core::ArgsParser::Required::No); 89 args_parser.parse(arguments); 90 91 auto app = TRY(GUI::Application::try_create(arguments)); 92 93 TRY(Core::System::pledge("stdio thread recvfd sendfd cpath rpath wpath fattr proc exec unix")); 94 95 Config::pledge_domains({ "FileManager", "WindowManager" }); 96 Config::monitor_domain("FileManager"); 97 Config::monitor_domain("WindowManager"); 98 99 if (is_desktop_mode) 100 return run_in_desktop_mode(); 101 102 // our initial location is defined as, in order of precedence: 103 // 1. the command-line path argument (e.g. FileManager /bin) 104 // 2. the current directory 105 // 3. the user's home directory 106 // 4. the root directory 107 108 if (!initial_location.is_empty()) { 109 if (!ignore_path_resolution) 110 initial_location = Core::DeprecatedFile::real_path_for(initial_location); 111 112 if (!Core::DeprecatedFile::is_directory(initial_location)) 113 is_selection_mode = true; 114 } 115 116 if (initial_location.is_empty()) 117 initial_location = Core::DeprecatedFile::current_working_directory(); 118 119 if (initial_location.is_empty()) 120 initial_location = Core::StandardPaths::home_directory(); 121 122 if (initial_location.is_empty()) 123 initial_location = "/"; 124 125 DeprecatedString focused_entry; 126 if (is_selection_mode) { 127 LexicalPath path(initial_location); 128 initial_location = path.dirname(); 129 focused_entry = path.basename(); 130 } 131 132 return run_in_windowed_mode(initial_location, focused_entry); 133} 134 135void do_copy(Vector<DeprecatedString> const& selected_file_paths, FileOperation file_operation) 136{ 137 VERIFY(!selected_file_paths.is_empty()); 138 139 StringBuilder copy_text; 140 if (file_operation == FileOperation::Move) { 141 copy_text.append("#cut\n"sv); // This exploits the comment lines in the text/uri-list specification, which might be a bit hackish 142 } 143 for (auto& path : selected_file_paths) { 144 auto url = URL::create_with_file_scheme(path); 145 copy_text.appendff("{}\n", url); 146 } 147 GUI::Clipboard::the().set_data(copy_text.to_deprecated_string().bytes(), "text/uri-list"); 148} 149 150void do_paste(DeprecatedString const& target_directory, GUI::Window* window) 151{ 152 auto data_and_type = GUI::Clipboard::the().fetch_data_and_type(); 153 if (data_and_type.mime_type != "text/uri-list") { 154 dbgln("Cannot paste clipboard type {}", data_and_type.mime_type); 155 return; 156 } 157 auto copied_lines = DeprecatedString::copy(data_and_type.data).split('\n'); 158 if (copied_lines.is_empty()) { 159 dbgln("No files to paste"); 160 return; 161 } 162 163 FileOperation file_operation = FileOperation::Copy; 164 if (copied_lines[0] == "#cut") { // cut operation encoded as a text/uri-list comment 165 file_operation = FileOperation::Move; 166 copied_lines.remove(0); 167 } 168 169 Vector<DeprecatedString> source_paths; 170 for (auto& uri_as_string : copied_lines) { 171 if (uri_as_string.is_empty()) 172 continue; 173 URL url = uri_as_string; 174 if (!url.is_valid() || url.scheme() != "file") { 175 dbgln("Cannot paste URI {}", uri_as_string); 176 continue; 177 } 178 source_paths.append(url.path()); 179 } 180 181 if (!source_paths.is_empty()) { 182 if (auto result = run_file_operation(file_operation, source_paths, target_directory, window); result.is_error()) 183 dbgln("Failed to paste files: {}", result.error()); 184 } 185} 186 187void do_create_link(Vector<DeprecatedString> const& selected_file_paths, GUI::Window* window) 188{ 189 auto path = selected_file_paths.first(); 190 auto destination = DeprecatedString::formatted("{}/{}", Core::StandardPaths::desktop_directory(), LexicalPath::basename(path)); 191 if (auto result = Core::DeprecatedFile::link_file(destination, path); result.is_error()) { 192 GUI::MessageBox::show(window, DeprecatedString::formatted("Could not create desktop shortcut:\n{}", result.error()), "File Manager"sv, 193 GUI::MessageBox::Type::Error); 194 } 195} 196 197void do_create_archive(Vector<DeprecatedString> const& selected_file_paths, GUI::Window* window) 198{ 199 DeprecatedString archive_name; 200 if (GUI::InputBox::show(window, archive_name, "Enter name:"sv, "Create Archive"sv) != GUI::InputBox::ExecResult::OK) 201 return; 202 203 auto output_directory_path = LexicalPath(selected_file_paths.first()); 204 205 StringBuilder path_builder; 206 path_builder.append(output_directory_path.dirname()); 207 path_builder.append('/'); 208 if (archive_name.is_empty()) { 209 path_builder.append(output_directory_path.parent().basename()); 210 path_builder.append(".zip"sv); 211 } else { 212 path_builder.append(archive_name); 213 if (!archive_name.ends_with(".zip"sv)) 214 path_builder.append(".zip"sv); 215 } 216 auto output_path = path_builder.to_deprecated_string(); 217 218 pid_t zip_pid = fork(); 219 if (zip_pid < 0) { 220 perror("fork"); 221 VERIFY_NOT_REACHED(); 222 } 223 224 if (!zip_pid) { 225 Vector<DeprecatedString> relative_paths; 226 Vector<char const*> arg_list; 227 arg_list.append("/bin/zip"); 228 arg_list.append("-r"); 229 arg_list.append("-f"); 230 arg_list.append(output_path.characters()); 231 for (auto const& path : selected_file_paths) { 232 relative_paths.append(LexicalPath::relative_path(path, output_directory_path.dirname())); 233 arg_list.append(relative_paths.last().characters()); 234 } 235 arg_list.append(nullptr); 236 int rc = execvp("/bin/zip", const_cast<char* const*>(arg_list.data())); 237 if (rc < 0) { 238 perror("execvp"); 239 _exit(1); 240 } 241 } else { 242 int status; 243 int rc = waitpid(zip_pid, &status, 0); 244 if (rc < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) 245 GUI::MessageBox::show(window, "Could not create archive"sv, "Archive Error"sv, GUI::MessageBox::Type::Error); 246 } 247} 248 249void do_set_wallpaper(DeprecatedString const& file_path, GUI::Window* window) 250{ 251 auto show_error = [&] { 252 GUI::MessageBox::show(window, DeprecatedString::formatted("Failed to set {} as wallpaper.", file_path), "Failed to set wallpaper"sv, GUI::MessageBox::Type::Error); 253 }; 254 255 auto bitmap_or_error = Gfx::Bitmap::load_from_file(file_path); 256 if (bitmap_or_error.is_error()) { 257 show_error(); 258 return; 259 } 260 261 if (!GUI::Desktop::the().set_wallpaper(bitmap_or_error.release_value(), file_path)) 262 show_error(); 263} 264 265void do_unzip_archive(Vector<DeprecatedString> const& selected_file_paths, GUI::Window* window) 266{ 267 DeprecatedString archive_file_path = selected_file_paths.first(); 268 DeprecatedString output_directory_path = archive_file_path.substring(0, archive_file_path.length() - 4); 269 270 pid_t unzip_pid = fork(); 271 if (unzip_pid < 0) { 272 perror("fork"); 273 VERIFY_NOT_REACHED(); 274 } 275 276 if (!unzip_pid) { 277 int rc = execlp("/bin/unzip", "/bin/unzip", "-d", output_directory_path.characters(), archive_file_path.characters(), nullptr); 278 if (rc < 0) { 279 perror("execlp"); 280 _exit(1); 281 } 282 } else { 283 // FIXME: this could probably be tied in with the new file operation progress tracking 284 int status; 285 int rc = waitpid(unzip_pid, &status, 0); 286 if (rc < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) 287 GUI::MessageBox::show(window, "Could not extract archive"sv, "Extract Archive Error"sv, GUI::MessageBox::Type::Error); 288 } 289} 290 291void show_properties(DeprecatedString const& container_dir_path, DeprecatedString const& path, Vector<DeprecatedString> const& selected, GUI::Window* window) 292{ 293 ErrorOr<RefPtr<PropertiesWindow>> properties_or_error = nullptr; 294 if (selected.is_empty()) { 295 properties_or_error = window->try_add<PropertiesWindow>(path, true); 296 } else { 297 properties_or_error = window->try_add<PropertiesWindow>(selected.first(), access(container_dir_path.characters(), W_OK) != 0); 298 } 299 300 if (properties_or_error.is_error()) { 301 GUI::MessageBox::show(window, "Could not show properties"sv, "Properties Error"sv, GUI::MessageBox::Type::Error); 302 return; 303 } 304 305 auto properties = properties_or_error.release_value(); 306 properties->on_close = [properties = properties.ptr()] { 307 properties->remove_from_parent(); 308 }; 309 properties->center_on_screen(); 310 properties->show(); 311} 312 313bool add_launch_handler_actions_to_menu(RefPtr<GUI::Menu>& menu, DirectoryView const& directory_view, DeprecatedString const& full_path, RefPtr<GUI::Action>& default_action, Vector<NonnullRefPtr<LauncherHandler>>& current_file_launch_handlers) 314{ 315 current_file_launch_handlers = directory_view.get_launch_handlers(full_path); 316 317 bool added_open_menu_items = false; 318 auto default_file_handler = directory_view.get_default_launch_handler(current_file_launch_handlers); 319 if (default_file_handler) { 320 auto file_open_action = default_file_handler->create_launch_action([&, full_path = move(full_path)](auto& launcher_handler) { 321 directory_view.launch(URL::create_with_file_scheme(full_path), launcher_handler); 322 }); 323 if (default_file_handler->details().launcher_type == Desktop::Launcher::LauncherType::Application) 324 file_open_action->set_text(DeprecatedString::formatted("Run {}", file_open_action->text())); 325 else 326 file_open_action->set_text(DeprecatedString::formatted("Open in {}", file_open_action->text())); 327 328 default_action = file_open_action; 329 330 menu->add_action(move(file_open_action)); 331 added_open_menu_items = true; 332 } else { 333 default_action.clear(); 334 } 335 336 if (current_file_launch_handlers.size() > 1) { 337 added_open_menu_items = true; 338 auto& file_open_with_menu = menu->add_submenu("Open with"); 339 for (auto& handler : current_file_launch_handlers) { 340 if (handler == default_file_handler) 341 continue; 342 file_open_with_menu.add_action(handler->create_launch_action([&, full_path = move(full_path)](auto& launcher_handler) { 343 directory_view.launch(URL::create_with_file_scheme(full_path), launcher_handler); 344 })); 345 } 346 } 347 348 return added_open_menu_items; 349} 350 351ErrorOr<int> run_in_desktop_mode() 352{ 353 (void)Core::Process::set_name("FileManager (Desktop)"sv, Core::Process::SetThreadName::Yes); 354 355 auto window = TRY(GUI::Window::try_create()); 356 window->set_title("Desktop Manager"); 357 window->set_window_type(GUI::WindowType::Desktop); 358 window->set_has_alpha_channel(true); 359 360 auto desktop_widget = TRY(window->set_main_widget<FileManager::DesktopWidget>()); 361 TRY(desktop_widget->try_set_layout<GUI::VerticalBoxLayout>()); 362 363 auto directory_view = TRY(desktop_widget->try_add<DirectoryView>(DirectoryView::Mode::Desktop)); 364 directory_view->set_name("directory_view"); 365 366 auto cut_action = GUI::CommonActions::make_cut_action( 367 [&](auto&) { 368 auto paths = directory_view->selected_file_paths(); 369 VERIFY(!paths.is_empty()); 370 371 do_copy(paths, FileOperation::Move); 372 }, 373 window); 374 cut_action->set_enabled(false); 375 376 auto copy_action = GUI::CommonActions::make_copy_action( 377 [&](auto&) { 378 auto paths = directory_view->selected_file_paths(); 379 VERIFY(!paths.is_empty()); 380 381 do_copy(paths, FileOperation::Copy); 382 }, 383 window); 384 copy_action->set_enabled(false); 385 386 auto create_archive_action 387 = GUI::Action::create( 388 "Create &Archive", 389 TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-archive.png"sv)), 390 [&](GUI::Action const&) { 391 auto paths = directory_view->selected_file_paths(); 392 if (paths.is_empty()) 393 return; 394 395 do_create_archive(paths, directory_view->window()); 396 }, 397 window); 398 399 auto unzip_archive_action 400 = GUI::Action::create( 401 "E&xtract Here", 402 [&](GUI::Action const&) { 403 auto paths = directory_view->selected_file_paths(); 404 if (paths.is_empty()) 405 return; 406 407 do_unzip_archive(paths, directory_view->window()); 408 }, 409 window); 410 411 auto set_wallpaper_action 412 = GUI::Action::create( 413 "Set as Desktop &Wallpaper", 414 TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-display-settings.png"sv)), 415 [&](GUI::Action const&) { 416 auto paths = directory_view->selected_file_paths(); 417 if (paths.is_empty()) 418 return; 419 420 do_set_wallpaper(paths.first(), directory_view->window()); 421 }, 422 window); 423 424 directory_view->on_selection_change = [&](GUI::AbstractView const& view) { 425 cut_action->set_enabled(!view.selection().is_empty()); 426 copy_action->set_enabled(!view.selection().is_empty()); 427 }; 428 429 auto properties_action = GUI::CommonActions::make_properties_action( 430 [&](auto&) { 431 DeprecatedString path = directory_view->path(); 432 Vector<DeprecatedString> selected = directory_view->selected_file_paths(); 433 434 show_properties(path, path, selected, directory_view->window()); 435 }, 436 window); 437 438 auto paste_action = GUI::CommonActions::make_paste_action( 439 [&](GUI::Action const&) { 440 do_paste(directory_view->path(), directory_view->window()); 441 }, 442 window); 443 paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type() == "text/uri-list" && access(directory_view->path().characters(), W_OK) == 0); 444 445 GUI::Clipboard::the().on_change = [&](DeprecatedString const& data_type) { 446 paste_action->set_enabled(data_type == "text/uri-list" && access(directory_view->path().characters(), W_OK) == 0); 447 }; 448 449 auto desktop_view_context_menu = TRY(GUI::Menu::try_create("Directory View")); 450 451 auto file_manager_action = GUI::Action::create("Open in File &Manager", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-file-manager.png"sv)), [&](auto&) { 452 auto paths = directory_view->selected_file_paths(); 453 if (paths.is_empty()) { 454 Desktop::Launcher::open(URL::create_with_file_scheme(directory_view->path())); 455 return; 456 } 457 458 for (auto& path : paths) { 459 if (Core::DeprecatedFile::is_directory(path)) 460 Desktop::Launcher::open(URL::create_with_file_scheme(path)); 461 } 462 }); 463 464 auto open_terminal_action = GUI::Action::create("Open in &Terminal", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"sv)), [&](auto&) { 465 auto paths = directory_view->selected_file_paths(); 466 if (paths.is_empty()) { 467 spawn_terminal(directory_view->path()); 468 return; 469 } 470 471 for (auto& path : paths) { 472 if (Core::DeprecatedFile::is_directory(path)) { 473 spawn_terminal(path); 474 } 475 } 476 }); 477 478 auto display_properties_action = GUI::Action::create("&Display Settings", {}, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-display-settings.png"sv)), [&](GUI::Action const&) { 479 Desktop::Launcher::open(URL::create_with_file_scheme("/bin/DisplaySettings")); 480 }); 481 482 TRY(desktop_view_context_menu->try_add_action(directory_view->mkdir_action())); 483 TRY(desktop_view_context_menu->try_add_action(directory_view->touch_action())); 484 TRY(desktop_view_context_menu->try_add_action(paste_action)); 485 TRY(desktop_view_context_menu->try_add_separator()); 486 TRY(desktop_view_context_menu->try_add_action(file_manager_action)); 487 TRY(desktop_view_context_menu->try_add_action(open_terminal_action)); 488 TRY(desktop_view_context_menu->try_add_separator()); 489 TRY(desktop_view_context_menu->try_add_action(display_properties_action)); 490 491 auto desktop_context_menu = TRY(GUI::Menu::try_create("Directory View Directory")); 492 493 TRY(desktop_context_menu->try_add_action(file_manager_action)); 494 TRY(desktop_context_menu->try_add_action(open_terminal_action)); 495 TRY(desktop_context_menu->try_add_separator()); 496 TRY(desktop_context_menu->try_add_action(cut_action)); 497 TRY(desktop_context_menu->try_add_action(copy_action)); 498 TRY(desktop_context_menu->try_add_action(paste_action)); 499 TRY(desktop_context_menu->try_add_action(directory_view->delete_action())); 500 TRY(desktop_context_menu->try_add_action(directory_view->rename_action())); 501 TRY(desktop_context_menu->try_add_separator()); 502 TRY(desktop_context_menu->try_add_action(properties_action)); 503 504 RefPtr<GUI::Menu> file_context_menu; 505 Vector<NonnullRefPtr<LauncherHandler>> current_file_handlers; 506 RefPtr<GUI::Action> file_context_menu_action_default_action; 507 508 directory_view->on_context_menu_request = [&](GUI::ModelIndex const& index, GUI::ContextMenuEvent const& event) { 509 if (index.is_valid()) { 510 auto& node = directory_view->node(index); 511 if (node.is_directory()) { 512 desktop_context_menu->popup(event.screen_position(), file_manager_action); 513 } else { 514 file_context_menu = GUI::Menu::construct("Directory View File"); 515 516 bool added_open_menu_items = add_launch_handler_actions_to_menu(file_context_menu, directory_view, node.full_path(), file_context_menu_action_default_action, current_file_handlers); 517 if (added_open_menu_items) 518 file_context_menu->add_separator(); 519 520 file_context_menu->add_action(cut_action); 521 file_context_menu->add_action(copy_action); 522 file_context_menu->add_action(paste_action); 523 file_context_menu->add_action(directory_view->delete_action()); 524 file_context_menu->add_action(directory_view->rename_action()); 525 file_context_menu->add_action(create_archive_action); 526 file_context_menu->add_separator(); 527 528 if (Gfx::Bitmap::is_path_a_supported_image_format(node.name)) { 529 file_context_menu->add_action(set_wallpaper_action); 530 file_context_menu->add_separator(); 531 } 532 533 if (node.full_path().ends_with(".zip"sv, AK::CaseSensitivity::CaseInsensitive)) { 534 file_context_menu->add_action(unzip_archive_action); 535 file_context_menu->add_separator(); 536 } 537 538 file_context_menu->add_action(properties_action); 539 file_context_menu->popup(event.screen_position(), file_context_menu_action_default_action); 540 } 541 } else { 542 desktop_view_context_menu->popup(event.screen_position()); 543 } 544 }; 545 546 struct BackgroundWallpaperListener : Config::Listener { 547 virtual void config_string_did_change(DeprecatedString const& domain, DeprecatedString const& group, DeprecatedString const& key, DeprecatedString const& value) override 548 { 549 if (domain == "WindowManager" && group == "Background" && key == "Wallpaper") { 550 if (value.is_empty()) { 551 GUI::Desktop::the().set_wallpaper(nullptr, {}); 552 return; 553 } 554 auto wallpaper_bitmap_or_error = Gfx::Bitmap::load_from_file(value); 555 if (wallpaper_bitmap_or_error.is_error()) 556 dbgln("Failed to load wallpaper bitmap from path: {}", wallpaper_bitmap_or_error.error()); 557 else 558 GUI::Desktop::the().set_wallpaper(wallpaper_bitmap_or_error.release_value(), {}); 559 } 560 } 561 } wallpaper_listener; 562 563 auto selected_wallpaper = Config::read_string("WindowManager"sv, "Background"sv, "Wallpaper"sv, ""sv); 564 RefPtr<Gfx::Bitmap> wallpaper_bitmap {}; 565 if (!selected_wallpaper.is_empty()) { 566 wallpaper_bitmap = TRY(Gfx::Bitmap::load_from_file(selected_wallpaper)); 567 } 568 // This sets the wallpaper at startup, even if there is no wallpaper, the 569 // desktop should still show the background color. It's fine to pass a 570 // nullptr to Desktop::set_wallpaper. 571 GUI::Desktop::the().set_wallpaper(wallpaper_bitmap, {}); 572 573 window->show(); 574 return GUI::Application::the()->exec(); 575} 576 577ErrorOr<int> run_in_windowed_mode(DeprecatedString const& initial_location, DeprecatedString const& entry_focused_on_init) 578{ 579 auto window = TRY(GUI::Window::try_create()); 580 window->set_title("File Manager"); 581 582 auto left = Config::read_i32("FileManager"sv, "Window"sv, "Left"sv, 150); 583 auto top = Config::read_i32("FileManager"sv, "Window"sv, "Top"sv, 75); 584 auto width = Config::read_i32("FileManager"sv, "Window"sv, "Width"sv, 640); 585 auto height = Config::read_i32("FileManager"sv, "Window"sv, "Height"sv, 480); 586 auto was_maximized = Config::read_bool("FileManager"sv, "Window"sv, "Maximized"sv, false); 587 588 auto widget = TRY(window->set_main_widget<GUI::Widget>()); 589 TRY(widget->load_from_gml(file_manager_window_gml)); 590 591 auto& toolbar_container = *widget->find_descendant_of_type_named<GUI::ToolbarContainer>("toolbar_container"); 592 auto& main_toolbar = *widget->find_descendant_of_type_named<GUI::Toolbar>("main_toolbar"); 593 594 auto& breadcrumb_toolbar = *widget->find_descendant_of_type_named<GUI::Toolbar>("breadcrumb_toolbar"); 595 breadcrumb_toolbar.layout()->set_margins({ 0, 6 }); 596 auto& breadcrumbbar = *widget->find_descendant_of_type_named<GUI::PathBreadcrumbbar>("breadcrumbbar"); 597 598 auto& splitter = *widget->find_descendant_of_type_named<GUI::HorizontalSplitter>("splitter"); 599 auto& tree_view = *widget->find_descendant_of_type_named<GUI::TreeView>("tree_view"); 600 601 auto directories_model = GUI::FileSystemModel::create({}, GUI::FileSystemModel::Mode::DirectoriesOnly); 602 tree_view.set_model(directories_model); 603 tree_view.set_column_visible(GUI::FileSystemModel::Column::Icon, false); 604 tree_view.set_column_visible(GUI::FileSystemModel::Column::Size, false); 605 tree_view.set_column_visible(GUI::FileSystemModel::Column::User, false); 606 tree_view.set_column_visible(GUI::FileSystemModel::Column::Group, false); 607 tree_view.set_column_visible(GUI::FileSystemModel::Column::Permissions, false); 608 tree_view.set_column_visible(GUI::FileSystemModel::Column::ModificationTime, false); 609 tree_view.set_column_visible(GUI::FileSystemModel::Column::Inode, false); 610 tree_view.set_column_visible(GUI::FileSystemModel::Column::SymlinkTarget, false); 611 bool is_reacting_to_tree_view_selection_change = false; 612 613 auto directory_view = TRY(splitter.try_add<DirectoryView>(DirectoryView::Mode::Normal)); 614 directory_view->set_name("directory_view"); 615 616 // Open the root directory. FIXME: This is awkward. 617 tree_view.toggle_index(directories_model->index(0, 0, {})); 618 619 auto& statusbar = *widget->find_descendant_of_type_named<GUI::Statusbar>("statusbar"); 620 621 GUI::Application::the()->on_action_enter = [&statusbar](GUI::Action& action) { 622 auto text = action.status_tip(); 623 if (text.is_empty()) 624 text = Gfx::parse_ampersand_string(action.text()); 625 statusbar.set_override_text(move(text)); 626 }; 627 628 GUI::Application::the()->on_action_leave = [&statusbar](GUI::Action&) { 629 statusbar.set_override_text({}); 630 }; 631 632 auto& progressbar = *widget->find_descendant_of_type_named<GUI::Progressbar>("progressbar"); 633 progressbar.set_format(GUI::Progressbar::Format::ValueSlashMax); 634 progressbar.set_frame_shape(Gfx::FrameShape::Panel); 635 progressbar.set_frame_shadow(Gfx::FrameShadow::Sunken); 636 progressbar.set_frame_thickness(1); 637 638 auto refresh_tree_view = [&] { 639 directories_model->invalidate(); 640 641 auto current_path = directory_view->path(); 642 643 struct stat st; 644 // If the directory no longer exists, we find a parent that does. 645 while (stat(current_path.characters(), &st) != 0) { 646 directory_view->open_parent_directory(); 647 current_path = directory_view->path(); 648 if (current_path == directories_model->root_path()) { 649 break; 650 } 651 } 652 653 // Reselect the existing folder in the tree. 654 auto new_index = directories_model->index(current_path, GUI::FileSystemModel::Column::Name); 655 if (new_index.is_valid()) { 656 tree_view.expand_all_parents_of(new_index); 657 tree_view.set_cursor(new_index, GUI::AbstractView::SelectionUpdate::Set, true); 658 } 659 660 directory_view->refresh(); 661 }; 662 663 auto directory_context_menu = TRY(GUI::Menu::try_create("Directory View Directory")); 664 auto directory_view_context_menu = TRY(GUI::Menu::try_create("Directory View")); 665 auto tree_view_directory_context_menu = TRY(GUI::Menu::try_create("Tree View Directory")); 666 667 auto open_parent_directory_action = GUI::Action::create("Open &Parent Directory", { Mod_Alt, Key_Up }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/open-parent-directory.png"sv)), [&](GUI::Action const&) { 668 directory_view->open_parent_directory(); 669 }); 670 671 auto open_child_directory_action = GUI::Action::create("Open &Child Directory", { Mod_Alt, Key_Down }, [&](GUI::Action const&) { 672 breadcrumbbar.select_child_segment(); 673 }); 674 675 RefPtr<GUI::Action> layout_toolbar_action; 676 RefPtr<GUI::Action> layout_location_action; 677 RefPtr<GUI::Action> layout_statusbar_action; 678 RefPtr<GUI::Action> layout_folderpane_action; 679 680 auto show_toolbar = Config::read_bool("FileManager"sv, "Layout"sv, "ShowToolbar"sv, true); 681 layout_toolbar_action = GUI::Action::create_checkable("&Toolbar", [&](auto& action) { 682 if (action.is_checked()) { 683 main_toolbar.set_visible(true); 684 toolbar_container.set_visible(true); 685 } else { 686 main_toolbar.set_visible(false); 687 if (!breadcrumb_toolbar.is_visible()) 688 toolbar_container.set_visible(false); 689 } 690 show_toolbar = action.is_checked(); 691 Config::write_bool("FileManager"sv, "Layout"sv, "ShowToolbar"sv, action.is_checked()); 692 }); 693 layout_toolbar_action->set_checked(show_toolbar); 694 main_toolbar.set_visible(show_toolbar); 695 696 auto show_location = Config::read_bool("FileManager"sv, "Layout"sv, "ShowLocationBar"sv, true); 697 layout_location_action = GUI::Action::create_checkable("&Location Bar", [&](auto& action) { 698 if (action.is_checked()) { 699 breadcrumb_toolbar.set_visible(true); 700 toolbar_container.set_visible(true); 701 } else { 702 breadcrumb_toolbar.set_visible(false); 703 if (!main_toolbar.is_visible()) 704 toolbar_container.set_visible(false); 705 } 706 show_location = action.is_checked(); 707 Config::write_bool("FileManager"sv, "Layout"sv, "ShowLocationBar"sv, action.is_checked()); 708 }); 709 layout_location_action->set_checked(show_location); 710 breadcrumb_toolbar.set_visible(show_location); 711 712 toolbar_container.set_visible(show_location || show_toolbar); 713 714 layout_statusbar_action = GUI::Action::create_checkable("&Status Bar", [&](auto& action) { 715 action.is_checked() ? statusbar.set_visible(true) : statusbar.set_visible(false); 716 Config::write_bool("FileManager"sv, "Layout"sv, "ShowStatusbar"sv, action.is_checked()); 717 }); 718 719 auto show_statusbar = Config::read_bool("FileManager"sv, "Layout"sv, "ShowStatusbar"sv, true); 720 layout_statusbar_action->set_checked(show_statusbar); 721 statusbar.set_visible(show_statusbar); 722 723 layout_folderpane_action = GUI::Action::create_checkable("&Folder Pane", { Mod_Ctrl, Key_P }, [&](auto& action) { 724 action.is_checked() ? tree_view.set_visible(true) : tree_view.set_visible(false); 725 Config::write_bool("FileManager"sv, "Layout"sv, "ShowFolderPane"sv, action.is_checked()); 726 }); 727 728 auto show_folderpane = Config::read_bool("FileManager"sv, "Layout"sv, "ShowFolderPane"sv, true); 729 layout_folderpane_action->set_checked(show_folderpane); 730 tree_view.set_visible(show_folderpane); 731 732 breadcrumbbar.on_hide_location_box = [&] { 733 if (show_location) 734 breadcrumb_toolbar.set_visible(true); 735 if (!(show_location || show_toolbar)) 736 toolbar_container.set_visible(false); 737 }; 738 739 auto view_type_action_group = make<GUI::ActionGroup>(); 740 view_type_action_group->set_exclusive(true); 741 view_type_action_group->add_action(directory_view->view_as_icons_action()); 742 view_type_action_group->add_action(directory_view->view_as_table_action()); 743 view_type_action_group->add_action(directory_view->view_as_columns_action()); 744 745 auto tree_view_selected_file_paths = [&] { 746 Vector<DeprecatedString> paths; 747 auto& view = tree_view; 748 view.selection().for_each_index([&](GUI::ModelIndex const& index) { 749 paths.append(directories_model->full_path(index)); 750 }); 751 return paths; 752 }; 753 754 auto select_all_action = GUI::CommonActions::make_select_all_action([&](auto&) { 755 directory_view->current_view().select_all(); 756 }); 757 758 auto cut_action = GUI::CommonActions::make_cut_action( 759 [&](auto&) { 760 auto paths = directory_view->selected_file_paths(); 761 if (paths.is_empty()) 762 paths = tree_view_selected_file_paths(); 763 VERIFY(!paths.is_empty()); 764 765 do_copy(paths, FileOperation::Move); 766 refresh_tree_view(); 767 }, 768 window); 769 cut_action->set_enabled(false); 770 771 auto copy_action = GUI::CommonActions::make_copy_action( 772 [&](auto&) { 773 auto paths = directory_view->selected_file_paths(); 774 if (paths.is_empty()) 775 paths = tree_view_selected_file_paths(); 776 VERIFY(!paths.is_empty()); 777 778 do_copy(paths, FileOperation::Copy); 779 refresh_tree_view(); 780 }, 781 window); 782 copy_action->set_enabled(false); 783 784 auto open_in_new_window_action 785 = GUI::Action::create( 786 "Open in New &Window", 787 {}, 788 TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-file-manager.png"sv)), 789 [&](GUI::Action const& action) { 790 Vector<DeprecatedString> paths; 791 if (action.activator() == tree_view_directory_context_menu) 792 paths = tree_view_selected_file_paths(); 793 else 794 paths = directory_view->selected_file_paths(); 795 796 for (auto& path : paths) { 797 if (Core::DeprecatedFile::is_directory(path)) 798 Desktop::Launcher::open(URL::create_with_file_scheme(path)); 799 } 800 }, 801 window); 802 803 auto open_in_new_terminal_action 804 = GUI::Action::create( 805 "Open in &Terminal", 806 {}, 807 TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"sv)), 808 [&](GUI::Action const& action) { 809 Vector<DeprecatedString> paths; 810 if (action.activator() == tree_view_directory_context_menu) 811 paths = tree_view_selected_file_paths(); 812 else 813 paths = directory_view->selected_file_paths(); 814 815 for (auto& path : paths) { 816 if (Core::DeprecatedFile::is_directory(path)) { 817 spawn_terminal(path); 818 } 819 } 820 }, 821 window); 822 823 auto shortcut_action 824 = GUI::Action::create( 825 "Create Desktop &Shortcut", 826 {}, 827 TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-symlink.png"sv)), 828 [&](GUI::Action const&) { 829 auto paths = directory_view->selected_file_paths(); 830 if (paths.is_empty()) { 831 return; 832 } 833 do_create_link(paths, directory_view->window()); 834 }, 835 window); 836 837 auto create_archive_action 838 = GUI::Action::create( 839 "Create &Archive", 840 TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-archive.png"sv)), 841 [&](GUI::Action const&) { 842 auto paths = directory_view->selected_file_paths(); 843 if (paths.is_empty()) 844 return; 845 846 do_create_archive(paths, directory_view->window()); 847 refresh_tree_view(); 848 }, 849 window); 850 851 auto unzip_archive_action 852 = GUI::Action::create( 853 "E&xtract Here", 854 [&](GUI::Action const&) { 855 auto paths = directory_view->selected_file_paths(); 856 if (paths.is_empty()) 857 return; 858 859 do_unzip_archive(paths, directory_view->window()); 860 refresh_tree_view(); 861 }, 862 window); 863 864 auto set_wallpaper_action 865 = GUI::Action::create( 866 "Set as Desktop &Wallpaper", 867 TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-display-settings.png"sv)), 868 [&](GUI::Action const&) { 869 auto paths = directory_view->selected_file_paths(); 870 if (paths.is_empty()) 871 return; 872 873 do_set_wallpaper(paths.first(), directory_view->window()); 874 }, 875 window); 876 877 auto properties_action = GUI::CommonActions::make_properties_action( 878 [&](auto& action) { 879 DeprecatedString container_dir_path; 880 DeprecatedString path; 881 Vector<DeprecatedString> selected; 882 if (action.activator() == directory_context_menu || directory_view->active_widget()->is_focused()) { 883 path = directory_view->path(); 884 container_dir_path = path; 885 selected = directory_view->selected_file_paths(); 886 } else { 887 path = directories_model->full_path(tree_view.selection().first()); 888 container_dir_path = LexicalPath::basename(path); 889 selected = tree_view_selected_file_paths(); 890 } 891 892 show_properties(container_dir_path, path, selected, directory_view->window()); 893 }, 894 window); 895 896 auto paste_action = GUI::CommonActions::make_paste_action( 897 [&](GUI::Action const& action) { 898 DeprecatedString target_directory; 899 if (action.activator() == directory_context_menu) 900 target_directory = directory_view->selected_file_paths()[0]; 901 else 902 target_directory = directory_view->path(); 903 do_paste(target_directory, directory_view->window()); 904 refresh_tree_view(); 905 }, 906 window); 907 908 auto folder_specific_paste_action = GUI::CommonActions::make_paste_action( 909 [&](GUI::Action const& action) { 910 DeprecatedString target_directory; 911 if (action.activator() == directory_context_menu) 912 target_directory = directory_view->selected_file_paths()[0]; 913 else 914 target_directory = directory_view->path(); 915 do_paste(target_directory, directory_view->window()); 916 refresh_tree_view(); 917 }, 918 window); 919 920 auto go_back_action = GUI::CommonActions::make_go_back_action( 921 [&](auto&) { 922 directory_view->open_previous_directory(); 923 }, 924 window); 925 926 auto go_forward_action = GUI::CommonActions::make_go_forward_action( 927 [&](auto&) { 928 directory_view->open_next_directory(); 929 }, 930 window); 931 932 auto go_home_action = GUI::CommonActions::make_go_home_action( 933 [&](auto&) { 934 directory_view->open(Core::StandardPaths::home_directory()); 935 }, 936 window); 937 938 GUI::Clipboard::the().on_change = [&](DeprecatedString const& data_type) { 939 auto current_location = directory_view->path(); 940 paste_action->set_enabled(data_type == "text/uri-list" && access(current_location.characters(), W_OK) == 0); 941 }; 942 943 auto tree_view_delete_action = GUI::CommonActions::make_delete_action( 944 [&](auto&) { 945 delete_paths(tree_view_selected_file_paths(), true, window); 946 refresh_tree_view(); 947 }, 948 &tree_view); 949 950 // This is a little awkward. The menu action does something different depending on which view has focus. 951 // It would be nice to find a good abstraction for this instead of creating a branching action like this. 952 auto focus_dependent_delete_action = GUI::CommonActions::make_delete_action([&](auto&) { 953 if (tree_view.is_focused()) 954 tree_view_delete_action->activate(); 955 else 956 directory_view->delete_action().activate(); 957 refresh_tree_view(); 958 }); 959 focus_dependent_delete_action->set_enabled(false); 960 961 auto mkdir_action = GUI::Action::create("&New Directory...", { Mod_Ctrl | Mod_Shift, Key_N }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"sv)), [&](GUI::Action const&) { 962 directory_view->mkdir_action().activate(); 963 refresh_tree_view(); 964 }); 965 966 auto touch_action = GUI::Action::create("New &File...", { Mod_Ctrl | Mod_Shift, Key_F }, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"sv)), [&](GUI::Action const&) { 967 directory_view->touch_action().activate(); 968 refresh_tree_view(); 969 }); 970 971 auto file_menu = TRY(window->try_add_menu("&File")); 972 TRY(file_menu->try_add_action(mkdir_action)); 973 TRY(file_menu->try_add_action(touch_action)); 974 TRY(file_menu->try_add_action(focus_dependent_delete_action)); 975 TRY(file_menu->try_add_action(directory_view->rename_action())); 976 TRY(file_menu->try_add_separator()); 977 TRY(file_menu->try_add_action(properties_action)); 978 TRY(file_menu->try_add_separator()); 979 TRY(file_menu->try_add_action(GUI::CommonActions::make_quit_action([](auto&) { 980 GUI::Application::the()->quit(); 981 }))); 982 983 auto edit_menu = TRY(window->try_add_menu("&Edit")); 984 TRY(edit_menu->try_add_action(cut_action)); 985 TRY(edit_menu->try_add_action(copy_action)); 986 TRY(edit_menu->try_add_action(paste_action)); 987 TRY(edit_menu->try_add_separator()); 988 TRY(edit_menu->try_add_action(select_all_action)); 989 990 auto show_dotfiles_in_view = [&](bool show_dotfiles) { 991 directory_view->set_should_show_dotfiles(show_dotfiles); 992 directories_model->set_should_show_dotfiles(show_dotfiles); 993 }; 994 995 auto show_dotfiles_action = GUI::Action::create_checkable("&Show Dotfiles", { Mod_Ctrl, Key_H }, [&](auto& action) { 996 show_dotfiles_in_view(action.is_checked()); 997 refresh_tree_view(); 998 Config::write_bool("FileManager"sv, "DirectoryView"sv, "ShowDotFiles"sv, action.is_checked()); 999 }); 1000 1001 auto show_dotfiles = Config::read_bool("FileManager"sv, "DirectoryView"sv, "ShowDotFiles"sv, false); 1002 show_dotfiles |= initial_location.contains("/."sv); 1003 show_dotfiles_action->set_checked(show_dotfiles); 1004 show_dotfiles_in_view(show_dotfiles); 1005 1006 auto view_menu = TRY(window->try_add_menu("&View")); 1007 auto layout_menu = TRY(view_menu->try_add_submenu("&Layout")); 1008 TRY(layout_menu->try_add_action(*layout_toolbar_action)); 1009 TRY(layout_menu->try_add_action(*layout_location_action)); 1010 TRY(layout_menu->try_add_action(*layout_statusbar_action)); 1011 TRY(layout_menu->try_add_action(*layout_folderpane_action)); 1012 1013 TRY(view_menu->try_add_separator()); 1014 1015 TRY(view_menu->try_add_action(directory_view->view_as_icons_action())); 1016 TRY(view_menu->try_add_action(directory_view->view_as_table_action())); 1017 TRY(view_menu->try_add_action(directory_view->view_as_columns_action())); 1018 TRY(view_menu->try_add_separator()); 1019 TRY(view_menu->try_add_action(show_dotfiles_action)); 1020 1021 auto go_to_location_action = GUI::Action::create("Go to &Location...", { Mod_Ctrl, Key_L }, Key_F6, TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-to.png"sv)), [&](auto&) { 1022 toolbar_container.set_visible(true); 1023 breadcrumb_toolbar.set_visible(true); 1024 breadcrumbbar.show_location_text_box(); 1025 }); 1026 1027 auto go_menu = TRY(window->try_add_menu("&Go")); 1028 TRY(go_menu->try_add_action(go_back_action)); 1029 TRY(go_menu->try_add_action(go_forward_action)); 1030 TRY(go_menu->try_add_action(open_parent_directory_action)); 1031 TRY(go_menu->try_add_action(open_child_directory_action)); 1032 TRY(go_menu->try_add_action(go_home_action)); 1033 TRY(go_menu->try_add_action(go_to_location_action)); 1034 TRY(go_menu->try_add_separator()); 1035 TRY(go_menu->try_add_action(directory_view->open_terminal_action())); 1036 1037 auto help_menu = TRY(window->try_add_menu("&Help")); 1038 TRY(help_menu->try_add_action(GUI::CommonActions::make_command_palette_action(window))); 1039 TRY(help_menu->try_add_action(GUI::CommonActions::make_about_action("File Manager"sv, GUI::Icon::default_icon("app-file-manager"sv), window))); 1040 1041 (void)TRY(main_toolbar.try_add_action(go_back_action)); 1042 (void)TRY(main_toolbar.try_add_action(go_forward_action)); 1043 (void)TRY(main_toolbar.try_add_action(open_parent_directory_action)); 1044 (void)TRY(main_toolbar.try_add_action(go_home_action)); 1045 1046 TRY(main_toolbar.try_add_separator()); 1047 (void)TRY(main_toolbar.try_add_action(directory_view->open_terminal_action())); 1048 1049 TRY(main_toolbar.try_add_separator()); 1050 (void)TRY(main_toolbar.try_add_action(mkdir_action)); 1051 (void)TRY(main_toolbar.try_add_action(touch_action)); 1052 TRY(main_toolbar.try_add_separator()); 1053 1054 (void)TRY(main_toolbar.try_add_action(focus_dependent_delete_action)); 1055 (void)TRY(main_toolbar.try_add_action(directory_view->rename_action())); 1056 1057 TRY(main_toolbar.try_add_separator()); 1058 (void)TRY(main_toolbar.try_add_action(cut_action)); 1059 (void)TRY(main_toolbar.try_add_action(copy_action)); 1060 (void)TRY(main_toolbar.try_add_action(paste_action)); 1061 1062 TRY(main_toolbar.try_add_separator()); 1063 (void)TRY(main_toolbar.try_add_action(directory_view->view_as_icons_action())); 1064 (void)TRY(main_toolbar.try_add_action(directory_view->view_as_table_action())); 1065 (void)TRY(main_toolbar.try_add_action(directory_view->view_as_columns_action())); 1066 1067 breadcrumbbar.on_path_change = [&](auto selected_path) { 1068 if (Core::DeprecatedFile::is_directory(selected_path)) { 1069 directory_view->open(selected_path); 1070 } else { 1071 dbgln("Breadcrumb path '{}' doesn't exist", selected_path); 1072 breadcrumbbar.set_current_path(directory_view->path()); 1073 } 1074 }; 1075 1076 directory_view->on_path_change = [&](DeprecatedString const& new_path, bool can_read_in_path, bool can_write_in_path) { 1077 auto icon = GUI::FileIconProvider::icon_for_path(new_path); 1078 auto* bitmap = icon.bitmap_for_size(16); 1079 window->set_icon(bitmap); 1080 1081 window->set_title(DeprecatedString::formatted("{} - File Manager", new_path)); 1082 1083 breadcrumbbar.set_current_path(new_path); 1084 1085 if (!is_reacting_to_tree_view_selection_change) { 1086 auto new_index = directories_model->index(new_path, GUI::FileSystemModel::Column::Name); 1087 if (new_index.is_valid()) { 1088 tree_view.expand_all_parents_of(new_index); 1089 tree_view.set_cursor(new_index, GUI::AbstractView::SelectionUpdate::Set); 1090 } 1091 } 1092 1093 mkdir_action->set_enabled(can_write_in_path); 1094 touch_action->set_enabled(can_write_in_path); 1095 paste_action->set_enabled(can_write_in_path && GUI::Clipboard::the().fetch_mime_type() == "text/uri-list"); 1096 go_forward_action->set_enabled(directory_view->path_history_position() < directory_view->path_history_size() - 1); 1097 go_back_action->set_enabled(directory_view->path_history_position() > 0); 1098 open_parent_directory_action->set_enabled(breadcrumbbar.has_parent_segment()); 1099 open_child_directory_action->set_enabled(breadcrumbbar.has_child_segment()); 1100 directory_view->view_as_table_action().set_enabled(can_read_in_path); 1101 directory_view->view_as_icons_action().set_enabled(can_read_in_path); 1102 directory_view->view_as_columns_action().set_enabled(can_read_in_path); 1103 }; 1104 1105 directory_view->on_accepted_drop = [&] { 1106 refresh_tree_view(); 1107 }; 1108 1109 directory_view->on_status_message = [&](StringView message) { 1110 statusbar.set_text(message); 1111 }; 1112 1113 directory_view->on_thumbnail_progress = [&](int done, int total) { 1114 if (done == total) { 1115 progressbar.set_visible(false); 1116 return; 1117 } 1118 progressbar.set_range(0, total); 1119 progressbar.set_value(done); 1120 progressbar.set_visible(true); 1121 }; 1122 1123 directory_view->on_selection_change = [&](GUI::AbstractView& view) { 1124 auto& selection = view.selection(); 1125 cut_action->set_enabled(!selection.is_empty() && access(directory_view->path().characters(), W_OK) == 0); 1126 copy_action->set_enabled(!selection.is_empty()); 1127 focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && tree_view.is_focused()) 1128 || (!directory_view->current_view().selection().is_empty() && access(directory_view->path().characters(), W_OK) == 0)); 1129 }; 1130 1131 auto directory_open_action = GUI::Action::create("Open", TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"sv)), [&](auto&) { 1132 directory_view->open(directory_view->selected_file_paths().first()); 1133 }); 1134 1135 TRY(directory_context_menu->try_add_action(directory_open_action)); 1136 TRY(directory_context_menu->try_add_action(open_in_new_window_action)); 1137 TRY(directory_context_menu->try_add_action(open_in_new_terminal_action)); 1138 TRY(directory_context_menu->try_add_separator()); 1139 TRY(directory_context_menu->try_add_action(cut_action)); 1140 TRY(directory_context_menu->try_add_action(copy_action)); 1141 TRY(directory_context_menu->try_add_action(folder_specific_paste_action)); 1142 TRY(directory_context_menu->try_add_action(directory_view->delete_action())); 1143 TRY(directory_context_menu->try_add_action(directory_view->rename_action())); 1144 TRY(directory_context_menu->try_add_action(shortcut_action)); 1145 TRY(directory_context_menu->try_add_action(create_archive_action)); 1146 TRY(directory_context_menu->try_add_separator()); 1147 TRY(directory_context_menu->try_add_action(properties_action)); 1148 1149 TRY(directory_view_context_menu->try_add_action(mkdir_action)); 1150 TRY(directory_view_context_menu->try_add_action(touch_action)); 1151 TRY(directory_view_context_menu->try_add_action(paste_action)); 1152 TRY(directory_view_context_menu->try_add_action(directory_view->open_terminal_action())); 1153 TRY(directory_view_context_menu->try_add_separator()); 1154 TRY(directory_view_context_menu->try_add_action(show_dotfiles_action)); 1155 TRY(directory_view_context_menu->try_add_separator()); 1156 TRY(directory_view_context_menu->try_add_action(properties_action)); 1157 1158 TRY(tree_view_directory_context_menu->try_add_action(open_in_new_window_action)); 1159 TRY(tree_view_directory_context_menu->try_add_action(open_in_new_terminal_action)); 1160 TRY(tree_view_directory_context_menu->try_add_separator()); 1161 TRY(tree_view_directory_context_menu->try_add_action(mkdir_action)); 1162 TRY(tree_view_directory_context_menu->try_add_action(touch_action)); 1163 TRY(tree_view_directory_context_menu->try_add_action(cut_action)); 1164 TRY(tree_view_directory_context_menu->try_add_action(copy_action)); 1165 TRY(tree_view_directory_context_menu->try_add_action(paste_action)); 1166 TRY(tree_view_directory_context_menu->try_add_action(tree_view_delete_action)); 1167 TRY(tree_view_directory_context_menu->try_add_separator()); 1168 TRY(tree_view_directory_context_menu->try_add_action(properties_action)); 1169 1170 RefPtr<GUI::Menu> file_context_menu; 1171 Vector<NonnullRefPtr<LauncherHandler>> current_file_handlers; 1172 RefPtr<GUI::Action> file_context_menu_action_default_action; 1173 1174 directory_view->on_context_menu_request = [&](GUI::ModelIndex const& index, GUI::ContextMenuEvent const& event) { 1175 if (index.is_valid()) { 1176 auto& node = directory_view->node(index); 1177 1178 if (node.is_directory()) { 1179 auto should_get_enabled = access(node.full_path().characters(), W_OK) == 0 && GUI::Clipboard::the().fetch_mime_type() == "text/uri-list"; 1180 folder_specific_paste_action->set_enabled(should_get_enabled); 1181 directory_context_menu->popup(event.screen_position(), directory_open_action); 1182 } else { 1183 file_context_menu = GUI::Menu::construct("Directory View File"); 1184 1185 bool added_launch_file_handlers = add_launch_handler_actions_to_menu(file_context_menu, directory_view, node.full_path(), file_context_menu_action_default_action, current_file_handlers); 1186 if (added_launch_file_handlers) 1187 file_context_menu->add_separator(); 1188 1189 file_context_menu->add_action(cut_action); 1190 file_context_menu->add_action(copy_action); 1191 file_context_menu->add_action(paste_action); 1192 file_context_menu->add_action(directory_view->delete_action()); 1193 file_context_menu->add_action(directory_view->rename_action()); 1194 file_context_menu->add_action(shortcut_action); 1195 file_context_menu->add_action(create_archive_action); 1196 file_context_menu->add_separator(); 1197 1198 if (Gfx::Bitmap::is_path_a_supported_image_format(node.name)) { 1199 file_context_menu->add_action(set_wallpaper_action); 1200 file_context_menu->add_separator(); 1201 } 1202 1203 if (node.full_path().ends_with(".zip"sv, AK::CaseSensitivity::CaseInsensitive)) { 1204 file_context_menu->add_action(unzip_archive_action); 1205 file_context_menu->add_separator(); 1206 } 1207 1208 file_context_menu->add_action(properties_action); 1209 file_context_menu->popup(event.screen_position(), file_context_menu_action_default_action); 1210 } 1211 } else { 1212 directory_view_context_menu->popup(event.screen_position()); 1213 } 1214 }; 1215 1216 tree_view.on_selection_change = [&] { 1217 focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && tree_view.is_focused()) 1218 || !directory_view->current_view().selection().is_empty()); 1219 1220 if (tree_view.selection().is_empty()) 1221 return; 1222 1223 if (directories_model->m_previously_selected_index.is_valid()) 1224 directories_model->update_node_on_selection(directories_model->m_previously_selected_index, false); 1225 1226 auto const& index = tree_view.selection().first(); 1227 directories_model->update_node_on_selection(index, true); 1228 directories_model->m_previously_selected_index = index; 1229 1230 auto path = directories_model->full_path(index); 1231 if (directory_view->path() == path) 1232 return; 1233 TemporaryChange change(is_reacting_to_tree_view_selection_change, true); 1234 directory_view->open(path); 1235 cut_action->set_enabled(!tree_view.selection().is_empty()); 1236 copy_action->set_enabled(!tree_view.selection().is_empty()); 1237 directory_view->delete_action().set_enabled(!tree_view.selection().is_empty()); 1238 }; 1239 1240 tree_view.on_focus_change = [&](bool has_focus, [[maybe_unused]] GUI::FocusSource const source) { 1241 focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && has_focus) 1242 || !directory_view->current_view().selection().is_empty()); 1243 }; 1244 1245 tree_view.on_context_menu_request = [&](GUI::ModelIndex const& index, GUI::ContextMenuEvent const& event) { 1246 if (index.is_valid()) { 1247 tree_view_directory_context_menu->popup(event.screen_position()); 1248 } 1249 }; 1250 1251 breadcrumbbar.on_paths_drop = [&](auto path, GUI::DropEvent const& event) { 1252 bool const has_accepted_drop = handle_drop(event, path, window).release_value_but_fixme_should_propagate_errors(); 1253 if (has_accepted_drop) 1254 refresh_tree_view(); 1255 }; 1256 1257 tree_view.on_drop = [&](GUI::ModelIndex const& index, GUI::DropEvent const& event) { 1258 auto const& target_node = directories_model->node(index); 1259 bool const has_accepted_drop = handle_drop(event, target_node.full_path(), window).release_value_but_fixme_should_propagate_errors(); 1260 if (has_accepted_drop) { 1261 refresh_tree_view(); 1262 const_cast<GUI::DropEvent&>(event).accept(); 1263 } 1264 }; 1265 1266 directory_view->open(initial_location); 1267 directory_view->set_focus(true); 1268 1269 paste_action->set_enabled(GUI::Clipboard::the().fetch_mime_type() == "text/uri-list" && access(initial_location.characters(), W_OK) == 0); 1270 1271 window->set_rect({ left, top, width, height }); 1272 if (was_maximized) 1273 window->set_maximized(true); 1274 1275 window->show(); 1276 1277 directory_view->set_view_mode_from_string(Config::read_string("FileManager"sv, "DirectoryView"sv, "ViewMode"sv, "Icon"sv)); 1278 1279 if (!entry_focused_on_init.is_empty()) { 1280 auto matches = directory_view->current_view().model()->matches(entry_focused_on_init, GUI::Model::MatchesFlag::MatchFull | GUI::Model::MatchesFlag::FirstMatchOnly); 1281 if (!matches.is_empty()) 1282 directory_view->current_view().set_cursor(matches.first(), GUI::AbstractView::SelectionUpdate::Set); 1283 } 1284 1285 // Write window position to config file on close request. 1286 window->on_close_request = [&] { 1287 Config::write_bool("FileManager"sv, "Window"sv, "Maximized"sv, window->is_maximized()); 1288 if (!window->is_maximized()) { 1289 Config::write_i32("FileManager"sv, "Window"sv, "Left"sv, window->x()); 1290 Config::write_i32("FileManager"sv, "Window"sv, "Top"sv, window->y()); 1291 Config::write_i32("FileManager"sv, "Window"sv, "Width"sv, window->width()); 1292 Config::write_i32("FileManager"sv, "Window"sv, "Height"sv, window->height()); 1293 } 1294 return GUI::Window::CloseRequestDecision::Close; 1295 }; 1296 1297 return GUI::Application::the()->exec(); 1298}