Serenity Operating System
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}