// Copyright (c) 2013 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/base/x/selection_owner.h" #include #include "base/logging.h" #include "base/memory/ref_counted_memory.h" #include "ui/base/x/selection_utils.h" #include "ui/base/x/x11_util.h" #include "ui/events/platform/x11/x11_event_source.h" #include "ui/events/x/x11_window_event_manager.h" #include "ui/gfx/x/x11_atom_cache.h" #include "ui/gfx/x/xproto.h" #include "ui/gfx/x/xproto_util.h" namespace ui { const char kIncr[] = "INCR"; const char kSaveTargets[] = "SAVE_TARGETS"; const char kTargets[] = "TARGETS"; namespace { const char kAtomPair[] = "ATOM_PAIR"; const char kMultiple[] = "MULTIPLE"; const char kTimestamp[] = "TIMESTAMP"; // The period of |incremental_transfer_abort_timer_|. Arbitrary but must be <= // than kIncrementalTransferTimeoutMs. const int KSelectionOwnerTimerPeriodMs = 1000; // The amount of time to wait for the selection requestor to process the data // sent by the selection owner before aborting an incremental data transfer. const int kIncrementalTransferTimeoutMs = 10000; static_assert(KSelectionOwnerTimerPeriodMs <= kIncrementalTransferTimeoutMs, "timer period must be <= transfer timeout"); // Returns a conservative max size of the data we can pass into // XChangeProperty(). Copied from GTK. size_t GetMaxRequestSize(x11::Connection* connection) { long extended_max_size = connection->extended_max_request_length(); long max_size = (extended_max_size ? extended_max_size : connection->setup().maximum_request_length) - 100; return std::min(static_cast(0x40000), std::max(static_cast(0), max_size)); } // Gets the value of an atom pair array property. On success, true is returned // and the value is stored in |value|. bool GetAtomPairArrayProperty( x11::Window window, x11::Atom property, std::vector>* value) { std::vector atoms; // Since this is an array of atom pairs, ensure ensure |atoms| // has an element count that's a multiple of 2. if (!ui::GetArrayProperty(window, property, &atoms) || atoms.size() % 2 != 0) return false; value->clear(); for (size_t i = 0; i < atoms.size(); i += 2) value->push_back(std::make_pair(atoms[i], atoms[i + 1])); return true; } x11::Window GetSelectionOwner(x11::Atom selection) { auto response = x11::Connection::Get()->GetSelectionOwner({selection}).Sync(); return response ? response->owner : x11::Window::None; } void SetSelectionOwner(x11::Window window, x11::Atom selection, x11::Time time = x11::Time::CurrentTime) { x11::Connection::Get()->SetSelectionOwner({window, selection, time}); } } // namespace SelectionOwner::SelectionOwner(x11::Connection* connection, x11::Window x_window, x11::Atom selection_name) : x_window_(x_window), selection_name_(selection_name), max_request_size_(GetMaxRequestSize(connection)) {} SelectionOwner::~SelectionOwner() { // If we are the selection owner, we need to release the selection so we // don't receive further events. However, we don't call ClearSelectionOwner() // because we don't want to do this indiscriminately. if (GetSelectionOwner(selection_name_) == x_window_) SetSelectionOwner(x11::Window::None, selection_name_); } void SelectionOwner::RetrieveTargets(std::vector* targets) { for (const auto& format_target : format_map_) targets->push_back(format_target.first); } void SelectionOwner::TakeOwnershipOfSelection(const SelectionFormatMap& data) { acquired_selection_timestamp_ = X11EventSource::GetInstance()->GetTimestamp(); SetSelectionOwner(x_window_, selection_name_, acquired_selection_timestamp_); if (GetSelectionOwner(selection_name_) == x_window_) { // The X server agrees that we are the selection owner. Commit our data. format_map_ = data; } } void SelectionOwner::ClearSelectionOwner() { SetSelectionOwner(x11::Window::None, selection_name_); format_map_ = SelectionFormatMap(); } void SelectionOwner::OnSelectionRequest(const x11::Event& x11_event) { auto& request = *x11_event.As(); auto requestor = request.requestor; x11::Atom requested_target = request.target; x11::Atom requested_property = request.property; // Incrementally build our selection. By default this is a refusal, and we'll // override the parts indicating success in the different cases. x11::SelectionNotifyEvent reply{ .time = request.time, .requestor = requestor, .selection = request.selection, .target = requested_target, .property = x11::Atom::None, // Indicates failure }; if (requested_target == gfx::GetAtom(kMultiple)) { // The contents of |requested_property| should be a list of // pairs. std::vector> conversions; if (GetAtomPairArrayProperty(requestor, requested_property, &conversions)) { std::vector conversion_results; for (const std::pair& conversion : conversions) { bool conversion_successful = ProcessTarget(conversion.first, requestor, conversion.second); conversion_results.push_back(conversion.first); conversion_results.push_back(conversion_successful ? conversion.second : x11::Atom::None); } // Set the property to indicate which conversions succeeded. This matches // what GTK does. ui::SetArrayProperty(requestor, requested_property, gfx::GetAtom(kAtomPair), conversion_results); reply.property = requested_property; } } else { if (ProcessTarget(requested_target, requestor, requested_property)) reply.property = requested_property; } // Send off the reply. x11::SendEvent(reply, requestor, x11::EventMask::NoEvent); } void SelectionOwner::OnSelectionClear(const x11::Event& event) { DLOG(ERROR) << "SelectionClear"; // TODO(erg): If we receive a SelectionClear event while we're handling data, // we need to delay clearing. } bool SelectionOwner::CanDispatchPropertyEvent(const x11::Event& event) { return event.As()->state == x11::Property::Delete && FindIncrementalTransferForEvent(event) != incremental_transfers_.end(); } void SelectionOwner::OnPropertyEvent(const x11::Event& event) { auto it = FindIncrementalTransferForEvent(event); if (it == incremental_transfers_.end()) return; ProcessIncrementalTransfer(&(*it)); if (!it->data.get()) CompleteIncrementalTransfer(it); } bool SelectionOwner::ProcessTarget(x11::Atom target, x11::Window requestor, x11::Atom property) { x11::Atom multiple_atom = gfx::GetAtom(kMultiple); x11::Atom save_targets_atom = gfx::GetAtom(kSaveTargets); x11::Atom targets_atom = gfx::GetAtom(kTargets); x11::Atom timestamp_atom = gfx::GetAtom(kTimestamp); if (target == multiple_atom || target == save_targets_atom) return false; if (target == timestamp_atom) { ui::SetProperty(requestor, property, x11::Atom::INTEGER, acquired_selection_timestamp_); return true; } if (target == targets_atom) { // We have been asked for TARGETS. Send an atom array back with the data // types we support. std::vector targets = {timestamp_atom, targets_atom, save_targets_atom, multiple_atom}; RetrieveTargets(&targets); ui::SetArrayProperty(requestor, property, x11::Atom::ATOM, targets); return true; } // Try to find the data type in map. auto it = format_map_.find(target); if (it != format_map_.end()) { if (it->second->size() > max_request_size_) { // We must send the data back in several chunks due to a limitation in // the size of X requests. Notify the selection requestor that the data // will be sent incrementally by returning data of type "INCR". uint32_t length = it->second->size(); ui::SetProperty(requestor, property, gfx::GetAtom(kIncr), length); // Wait for the selection requestor to indicate that it has processed // the selection result before sending the first chunk of data. The // selection requestor indicates this by deleting |property|. base::TimeTicks timeout = base::TimeTicks::Now() + base::TimeDelta::FromMilliseconds(kIncrementalTransferTimeoutMs); incremental_transfers_.emplace_back( requestor, target, property, std::make_unique( requestor, x11::EventMask::PropertyChange), it->second, 0, timeout); // Start a timer to abort the data transfer in case that the selection // requestor does not support the INCR property or gets destroyed during // the data transfer. if (!incremental_transfer_abort_timer_.IsRunning()) { incremental_transfer_abort_timer_.Start( FROM_HERE, base::TimeDelta::FromMilliseconds(KSelectionOwnerTimerPeriodMs), this, &SelectionOwner::AbortStaleIncrementalTransfers); } } else { auto& mem = it->second; std::vector data(mem->data(), mem->data() + mem->size()); ui::SetArrayProperty(requestor, property, target, data); } return true; } // I would put error logging here, but GTK ignores TARGETS and spams us // looking for its own internal types. return false; } void SelectionOwner::ProcessIncrementalTransfer(IncrementalTransfer* transfer) { size_t remaining = transfer->data->size() - transfer->offset; size_t chunk_length = std::min(remaining, max_request_size_); const uint8_t* data = transfer->data->front() + transfer->offset; std::vector buf(data, data + chunk_length); ui::SetArrayProperty(transfer->window, transfer->property, transfer->target, buf); transfer->offset += chunk_length; transfer->timeout = base::TimeTicks::Now() + base::TimeDelta::FromMilliseconds(kIncrementalTransferTimeoutMs); // When offset == data->size(), we still need to transfer a zero-sized chunk // to notify the selection requestor that the transfer is complete. Clear // transfer->data once the zero-sized chunk is sent to indicate that state // related to this data transfer can be cleared. if (chunk_length == 0) transfer->data = nullptr; } void SelectionOwner::AbortStaleIncrementalTransfers() { base::TimeTicks now = base::TimeTicks::Now(); for (int i = static_cast(incremental_transfers_.size()) - 1; i >= 0; --i) { if (incremental_transfers_[i].timeout <= now) CompleteIncrementalTransfer(incremental_transfers_.begin() + i); } } void SelectionOwner::CompleteIncrementalTransfer( std::vector::iterator it) { incremental_transfers_.erase(it); if (incremental_transfers_.empty()) incremental_transfer_abort_timer_.Stop(); } std::vector::iterator SelectionOwner::FindIncrementalTransferForEvent(const x11::Event& event) { for (auto it = incremental_transfers_.begin(); it != incremental_transfers_.end(); ++it) { const auto* prop = event.As(); if (it->window == prop->window && it->property == prop->atom) return it; } return incremental_transfers_.end(); } SelectionOwner::IncrementalTransfer::IncrementalTransfer( x11::Window window, x11::Atom target, x11::Atom property, std::unique_ptr event_selector, const scoped_refptr& data, int offset, base::TimeTicks timeout) : window(window), target(target), property(property), event_selector(std::move(event_selector)), data(data), offset(offset), timeout(timeout) {} SelectionOwner::IncrementalTransfer::IncrementalTransfer( IncrementalTransfer&& other) = default; SelectionOwner::IncrementalTransfer& SelectionOwner::IncrementalTransfer::operator=(IncrementalTransfer&&) = default; SelectionOwner::IncrementalTransfer::~IncrementalTransfer() = default; } // namespace ui