| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/ozone/platform/x11/x11_clipboard_ozone.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/logging.h" |
| #include "ui/base/clipboard/clipboard_constants.h" |
| #include "ui/base/x/x11_util.h" |
| #include "ui/events/platform/x11/x11_event_source.h" |
| #include "ui/gfx/x/extension_manager.h" |
| #include "ui/gfx/x/x11_atom_cache.h" |
| #include "ui/gfx/x/xproto.h" |
| #include "ui/gfx/x/xproto_util.h" |
| |
| using base::Contains; |
| |
| namespace ui { |
| |
| namespace { |
| |
| const char kChromeSelection[] = "CHROME_SELECTION"; |
| const char kClipboard[] = "CLIPBOARD"; |
| const char kTargets[] = "TARGETS"; |
| const char kTimestamp[] = "TIMESTAMP"; |
| |
| // Helps to allow conversions for text/plain[;charset=utf-8] <=> [UTF8_]STRING. |
| void ExpandTypes(std::vector<std::string>* list) { |
| bool has_mime_type_text = Contains(*list, ui::kMimeTypeText); |
| bool has_string = Contains(*list, kMimeTypeLinuxString); |
| bool has_mime_type_utf8 = Contains(*list, kMimeTypeTextUtf8); |
| bool has_utf8_string = Contains(*list, kMimeTypeLinuxUtf8String); |
| if (has_mime_type_text && !has_string) |
| list->push_back(kMimeTypeLinuxString); |
| if (has_string && !has_mime_type_text) |
| list->push_back(ui::kMimeTypeText); |
| if (has_mime_type_utf8 && !has_utf8_string) |
| list->push_back(kMimeTypeLinuxUtf8String); |
| if (has_utf8_string && !has_mime_type_utf8) |
| list->push_back(kMimeTypeTextUtf8); |
| } |
| |
| } // namespace |
| |
| // Maintains state of a single selection (aka system clipboard buffer). |
| struct X11ClipboardOzone::SelectionState { |
| SelectionState() = default; |
| ~SelectionState() = default; |
| |
| // DataMap we keep from |OfferClipboardData| to service remote requests for |
| // the clipboard. |
| PlatformClipboard::DataMap offer_data_map; |
| |
| // DataMap from |RequestClipboardData| that we write remote clipboard |
| // contents to before calling the completion callback. |
| PlatformClipboard::DataMap* request_data_map = nullptr; |
| |
| // Mime types supported by remote clipboard. |
| std::vector<std::string> mime_types; |
| |
| // Data most recently read from remote clipboard. |
| PlatformClipboard::Data data; |
| |
| // Mime type of most recently read data from remote clipboard. |
| std::string data_mime_type; |
| |
| // Callbacks are stored when we haven't already prefetched the remote |
| // clipboard. |
| PlatformClipboard::GetMimeTypesClosure get_available_mime_types_callback; |
| PlatformClipboard::RequestDataClosure request_clipboard_data_callback; |
| |
| // The time that this instance took ownership of the clipboard. |
| x11::Time acquired_selection_timestamp; |
| }; |
| |
| X11ClipboardOzone::X11ClipboardOzone() |
| : atom_clipboard_(x11::GetAtom(kClipboard)), |
| atom_targets_(x11::GetAtom(kTargets)), |
| atom_timestamp_(x11::GetAtom(kTimestamp)), |
| x_property_(x11::GetAtom(kChromeSelection)), |
| connection_(x11::Connection::Get()), |
| x_window_(x11::CreateDummyWindow("Chromium Clipboard Window")) { |
| connection_->xfixes().QueryVersion( |
| {x11::XFixes::major_version, x11::XFixes::minor_version}); |
| if (!connection_->xfixes().present()) |
| return; |
| using_xfixes_ = true; |
| |
| // Register to receive standard X11 events. |
| connection_->AddEventObserver(this); |
| |
| for (auto atom : {atom_clipboard_, x11::Atom::PRIMARY}) { |
| // Register the selection state. |
| selection_state_.emplace(atom, std::make_unique<SelectionState>()); |
| // Register to receive XFixes notification when selection owner changes. |
| connection_->xfixes().SelectSelectionInput( |
| {x_window_, atom, x11::XFixes::SelectionEventMask::SetSelectionOwner}); |
| // Prefetch the current remote clipboard contents. |
| QueryTargets(atom); |
| } |
| } |
| |
| X11ClipboardOzone::~X11ClipboardOzone() { |
| connection_->RemoveEventObserver(this); |
| } |
| |
| void X11ClipboardOzone::OnEvent(const x11::Event& xev) { |
| if (auto* request = xev.As<x11::SelectionRequestEvent>()) { |
| if (request->owner == x_window_) |
| OnSelectionRequest(*request); |
| } else if (auto* notify = xev.As<x11::SelectionNotifyEvent>()) { |
| if (notify->requestor == x_window_) |
| OnSelectionNotify(*notify); |
| } else if (auto* notify = xev.As<x11::XFixes::SelectionNotifyEvent>()) { |
| if (notify->window == x_window_) |
| OnSetSelectionOwnerNotify(*notify); |
| } |
| } |
| |
| // We are the clipboard owner, and a remote peer has requested either: |
| // TARGETS: List of mime types that we support for the clipboard. |
| // TIMESTAMP: Time when we took ownership of the clipboard. |
| // <mime-type>: Mime type to receive clipboard as. |
| void X11ClipboardOzone::OnSelectionRequest( |
| const x11::SelectionRequestEvent& event) { |
| // The property must be set. |
| if (event.property == x11::Atom::None) |
| return; |
| |
| // target=TARGETS. |
| auto& selection_state = |
| GetSelectionState(static_cast<x11::Atom>(event.selection)); |
| auto target = static_cast<x11::Atom>(event.target); |
| PlatformClipboard::DataMap& offer_data_map = selection_state.offer_data_map; |
| if (target == atom_targets_) { |
| std::vector<std::string> targets; |
| // Add TIMESTAMP. |
| targets.push_back(kTimestamp); |
| for (auto& entry : offer_data_map) { |
| targets.push_back(entry.first); |
| } |
| // Expand types, then convert from string to atom. |
| ExpandTypes(&targets); |
| std::vector<x11::Atom> atoms; |
| for (auto& entry : targets) |
| atoms.push_back(x11::GetAtom(entry.c_str())); |
| x11::SetArrayProperty(event.requestor, event.property, x11::Atom::ATOM, |
| atoms); |
| |
| } else if (target == atom_timestamp_) { |
| // target=TIMESTAMP. |
| x11::SetProperty(event.requestor, event.property, x11::Atom::INTEGER, |
| selection_state.acquired_selection_timestamp); |
| } else { |
| // Send clipboard data. |
| std::string target_name; |
| if (auto reply = connection_->GetAtomName({event.target}).Sync()) |
| target_name = std::move(reply->name); |
| |
| std::string key = target_name; |
| // Allow conversions for text/plain[;charset=utf-8] <=> [UTF8_]STRING. |
| if (key == kMimeTypeLinuxUtf8String && |
| !Contains(offer_data_map, kMimeTypeLinuxUtf8String)) { |
| key = kMimeTypeTextUtf8; |
| } else if (key == kMimeTypeLinuxString && |
| !Contains(offer_data_map, kMimeTypeLinuxString)) { |
| key = kMimeTypeText; |
| } |
| auto it = offer_data_map.find(key); |
| if (it != offer_data_map.end()) { |
| x11::SetArrayProperty(event.requestor, event.property, event.target, |
| it->second->data()); |
| } |
| } |
| |
| // Notify remote peer that clipboard has been sent. |
| x11::SelectionNotifyEvent selection_event{ |
| .time = event.time, |
| .requestor = event.requestor, |
| .selection = event.selection, |
| .target = event.target, |
| .property = event.property, |
| }; |
| x11::SendEvent(selection_event, selection_event.requestor, |
| x11::EventMask::NoEvent); |
| } |
| |
| // A remote peer owns the clipboard. This event is received in response to |
| // our request for TARGETS (GetAvailableMimeTypes), or a specific mime type |
| // (RequestClipboardData). |
| void X11ClipboardOzone::OnSelectionNotify( |
| const x11::SelectionNotifyEvent& event) { |
| // GetAvailableMimeTypes. |
| auto selection = static_cast<x11::Atom>(event.selection); |
| auto& selection_state = GetSelectionState(selection); |
| if (static_cast<x11::Atom>(event.target) == atom_targets_) { |
| std::vector<x11::Atom> targets; |
| x11::GetArrayProperty(x_window_, x_property_, &targets); |
| |
| selection_state.mime_types.clear(); |
| for (auto target : targets) { |
| if (auto reply = connection_->GetAtomName({target}).Sync()) |
| selection_state.mime_types.push_back(std::move(reply->name)); |
| } |
| |
| // If we have a saved callback, invoke it now with expanded types, otherwise |
| // guess that we will want 'text/plain' and fetch it now. |
| if (selection_state.get_available_mime_types_callback) { |
| std::vector<std::string> result(selection_state.mime_types); |
| ExpandTypes(&result); |
| std::move(selection_state.get_available_mime_types_callback) |
| .Run(std::move(result)); |
| } else { |
| selection_state.data_mime_type = kMimeTypeText; |
| ReadRemoteClipboard(selection); |
| } |
| } |
| |
| // RequestClipboardData. |
| if (static_cast<x11::Atom>(event.property) == x_property_) { |
| x11::Atom type; |
| std::vector<uint8_t> data; |
| x11::GetArrayProperty(x_window_, x_property_, &data, &type); |
| x11::DeleteProperty(x_window_, x_property_); |
| if (type != x11::Atom::None) |
| selection_state.data = scoped_refptr<base::RefCountedBytes>( |
| base::RefCountedBytes::TakeVector(&data)); |
| |
| // If we have a saved callback, invoke it now, otherwise this was a prefetch |
| // and we have already saved |data_| for the next call to |
| // |RequestClipboardData|. |
| if (selection_state.request_clipboard_data_callback) { |
| selection_state.request_data_map->emplace(selection_state.data_mime_type, |
| selection_state.data); |
| std::move(selection_state.request_clipboard_data_callback) |
| .Run(selection_state.data); |
| } |
| } else if (static_cast<x11::Atom>(event.property) == x11::Atom::None && |
| selection_state.request_clipboard_data_callback) { |
| // If the remote peer could not send data in the format we requested, |
| // or failed for any reason, we will send empty data. |
| std::move(selection_state.request_clipboard_data_callback) |
| .Run(selection_state.data); |
| } |
| } |
| |
| void X11ClipboardOzone::OnSetSelectionOwnerNotify( |
| const x11::XFixes::SelectionNotifyEvent& event) { |
| // Reset state and fetch remote clipboard if there is a new remote owner. |
| x11::Atom selection = event.selection; |
| if (!IsSelectionOwner(BufferForSelectionAtom(selection))) { |
| auto& selection_state = GetSelectionState(selection); |
| selection_state.mime_types.clear(); |
| selection_state.data_mime_type.clear(); |
| selection_state.data.reset(); |
| QueryTargets(selection); |
| } |
| |
| // Increase the sequence number if the callback is set. |
| if (update_sequence_cb_) |
| update_sequence_cb_.Run(BufferForSelectionAtom(selection)); |
| } |
| |
| x11::Atom X11ClipboardOzone::SelectionAtomForBuffer( |
| ClipboardBuffer buffer) const { |
| switch (buffer) { |
| case ClipboardBuffer::kCopyPaste: |
| return atom_clipboard_; |
| case ClipboardBuffer::kSelection: |
| return x11::Atom::PRIMARY; |
| default: |
| NOTREACHED(); |
| return x11::Atom::None; |
| } |
| } |
| |
| ClipboardBuffer X11ClipboardOzone::BufferForSelectionAtom( |
| x11::Atom selection) const { |
| if (selection == x11::Atom::PRIMARY) |
| return ClipboardBuffer::kSelection; |
| if (selection == atom_clipboard_) |
| return ClipboardBuffer::kCopyPaste; |
| NOTREACHED(); |
| return ClipboardBuffer::kCopyPaste; |
| } |
| |
| X11ClipboardOzone::SelectionState& X11ClipboardOzone::GetSelectionState( |
| x11::Atom selection) { |
| DCHECK(Contains(selection_state_, selection)); |
| return *selection_state_[selection]; |
| } |
| |
| void X11ClipboardOzone::QueryTargets(x11::Atom selection) { |
| GetSelectionState(selection).mime_types.clear(); |
| connection_->ConvertSelection({x_window_, selection, atom_targets_, |
| x_property_, x11::Time::CurrentTime}); |
| } |
| |
| void X11ClipboardOzone::ReadRemoteClipboard(x11::Atom selection) { |
| auto& selection_state = GetSelectionState(selection); |
| selection_state.data.reset(); |
| // Allow conversions for text/plain[;charset=utf-8] <=> [UTF8_]STRING. |
| std::string target = selection_state.data_mime_type; |
| if (!Contains(selection_state.mime_types, target)) { |
| if (target == kMimeTypeText) { |
| target = kMimeTypeLinuxString; |
| } else if (target == kMimeTypeTextUtf8) { |
| target = kMimeTypeLinuxUtf8String; |
| } |
| } |
| |
| connection_->ConvertSelection({x_window_, selection, x11::GetAtom(target), |
| x_property_, x11::Time::CurrentTime}); |
| } |
| |
| void X11ClipboardOzone::OfferClipboardData( |
| ClipboardBuffer buffer, |
| const PlatformClipboard::DataMap& data_map, |
| PlatformClipboard::OfferDataClosure callback) { |
| const x11::Atom selection = SelectionAtomForBuffer(buffer); |
| auto& selection_state = GetSelectionState(selection); |
| const auto timestamp = |
| static_cast<x11::Time>(X11EventSource::GetInstance()->GetTimestamp()); |
| selection_state.acquired_selection_timestamp = timestamp; |
| selection_state.offer_data_map = data_map; |
| // Only take ownership if we are using xfixes. |
| // TODO(joelhockey): Make clipboard work without xfixes. |
| if (using_xfixes_) { |
| connection_->SetSelectionOwner({x_window_, selection, timestamp}); |
| } |
| std::move(callback).Run(); |
| } |
| |
| void X11ClipboardOzone::RequestClipboardData( |
| ClipboardBuffer buffer, |
| const std::string& mime_type, |
| PlatformClipboard::DataMap* data_map, |
| PlatformClipboard::RequestDataClosure callback) { |
| const x11::Atom selection = SelectionAtomForBuffer(buffer); |
| auto& selection_state = GetSelectionState(selection); |
| // If we are not using xfixes, return empty data. |
| // TODO(joelhockey): Make clipboard work without xfixes. |
| // If we have already prefetched the clipboard for the correct mime type, |
| // then send it right away, otherwise save the callback and attempt to get the |
| // requested mime type from the remote clipboard. |
| if (!using_xfixes_ || |
| (selection_state.data_mime_type == mime_type && selection_state.data && |
| !selection_state.data->data().empty())) { |
| data_map->emplace(mime_type, selection_state.data); |
| std::move(callback).Run(selection_state.data); |
| return; |
| } |
| selection_state.data_mime_type = mime_type; |
| selection_state.request_data_map = data_map; |
| DCHECK(selection_state.request_clipboard_data_callback.is_null()); |
| selection_state.request_clipboard_data_callback = std::move(callback); |
| ReadRemoteClipboard(selection); |
| } |
| |
| void X11ClipboardOzone::GetAvailableMimeTypes( |
| ClipboardBuffer buffer, |
| PlatformClipboard::GetMimeTypesClosure callback) { |
| const x11::Atom selection = SelectionAtomForBuffer(buffer); |
| auto& selection_state = GetSelectionState(selection); |
| // If we are not using xfixes, return empty data. |
| // TODO(joelhockey): Make clipboard work without xfixes. |
| // If we already have the list of supported mime types, send the expanded list |
| // of types right away, otherwise save the callback and get the list of |
| // TARGETS from the remote clipboard. |
| if (!using_xfixes_ || !selection_state.mime_types.empty()) { |
| std::vector<std::string> result(selection_state.mime_types); |
| ExpandTypes(&result); |
| std::move(callback).Run(std::move(result)); |
| return; |
| } |
| DCHECK(selection_state.get_available_mime_types_callback.is_null()); |
| selection_state.get_available_mime_types_callback = std::move(callback); |
| QueryTargets(selection); |
| } |
| |
| bool X11ClipboardOzone::IsSelectionOwner(ClipboardBuffer buffer) { |
| // If we are not using xfixes, then we are always the owner. |
| // TODO(joelhockey): Make clipboard work without xfixes. |
| if (!using_xfixes_) |
| return true; |
| |
| auto reply = |
| connection_->GetSelectionOwner({SelectionAtomForBuffer(buffer)}).Sync(); |
| return reply && reply->owner == x_window_; |
| } |
| |
| void X11ClipboardOzone::SetSequenceNumberUpdateCb( |
| PlatformClipboard::SequenceNumberUpdateCb cb) { |
| update_sequence_cb_ = std::move(cb); |
| } |
| |
| bool X11ClipboardOzone::IsSelectionBufferAvailable() const { |
| return true; |
| } |
| |
| } // namespace ui |