diff --git a/Userland/Applications/SpaceAnalyzer/CMakeLists.txt b/Userland/Applications/SpaceAnalyzer/CMakeLists.txt index 23cc18f13e..2b1762aaa3 100644 --- a/Userland/Applications/SpaceAnalyzer/CMakeLists.txt +++ b/Userland/Applications/SpaceAnalyzer/CMakeLists.txt @@ -7,4 +7,4 @@ set(SOURCES ) serenity_app(SpaceAnalyzer ICON app-space-analyzer) -target_link_libraries(SpaceAnalyzer LibGfx LibGUI) +target_link_libraries(SpaceAnalyzer LibDesktop LibGfx LibGUI) diff --git a/Userland/Applications/SpaceAnalyzer/TreeMapWidget.cpp b/Userland/Applications/SpaceAnalyzer/TreeMapWidget.cpp index a31c144cda..45866de4fb 100644 --- a/Userland/Applications/SpaceAnalyzer/TreeMapWidget.cpp +++ b/Userland/Applications/SpaceAnalyzer/TreeMapWidget.cpp @@ -312,6 +312,8 @@ void TreeMapWidget::mousedown_event(GUI::MouseEvent& event) void TreeMapWidget::doubleclick_event(GUI::MouseEvent& event) { + if (event.button() != GUI::MouseButton::Left) + return; const TreeMapNode* node = path_node(m_viewpoint); if (node && !node_is_leaf(*node)) { Vector path = path_to_position(event.position()); @@ -341,6 +343,12 @@ void TreeMapWidget::mousewheel_event(GUI::MouseEvent& event) } } +void TreeMapWidget::context_menu_event(GUI::ContextMenuEvent& context_menu_event) +{ + if (on_context_menu_request) + on_context_menu_request(context_menu_event); +} + void TreeMapWidget::set_tree(RefPtr tree) { m_tree = tree; diff --git a/Userland/Applications/SpaceAnalyzer/TreeMapWidget.h b/Userland/Applications/SpaceAnalyzer/TreeMapWidget.h index 8b4e1e9269..0a14c4f327 100644 --- a/Userland/Applications/SpaceAnalyzer/TreeMapWidget.h +++ b/Userland/Applications/SpaceAnalyzer/TreeMapWidget.h @@ -50,6 +50,7 @@ class TreeMapWidget final : public GUI::Frame { public: virtual ~TreeMapWidget() override; Function on_path_change; + Function on_context_menu_request; size_t path_size() const; const TreeMapNode* path_node(size_t n) const; size_t viewpoint() const; @@ -62,6 +63,7 @@ private: virtual void mousedown_event(GUI::MouseEvent&) override; virtual void doubleclick_event(GUI::MouseEvent&) override; virtual void mousewheel_event(GUI::MouseEvent&) override; + virtual void context_menu_event(GUI::ContextMenuEvent&) override; bool rect_can_contain_children(const Gfx::IntRect& rect) const; bool rect_can_contain_label(const Gfx::IntRect& rect) const; diff --git a/Userland/Applications/SpaceAnalyzer/main.cpp b/Userland/Applications/SpaceAnalyzer/main.cpp index 2ce061f786..95644d00d6 100644 --- a/Userland/Applications/SpaceAnalyzer/main.cpp +++ b/Userland/Applications/SpaceAnalyzer/main.cpp @@ -27,15 +27,19 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include @@ -241,6 +245,28 @@ static void analyze(RefPtr tree, SpaceAnalyzer::TreeMapWidget& treemapwidg treemapwidget.set_tree(tree); } +static bool is_removable(const String& absolute_path) +{ + ASSERT(!absolute_path.is_empty()); + int access_result = access(absolute_path.characters(), W_OK); + if (access_result != 0 && errno != EACCES) + perror("access"); + return access_result == 0; +} + +static String get_absolute_path_to_selected_node(const SpaceAnalyzer::TreeMapWidget& treemapwidget, bool include_last_node = true) +{ + StringBuilder path_builder; + for (size_t k = 0; k < treemapwidget.path_size() - (include_last_node ? 0 : 1); k++) { + if (k != 0) { + path_builder.append('/'); + } + const SpaceAnalyzer::TreeMapNode* node = treemapwidget.path_node(k); + path_builder.append(node->name()); + } + return path_builder.build(); +} + int main(int argc, char* argv[]) { auto app = GUI::Application::construct(argc, argv); @@ -274,6 +300,57 @@ int main(int argc, char* argv[]) help_menu.add_action(GUI::CommonActions::make_about_action(APP_NAME, app_icon, window)); app->set_menubar(move(menubar)); + // Configure the nodes context menu. + auto open_folder_action = GUI::Action::create("Open Folder", { Mod_Ctrl, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), [&](auto&) { + Desktop::Launcher::open(URL::create_with_file_protocol(get_absolute_path_to_selected_node(treemapwidget))); + }); + auto open_containing_folder_action = GUI::Action::create("Open Containing Folder", { Mod_Ctrl, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), [&](auto&) { + Desktop::Launcher::open(URL::create_with_file_protocol(get_absolute_path_to_selected_node(treemapwidget, false))); + }); + auto copy_path_action = GUI::Action::create("Copy Path to Clipboard", { Mod_Ctrl, Key_C }, Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-copy.png"), [&](auto&) { + GUI::Clipboard::the().set_plain_text(get_absolute_path_to_selected_node(treemapwidget)); + }); + auto delete_action = GUI::CommonActions::make_delete_action([&](auto&) { + String selected_node_path = get_absolute_path_to_selected_node(treemapwidget); + bool try_again = true; + while (try_again) { + try_again = false; + + auto deletion_result = Core::File::remove(selected_node_path, Core::File::RecursionMode::Allowed, true); + if (deletion_result.is_error()) { + auto retry_message_result = GUI::MessageBox::show(window, + String::formatted("Failed to delete \"{}\": {}. Retry?", + deletion_result.error().file, + deletion_result.error().error_code.string()), + "Deletion failed", + GUI::MessageBox::Type::Error, + GUI::MessageBox::InputType::YesNo); + if (retry_message_result == GUI::MessageBox::ExecYes) { + try_again = true; + } + } else { + GUI::MessageBox::show(window, + String::formatted("Successfuly deleted \"{}\".", selected_node_path), + "Deletion completed", + GUI::MessageBox::Type::Information, + GUI::MessageBox::InputType::OK); + } + } + + // TODO: Refreshing data always causes resetting the viewport back to "/". + // It would be great if we found a way to preserve viewport across refreshes. + analyze(tree, treemapwidget, statusbar); + }); + // TODO: Both these menus could've been implemented as one, but it's impossible to change action text after it's shown once. + auto folder_node_context_menu = GUI::Menu::construct(); + folder_node_context_menu->add_action(*open_folder_action); + folder_node_context_menu->add_action(*copy_path_action); + folder_node_context_menu->add_action(*delete_action); + auto file_node_context_menu = GUI::Menu::construct(); + file_node_context_menu->add_action(*open_containing_folder_action); + file_node_context_menu->add_action(*copy_path_action); + file_node_context_menu->add_action(*delete_action); + // Configure event handlers. breadcrumbbar.on_segment_click = [&](size_t index) { ASSERT(index < treemapwidget.path_size()); @@ -291,6 +368,17 @@ int main(int argc, char* argv[]) } breadcrumbbar.set_selected_segment(treemapwidget.viewpoint()); }; + treemapwidget.on_context_menu_request = [&](const GUI::ContextMenuEvent& event) { + String selected_node_path = get_absolute_path_to_selected_node(treemapwidget); + if (selected_node_path.is_empty()) + return; + delete_action->set_enabled(is_removable(selected_node_path)); + if (Core::File::is_directory(selected_node_path)) { + folder_node_context_menu->popup(event.screen_position()); + } else { + file_node_context_menu->popup(event.screen_position()); + } + }; // At startup automatically do an analysis of root. analyze(tree, treemapwidget, statusbar);