| // 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 "ui/base/x/selection_utils.h" |
| #include "ui/base/x/x11_window_event_manager.h" |
| #include "ui/events/platform/x11/x11_event_source.h" |
| #include "ui/gfx/x/x11.h" |
| #include "ui/gfx/x/x11_atom_cache.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(XDisplay* display) { |
| long extended_max_size = XExtendedMaxRequestSize(display); |
| long max_size = |
| (extended_max_size ? extended_max_size : XMaxRequestSize(display)) - 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(XID window, |
| XAtom property, |
| std::vector<std::pair<XAtom,XAtom> >* value) { |
| XAtom type = x11::None; |
| int format = 0; // size in bits of each item in 'property' |
| unsigned long num_items = 0; |
| unsigned char* properties = NULL; |
| unsigned long remaining_bytes = 0; |
| |
| int result = XGetWindowProperty(gfx::GetXDisplay(), window, property, |
| 0, // offset into property data to |
| // read |
| (~0L), // entire array |
| x11::False, // deleted |
| AnyPropertyType, &type, &format, &num_items, |
| &remaining_bytes, &properties); |
| gfx::XScopedPtr<unsigned char> scoped_properties(properties); |
| |
| if (result != x11::Success) |
| return false; |
| |
| // GTK does not require |type| to be kAtomPair. |
| if (format != 32 || num_items % 2 != 0) |
| return false; |
| |
| XAtom* atom_properties = reinterpret_cast<XAtom*>(properties); |
| value->clear(); |
| for (size_t i = 0; i < num_items; i+=2) |
| value->push_back(std::make_pair(atom_properties[i], atom_properties[i+1])); |
| return true; |
| } |
| |
| } // namespace |
| |
| SelectionOwner::SelectionOwner(XDisplay* x_display, |
| XID x_window, |
| XAtom selection_name) |
| : x_display_(x_display), |
| x_window_(x_window), |
| selection_name_(selection_name), |
| max_request_size_(GetMaxRequestSize(x_display)) {} |
| |
| 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 (XGetSelectionOwner(x_display_, selection_name_) == x_window_) |
| XSetSelectionOwner(x_display_, selection_name_, x11::None, |
| x11::CurrentTime); |
| } |
| |
| void SelectionOwner::RetrieveTargets(std::vector<XAtom>* targets) { |
| for (auto it = format_map_.begin(); it != format_map_.end(); ++it) { |
| targets->push_back(it->first); |
| } |
| } |
| |
| void SelectionOwner::TakeOwnershipOfSelection( |
| const SelectionFormatMap& data) { |
| acquired_selection_timestamp_ = X11EventSource::GetInstance()->GetTimestamp(); |
| XSetSelectionOwner(x_display_, selection_name_, x_window_, |
| acquired_selection_timestamp_); |
| |
| if (XGetSelectionOwner(x_display_, selection_name_) == x_window_) { |
| // The X server agrees that we are the selection owner. Commit our data. |
| format_map_ = data; |
| } |
| } |
| |
| void SelectionOwner::ClearSelectionOwner() { |
| XSetSelectionOwner(x_display_, selection_name_, x11::None, x11::CurrentTime); |
| format_map_ = SelectionFormatMap(); |
| } |
| |
| void SelectionOwner::OnSelectionRequest(const XEvent& event) { |
| XID requestor = event.xselectionrequest.requestor; |
| XAtom requested_target = event.xselectionrequest.target; |
| XAtom requested_property = event.xselectionrequest.property; |
| |
| // Incrementally build our selection. By default this is a refusal, and we'll |
| // override the parts indicating success in the different cases. |
| XEvent reply; |
| reply.xselection.type = SelectionNotify; |
| reply.xselection.requestor = requestor; |
| reply.xselection.selection = event.xselectionrequest.selection; |
| reply.xselection.target = requested_target; |
| reply.xselection.property = x11::None; // Indicates failure |
| reply.xselection.time = event.xselectionrequest.time; |
| |
| if (requested_target == gfx::GetAtom(kMultiple)) { |
| // The contents of |requested_property| should be a list of |
| // <target,property> pairs. |
| std::vector<std::pair<XAtom,XAtom> > conversions; |
| if (GetAtomPairArrayProperty(requestor, |
| requested_property, |
| &conversions)) { |
| std::vector<XAtom> conversion_results; |
| for (size_t i = 0; i < conversions.size(); ++i) { |
| bool conversion_successful = ProcessTarget(conversions[i].first, |
| requestor, |
| conversions[i].second); |
| conversion_results.push_back(conversions[i].first); |
| conversion_results.push_back( |
| conversion_successful ? conversions[i].second : x11::None); |
| } |
| |
| // Set the property to indicate which conversions succeeded. This matches |
| // what GTK does. |
| XChangeProperty( |
| x_display_, requestor, requested_property, gfx::GetAtom(kAtomPair), |
| 32, PropModeReplace, |
| reinterpret_cast<const unsigned char*>(&conversion_results.front()), |
| conversion_results.size()); |
| |
| reply.xselection.property = requested_property; |
| } |
| } else { |
| if (ProcessTarget(requested_target, requestor, requested_property)) |
| reply.xselection.property = requested_property; |
| } |
| |
| // Send off the reply. |
| XSendEvent(x_display_, requestor, x11::False, 0, &reply); |
| } |
| |
| void SelectionOwner::OnSelectionClear(const XEvent& 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 XEvent& event) { |
| return event.xproperty.state == PropertyDelete && |
| FindIncrementalTransferForEvent(event) != incremental_transfers_.end(); |
| } |
| |
| void SelectionOwner::OnPropertyEvent(const XEvent& event) { |
| auto it = FindIncrementalTransferForEvent(event); |
| if (it == incremental_transfers_.end()) |
| return; |
| |
| ProcessIncrementalTransfer(&(*it)); |
| if (!it->data.get()) |
| CompleteIncrementalTransfer(it); |
| } |
| |
| bool SelectionOwner::ProcessTarget(XAtom target, |
| XID requestor, |
| XAtom property) { |
| XAtom multiple_atom = gfx::GetAtom(kMultiple); |
| XAtom save_targets_atom = gfx::GetAtom(kSaveTargets); |
| XAtom targets_atom = gfx::GetAtom(kTargets); |
| XAtom timestamp_atom = gfx::GetAtom(kTimestamp); |
| |
| if (target == multiple_atom || target == save_targets_atom) |
| return false; |
| |
| if (target == timestamp_atom) { |
| XChangeProperty( |
| x_display_, requestor, property, XA_INTEGER, 32, PropModeReplace, |
| reinterpret_cast<unsigned char*>(&acquired_selection_timestamp_), 1); |
| 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<XAtom> targets; |
| targets.push_back(timestamp_atom); |
| targets.push_back(targets_atom); |
| targets.push_back(save_targets_atom); |
| targets.push_back(multiple_atom); |
| RetrieveTargets(&targets); |
| |
| XChangeProperty(x_display_, requestor, property, XA_ATOM, 32, |
| PropModeReplace, |
| reinterpret_cast<unsigned char*>(&targets.front()), |
| targets.size()); |
| 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". |
| long length = it->second->size(); |
| XChangeProperty(x_display_, requestor, property, gfx::GetAtom(kIncr), 32, |
| PropModeReplace, |
| reinterpret_cast<unsigned char*>(&length), 1); |
| |
| // 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_.push_back(IncrementalTransfer( |
| requestor, target, property, |
| std::make_unique<XScopedEventSelector>(requestor, PropertyChangeMask), |
| 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 { |
| XChangeProperty( |
| x_display_, |
| requestor, |
| property, |
| target, |
| 8, |
| PropModeReplace, |
| const_cast<unsigned char*>(it->second->front()), |
| it->second->size()); |
| } |
| 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_); |
| XChangeProperty( |
| x_display_, |
| transfer->window, |
| transfer->property, |
| transfer->target, |
| 8, |
| PropModeReplace, |
| const_cast<unsigned char*>(transfer->data->front() + transfer->offset), |
| chunk_length); |
| 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 = NULL; |
| } |
| |
| 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 XEvent& event) { |
| for (auto it = incremental_transfers_.begin(); |
| it != incremental_transfers_.end(); ++it) { |
| if (it->window == event.xproperty.window && |
| it->property == event.xproperty.atom) { |
| return it; |
| } |
| } |
| return incremental_transfers_.end(); |
| } |
| |
| SelectionOwner::IncrementalTransfer::IncrementalTransfer( |
| XID window, |
| XAtom target, |
| XAtom 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() { |
| } |
| |
| } // namespace ui |