// 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 #include #include "base/logging.h" #include "ui/base/x/selection_utils.h" #include "ui/base/x/x11_foreign_window_manager.h" #include "ui/base/x/x11_util.h" namespace ui { namespace { const char kAtomPair[] = "ATOM_PAIR"; const char kIncr[] = "INCR"; const char kMultiple[] = "MULTIPLE"; const char kSaveTargets[] = "SAVE_TARGETS"; const char kTargets[] = "TARGETS"; const char* kAtomsToCache[] = { kAtomPair, kIncr, kMultiple, kSaveTargets, kTargets, NULL }; // The period of |incremental_transfer_abort_timer_|. Arbitrary but must be <= // than kIncrementalTransferTimeoutMs. const int kTimerPeriodMs = 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(kTimerPeriodMs <= 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(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(XID window, XAtom property, std::vector >* value) { XAtom type = 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 False, // deleted AnyPropertyType, &type, &format, &num_items, &remaining_bytes, &properties); gfx::XScopedPtr scoped_properties(properties); if (result != 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(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)), atom_cache_(x_display_, kAtomsToCache) { } 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_, None, CurrentTime); } void SelectionOwner::RetrieveTargets(std::vector* targets) { for (SelectionFormatMap::const_iterator it = format_map_.begin(); it != format_map_.end(); ++it) { targets->push_back(it->first); } } void SelectionOwner::TakeOwnershipOfSelection( const SelectionFormatMap& data) { XSetSelectionOwner(x_display_, selection_name_, x_window_, CurrentTime); 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_, None, 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 = None; // Indicates failure reply.xselection.time = event.xselectionrequest.time; if (requested_target == atom_cache_.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 (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 : None); } // Set the property to indicate which conversions succeeded. This matches // what GTK does. XChangeProperty( x_display_, requestor, requested_property, atom_cache_.GetAtom(kAtomPair), 32, PropModeReplace, reinterpret_cast(&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, 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) { std::vector::iterator 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 = atom_cache_.GetAtom(kMultiple); XAtom save_targets_atom = atom_cache_.GetAtom(kSaveTargets); XAtom targets_atom = atom_cache_.GetAtom(kTargets); if (target == multiple_atom || target == save_targets_atom) return false; if (target == targets_atom) { // We have been asked for TARGETS. Send an atom array back with the data // types we support. std::vector targets; 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(&targets.front()), targets.size()); return true; } else { // Try to find the data type in map. SelectionFormatMap::const_iterator 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, atom_cache_.GetAtom(kIncr), 32, PropModeReplace, reinterpret_cast(&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); int foreign_window_manager_id = ui::XForeignWindowManager::GetInstance()->RequestEvents( requestor, PropertyChangeMask); incremental_transfers_.push_back( IncrementalTransfer(requestor, target, property, it->second, 0, timeout, foreign_window_manager_id)); // 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(kTimerPeriodMs), this, &SelectionOwner::AbortStaleIncrementalTransfers); } } else { XChangeProperty( x_display_, requestor, property, target, 8, PropModeReplace, const_cast(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(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(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) { ui::XForeignWindowManager::GetInstance()->CancelRequest( it->foreign_window_manager_id); incremental_transfers_.erase(it); if (incremental_transfers_.empty()) incremental_transfer_abort_timer_.Stop(); } std::vector::iterator SelectionOwner::FindIncrementalTransferForEvent(const XEvent& event) { for (std::vector::iterator 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, const scoped_refptr& data, int offset, base::TimeTicks timeout, int foreign_window_manager_id) : window(window), target(target), property(property), data(data), offset(offset), timeout(timeout), foreign_window_manager_id(foreign_window_manager_id) { } SelectionOwner::IncrementalTransfer::~IncrementalTransfer() { } } // namespace ui