1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 14:57:35 +00:00

Applications: Move to Userland/Applications/

This commit is contained in:
Andreas Kling 2021-01-12 12:05:23 +01:00
parent aa939c4b4b
commit dc28c07fa5
287 changed files with 1 additions and 1 deletions

View file

@ -0,0 +1,13 @@
compile_gml(FileManagerWindow.gml FileManagerWindowGML.h file_manager_window_gml)
set(SOURCES
DesktopWidget.cpp
DirectoryView.cpp
FileManagerWindowGML.h
FileUtils.cpp
main.cpp
PropertiesWindow.cpp
)
serenity_app(FileManager ICON filetype-folder)
target_link_libraries(FileManager LibGUI LibDesktop)

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "DesktopWidget.h"
#include <LibGUI/Painter.h>
namespace FileManager {
DesktopWidget::DesktopWidget()
{
}
DesktopWidget::~DesktopWidget()
{
}
void DesktopWidget::paint_event(GUI::PaintEvent& event)
{
GUI::Painter painter(*this);
painter.add_clip_rect(event.rect());
painter.clear_rect(event.rect(), Color(0, 0, 0, 0));
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibGUI/Widget.h>
namespace FileManager {
class DesktopWidget final : public GUI::Widget {
C_OBJECT(DesktopWidget);
public:
virtual ~DesktopWidget() override;
private:
virtual void paint_event(GUI::PaintEvent&) override;
DesktopWidget();
};
}

View file

@ -0,0 +1,591 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "DirectoryView.h"
#include "FileUtils.h"
#include <AK/LexicalPath.h>
#include <AK/NumberFormat.h>
#include <AK/StringBuilder.h>
#include <LibCore/MimeData.h>
#include <LibCore/StandardPaths.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/ModelEditingDelegate.h>
#include <LibGUI/SortingProxyModel.h>
#include <serenity.h>
#include <spawn.h>
#include <stdio.h>
#include <unistd.h>
namespace FileManager {
NonnullRefPtr<GUI::Action> LauncherHandler::create_launch_action(Function<void(const LauncherHandler&)> launch_handler)
{
auto icon = GUI::FileIconProvider::icon_for_executable(details().executable).bitmap_for_size(16);
return GUI::Action::create(details().name, move(icon), [this, launch_handler = move(launch_handler)](auto&) {
launch_handler(*this);
});
}
RefPtr<LauncherHandler> DirectoryView::get_default_launch_handler(const NonnullRefPtrVector<LauncherHandler>& handlers)
{
// If this is an application, pick it first
for (size_t i = 0; i < handlers.size(); i++) {
if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::Application)
return handlers[i];
}
// If there's a handler preferred by the user, pick this first
for (size_t i = 0; i < handlers.size(); i++) {
if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::UserPreferred)
return handlers[i];
}
// Otherwise, use the user's default, if available
for (size_t i = 0; i < handlers.size(); i++) {
if (handlers[i].details().launcher_type == Desktop::Launcher::LauncherType::UserDefault)
return handlers[i];
}
// If still no match, use the first one we find
if (!handlers.is_empty()) {
return handlers[0];
}
return {};
}
NonnullRefPtrVector<LauncherHandler> DirectoryView::get_launch_handlers(const URL& url)
{
NonnullRefPtrVector<LauncherHandler> handlers;
for (auto& h : Desktop::Launcher::get_handlers_with_details_for_url(url)) {
handlers.append(adopt(*new LauncherHandler(h)));
}
return handlers;
}
NonnullRefPtrVector<LauncherHandler> DirectoryView::get_launch_handlers(const String& path)
{
return get_launch_handlers(URL::create_with_file_protocol(path));
}
void DirectoryView::handle_activation(const GUI::ModelIndex& index)
{
if (!index.is_valid())
return;
dbgln("on activation: {},{}, this={:p}, m_model={:p}", index.row(), index.column(), this, m_model.ptr());
auto& node = this->node(index);
auto path = node.full_path();
struct stat st;
if (stat(path.characters(), &st) < 0) {
perror("stat");
return;
}
if (S_ISDIR(st.st_mode)) {
if (is_desktop()) {
Desktop::Launcher::open(URL::create_with_file_protocol(path));
return;
}
open(path);
return;
}
auto url = URL::create_with_file_protocol(path);
auto launcher_handlers = get_launch_handlers(url);
auto default_launcher = get_default_launch_handler(launcher_handlers);
if (default_launcher) {
launch(url, *default_launcher);
} else {
auto error_message = String::format("Could not open %s", path.characters());
GUI::MessageBox::show(window(), error_message, "File Manager", GUI::MessageBox::Type::Error);
}
}
DirectoryView::DirectoryView(Mode mode)
: m_mode(mode)
, m_model(GUI::FileSystemModel::create({}))
, m_sorting_model(GUI::SortingProxyModel::create(m_model))
{
set_active_widget(nullptr);
set_content_margins({ 2, 2, 2, 2 });
setup_actions();
m_error_label = add<GUI::Label>();
m_error_label->set_font(m_error_label->font().bold_variant());
setup_model();
setup_icon_view();
if (mode != Mode::Desktop) {
setup_columns_view();
setup_table_view();
}
set_view_mode(ViewMode::Icon);
}
const GUI::FileSystemModel::Node& DirectoryView::node(const GUI::ModelIndex& index) const
{
return model().node(m_sorting_model->map_to_source(index));
}
void DirectoryView::setup_model()
{
m_model->on_error = [this](int, const char* error_string) {
auto failed_path = m_model->root_path();
auto error_message = String::formatted("Could not read {}:\n{}", failed_path, error_string);
m_error_label->set_text(error_message);
set_active_widget(m_error_label);
m_mkdir_action->set_enabled(false);
m_touch_action->set_enabled(false);
add_path_to_history(model().root_path());
if (on_path_change)
on_path_change(failed_path, false);
};
m_model->on_complete = [this] {
if (m_table_view)
m_table_view->selection().clear();
if (m_icon_view)
m_icon_view->selection().clear();
add_path_to_history(model().root_path());
bool can_write_in_path = access(model().root_path().characters(), W_OK) == 0;
m_mkdir_action->set_enabled(can_write_in_path);
m_touch_action->set_enabled(can_write_in_path);
if (on_path_change)
on_path_change(model().root_path(), can_write_in_path);
};
m_model->register_client(*this);
m_model->on_thumbnail_progress = [this](int done, int total) {
if (on_thumbnail_progress)
on_thumbnail_progress(done, total);
};
if (is_desktop())
m_model->set_root_path(Core::StandardPaths::desktop_directory());
}
void DirectoryView::setup_icon_view()
{
m_icon_view = add<GUI::IconView>();
m_icon_view->set_should_hide_unnecessary_scrollbars(true);
m_icon_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
m_icon_view->set_editable(true);
m_icon_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
m_icon_view->aid_create_editing_delegate = [](auto&) {
return make<GUI::StringModelEditingDelegate>();
};
if (is_desktop()) {
m_icon_view->set_frame_shape(Gfx::FrameShape::NoFrame);
m_icon_view->set_scrollbars_enabled(false);
m_icon_view->set_fill_with_background_color(false);
m_icon_view->set_draw_item_text_with_shadow(true);
m_icon_view->set_flow_direction(GUI::IconView::FlowDirection::TopToBottom);
}
m_icon_view->set_model(m_sorting_model);
m_icon_view->set_model_column(GUI::FileSystemModel::Column::Name);
m_icon_view->on_activation = [&](auto& index) {
handle_activation(index);
};
m_icon_view->on_selection_change = [this] {
handle_selection_change();
};
m_icon_view->on_context_menu_request = [this](auto& index, auto& event) {
if (on_context_menu_request)
on_context_menu_request(index, event);
};
m_icon_view->on_drop = [this](auto& index, auto& event) {
handle_drop(index, event);
};
}
void DirectoryView::setup_columns_view()
{
m_columns_view = add<GUI::ColumnsView>();
m_columns_view->set_should_hide_unnecessary_scrollbars(true);
m_columns_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
m_columns_view->set_editable(true);
m_columns_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
m_columns_view->aid_create_editing_delegate = [](auto&) {
return make<GUI::StringModelEditingDelegate>();
};
m_columns_view->set_model(m_sorting_model);
m_columns_view->set_model_column(GUI::FileSystemModel::Column::Name);
m_columns_view->on_activation = [&](auto& index) {
handle_activation(index);
};
m_columns_view->on_selection_change = [this] {
handle_selection_change();
};
m_columns_view->on_context_menu_request = [this](auto& index, auto& event) {
if (on_context_menu_request)
on_context_menu_request(index, event);
};
m_columns_view->on_drop = [this](auto& index, auto& event) {
handle_drop(index, event);
};
}
void DirectoryView::setup_table_view()
{
m_table_view = add<GUI::TableView>();
m_table_view->set_should_hide_unnecessary_scrollbars(true);
m_table_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
m_table_view->set_editable(true);
m_table_view->set_edit_triggers(GUI::AbstractView::EditTrigger::EditKeyPressed);
m_table_view->aid_create_editing_delegate = [](auto&) {
return make<GUI::StringModelEditingDelegate>();
};
m_table_view->set_model(m_sorting_model);
m_table_view->set_key_column_and_sort_order(GUI::FileSystemModel::Column::Name, GUI::SortOrder::Ascending);
m_table_view->on_activation = [&](auto& index) {
handle_activation(index);
};
m_table_view->on_selection_change = [this] {
handle_selection_change();
};
m_table_view->on_context_menu_request = [this](auto& index, auto& event) {
if (on_context_menu_request)
on_context_menu_request(index, event);
};
m_table_view->on_drop = [this](auto& index, auto& event) {
handle_drop(index, event);
};
}
DirectoryView::~DirectoryView()
{
m_model->unregister_client(*this);
}
void DirectoryView::model_did_update(unsigned flags)
{
if (flags & GUI::Model::UpdateFlag::InvalidateAllIndexes) {
for_each_view_implementation([](auto& view) {
view.selection().clear();
});
}
update_statusbar();
}
void DirectoryView::set_view_mode(ViewMode mode)
{
if (m_view_mode == mode)
return;
m_view_mode = mode;
update();
if (mode == ViewMode::Table) {
set_active_widget(m_table_view);
return;
}
if (mode == ViewMode::Columns) {
set_active_widget(m_columns_view);
return;
}
if (mode == ViewMode::Icon) {
set_active_widget(m_icon_view);
return;
}
ASSERT_NOT_REACHED();
}
void DirectoryView::add_path_to_history(const StringView& path)
{
if (m_path_history.size() && m_path_history.at(m_path_history_position) == path)
return;
if (m_path_history_position < m_path_history.size())
m_path_history.resize(m_path_history_position + 1);
m_path_history.append(path);
m_path_history_position = m_path_history.size() - 1;
}
void DirectoryView::open(const StringView& path)
{
if (model().root_path() == path) {
model().update();
return;
}
set_active_widget(&current_view());
model().set_root_path(path);
}
void DirectoryView::set_status_message(const StringView& message)
{
if (on_status_message)
on_status_message(message);
}
void DirectoryView::open_parent_directory()
{
auto path = String::formatted("{}/..", model().root_path());
model().set_root_path(path);
}
void DirectoryView::refresh()
{
model().update();
}
void DirectoryView::open_previous_directory()
{
if (m_path_history_position > 0) {
set_active_widget(&current_view());
m_path_history_position--;
model().set_root_path(m_path_history[m_path_history_position]);
}
}
void DirectoryView::open_next_directory()
{
if (m_path_history_position < m_path_history.size() - 1) {
set_active_widget(&current_view());
m_path_history_position++;
model().set_root_path(m_path_history[m_path_history_position]);
}
}
void DirectoryView::update_statusbar()
{
// If we're triggered during widget construction, just ignore it.
if (m_view_mode == ViewMode::Invalid)
return;
size_t total_size = model().node({}).total_size;
if (current_view().selection().is_empty()) {
set_status_message(String::formatted("{} item(s) ({})",
model().row_count(),
human_readable_size(total_size)));
return;
}
int selected_item_count = current_view().selection().size();
size_t selected_byte_count = 0;
current_view().selection().for_each_index([&](auto& index) {
auto& model = *current_view().model();
auto size_index = model.index(index.row(), GUI::FileSystemModel::Column::Size, model.parent_index(index));
auto file_size = size_index.data().to_i32();
selected_byte_count += file_size;
});
StringBuilder builder;
builder.append(String::number(selected_item_count));
builder.append(" item");
if (selected_item_count != 1)
builder.append('s');
builder.append(" selected (");
builder.append(human_readable_size(selected_byte_count).characters());
builder.append(')');
if (selected_item_count == 1) {
auto& node = this->node(current_view().selection().first());
if (!node.symlink_target.is_empty()) {
builder.append(" -> ");
builder.append(node.symlink_target);
}
}
set_status_message(builder.to_string());
}
void DirectoryView::set_should_show_dotfiles(bool show_dotfiles)
{
m_model->set_should_show_dotfiles(show_dotfiles);
}
void DirectoryView::launch(const URL&, const LauncherHandler& launcher_handler)
{
pid_t child;
if (launcher_handler.details().launcher_type == Desktop::Launcher::LauncherType::Application) {
const char* argv[] = { launcher_handler.details().name.characters(), nullptr };
posix_spawn(&child, launcher_handler.details().executable.characters(), nullptr, nullptr, const_cast<char**>(argv), environ);
if (disown(child) < 0)
perror("disown");
} else {
for (auto& path : selected_file_paths()) {
const char* argv[] = { launcher_handler.details().name.characters(), path.characters(), nullptr };
posix_spawn(&child, launcher_handler.details().executable.characters(), nullptr, nullptr, const_cast<char**>(argv), environ);
if (disown(child) < 0)
perror("disown");
}
}
}
Vector<String> DirectoryView::selected_file_paths() const
{
Vector<String> paths;
auto& view = current_view();
auto& model = *view.model();
view.selection().for_each_index([&](const GUI::ModelIndex& index) {
auto parent_index = model.parent_index(index);
auto name_index = model.index(index.row(), GUI::FileSystemModel::Column::Name, parent_index);
auto path = name_index.data(GUI::ModelRole::Custom).to_string();
paths.append(path);
});
return paths;
}
void DirectoryView::do_delete(bool should_confirm)
{
auto paths = selected_file_paths();
ASSERT(!paths.is_empty());
FileUtils::delete_paths(paths, should_confirm, window());
}
void DirectoryView::handle_selection_change()
{
update_statusbar();
bool can_delete = !current_view().selection().is_empty() && access(path().characters(), W_OK) == 0;
m_delete_action->set_enabled(can_delete);
m_force_delete_action->set_enabled(can_delete);
if (on_selection_change)
on_selection_change(current_view());
}
void DirectoryView::setup_actions()
{
m_mkdir_action = GUI::Action::create("New directory...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [&](const GUI::Action&) {
String value;
if (GUI::InputBox::show(value, window(), "Enter name:", "New directory") == GUI::InputBox::ExecOK && !value.is_empty()) {
auto new_dir_path = LexicalPath::canonicalized_path(String::formatted("{}/{}", path(), value));
int rc = mkdir(new_dir_path.characters(), 0777);
if (rc < 0) {
auto saved_errno = errno;
GUI::MessageBox::show(window(), String::formatted("mkdir(\"{}\") failed: {}", new_dir_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
}
}
});
m_touch_action = GUI::Action::create("New file...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [&](const GUI::Action&) {
String value;
if (GUI::InputBox::show(value, window(), "Enter name:", "New file") == GUI::InputBox::ExecOK && !value.is_empty()) {
auto new_file_path = LexicalPath::canonicalized_path(String::formatted("{}/{}", path(), value));
struct stat st;
int rc = stat(new_file_path.characters(), &st);
if ((rc < 0 && errno != ENOENT)) {
auto saved_errno = errno;
GUI::MessageBox::show(window(), String::formatted("stat(\"{}\") failed: {}", new_file_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
return;
}
if (rc == 0) {
GUI::MessageBox::show(window(), String::formatted("{}: Already exists", new_file_path), "Error", GUI::MessageBox::Type::Error);
return;
}
int fd = creat(new_file_path.characters(), 0666);
if (fd < 0) {
auto saved_errno = errno;
GUI::MessageBox::show(window(), String::formatted("creat(\"{}\") failed: {}", new_file_path, strerror(saved_errno)), "Error", GUI::MessageBox::Type::Error);
return;
}
rc = close(fd);
ASSERT(rc >= 0);
}
});
m_open_terminal_action = GUI::Action::create("Open Terminal here", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"), [&](auto&) {
posix_spawn_file_actions_t spawn_actions;
posix_spawn_file_actions_init(&spawn_actions);
posix_spawn_file_actions_addchdir(&spawn_actions, path().characters());
pid_t pid;
const char* argv[] = { "Terminal", nullptr };
if ((errno = posix_spawn(&pid, "/bin/Terminal", &spawn_actions, nullptr, const_cast<char**>(argv), environ))) {
perror("posix_spawn");
} else {
if (disown(pid) < 0)
perror("disown");
}
posix_spawn_file_actions_destroy(&spawn_actions);
});
m_delete_action = GUI::CommonActions::make_delete_action([this](auto&) { do_delete(true); }, window());
m_force_delete_action = GUI::Action::create(
"Delete without confirmation", { Mod_Shift, Key_Delete },
[this](auto&) { do_delete(false); },
window());
}
void DirectoryView::handle_drop(const GUI::ModelIndex& index, const GUI::DropEvent& event)
{
if (!event.mime_data().has_urls())
return;
auto urls = event.mime_data().urls();
if (urls.is_empty()) {
dbgln("No files to drop");
return;
}
auto& target_node = node(index);
if (!target_node.is_directory())
return;
bool had_accepted_drop = false;
for (auto& url_to_copy : urls) {
if (!url_to_copy.is_valid() || url_to_copy.path() == target_node.full_path())
continue;
auto new_path = String::formatted("{}/{}", target_node.full_path(), LexicalPath(url_to_copy.path()).basename());
if (url_to_copy.path() == new_path)
continue;
if (!FileUtils::copy_file_or_directory(url_to_copy.path(), new_path)) {
auto error_message = String::formatted("Could not copy {} into {}.", url_to_copy.to_string(), new_path);
GUI::MessageBox::show(window(), error_message, "File Manager", GUI::MessageBox::Type::Error);
} else {
had_accepted_drop = true;
}
}
if (had_accepted_drop && on_accepted_drop)
on_accepted_drop();
}
}

View file

@ -0,0 +1,190 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/URL.h>
#include <AK/Vector.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/Action.h>
#include <LibGUI/ColumnsView.h>
#include <LibGUI/FileSystemModel.h>
#include <LibGUI/IconView.h>
#include <LibGUI/StackWidget.h>
#include <LibGUI/TableView.h>
#include <sys/stat.h>
namespace FileManager {
class LauncherHandler : public RefCounted<LauncherHandler> {
public:
LauncherHandler(const NonnullRefPtr<Desktop::Launcher::Details>& details)
: m_details(details)
{
}
NonnullRefPtr<GUI::Action> create_launch_action(Function<void(const LauncherHandler&)>);
const Desktop::Launcher::Details& details() const { return *m_details; }
private:
NonnullRefPtr<Desktop::Launcher::Details> m_details;
};
class DirectoryView final
: public GUI::StackWidget
, private GUI::ModelClient {
C_OBJECT(DirectoryView);
public:
enum class Mode {
Desktop,
Normal,
};
virtual ~DirectoryView() override;
void open(const StringView& path);
String path() const { return model().root_path(); }
void open_parent_directory();
void open_previous_directory();
void open_next_directory();
int path_history_size() const { return m_path_history.size(); }
int path_history_position() const { return m_path_history_position; }
static RefPtr<LauncherHandler> get_default_launch_handler(const NonnullRefPtrVector<LauncherHandler>& handlers);
NonnullRefPtrVector<LauncherHandler> get_launch_handlers(const URL& url);
NonnullRefPtrVector<LauncherHandler> get_launch_handlers(const String& path);
void refresh();
void launch(const AK::URL&, const LauncherHandler&);
Function<void(const StringView& path, bool can_write_in_path)> on_path_change;
Function<void(GUI::AbstractView&)> on_selection_change;
Function<void(const GUI::ModelIndex&, const GUI::ContextMenuEvent&)> on_context_menu_request;
Function<void(const StringView&)> on_status_message;
Function<void(int done, int total)> on_thumbnail_progress;
Function<void()> on_accepted_drop;
enum ViewMode {
Invalid,
Table,
Columns,
Icon
};
void set_view_mode(ViewMode);
ViewMode view_mode() const { return m_view_mode; }
GUI::AbstractView& current_view()
{
switch (m_view_mode) {
case ViewMode::Table:
return *m_table_view;
case ViewMode::Columns:
return *m_columns_view;
case ViewMode::Icon:
return *m_icon_view;
default:
ASSERT_NOT_REACHED();
}
}
const GUI::AbstractView& current_view() const
{
return const_cast<DirectoryView*>(this)->current_view();
}
template<typename Callback>
void for_each_view_implementation(Callback callback)
{
if (m_icon_view)
callback(*m_icon_view);
if (m_table_view)
callback(*m_table_view);
if (m_columns_view)
callback(*m_columns_view);
}
void set_should_show_dotfiles(bool);
const GUI::FileSystemModel::Node& node(const GUI::ModelIndex&) const;
bool is_desktop() const { return m_mode == Mode::Desktop; }
Vector<String> selected_file_paths() const;
GUI::Action& mkdir_action() { return *m_mkdir_action; }
GUI::Action& touch_action() { return *m_touch_action; }
GUI::Action& open_terminal_action() { return *m_open_terminal_action; }
GUI::Action& delete_action() { return *m_delete_action; }
GUI::Action& force_delete_action() { return *m_force_delete_action; }
private:
explicit DirectoryView(Mode);
const GUI::FileSystemModel& model() const { return *m_model; }
GUI::FileSystemModel& model() { return *m_model; }
void handle_selection_change();
void handle_drop(const GUI::ModelIndex&, const GUI::DropEvent&);
void do_delete(bool should_confirm);
// ^GUI::ModelClient
virtual void model_did_update(unsigned) override;
void setup_actions();
void setup_model();
void setup_icon_view();
void setup_columns_view();
void setup_table_view();
void handle_activation(const GUI::ModelIndex&);
void set_status_message(const StringView&);
void update_statusbar();
Mode m_mode { Mode::Normal };
ViewMode m_view_mode { Invalid };
NonnullRefPtr<GUI::FileSystemModel> m_model;
NonnullRefPtr<GUI::SortingProxyModel> m_sorting_model;
size_t m_path_history_position { 0 };
Vector<String> m_path_history;
void add_path_to_history(const StringView& path);
RefPtr<GUI::Label> m_error_label;
RefPtr<GUI::TableView> m_table_view;
RefPtr<GUI::IconView> m_icon_view;
RefPtr<GUI::ColumnsView> m_columns_view;
RefPtr<GUI::Action> m_mkdir_action;
RefPtr<GUI::Action> m_touch_action;
RefPtr<GUI::Action> m_open_terminal_action;
RefPtr<GUI::Action> m_delete_action;
RefPtr<GUI::Action> m_force_delete_action;
};
}

View file

@ -0,0 +1,53 @@
@GUI::Widget {
fill_with_background_color: true
layout: @GUI::VerticalBoxLayout {
spacing: 2
}
@GUI::ToolBarContainer {
@GUI::ToolBar {
name: "main_toolbar"
}
@GUI::ToolBar {
name: "location_toolbar"
visible: false
@GUI::Label {
text: "Location: "
autosize: true
}
@GUI::TextBox {
name: "location_textbox"
fixed_height: 22
}
}
@GUI::ToolBar {
name: "breadcrumb_toolbar"
@GUI::BreadcrumbBar {
name: "breadcrumb_bar"
}
}
}
@GUI::HorizontalSplitter {
name: "splitter"
@GUI::TreeView {
name: "tree_view"
fixed_width: 175
}
}
@GUI::StatusBar {
name: "statusbar"
@GUI::ProgressBar {
name: "progressbar"
text: "Generating thumbnails: "
visible: false
}
}
}

View file

@ -0,0 +1,280 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "FileUtils.h"
#include <AK/LexicalPath.h>
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <LibCore/DirIterator.h>
#include <LibCore/File.h>
#include <LibGUI/MessageBox.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
namespace FileUtils {
void delete_path(const String& path, GUI::Window* parent_window)
{
struct stat st;
if (lstat(path.characters(), &st)) {
GUI::MessageBox::show(parent_window,
String::formatted("lstat({}) failed: {}", path, strerror(errno)),
"Delete failed",
GUI::MessageBox::Type::Error);
}
if (S_ISDIR(st.st_mode)) {
String error_path;
int error = FileUtils::delete_directory(path, error_path);
if (error) {
GUI::MessageBox::show(parent_window,
String::formatted("Failed to delete directory \"{}\": {}", error_path, strerror(error)),
"Delete failed",
GUI::MessageBox::Type::Error);
}
} else if (unlink(path.characters()) < 0) {
int saved_errno = errno;
GUI::MessageBox::show(parent_window,
String::formatted("unlink(\"{}\") failed: {}", path, strerror(saved_errno)),
"Delete failed",
GUI::MessageBox::Type::Error);
}
}
void delete_paths(const Vector<String>& paths, bool should_confirm, GUI::Window* parent_window)
{
String message;
if (paths.size() == 1) {
message = String::formatted("Really delete {}?", LexicalPath(paths[0]).basename());
} else {
message = String::formatted("Really delete {} files?", paths.size());
}
if (should_confirm) {
auto result = GUI::MessageBox::show(parent_window,
message,
"Confirm deletion",
GUI::MessageBox::Type::Warning,
GUI::MessageBox::InputType::OKCancel);
if (result == GUI::MessageBox::ExecCancel)
return;
}
for (auto& path : paths) {
delete_path(path, parent_window);
}
}
int delete_directory(String directory, String& file_that_caused_error)
{
Core::DirIterator iterator(directory, Core::DirIterator::SkipDots);
if (iterator.has_error()) {
file_that_caused_error = directory;
return -1;
}
while (iterator.has_next()) {
auto file_to_delete = String::formatted("{}/{}", directory, iterator.next_path());
struct stat st;
if (lstat(file_to_delete.characters(), &st)) {
file_that_caused_error = file_to_delete;
return errno;
}
if (S_ISDIR(st.st_mode)) {
if (delete_directory(file_to_delete, file_to_delete)) {
file_that_caused_error = file_to_delete;
return errno;
}
} else if (unlink(file_to_delete.characters())) {
file_that_caused_error = file_to_delete;
return errno;
}
}
if (rmdir(directory.characters())) {
file_that_caused_error = directory;
return errno;
}
return 0;
}
bool copy_file_or_directory(const String& src_path, const String& dst_path)
{
int duplicate_count = 0;
while (access(get_duplicate_name(dst_path, duplicate_count).characters(), F_OK) == 0) {
++duplicate_count;
}
if (duplicate_count != 0) {
return copy_file_or_directory(src_path, get_duplicate_name(dst_path, duplicate_count));
}
auto source_or_error = Core::File::open(src_path, Core::IODevice::ReadOnly);
if (source_or_error.is_error())
return false;
auto& source = *source_or_error.value();
struct stat src_stat;
int rc = fstat(source.fd(), &src_stat);
if (rc < 0)
return false;
if (source.is_directory())
return copy_directory(src_path, dst_path, src_stat);
return copy_file(dst_path, src_stat, source);
}
bool copy_directory(const String& src_path, const String& dst_path, const struct stat& src_stat)
{
int rc = mkdir(dst_path.characters(), 0755);
if (rc < 0) {
return false;
}
Core::DirIterator di(src_path, Core::DirIterator::SkipDots);
if (di.has_error()) {
return false;
}
while (di.has_next()) {
String filename = di.next_path();
bool is_copied = copy_file_or_directory(
String::formatted("{}/{}", src_path, filename),
String::formatted("{}/{}", dst_path, filename));
if (!is_copied) {
return false;
}
}
auto my_umask = umask(0);
umask(my_umask);
rc = chmod(dst_path.characters(), src_stat.st_mode & ~my_umask);
if (rc < 0) {
return false;
}
return true;
}
bool copy_file(const String& dst_path, const struct stat& src_stat, Core::File& source)
{
int dst_fd = creat(dst_path.characters(), 0666);
if (dst_fd < 0) {
if (errno != EISDIR) {
return false;
}
auto dst_dir_path = String::formatted("{}/{}", dst_path, LexicalPath(source.filename()).basename());
dst_fd = creat(dst_dir_path.characters(), 0666);
if (dst_fd < 0) {
return false;
}
}
ScopeGuard close_fd_guard([dst_fd]() { close(dst_fd); });
if (src_stat.st_size > 0) {
if (ftruncate(dst_fd, src_stat.st_size) < 0) {
perror("cp: ftruncate");
return false;
}
}
for (;;) {
char buffer[32768];
ssize_t nread = read(source.fd(), buffer, sizeof(buffer));
if (nread < 0) {
return false;
}
if (nread == 0)
break;
ssize_t remaining_to_write = nread;
char* bufptr = buffer;
while (remaining_to_write) {
ssize_t nwritten = write(dst_fd, bufptr, remaining_to_write);
if (nwritten < 0) {
return false;
}
assert(nwritten > 0);
remaining_to_write -= nwritten;
bufptr += nwritten;
}
}
auto my_umask = umask(0);
umask(my_umask);
int rc = fchmod(dst_fd, src_stat.st_mode & ~my_umask);
if (rc < 0) {
return false;
}
return true;
}
bool link_file(const String& src_path, const String& dst_path)
{
int duplicate_count = 0;
while (access(get_duplicate_name(dst_path, duplicate_count).characters(), F_OK) == 0) {
++duplicate_count;
}
if (duplicate_count != 0) {
return link_file(src_path, get_duplicate_name(dst_path, duplicate_count));
}
int rc = symlink(src_path.characters(), dst_path.characters());
if (rc < 0) {
return false;
}
return true;
}
String get_duplicate_name(const String& path, int duplicate_count)
{
if (duplicate_count == 0) {
return path;
}
LexicalPath lexical_path(path);
StringBuilder duplicated_name;
duplicated_name.append('/');
for (size_t i = 0; i < lexical_path.parts().size() - 1; ++i) {
duplicated_name.appendff("{}/", lexical_path.parts()[i]);
}
auto prev_duplicate_tag = String::formatted("({})", duplicate_count);
auto title = lexical_path.title();
if (title.ends_with(prev_duplicate_tag)) {
// remove the previous duplicate tag "(n)" so we can add a new tag.
title = title.substring(0, title.length() - prev_duplicate_tag.length());
}
duplicated_name.appendff("{} ({})", lexical_path.title(), duplicate_count);
if (!lexical_path.extension().is_empty()) {
duplicated_name.appendff(".{}", lexical_path.extension());
}
return duplicated_name.build();
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <AK/String.h>
#include <LibCore/Forward.h>
#include <LibGUI/Forward.h>
#include <sys/stat.h>
namespace FileUtils {
enum class FileOperation {
Copy = 0,
Cut
};
void delete_path(const String&, GUI::Window*);
void delete_paths(const Vector<String>&, bool should_confirm, GUI::Window*);
int delete_directory(String directory, String& file_that_caused_error);
bool copy_file_or_directory(const String& src_path, const String& dst_path);
String get_duplicate_name(const String& path, int duplicate_count);
bool copy_file(const String& dst_path, const struct stat& src_stat, Core::File&);
bool copy_directory(const String& src_path, const String& dst_path, const struct stat& src_stat);
bool link_file(const String& src_path, const String& dst_path);
}

View file

@ -0,0 +1,304 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "PropertiesWindow.h"
#include <AK/LexicalPath.h>
#include <AK/StringBuilder.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/LinkLabel.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/SeparatorWidget.h>
#include <LibGUI/TabWidget.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
PropertiesWindow::PropertiesWindow(const String& path, bool disable_rename, Window* parent_window)
: Window(parent_window)
{
auto lexical_path = LexicalPath(path);
ASSERT(lexical_path.is_valid());
auto& main_widget = set_main_widget<GUI::Widget>();
main_widget.set_layout<GUI::VerticalBoxLayout>();
main_widget.layout()->set_margins({ 4, 4, 4, 4 });
main_widget.set_fill_with_background_color(true);
set_rect({ 0, 0, 360, 420 });
set_resizable(false);
auto& tab_widget = main_widget.add<GUI::TabWidget>();
auto& general_tab = tab_widget.add_tab<GUI::Widget>("General");
general_tab.set_layout<GUI::VerticalBoxLayout>();
general_tab.layout()->set_margins({ 12, 8, 12, 8 });
general_tab.layout()->set_spacing(10);
auto& file_container = general_tab.add<GUI::Widget>();
file_container.set_layout<GUI::HorizontalBoxLayout>();
file_container.layout()->set_spacing(20);
file_container.set_fixed_height(34);
m_icon = file_container.add<GUI::ImageWidget>();
m_icon->set_fixed_size(32, 32);
m_name = lexical_path.basename();
m_path = lexical_path.string();
m_parent_path = lexical_path.dirname();
m_name_box = file_container.add<GUI::TextBox>();
m_name_box->set_text(m_name);
m_name_box->set_mode(disable_rename ? GUI::TextBox::Mode::DisplayOnly : GUI::TextBox::Mode::Editable);
m_name_box->on_change = [&]() {
m_name_dirty = m_name != m_name_box->text();
m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty);
};
set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"));
general_tab.add<GUI::SeparatorWidget>(Gfx::Orientation::Horizontal);
struct stat st;
if (lstat(path.characters(), &st)) {
perror("stat");
return;
}
String owner_name;
String group_name;
if (auto* pw = getpwuid(st.st_uid)) {
owner_name = pw->pw_name;
} else {
owner_name = "n/a";
}
if (auto* gr = getgrgid(st.st_gid)) {
group_name = gr->gr_name;
} else {
group_name = "n/a";
}
m_mode = st.st_mode;
m_old_mode = st.st_mode;
auto properties = Vector<PropertyValuePair>();
properties.append({ "Type:", get_description(m_mode) });
auto parent_link = URL::create_with_file_protocol(m_parent_path);
properties.append(PropertyValuePair { "Location:", path, Optional(parent_link) });
if (S_ISLNK(m_mode)) {
auto link_destination = Core::File::read_link(path);
if (link_destination.is_null()) {
perror("readlink");
} else {
auto link_directory = LexicalPath(link_destination);
ASSERT(link_directory.is_valid());
auto link_parent = URL::create_with_file_protocol(link_directory.dirname());
properties.append({ "Link target:", link_destination, Optional(link_parent) });
}
}
properties.append({ "Size:", String::formatted("{} bytes", st.st_size) });
properties.append({ "Owner:", String::formatted("{} ({})", owner_name, st.st_uid) });
properties.append({ "Group:", String::formatted("{} ({})", group_name, st.st_gid) });
properties.append({ "Created at:", GUI::FileSystemModel::timestamp_string(st.st_ctime) });
properties.append({ "Last modified:", GUI::FileSystemModel::timestamp_string(st.st_mtime) });
make_property_value_pairs(properties, general_tab);
general_tab.add<GUI::SeparatorWidget>(Gfx::Orientation::Horizontal);
make_permission_checkboxes(general_tab, { S_IRUSR, S_IWUSR, S_IXUSR }, "Owner:", m_mode);
make_permission_checkboxes(general_tab, { S_IRGRP, S_IWGRP, S_IXGRP }, "Group:", m_mode);
make_permission_checkboxes(general_tab, { S_IROTH, S_IWOTH, S_IXOTH }, "Others:", m_mode);
general_tab.layout()->add_spacer();
auto& button_widget = main_widget.add<GUI::Widget>();
button_widget.set_layout<GUI::HorizontalBoxLayout>();
button_widget.set_fixed_height(24);
button_widget.layout()->set_spacing(5);
button_widget.layout()->add_spacer();
make_button("OK", button_widget).on_click = [this](auto) {
if (apply_changes())
close();
};
make_button("Cancel", button_widget).on_click = [this](auto) {
close();
};
m_apply_button = make_button("Apply", button_widget);
m_apply_button->on_click = [this](auto) { apply_changes(); };
m_apply_button->set_enabled(false);
update();
}
PropertiesWindow::~PropertiesWindow()
{
}
void PropertiesWindow::update()
{
m_icon->set_bitmap(GUI::FileIconProvider::icon_for_path(make_full_path(m_name), m_mode).bitmap_for_size(32));
set_title(String::formatted("{} - Properties", m_name));
}
void PropertiesWindow::permission_changed(mode_t mask, bool set)
{
if (set) {
m_mode |= mask;
} else {
m_mode &= ~mask;
}
m_permissions_dirty = m_mode != m_old_mode;
m_apply_button->set_enabled(m_name_dirty || m_permissions_dirty);
}
String PropertiesWindow::make_full_path(const String& name)
{
return String::formatted("{}/{}", m_parent_path, name);
}
bool PropertiesWindow::apply_changes()
{
if (m_name_dirty) {
String new_name = m_name_box->text();
String new_file = make_full_path(new_name).characters();
if (GUI::FilePicker::file_exists(new_file)) {
GUI::MessageBox::show(this, String::formatted("A file \"{}\" already exists!", new_name), "Error", GUI::MessageBox::Type::Error);
return false;
}
if (rename(make_full_path(m_name).characters(), new_file.characters())) {
GUI::MessageBox::show(this, String::formatted("Could not rename file: {}!", strerror(errno)), "Error", GUI::MessageBox::Type::Error);
return false;
}
m_name = new_name;
m_name_dirty = false;
update();
}
if (m_permissions_dirty) {
if (chmod(make_full_path(m_name).characters(), m_mode)) {
GUI::MessageBox::show(this, String::formatted("Could not update permissions: {}!", strerror(errno)), "Error", GUI::MessageBox::Type::Error);
return false;
}
m_old_mode = m_mode;
m_permissions_dirty = false;
}
update();
m_apply_button->set_enabled(false);
return true;
}
void PropertiesWindow::make_permission_checkboxes(GUI::Widget& parent, PermissionMasks masks, String label_string, mode_t mode)
{
auto& widget = parent.add<GUI::Widget>();
widget.set_layout<GUI::HorizontalBoxLayout>();
widget.set_fixed_height(16);
widget.layout()->set_spacing(10);
auto& label = widget.add<GUI::Label>(label_string);
label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
struct stat st;
if (lstat(m_path.characters(), &st)) {
perror("stat");
return;
}
auto can_edit_checkboxes = st.st_uid == getuid();
auto& box_read = widget.add<GUI::CheckBox>("Read");
box_read.set_checked(mode & masks.read);
box_read.on_checked = [&, masks](bool checked) { permission_changed(masks.read, checked); };
box_read.set_enabled(can_edit_checkboxes);
auto& box_write = widget.add<GUI::CheckBox>("Write");
box_write.set_checked(mode & masks.write);
box_write.on_checked = [&, masks](bool checked) { permission_changed(masks.write, checked); };
box_write.set_enabled(can_edit_checkboxes);
auto& box_execute = widget.add<GUI::CheckBox>("Execute");
box_execute.set_checked(mode & masks.execute);
box_execute.on_checked = [&, masks](bool checked) { permission_changed(masks.execute, checked); };
box_execute.set_enabled(can_edit_checkboxes);
}
void PropertiesWindow::make_property_value_pairs(const Vector<PropertyValuePair>& pairs, GUI::Widget& parent)
{
int max_width = 0;
Vector<NonnullRefPtr<GUI::Label>> property_labels;
property_labels.ensure_capacity(pairs.size());
for (auto pair : pairs) {
auto& label_container = parent.add<GUI::Widget>();
label_container.set_layout<GUI::HorizontalBoxLayout>();
label_container.set_fixed_height(14);
label_container.layout()->set_spacing(12);
auto& label_property = label_container.add<GUI::Label>(pair.property);
label_property.set_text_alignment(Gfx::TextAlignment::CenterLeft);
if (!pair.link.has_value()) {
label_container.add<GUI::Label>(pair.value).set_text_alignment(Gfx::TextAlignment::CenterLeft);
} else {
auto& link = label_container.add<GUI::LinkLabel>(pair.value);
link.set_text_alignment(Gfx::TextAlignment::CenterLeft);
link.on_click = [pair]() {
Desktop::Launcher::open(pair.link.value());
};
}
max_width = max(max_width, label_property.font().width(pair.property));
property_labels.append(label_property);
}
for (auto label : property_labels)
label->set_fixed_width(max_width);
}
GUI::Button& PropertiesWindow::make_button(String text, GUI::Widget& parent)
{
auto& button = parent.add<GUI::Button>(text);
button.set_fixed_size(70, 22);
return button;
}

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <LibCore/File.h>
#include <LibGUI/Button.h>
#include <LibGUI/Dialog.h>
#include <LibGUI/FileSystemModel.h>
#include <LibGUI/ImageWidget.h>
#include <LibGUI/Label.h>
#include <LibGUI/TextBox.h>
class PropertiesWindow final : public GUI::Window {
C_OBJECT(PropertiesWindow);
public:
virtual ~PropertiesWindow() override;
private:
PropertiesWindow(const String& path, bool disable_rename, Window* parent = nullptr);
struct PropertyValuePair {
String property;
String value;
Optional<URL> link = {};
};
struct PermissionMasks {
mode_t read;
mode_t write;
mode_t execute;
};
static const String get_description(const mode_t mode)
{
if (S_ISREG(mode))
return "File";
if (S_ISDIR(mode))
return "Directory";
if (S_ISLNK(mode))
return "Symbolic link";
if (S_ISCHR(mode))
return "Character device";
if (S_ISBLK(mode))
return "Block device";
if (S_ISFIFO(mode))
return "FIFO (named pipe)";
if (S_ISSOCK(mode))
return "Socket";
if (mode & S_IXUSR)
return "Executable";
return "Unknown";
}
GUI::Button& make_button(String, GUI::Widget& parent);
void make_property_value_pairs(const Vector<PropertyValuePair>& pairs, GUI::Widget& parent);
void make_permission_checkboxes(GUI::Widget& parent, PermissionMasks, String label_string, mode_t mode);
void permission_changed(mode_t mask, bool set);
bool apply_changes();
void update();
String make_full_path(const String& name);
RefPtr<GUI::Button> m_apply_button;
RefPtr<GUI::TextBox> m_name_box;
RefPtr<GUI::ImageWidget> m_icon;
String m_name;
String m_parent_path;
String m_path;
mode_t m_mode { 0 };
mode_t m_old_mode { 0 };
bool m_permissions_dirty { false };
bool m_name_dirty { false };
};

View file

@ -0,0 +1,974 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "DesktopWidget.h"
#include "DirectoryView.h"
#include "FileUtils.h"
#include "PropertiesWindow.h"
#include <AK/LexicalPath.h>
#include <AK/StringBuilder.h>
#include <AK/URL.h>
#include <Applications/FileManager/FileManagerWindowGML.h>
#include <LibCore/ConfigFile.h>
#include <LibCore/MimeData.h>
#include <LibCore/StandardPaths.h>
#include <LibDesktop/Launcher.h>
#include <LibGUI/Action.h>
#include <LibGUI/ActionGroup.h>
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/BreadcrumbBar.h>
#include <LibGUI/Clipboard.h>
#include <LibGUI/Desktop.h>
#include <LibGUI/FileIconProvider.h>
#include <LibGUI/FileSystemModel.h>
#include <LibGUI/InputBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/MenuBar.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ProgressBar.h>
#include <LibGUI/Splitter.h>
#include <LibGUI/StatusBar.h>
#include <LibGUI/TextEditor.h>
#include <LibGUI/ToolBar.h>
#include <LibGUI/ToolBarContainer.h>
#include <LibGUI/TreeView.h>
#include <LibGUI/Widget.h>
#include <LibGUI/Window.h>
#include <LibGfx/Palette.h>
#include <pthread.h>
#include <serenity.h>
#include <signal.h>
#include <spawn.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
using namespace FileManager;
static int run_in_desktop_mode(RefPtr<Core::ConfigFile>);
static int run_in_windowed_mode(RefPtr<Core::ConfigFile>, String initial_location);
static void do_copy(const Vector<String>& selected_file_paths, FileUtils::FileOperation file_operation);
static void do_paste(const String& target_directory, GUI::Window* window);
static void do_create_link(const Vector<String>& selected_file_paths, GUI::Window* window);
static void show_properties(const String& container_dir_path, const String& path, const Vector<String>& selected, GUI::Window* window);
int main(int argc, char** argv)
{
if (pledge("stdio thread shared_buffer accept unix cpath rpath wpath fattr proc exec sigaction", nullptr) < 0) {
perror("pledge");
return 1;
}
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_flags = SA_NOCLDWAIT;
act.sa_handler = SIG_IGN;
int rc = sigaction(SIGCHLD, &act, nullptr);
if (rc < 0) {
perror("sigaction");
return 1;
}
RefPtr<Core::ConfigFile> config = Core::ConfigFile::get_for_app("FileManager");
auto app = GUI::Application::construct(argc, argv);
if (pledge("stdio thread shared_buffer accept cpath rpath wpath fattr proc exec unix", nullptr) < 0) {
perror("pledge");
return 1;
}
if (app->args().contains_slow("--desktop") || app->args().contains_slow("-d"))
return run_in_desktop_mode(move(config));
// our initial location is defined as, in order of precedence:
// 1. the first command-line argument (e.g. FileManager /bin)
// 2. the user's home directory
// 3. the root directory
String initial_location;
if (argc >= 2) {
char* buffer = realpath(argv[1], nullptr);
initial_location = buffer;
free(buffer);
}
if (initial_location.is_empty())
initial_location = Core::StandardPaths::home_directory();
if (initial_location.is_empty())
initial_location = "/";
return run_in_windowed_mode(move(config), initial_location);
}
void do_copy(const Vector<String>& selected_file_paths, FileUtils::FileOperation file_operation)
{
if (selected_file_paths.is_empty())
ASSERT_NOT_REACHED();
StringBuilder copy_text;
if (file_operation == FileUtils::FileOperation::Cut) {
copy_text.append("#cut\n"); // This exploits the comment lines in the text/uri-list specification, which might be a bit hackish
}
for (auto& path : selected_file_paths) {
auto url = URL::create_with_file_protocol(path);
copy_text.appendff("{}\n", url);
}
GUI::Clipboard::the().set_data(copy_text.build().bytes(), "text/uri-list");
}
void do_paste(const String& target_directory, GUI::Window* window)
{
auto data_and_type = GUI::Clipboard::the().data_and_type();
if (data_and_type.mime_type != "text/uri-list") {
dbgln("Cannot paste clipboard type {}", data_and_type.mime_type);
return;
}
auto copied_lines = String::copy(data_and_type.data).split('\n');
if (copied_lines.is_empty()) {
dbgln("No files to paste");
return;
}
bool should_delete_src = false;
if (copied_lines[0] == "#cut") { // cut operation encoded as a text/uri-list commen
should_delete_src = true;
copied_lines.remove(0);
}
for (auto& uri_as_string : copied_lines) {
if (uri_as_string.is_empty())
continue;
URL url = uri_as_string;
if (!url.is_valid() || url.protocol() != "file") {
dbgln("Cannot paste URI {}", uri_as_string);
continue;
}
auto new_path = String::formatted("{}/{}", target_directory, url.basename());
if (!FileUtils::copy_file_or_directory(url.path(), new_path)) {
auto error_message = String::formatted("Could not paste {}.", url.path());
GUI::MessageBox::show(window, error_message, "File Manager", GUI::MessageBox::Type::Error);
} else if (should_delete_src) {
FileUtils::delete_path(url.path(), window);
}
}
}
void do_create_link(const Vector<String>& selected_file_paths, GUI::Window* window)
{
auto path = selected_file_paths.first();
auto destination = String::formatted("{}/{}", Core::StandardPaths::desktop_directory(), LexicalPath { path }.basename());
if (!FileUtils::link_file(path, destination)) {
GUI::MessageBox::show(window, "Could not create desktop shortcut", "File Manager",
GUI::MessageBox::Type::Error);
}
}
void show_properties(const String& container_dir_path, const String& path, const Vector<String>& selected, GUI::Window* window)
{
RefPtr<PropertiesWindow> properties;
if (selected.is_empty()) {
properties = window->add<PropertiesWindow>(path, true);
} else {
properties = window->add<PropertiesWindow>(selected.first(), access(container_dir_path.characters(), W_OK) != 0);
}
properties->on_close = [properties = properties.ptr()] {
properties->remove_from_parent();
};
properties->center_on_screen();
properties->show();
}
int run_in_desktop_mode([[maybe_unused]] RefPtr<Core::ConfigFile> config)
{
static constexpr const char* process_name = "FileManager (Desktop)";
set_process_name(process_name, strlen(process_name));
pthread_setname_np(pthread_self(), process_name);
auto window = GUI::Window::construct();
window->set_title("Desktop Manager");
window->set_window_type(GUI::WindowType::Desktop);
window->set_has_alpha_channel(true);
auto& desktop_widget = window->set_main_widget<FileManager::DesktopWidget>();
desktop_widget.set_layout<GUI::VerticalBoxLayout>();
[[maybe_unused]] auto& directory_view = desktop_widget.add<DirectoryView>(DirectoryView::Mode::Desktop);
auto copy_action = GUI::CommonActions::make_copy_action(
[&](auto&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty())
ASSERT_NOT_REACHED();
do_copy(paths, FileUtils::FileOperation::Copy);
},
window);
copy_action->set_enabled(false);
auto cut_action = GUI::CommonActions::make_cut_action(
[&](auto&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty())
ASSERT_NOT_REACHED();
do_copy(paths, FileUtils::FileOperation::Cut);
},
window);
cut_action->set_enabled(false);
directory_view.on_selection_change = [&](const GUI::AbstractView& view) {
copy_action->set_enabled(!view.selection().is_empty());
cut_action->set_enabled(!view.selection().is_empty());
};
auto properties_action
= GUI::Action::create(
"Properties", { Mod_Alt, Key_Return }, Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"), [&](const GUI::Action&) {
String path = directory_view.path();
Vector<String> selected = directory_view.selected_file_paths();
show_properties(path, path, selected, directory_view.window());
},
window);
auto paste_action = GUI::CommonActions::make_paste_action(
[&](const GUI::Action&) {
do_paste(directory_view.path(), directory_view.window());
},
window);
paste_action->set_enabled(GUI::Clipboard::the().mime_type() == "text/uri-list" && access(directory_view.path().characters(), W_OK) == 0);
GUI::Clipboard::the().on_change = [&](const String& data_type) {
paste_action->set_enabled(data_type == "text/uri-list" && access(directory_view.path().characters(), W_OK) == 0);
};
auto desktop_view_context_menu = GUI::Menu::construct("Directory View");
auto file_manager_action = GUI::Action::create("Show in File Manager", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-folder.png"), [&](const GUI::Action&) {
Desktop::Launcher::open(URL::create_with_file_protocol(directory_view.path()));
});
auto display_properties_action = GUI::Action::create("Display Settings", {}, Gfx::Bitmap::load_from_file("/res/icons/16x16/app-display-settings.png"), [&](const GUI::Action&) {
Desktop::Launcher::open(URL::create_with_file_protocol("/bin/DisplaySettings"));
});
desktop_view_context_menu->add_action(directory_view.mkdir_action());
desktop_view_context_menu->add_action(directory_view.touch_action());
desktop_view_context_menu->add_action(paste_action);
desktop_view_context_menu->add_separator();
desktop_view_context_menu->add_action(file_manager_action);
desktop_view_context_menu->add_action(directory_view.open_terminal_action());
desktop_view_context_menu->add_separator();
desktop_view_context_menu->add_action(display_properties_action);
auto desktop_context_menu = GUI::Menu::construct("Directory View Directory");
desktop_context_menu->add_action(copy_action);
desktop_context_menu->add_action(cut_action);
desktop_context_menu->add_action(paste_action);
desktop_context_menu->add_action(directory_view.delete_action());
desktop_context_menu->add_separator();
desktop_context_menu->add_action(properties_action);
directory_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
if (!index.is_valid())
desktop_view_context_menu->popup(event.screen_position());
else
desktop_context_menu->popup(event.screen_position());
};
auto wm_config = Core::ConfigFile::get_for_app("WindowManager");
auto selected_wallpaper = wm_config->read_entry("Background", "Wallpaper", "");
if (!selected_wallpaper.is_empty()) {
GUI::Desktop::the().set_wallpaper(selected_wallpaper, false);
}
window->show();
return GUI::Application::the()->exec();
}
int run_in_windowed_mode(RefPtr<Core::ConfigFile> config, String initial_location)
{
auto window = GUI::Window::construct();
window->set_title("File Manager");
auto left = config->read_num_entry("Window", "Left", 150);
auto top = config->read_num_entry("Window", "Top", 75);
auto width = config->read_num_entry("Window", "Width", 640);
auto height = config->read_num_entry("Window", "Height", 480);
window->set_rect({ left, top, width, height });
auto& widget = window->set_main_widget<GUI::Widget>();
widget.load_from_gml(file_manager_window_gml);
auto& main_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("main_toolbar");
auto& location_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("location_toolbar");
location_toolbar.layout()->set_margins({ 6, 3, 6, 3 });
auto& location_textbox = *widget.find_descendant_of_type_named<GUI::TextBox>("location_textbox");
auto& breadcrumb_toolbar = *widget.find_descendant_of_type_named<GUI::ToolBar>("breadcrumb_toolbar");
breadcrumb_toolbar.layout()->set_margins({});
auto& breadcrumb_bar = *widget.find_descendant_of_type_named<GUI::BreadcrumbBar>("breadcrumb_bar");
location_textbox.on_focusout = [&] {
location_toolbar.set_visible(false);
breadcrumb_toolbar.set_visible(true);
};
auto& splitter = *widget.find_descendant_of_type_named<GUI::HorizontalSplitter>("splitter");
auto& tree_view = *widget.find_descendant_of_type_named<GUI::TreeView>("tree_view");
auto directories_model = GUI::FileSystemModel::create({}, GUI::FileSystemModel::Mode::DirectoriesOnly);
tree_view.set_model(directories_model);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Icon, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Size, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Owner, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Group, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Permissions, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::ModificationTime, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::Inode, true);
tree_view.set_column_hidden(GUI::FileSystemModel::Column::SymlinkTarget, true);
bool is_reacting_to_tree_view_selection_change = false;
auto& directory_view = splitter.add<DirectoryView>(DirectoryView::Mode::Normal);
location_textbox.on_escape_pressed = [&] {
directory_view.set_focus(true);
};
// Open the root directory. FIXME: This is awkward.
tree_view.toggle_index(directories_model->index(0, 0, {}));
auto& statusbar = *widget.find_descendant_of_type_named<GUI::StatusBar>("statusbar");
auto& progressbar = *widget.find_descendant_of_type_named<GUI::ProgressBar>("progressbar");
progressbar.set_format(GUI::ProgressBar::Format::ValueSlashMax);
progressbar.set_frame_shape(Gfx::FrameShape::Panel);
progressbar.set_frame_shadow(Gfx::FrameShadow::Sunken);
progressbar.set_frame_thickness(1);
location_textbox.on_return_pressed = [&] {
directory_view.open(location_textbox.text());
};
auto refresh_tree_view = [&] {
directories_model->update();
auto current_path = directory_view.path();
struct stat st;
// If the directory no longer exists, we find a parent that does.
while (stat(current_path.characters(), &st) != 0) {
directory_view.open_parent_directory();
current_path = directory_view.path();
if (current_path == directories_model->root_path()) {
break;
}
}
// Reselect the existing folder in the tree.
auto new_index = directories_model->index(current_path, GUI::FileSystemModel::Column::Name);
if (new_index.is_valid()) {
tree_view.expand_all_parents_of(new_index);
tree_view.set_cursor(new_index, GUI::AbstractView::SelectionUpdate::Set, true);
}
directory_view.refresh();
};
auto directory_context_menu = GUI::Menu::construct("Directory View Directory");
auto directory_view_context_menu = GUI::Menu::construct("Directory View");
auto tree_view_directory_context_menu = GUI::Menu::construct("Tree View Directory");
auto tree_view_context_menu = GUI::Menu::construct("Tree View");
auto open_parent_directory_action = GUI::Action::create("Open parent directory", { Mod_Alt, Key_Up }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open-parent-directory.png"), [&](const GUI::Action&) {
directory_view.open_parent_directory();
});
RefPtr<GUI::Action> view_as_table_action;
RefPtr<GUI::Action> view_as_icons_action;
RefPtr<GUI::Action> view_as_columns_action;
view_as_icons_action = GUI::Action::create_checkable(
"Icon view", { Mod_Ctrl, KeyCode::Key_1 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/icon-view.png"), [&](const GUI::Action&) {
directory_view.set_view_mode(DirectoryView::ViewMode::Icon);
config->write_entry("DirectoryView", "ViewMode", "Icon");
config->sync();
},
window);
view_as_table_action = GUI::Action::create_checkable(
"Table view", { Mod_Ctrl, KeyCode::Key_2 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/table-view.png"), [&](const GUI::Action&) {
directory_view.set_view_mode(DirectoryView::ViewMode::Table);
config->write_entry("DirectoryView", "ViewMode", "Table");
config->sync();
},
window);
view_as_columns_action = GUI::Action::create_checkable(
"Columns view", { Mod_Ctrl, KeyCode::Key_3 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/columns-view.png"), [&](const GUI::Action&) {
directory_view.set_view_mode(DirectoryView::ViewMode::Columns);
config->write_entry("DirectoryView", "ViewMode", "Columns");
config->sync();
},
window);
auto view_type_action_group = make<GUI::ActionGroup>();
view_type_action_group->set_exclusive(true);
view_type_action_group->add_action(*view_as_icons_action);
view_type_action_group->add_action(*view_as_table_action);
view_type_action_group->add_action(*view_as_columns_action);
auto tree_view_selected_file_paths = [&] {
Vector<String> paths;
auto& view = tree_view;
view.selection().for_each_index([&](const GUI::ModelIndex& index) {
paths.append(directories_model->full_path(index));
});
return paths;
};
auto select_all_action = GUI::Action::create("Select all", { Mod_Ctrl, KeyCode::Key_A }, [&](const GUI::Action&) {
directory_view.current_view().select_all();
});
auto copy_action = GUI::CommonActions::make_copy_action(
[&](auto&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty())
paths = tree_view_selected_file_paths();
if (paths.is_empty())
ASSERT_NOT_REACHED();
do_copy(paths, FileUtils::FileOperation::Copy);
refresh_tree_view();
},
window);
copy_action->set_enabled(false);
auto cut_action = GUI::CommonActions::make_cut_action(
[&](auto&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty())
paths = tree_view_selected_file_paths();
if (paths.is_empty())
ASSERT_NOT_REACHED();
do_copy(paths, FileUtils::FileOperation::Cut);
refresh_tree_view();
},
window);
cut_action->set_enabled(false);
auto shortcut_action
= GUI::Action::create(
"Create desktop shortcut",
{},
Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-symlink.png"),
[&](const GUI::Action&) {
auto paths = directory_view.selected_file_paths();
if (paths.is_empty()) {
return;
}
do_create_link(paths, directory_view.window());
},
window);
auto properties_action
= GUI::Action::create(
"Properties", { Mod_Alt, Key_Return }, Gfx::Bitmap::load_from_file("/res/icons/16x16/properties.png"), [&](const GUI::Action& action) {
String container_dir_path;
String path;
Vector<String> selected;
if (action.activator() == directory_context_menu || directory_view.active_widget()->is_focused()) {
path = directory_view.path();
container_dir_path = path;
selected = directory_view.selected_file_paths();
} else {
path = directories_model->full_path(tree_view.selection().first());
container_dir_path = LexicalPath(path).basename();
selected = tree_view_selected_file_paths();
}
show_properties(container_dir_path, path, selected, directory_view.window());
},
window);
auto paste_action = GUI::CommonActions::make_paste_action(
[&](const GUI::Action& action) {
String target_directory;
if (action.activator() == directory_context_menu)
target_directory = directory_view.selected_file_paths()[0];
else
target_directory = directory_view.path();
do_paste(target_directory, directory_view.window());
refresh_tree_view();
},
window);
auto folder_specific_paste_action = GUI::CommonActions::make_paste_action(
[&](const GUI::Action& action) {
String target_directory;
if (action.activator() == directory_context_menu)
target_directory = directory_view.selected_file_paths()[0];
else
target_directory = directory_view.path();
do_paste(target_directory, directory_view.window());
refresh_tree_view();
},
window);
auto go_back_action = GUI::CommonActions::make_go_back_action(
[&](auto&) {
directory_view.open_previous_directory();
},
window);
auto go_forward_action = GUI::CommonActions::make_go_forward_action(
[&](auto&) {
directory_view.open_next_directory();
},
window);
auto go_home_action = GUI::CommonActions::make_go_home_action(
[&](auto&) {
directory_view.open(Core::StandardPaths::home_directory());
},
window);
GUI::Clipboard::the().on_change = [&](const String& data_type) {
auto current_location = directory_view.path();
paste_action->set_enabled(data_type == "text/uri-list" && access(current_location.characters(), W_OK) == 0);
};
auto tree_view_delete_action = GUI::CommonActions::make_delete_action(
[&](auto&) {
FileUtils::delete_paths(tree_view_selected_file_paths(), true, window);
refresh_tree_view();
},
&tree_view);
// This is a little awkward. The menu action does something different depending on which view has focus.
// It would be nice to find a good abstraction for this instead of creating a branching action like this.
auto focus_dependent_delete_action = GUI::CommonActions::make_delete_action([&](auto&) {
if (tree_view.is_focused())
tree_view_delete_action->activate();
else
directory_view.delete_action().activate();
refresh_tree_view();
});
focus_dependent_delete_action->set_enabled(false);
auto mkdir_action = GUI::Action::create("New directory...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [&](const GUI::Action&) {
directory_view.mkdir_action().activate();
refresh_tree_view();
});
auto touch_action = GUI::Action::create("New file...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [&](const GUI::Action&) {
directory_view.touch_action().activate();
refresh_tree_view();
});
auto menubar = GUI::MenuBar::construct();
auto& app_menu = menubar->add_menu("File Manager");
app_menu.add_action(mkdir_action);
app_menu.add_action(touch_action);
app_menu.add_action(copy_action);
app_menu.add_action(cut_action);
app_menu.add_action(paste_action);
app_menu.add_action(focus_dependent_delete_action);
app_menu.add_action(directory_view.open_terminal_action());
app_menu.add_separator();
app_menu.add_action(properties_action);
app_menu.add_separator();
app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
GUI::Application::the()->quit();
}));
auto action_show_dotfiles = GUI::Action::create_checkable("Show dotfiles", { Mod_Ctrl, Key_H }, [&](auto& action) {
directory_view.set_should_show_dotfiles(action.is_checked());
refresh_tree_view();
});
auto& view_menu = menubar->add_menu("View");
view_menu.add_action(*view_as_icons_action);
view_menu.add_action(*view_as_table_action);
view_menu.add_action(*view_as_columns_action);
view_menu.add_separator();
view_menu.add_action(action_show_dotfiles);
auto go_to_location_action = GUI::Action::create("Go to location...", { Mod_Ctrl, Key_L }, [&](auto&) {
location_toolbar.set_visible(true);
breadcrumb_toolbar.set_visible(false);
location_textbox.select_all();
location_textbox.set_focus(true);
});
auto& go_menu = menubar->add_menu("Go");
go_menu.add_action(go_back_action);
go_menu.add_action(go_forward_action);
go_menu.add_action(open_parent_directory_action);
go_menu.add_action(go_home_action);
go_menu.add_action(go_to_location_action);
auto& help_menu = menubar->add_menu("Help");
help_menu.add_action(GUI::CommonActions::make_about_action("File Manager", GUI::Icon::default_icon("filetype-folder")));
GUI::Application::the()->set_menubar(move(menubar));
main_toolbar.add_action(go_back_action);
main_toolbar.add_action(go_forward_action);
main_toolbar.add_action(open_parent_directory_action);
main_toolbar.add_action(go_home_action);
main_toolbar.add_separator();
main_toolbar.add_action(mkdir_action);
main_toolbar.add_action(touch_action);
main_toolbar.add_action(copy_action);
main_toolbar.add_action(cut_action);
main_toolbar.add_action(paste_action);
main_toolbar.add_action(focus_dependent_delete_action);
main_toolbar.add_action(directory_view.open_terminal_action());
main_toolbar.add_separator();
main_toolbar.add_action(*view_as_icons_action);
main_toolbar.add_action(*view_as_table_action);
main_toolbar.add_action(*view_as_columns_action);
directory_view.on_path_change = [&](const String& new_path, bool can_write_in_path) {
auto icon = GUI::FileIconProvider::icon_for_path(new_path);
auto* bitmap = icon.bitmap_for_size(16);
window->set_icon(bitmap);
location_textbox.set_icon(bitmap);
window->set_title(String::formatted("{} - File Manager", new_path));
location_textbox.set_text(new_path);
{
LexicalPath lexical_path(new_path);
auto segment_index_of_new_path_in_breadcrumb_bar = [&]() -> Optional<size_t> {
for (size_t i = 0; i < breadcrumb_bar.segment_count(); ++i) {
if (breadcrumb_bar.segment_data(i) == new_path)
return i;
}
return {};
}();
if (segment_index_of_new_path_in_breadcrumb_bar.has_value()) {
breadcrumb_bar.set_selected_segment(segment_index_of_new_path_in_breadcrumb_bar.value());
} else {
breadcrumb_bar.clear_segments();
breadcrumb_bar.append_segment("/", GUI::FileIconProvider::icon_for_path("/").bitmap_for_size(16), "/");
StringBuilder builder;
for (auto& part : lexical_path.parts()) {
// NOTE: We rebuild the path as we go, so we have something to pass to GUI::FileIconProvider.
builder.append('/');
builder.append(part);
breadcrumb_bar.append_segment(part, GUI::FileIconProvider::icon_for_path(builder.string_view()).bitmap_for_size(16), builder.string_view());
}
breadcrumb_bar.set_selected_segment(breadcrumb_bar.segment_count() - 1);
breadcrumb_bar.on_segment_click = [&](size_t segment_index) {
directory_view.open(breadcrumb_bar.segment_data(segment_index));
};
}
}
if (!is_reacting_to_tree_view_selection_change) {
auto new_index = directories_model->index(new_path, GUI::FileSystemModel::Column::Name);
if (new_index.is_valid()) {
tree_view.expand_all_parents_of(new_index);
tree_view.set_cursor(new_index, GUI::AbstractView::SelectionUpdate::Set);
}
}
struct stat st;
if (lstat(new_path.characters(), &st)) {
perror("stat");
return;
}
paste_action->set_enabled(can_write_in_path && GUI::Clipboard::the().mime_type() == "text/uri-list");
go_forward_action->set_enabled(directory_view.path_history_position() < directory_view.path_history_size() - 1);
go_back_action->set_enabled(directory_view.path_history_position() > 0);
open_parent_directory_action->set_enabled(new_path != "/");
};
directory_view.on_accepted_drop = [&]() {
refresh_tree_view();
};
directory_view.on_status_message = [&](const StringView& message) {
statusbar.set_text(message);
};
directory_view.on_thumbnail_progress = [&](int done, int total) {
if (done == total) {
progressbar.set_visible(false);
return;
}
progressbar.set_range(0, total);
progressbar.set_value(done);
progressbar.set_visible(true);
};
directory_view.on_selection_change = [&](GUI::AbstractView& view) {
auto& selection = view.selection();
copy_action->set_enabled(!selection.is_empty());
cut_action->set_enabled(!selection.is_empty());
focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && tree_view.is_focused())
|| !directory_view.current_view().selection().is_empty());
};
directory_context_menu->add_action(copy_action);
directory_context_menu->add_action(cut_action);
directory_context_menu->add_action(folder_specific_paste_action);
directory_context_menu->add_action(directory_view.delete_action());
directory_context_menu->add_action(shortcut_action);
directory_context_menu->add_separator();
directory_context_menu->add_action(properties_action);
directory_view_context_menu->add_action(mkdir_action);
directory_view_context_menu->add_action(touch_action);
directory_view_context_menu->add_action(paste_action);
directory_view_context_menu->add_action(directory_view.open_terminal_action());
directory_view_context_menu->add_separator();
directory_view_context_menu->add_action(action_show_dotfiles);
directory_view_context_menu->add_separator();
directory_view_context_menu->add_action(properties_action);
tree_view_directory_context_menu->add_action(copy_action);
tree_view_directory_context_menu->add_action(cut_action);
tree_view_directory_context_menu->add_action(paste_action);
tree_view_directory_context_menu->add_action(tree_view_delete_action);
tree_view_directory_context_menu->add_separator();
tree_view_directory_context_menu->add_action(properties_action);
tree_view_directory_context_menu->add_separator();
tree_view_directory_context_menu->add_action(mkdir_action);
tree_view_directory_context_menu->add_action(touch_action);
RefPtr<GUI::Menu> file_context_menu;
NonnullRefPtrVector<LauncherHandler> current_file_handlers;
RefPtr<GUI::Action> file_context_menu_action_default_action;
directory_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
if (index.is_valid()) {
auto& node = directory_view.node(index);
if (node.is_directory()) {
auto should_get_enabled = access(node.full_path().characters(), W_OK) == 0 && GUI::Clipboard::the().mime_type() == "text/uri-list";
folder_specific_paste_action->set_enabled(should_get_enabled);
directory_context_menu->popup(event.screen_position());
} else {
auto full_path = node.full_path();
current_file_handlers = directory_view.get_launch_handlers(full_path);
file_context_menu = GUI::Menu::construct("Directory View File");
file_context_menu->add_action(copy_action);
file_context_menu->add_action(cut_action);
file_context_menu->add_action(paste_action);
file_context_menu->add_action(directory_view.delete_action());
file_context_menu->add_action(shortcut_action);
file_context_menu->add_separator();
bool added_open_menu_items = false;
auto default_file_handler = directory_view.get_default_launch_handler(current_file_handlers);
if (default_file_handler) {
auto file_open_action = default_file_handler->create_launch_action([&, full_path = move(full_path)](auto& launcher_handler) {
directory_view.launch(URL::create_with_file_protocol(full_path), launcher_handler);
});
if (default_file_handler->details().launcher_type == Desktop::Launcher::LauncherType::Application)
file_open_action->set_text(String::formatted("Run {}", file_open_action->text()));
else
file_open_action->set_text(String::formatted("Open in {}", file_open_action->text()));
file_context_menu_action_default_action = file_open_action;
file_context_menu->add_action(move(file_open_action));
added_open_menu_items = true;
} else {
file_context_menu_action_default_action.clear();
}
if (current_file_handlers.size() > 1) {
added_open_menu_items = true;
auto& file_open_with_menu = file_context_menu->add_submenu("Open with");
for (auto& handler : current_file_handlers) {
if (&handler == default_file_handler.ptr())
continue;
file_open_with_menu.add_action(handler.create_launch_action([&, full_path = move(full_path)](auto& launcher_handler) {
directory_view.launch(URL::create_with_file_protocol(full_path), launcher_handler);
}));
}
}
if (added_open_menu_items)
file_context_menu->add_separator();
file_context_menu->add_action(properties_action);
file_context_menu->popup(event.screen_position(), file_context_menu_action_default_action);
}
} else {
directory_view_context_menu->popup(event.screen_position());
}
};
tree_view.on_selection = [&](const GUI::ModelIndex& index) {
if (directories_model->m_previously_selected_index.is_valid())
directories_model->update_node_on_selection(directories_model->m_previously_selected_index, false);
directories_model->update_node_on_selection(index, true);
directories_model->m_previously_selected_index = index;
};
tree_view.on_selection_change = [&] {
focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && tree_view.is_focused())
|| !directory_view.current_view().selection().is_empty());
if (tree_view.selection().is_empty())
return;
auto path = directories_model->full_path(tree_view.selection().first());
if (directory_view.path() == path)
return;
TemporaryChange change(is_reacting_to_tree_view_selection_change, true);
directory_view.open(path);
copy_action->set_enabled(!tree_view.selection().is_empty());
cut_action->set_enabled(!tree_view.selection().is_empty());
directory_view.delete_action().set_enabled(!tree_view.selection().is_empty());
};
tree_view.on_focus_change = [&]([[maybe_unused]] const bool has_focus, [[maybe_unused]] const GUI::FocusSource source) {
focus_dependent_delete_action->set_enabled((!tree_view.selection().is_empty() && has_focus)
|| !directory_view.current_view().selection().is_empty());
};
tree_view.on_context_menu_request = [&](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
if (index.is_valid()) {
tree_view_directory_context_menu->popup(event.screen_position());
}
};
auto copy_urls_to_directory = [&](const Vector<URL>& urls, const String& directory) {
if (urls.is_empty()) {
dbgln("No files to copy");
return;
}
bool had_accepted_copy = false;
for (auto& url_to_copy : urls) {
if (!url_to_copy.is_valid() || url_to_copy.path() == directory)
continue;
auto new_path = String::formatted("{}/{}", directory, LexicalPath(url_to_copy.path()).basename());
if (url_to_copy.path() == new_path)
continue;
if (!FileUtils::copy_file_or_directory(url_to_copy.path(), new_path)) {
auto error_message = String::formatted("Could not copy {} into {}.", url_to_copy.to_string(), new_path);
GUI::MessageBox::show(window, error_message, "File Manager", GUI::MessageBox::Type::Error);
} else {
had_accepted_copy = true;
}
}
if (had_accepted_copy)
refresh_tree_view();
};
breadcrumb_bar.on_segment_drop = [&](size_t segment_index, const GUI::DropEvent& event) {
if (!event.mime_data().has_urls())
return;
copy_urls_to_directory(event.mime_data().urls(), breadcrumb_bar.segment_data(segment_index));
};
breadcrumb_bar.on_segment_drag_enter = [&](size_t, GUI::DragEvent& event) {
if (event.mime_types().contains_slow("text/uri-list"))
event.accept();
};
breadcrumb_bar.on_doubleclick = [&](const GUI::MouseEvent&) {
go_to_location_action->activate();
};
tree_view.on_drop = [&](const GUI::ModelIndex& index, const GUI::DropEvent& event) {
if (!event.mime_data().has_urls())
return;
auto& target_node = directories_model->node(index);
if (!target_node.is_directory())
return;
copy_urls_to_directory(event.mime_data().urls(), target_node.full_path());
};
directory_view.open(initial_location);
directory_view.set_focus(true);
paste_action->set_enabled(GUI::Clipboard::the().mime_type() == "text/uri-list" && access(initial_location.characters(), W_OK) == 0);
window->show();
// Read directory read mode from config.
auto dir_view_mode = config->read_entry("DirectoryView", "ViewMode", "Icon");
if (dir_view_mode.contains("Table")) {
directory_view.set_view_mode(DirectoryView::ViewMode::Table);
view_as_table_action->set_checked(true);
} else if (dir_view_mode.contains("Columns")) {
directory_view.set_view_mode(DirectoryView::ViewMode::Columns);
view_as_columns_action->set_checked(true);
} else {
directory_view.set_view_mode(DirectoryView::ViewMode::Icon);
view_as_icons_action->set_checked(true);
}
// Write window position to config file on close request.
window->on_close_request = [&] {
config->write_num_entry("Window", "Left", window->x());
config->write_num_entry("Window", "Top", window->y());
config->write_num_entry("Window", "Width", window->width());
config->write_num_entry("Window", "Height", window->height());
config->sync();
return GUI::Window::CloseRequestDecision::Close;
};
return GUI::Application::the()->exec();
}