LibWeb+LibWebView+WebContent+UI: Add an action to cut text

This commit is contained in:
Timothy Flynn 2026-05-06 12:00:59 -04:00 committed by Tim Flynn
parent c4cef88c1a
commit b221d7fe8b
26 changed files with 99 additions and 21 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -58,6 +58,7 @@
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Selection/Selection.h>
#include <LibWeb/UIEvents/InputTypes.h>
#include <LibWeb/XHR/FormData.h>
namespace Web::HTML {
@ -3010,6 +3011,24 @@ String Navigable::selected_text() const
return visible_text_in_range(*range);
}
String Navigable::cut_selected_text() const
{
auto document = active_document();
if (!document)
return {};
auto* target = document->active_input_events_target();
if (!target)
return {};
auto text = selected_text();
if (text.is_empty())
return {};
target->handle_delete(UIEvents::InputTypes::deleteByCut);
return text;
}
void Navigable::select_all()
{
auto document = active_document();

View file

@ -208,6 +208,7 @@ public:
Page const& page() const { return m_page; }
String selected_text() const;
String cut_selected_text() const;
void select_all();
void paste(Utf16String const&);

View file

@ -923,12 +923,13 @@ void EventHandler::maybe_show_context_menu(GC::Ref<DOM::Node> node, MouseEventCo
// context menu is open.
clear_mousedown_tracking();
GC::Ref<DOM::Document> document = *m_navigable->active_document();
// NB: Event dispatches above may have run JS that invalidated layout.
m_navigable->active_document()->update_layout(DOM::UpdateLayoutReason::EventHandlerShowContextMenu);
document->update_layout(DOM::UpdateLayoutReason::EventHandlerShowContextMenu);
auto top_level_viewport_position = m_navigable->to_top_level_position(viewport_position);
if (GC::Ptr<HTML::HTMLAnchorElement const> link = node->enclosing_link_element()) {
GC::Ref<DOM::Document> document = *m_navigable->active_document();
auto href = link->href();
auto url = document->encoding_parse_url(href);
if (url.has_value())
@ -971,7 +972,8 @@ void EventHandler::maybe_show_context_menu(GC::Ref<DOM::Node> node, MouseEventCo
m_navigable->page().did_request_media_context_menu(media_element.unique_id(), top_level_viewport_position, "", modifiers, menu);
} else {
m_navigable->page().client().page_did_request_context_menu(top_level_viewport_position);
auto for_input_events_target = document->active_input_events_target() ? ContextMenuForInputEventsTarget::Yes : ContextMenuForInputEventsTarget::No;
m_navigable->page().client().page_did_request_context_menu(top_level_viewport_position, for_input_events_target);
}
}
}

View file

@ -364,6 +364,11 @@ enum class DisplayListPlayerType {
SkiaCPU,
};
enum class ContextMenuForInputEventsTarget : u8 {
No,
Yes,
};
class PageClient : public JS::Cell {
GC_CELL(PageClient, JS::Cell);
@ -401,7 +406,7 @@ public:
virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) { }
virtual void page_did_finish_loading(URL::URL const&) { }
virtual void page_did_request_cursor_change(Gfx::Cursor const&) { }
virtual void page_did_request_context_menu(CSSPixelPoint) { }
virtual void page_did_request_context_menu(CSSPixelPoint, ContextMenuForInputEventsTarget) { }
virtual void page_did_request_link_context_menu(CSSPixelPoint, URL::URL const&, [[maybe_unused]] ByteString const& target, [[maybe_unused]] unsigned modifiers) { }
virtual void page_did_request_image_context_menu(CSSPixelPoint, URL::URL const&, [[maybe_unused]] ByteString const& target, [[maybe_unused]] unsigned modifiers, Optional<Gfx::Bitmap const*>) { }
virtual void page_did_request_media_context_menu(CSSPixelPoint, [[maybe_unused]] ByteString const& target, [[maybe_unused]] unsigned modifiers, Page::MediaContextMenu const&) { }

View file

@ -12,6 +12,7 @@ namespace Web::UIEvents::InputTypes {
// https://w3c.github.io/input-events/#interface-InputEvent-Attributes
#define ENUMERATE_INPUT_TYPES \
__ENUMERATE_INPUT_TYPE(deleteByCut) \
__ENUMERATE_INPUT_TYPE(deleteContentBackward) \
__ENUMERATE_INPUT_TYPE(deleteContentForward) \
__ENUMERATE_INPUT_TYPE(insertFromPaste) \

View file

@ -911,6 +911,12 @@ void Application::initialize_actions()
insert_clipboard_entry({ move(text), "text/plain"_string });
}
});
m_cut_selection_action = Action::create("Cut"sv, ActionID::CutSelection, [this]() {
if (auto view = active_web_view(); view.has_value()) {
if (auto text = view->cut_selected_text(); !text.is_empty())
insert_clipboard_entry({ move(text), "text/plain"_string });
}
});
m_paste_action = Action::create("Paste"sv, ActionID::Paste, [this]() {
if (auto view = active_web_view(); view.has_value())
view->paste_text_from_clipboard();

View file

@ -133,6 +133,7 @@ public:
Action& reload_action() { return *m_reload_action; }
Action& copy_selection_action() { return *m_copy_selection_action; }
Action& cut_selection_action() { return *m_cut_selection_action; }
Action& paste_action() { return *m_paste_action; }
Action& select_all_action() { return *m_select_all_action; }
@ -287,6 +288,7 @@ private:
RefPtr<Action> m_reload_action;
RefPtr<Action> m_copy_selection_action;
RefPtr<Action> m_cut_selection_action;
RefPtr<Action> m_paste_action;
RefPtr<Action> m_select_all_action;

View file

@ -29,6 +29,7 @@ enum class ActionID {
Reload,
CopySelection,
CutSelection,
Paste,
SelectAll,

View file

@ -5,8 +5,8 @@
*/
#include <AK/Error.h>
#include <AK/ScopeGuard.h>
#include <AK/String.h>
#include <AK/TemporaryChange.h>
#include <AK/Time.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/Timer.h>
@ -348,6 +348,11 @@ ByteString ViewImplementation::selected_text()
return client().get_selected_text(page_id());
}
ByteString ViewImplementation::cut_selected_text()
{
return client().cut_selected_text(page_id());
}
Optional<String> ViewImplementation::selected_text_with_whitespace_collapsed()
{
auto selected_text = MUST(Web::Infra::strip_and_collapse_whitespace(this->selected_text()));
@ -1069,6 +1074,7 @@ void ViewImplementation::initialize_context_menus()
m_page_context_menu->add_action(*m_navigate_forward_action);
m_page_context_menu->add_action(application.reload_action());
m_page_context_menu->add_separator();
m_page_context_menu->add_action(application.cut_selection_action());
m_page_context_menu->add_action(application.copy_selection_action());
m_page_context_menu->add_action(application.paste_action());
m_page_context_menu->add_action(application.select_all_action());
@ -1111,12 +1117,18 @@ void ViewImplementation::initialize_context_menus()
m_media_context_menu->add_action(*m_copy_url_action);
}
void ViewImplementation::did_request_page_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position)
void ViewImplementation::did_request_page_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, Web::ContextMenuForInputEventsTarget for_input_events_target)
{
auto const& search_engine = Application::settings().search_engine();
auto& cut_selection_action = Application::the().cut_selection_action();
cut_selection_action.set_visible(for_input_events_target == Web::ContextMenuForInputEventsTarget::Yes);
auto selected_text = search_engine.has_value() ? selected_text_with_whitespace_collapsed() : OptionalNone {};
TemporaryChange change_url { m_search_text, move(selected_text) };
auto const& search_engine = Application::settings().search_engine();
m_search_text = search_engine.has_value() ? selected_text_with_whitespace_collapsed() : OptionalNone {};
ScopeGuard guard { [&]() {
cut_selection_action.set_visible(true);
m_search_text.clear();
} };
if (m_search_text.has_value()) {
m_search_selected_text_action->set_text(search_engine->format_search_query_for_display(*m_search_text));

View file

@ -104,6 +104,7 @@ public:
Optional<Core::SharedVersion> document_cookie_version(URL::URL const&) const;
ByteString selected_text();
ByteString cut_selected_text();
Optional<String> selected_text_with_whitespace_collapsed();
void select_all();
void find_in_page(String const& query, CaseSensitivity = CaseSensitivity::CaseInsensitive);
@ -264,7 +265,7 @@ public:
Menu& image_context_menu() { return *m_image_context_menu; }
Menu& media_context_menu() { return *m_media_context_menu; }
void did_request_page_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position);
void did_request_page_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, Web::ContextMenuForInputEventsTarget for_input_events_target);
void did_request_link_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, URL::URL url);
void did_request_image_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, URL::URL url, Optional<Gfx::ShareableBitmap> bitmap);
void did_request_media_context_menu(Badge<WebContentClient>, Gfx::IntPoint content_position, Web::Page::MediaContextMenu menu);

View file

@ -355,10 +355,10 @@ void WebContentClient::did_middle_click_link(u64, URL::URL url, ByteString, unsi
Application::the().open_url_in_new_tab(url, Web::HTML::ActivateTab::No);
}
void WebContentClient::did_request_context_menu(u64 page_id, Gfx::IntPoint content_position)
void WebContentClient::did_request_context_menu(u64 page_id, Gfx::IntPoint content_position, Web::ContextMenuForInputEventsTarget for_input_events_target)
{
if (auto view = view_for_page_id(page_id); view.has_value())
view->did_request_page_context_menu({}, content_position);
view->did_request_page_context_menu({}, content_position, for_input_events_target);
}
void WebContentClient::did_request_link_context_menu(u64 page_id, Gfx::IntPoint content_position, URL::URL url, ByteString, unsigned)

View file

@ -83,7 +83,7 @@ private:
virtual void did_click_link(u64 page_id, URL::URL, ByteString, unsigned) override;
virtual void did_middle_click_link(u64 page_id, URL::URL, ByteString, unsigned) override;
virtual void did_start_loading(u64 page_id, URL::URL, bool) override;
virtual void did_request_context_menu(u64 page_id, Gfx::IntPoint) override;
virtual void did_request_context_menu(u64 page_id, Gfx::IntPoint, Web::ContextMenuForInputEventsTarget) override;
virtual void did_request_link_context_menu(u64 page_id, Gfx::IntPoint, URL::URL, ByteString, unsigned) override;
virtual void did_request_image_context_menu(u64 page_id, Gfx::IntPoint, URL::URL, ByteString, unsigned, Optional<Gfx::ShareableBitmap>) override;
virtual void did_request_media_context_menu(u64 page_id, Gfx::IntPoint, ByteString, unsigned, Web::Page::MediaContextMenu) override;

View file

@ -1088,6 +1088,13 @@ Messages::WebContentServer::GetSelectedTextResponse ConnectionFromClient::get_se
return ByteString {};
}
Messages::WebContentServer::CutSelectedTextResponse ConnectionFromClient::cut_selected_text(u64 page_id)
{
if (auto page = this->page(page_id); page.has_value())
return page->page().focused_navigable().cut_selected_text().to_byte_string();
return ByteString {};
}
void ConnectionFromClient::select_all(u64 page_id)
{
if (auto page = this->page(page_id); page.has_value())

View file

@ -155,6 +155,7 @@ private:
virtual void request_internal_page_info(u64 page_id, WebView::PageInfoType) override;
virtual Messages::WebContentServer::GetSelectedTextResponse get_selected_text(u64 page_id) override;
virtual Messages::WebContentServer::CutSelectedTextResponse cut_selected_text(u64 page_id) override;
virtual void select_all(u64 page_id) override;
virtual void find_in_page(u64 page_id, String query, CaseSensitivity) override;

View file

@ -403,9 +403,9 @@ void PageClient::page_did_set_device_pixel_ratio_for_testing(double ratio)
set_viewport(m_viewport_size, ratio);
}
void PageClient::page_did_request_context_menu(Web::CSSPixelPoint content_position)
void PageClient::page_did_request_context_menu(Web::CSSPixelPoint content_position, Web::ContextMenuForInputEventsTarget for_input_events_target)
{
client().async_did_request_context_menu(m_id, page().css_to_device_point(content_position).to_type<int>());
client().async_did_request_context_menu(m_id, page().css_to_device_point(content_position).to_type<int>(), for_input_events_target);
}
void PageClient::page_did_request_link_context_menu(Web::CSSPixelPoint content_position, URL::URL const& url, ByteString const& target, unsigned modifiers)

View file

@ -139,7 +139,7 @@ private:
virtual void page_did_unhover_link() override;
virtual void page_did_click_link(URL::URL const&, ByteString const& target, unsigned modifiers) override;
virtual void page_did_middle_click_link(URL::URL const&, ByteString const& target, unsigned modifiers) override;
virtual void page_did_request_context_menu(Web::CSSPixelPoint) override;
virtual void page_did_request_context_menu(Web::CSSPixelPoint, Web::ContextMenuForInputEventsTarget) override;
virtual void page_did_request_link_context_menu(Web::CSSPixelPoint, URL::URL const&, ByteString const& target, unsigned modifiers) override;
virtual void page_did_request_image_context_menu(Web::CSSPixelPoint, URL::URL const&, ByteString const& target, unsigned modifiers, Optional<Gfx::Bitmap const*>) override;
virtual void page_did_request_media_context_menu(Web::CSSPixelPoint, ByteString const& target, unsigned modifiers, Web::Page::MediaContextMenu const&) override;

View file

@ -50,7 +50,7 @@ endpoint WebContentClient
did_unhover_link(u64 page_id) =|
did_click_link(u64 page_id, URL::URL url, ByteString target, unsigned modifiers) =|
did_middle_click_link(u64 page_id, URL::URL url, ByteString target, unsigned modifiers) =|
did_request_context_menu(u64 page_id, Gfx::IntPoint content_position) =|
did_request_context_menu(u64 page_id, Gfx::IntPoint content_position, Web::ContextMenuForInputEventsTarget for_input_events_target) =|
did_request_link_context_menu(u64 page_id, Gfx::IntPoint content_position, URL::URL url, ByteString target, unsigned modifiers) =|
did_request_image_context_menu(u64 page_id, Gfx::IntPoint content_position, URL::URL url, ByteString target, unsigned modifiers, Optional<Gfx::ShareableBitmap> bitmap) =|
did_request_media_context_menu(u64 page_id, Gfx::IntPoint content_position, ByteString target, unsigned modifiers, Web::Page::MediaContextMenu menu) =|

View file

@ -89,6 +89,7 @@ endpoint WebContentServer
request_internal_page_info(u64 page_id, WebView::PageInfoType type) =|
get_selected_text(u64 page_id) => (ByteString selection)
cut_selected_text(u64 page_id) => (ByteString selection)
select_all(u64 page_id) =|
paste(u64 page_id, Utf16String text) =|

View file

@ -299,10 +299,7 @@
keyEquivalent:@"y"]];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Cut"
action:@selector(cut:)
keyEquivalent:@"x"]];
[submenu addItem:Ladybird::create_application_menu_item(WebView::Application::the().cut_selection_action())];
[submenu addItem:Ladybird::create_application_menu_item(WebView::Application::the().copy_selection_action())];
[submenu addItem:Ladybird::create_application_menu_item(WebView::Application::the().paste_action())];
[submenu addItem:[NSMenuItem separatorItem]];

View file

@ -43,6 +43,9 @@
case WebView::ActionID::CopySelection:
[NSApp sendAction:@selector(copy:) to:nil from:sender];
return;
case WebView::ActionID::CutSelection:
[NSApp sendAction:@selector(cut:) to:nil from:sender];
return;
case WebView::ActionID::Paste:
[NSApp sendAction:@selector(paste:) to:nil from:sender];
return;
@ -206,6 +209,10 @@ static void initialize_native_icon(WebView::Action& action, id control)
set_control_image(control, @"document.on.document");
[control setKeyEquivalent:@"c"];
break;
case WebView::ActionID::CutSelection:
set_control_image(control, @"scissors");
[control setKeyEquivalent:@"x"];
break;
case WebView::ActionID::Paste:
set_control_image(control, @"document.on.clipboard");
[control setKeyEquivalent:@"v"];

View file

@ -60,6 +60,8 @@ static char const* primary_accelerator_for_action(WebView::ActionID id)
return "<Ctrl>r";
case WebView::ActionID::CopySelection:
return "<Ctrl>c";
case WebView::ActionID::CutSelection:
return "<Ctrl>x";
case WebView::ActionID::Paste:
return "<Ctrl>v";
case WebView::ActionID::SelectAll:
@ -122,6 +124,10 @@ static void initialize_native_control(WebView::Action& action, GSimpleAction* ga
set_icon("edit-copy-symbolic");
set_accel("<Ctrl>c");
break;
case WebView::ActionID::CutSelection:
set_icon("edit-cut-symbolic");
set_accel("<Ctrl>x");
break;
case WebView::ActionID::Paste:
set_icon("edit-paste-symbolic");
set_accel("<Ctrl>v");

View file

@ -121,6 +121,9 @@ void WebContentView::finish_handling_key_event(Web::KeyEvent const& event)
case Web::UIEvents::Key_C:
app.copy_selection_action().activate();
break;
case Web::UIEvents::Key_X:
app.cut_selection_action().activate();
break;
case Web::UIEvents::Key_V:
app.paste_action().activate();
break;

View file

@ -233,6 +233,7 @@ BrowserWindow::BrowserWindow(Vector<URL::URL> const& initial_urls, IsPopupWindow
auto* edit_menu = m_hamburger_menu->addMenu("&Edit");
menuBar()->addMenu(edit_menu);
edit_menu->addAction(create_application_action(*this, Application::the().cut_selection_action()));
edit_menu->addAction(create_application_action(*this, Application::the().copy_selection_action()));
edit_menu->addAction(create_application_action(*this, Application::the().paste_action()));
edit_menu->addAction(create_application_action(*this, Application::the().select_all_action()));

View file

@ -134,6 +134,10 @@ static void initialize_native_control(WebView::Action& action, QAction& qaction,
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/edit-copy.png"sv));
qaction.setShortcut(QKeySequence::StandardKey::Copy);
break;
case WebView::ActionID::CutSelection:
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/edit-cut.png"sv));
qaction.setShortcut(QKeySequence::StandardKey::Cut);
break;
case WebView::ActionID::Paste:
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/paste.png"sv));
qaction.setShortcut(QKeySequence::StandardKey::Paste);

View file

@ -12,6 +12,7 @@ set(16x16_ICONS
close-tab.png
download.png
edit-copy.png
edit-cut.png
error.png
filetype-css.png
filetype-folder-open.png