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:
parent
aa939c4b4b
commit
dc28c07fa5
287 changed files with 1 additions and 1 deletions
13
Userland/Applications/FileManager/CMakeLists.txt
Normal file
13
Userland/Applications/FileManager/CMakeLists.txt
Normal 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)
|
47
Userland/Applications/FileManager/DesktopWidget.cpp
Normal file
47
Userland/Applications/FileManager/DesktopWidget.cpp
Normal 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));
|
||||
}
|
||||
|
||||
}
|
45
Userland/Applications/FileManager/DesktopWidget.h
Normal file
45
Userland/Applications/FileManager/DesktopWidget.h
Normal 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();
|
||||
};
|
||||
|
||||
}
|
591
Userland/Applications/FileManager/DirectoryView.cpp
Normal file
591
Userland/Applications/FileManager/DirectoryView.cpp
Normal 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(¤t_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(¤t_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(¤t_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();
|
||||
}
|
||||
|
||||
}
|
190
Userland/Applications/FileManager/DirectoryView.h
Normal file
190
Userland/Applications/FileManager/DirectoryView.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
53
Userland/Applications/FileManager/FileManagerWindow.gml
Normal file
53
Userland/Applications/FileManager/FileManagerWindow.gml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
280
Userland/Applications/FileManager/FileUtils.cpp
Normal file
280
Userland/Applications/FileManager/FileUtils.cpp
Normal 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();
|
||||
}
|
||||
}
|
50
Userland/Applications/FileManager/FileUtils.h
Normal file
50
Userland/Applications/FileManager/FileUtils.h
Normal 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);
|
||||
|
||||
}
|
304
Userland/Applications/FileManager/PropertiesWindow.cpp
Normal file
304
Userland/Applications/FileManager/PropertiesWindow.cpp
Normal 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;
|
||||
}
|
98
Userland/Applications/FileManager/PropertiesWindow.h
Normal file
98
Userland/Applications/FileManager/PropertiesWindow.h
Normal 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 };
|
||||
};
|
974
Userland/Applications/FileManager/main.cpp
Normal file
974
Userland/Applications/FileManager/main.cpp
Normal 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();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue