| // 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 <algorithm> |
| |
| #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<long>(0x40000), |
| std::max(static_cast<long>(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<std::pair<x11::Atom, x11::Atom>>* value) { |
| std::vector<x11::Atom> 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<x11::Atom>* 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<x11::SelectionRequestEvent>(); |
| 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 |
| // <target,property> pairs. |
| std::vector<std::pair<x11::Atom, x11::Atom>> conversions; |
| if (GetAtomPairArrayProperty(requestor, requested_property, &conversions)) { |
| std::vector<x11::Atom> conversion_results; |
| for (const std::pair<x11::Atom, x11::Atom>& 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<x11::PropertyNotifyEvent>()->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<x11::Atom> 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<XScopedEventSelector>( |
| 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<uint8_t> 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<uint8_t> 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<int>(incremental_transfers_.size()) - 1; i >= 0; |
| --i) { |
| if (incremental_transfers_[i].timeout <= now) |
| CompleteIncrementalTransfer(incremental_transfers_.begin() + i); |
| } |
| } |
| |
| void SelectionOwner::CompleteIncrementalTransfer( |
| std::vector<IncrementalTransfer>::iterator it) { |
| incremental_transfers_.erase(it); |
| |
| if (incremental_transfers_.empty()) |
| incremental_transfer_abort_timer_.Stop(); |
| } |
| |
| std::vector<SelectionOwner::IncrementalTransfer>::iterator |
| SelectionOwner::FindIncrementalTransferForEvent(const x11::Event& event) { |
| for (auto it = incremental_transfers_.begin(); |
| it != incremental_transfers_.end(); ++it) { |
| const auto* prop = event.As<x11::PropertyNotifyEvent>(); |
| 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<XScopedEventSelector> event_selector, |
| const scoped_refptr<base::RefCountedMemory>& 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 |