Chromium Code Reviews| Index: remoting/host/x_server_clipboard.cc |
| diff --git a/remoting/host/x_server_clipboard.cc b/remoting/host/x_server_clipboard.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..eca6092e6f060fd19bf90e3c1c413d4636eaed92 |
| --- /dev/null |
| +++ b/remoting/host/x_server_clipboard.cc |
| @@ -0,0 +1,432 @@ |
| +// Copyright (c) 2012 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 "remoting/host/x_server_clipboard.h" |
| + |
| +#include <X11/extensions/Xfixes.h> |
| + |
| +#include "base/callback.h" |
| +#include "base/logging.h" |
| +#include "remoting/base/constants.h" |
| + |
| +namespace { |
| + |
| +// Returns true if |a| is later than |b|, treating 0 as being a long time ago. |
| +// This is different from the usual meaning of CurrentTime (which is defined |
| +// to be 0). |
| +bool IsTimeLater(Time a, Time b) { |
| + if (a == 0) { |
| + return false; |
| + } |
| + if (b == 0) { |
| + return true; |
| + } |
| + return a > b; |
| +} |
| + |
| +} // namespace |
| + |
| +namespace remoting { |
| + |
| +XServerClipboard::XServerClipboard() |
| + : display_(NULL), |
| + clipboard_window_(BadValue), |
| + have_xfixes_(false), |
| + xfixes_event_base_(-1), |
| + xfixes_error_base_(-1), |
| + clipboard_atom_(None), |
| + large_selection_atom_(None), |
| + selection_string_atom_(None), |
| + targets_atom_(None), |
| + timestamp_atom_(None), |
| + utf8_string_atom_(None), |
| + large_selection_property_(None), |
| + current_selection_time_(0), |
| + new_selection_(None), |
| + getting_initial_selection_(true), |
| + new_cut_text_(false), |
| + had_valid_timestamp_(false) { |
| +} |
| + |
| +XServerClipboard::~XServerClipboard() { |
| +} |
| + |
| +void XServerClipboard::Init(Display* display, |
| + const ClipboardChangedCallback& callback) { |
| + display_ = display; |
| + callback_ = callback; |
| + |
| + // If any of these X API calls fail, an X Error will be raised, crashing the |
| + // process. This is unlikely to occur in practice, and even if it does, it |
| + // would mean the X server is in a bad state, so it's not worth trying to |
| + // trap such errors here. |
| + clipboard_window_ = XCreateSimpleWindow(display_, |
| + DefaultRootWindow(display_), |
| + 0, 0, 1, 1, // x, y, width, height |
| + 0, 0, 0); |
| + clipboard_atom_ = XInternAtom(display_, "CLIPBOARD", False); |
|
Elliot Glaysher
2012/09/10 22:44:21
Probably want to replace this with a single XInter
Lambros
2012/09/12 23:42:32
Done.
|
| + large_selection_atom_ = XInternAtom(display_, "INCR", False); |
| + selection_string_atom_ = XInternAtom(display_, "SELECTION_STRING", False); |
| + targets_atom_ = XInternAtom(display_, "TARGETS", False); |
| + timestamp_atom_ = XInternAtom(display_, "TIMESTAMP", False); |
| + utf8_string_atom_ = XInternAtom(display_, "UTF8_STRING", False); |
| + |
| + if (XFixesQueryExtension(display_, &xfixes_event_base_, |
| + &xfixes_error_base_)) { |
| + have_xfixes_ = true; |
| + XFixesSelectSelectionInput(display_, clipboard_window_, XA_PRIMARY, |
| + XFixesSetSelectionOwnerNotifyMask); |
| + XFixesSelectSelectionInput(display_, clipboard_window_, clipboard_atom_, |
| + XFixesSetSelectionOwnerNotifyMask); |
| + } else { |
| + LOG(INFO) << "X server does not support XFixes."; |
| + } |
| + |
| + GetSelections(None, 0); |
| +} |
| + |
| +void XServerClipboard::SetClipboard(const std::string& mime_type, |
| + const std::string& data) { |
| + if (!display_) { |
| + return; |
| + } |
| + |
| + // Currently only UTF-8 is supported. |
| + if (mime_type != kMimeTypeTextUtf8) { |
| + return; |
| + } |
| + |
| + data_ = data; |
| + |
| + OwnSelection(XA_PRIMARY, CurrentTime); |
| + OwnSelection(clipboard_atom_, CurrentTime); |
| + current_selection_time_ = CurrentTime; |
| +} |
| + |
| +void XServerClipboard::ProcessXEvent(XEvent* event) { |
| + if (event->xany.window != clipboard_window_) { |
| + return; |
| + } |
| + |
| + switch (event->type) { |
| + case PropertyNotify: |
| + OnPropertyNotify(event); |
| + break; |
| + case SelectionNotify: |
| + OnSelectionNotify(event); |
| + break; |
| + case SelectionRequest: |
| + OnSelectionRequest(event); |
| + break; |
| + default: |
| + break; |
| + } |
| + |
| + if (have_xfixes_) { |
| + if (event->type == xfixes_event_base_ + XFixesSetSelectionOwnerNotify) { |
| + XFixesSelectionNotifyEvent* notify_event = |
| + reinterpret_cast<XFixesSelectionNotifyEvent*>(event); |
| + GetSelections(notify_event->selection, notify_event->selection_timestamp); |
| + } |
| + } |
| +} |
| + |
| +void XServerClipboard::GetSelections(Atom selection, Time timestamp) { |
| + // There are several different "targets" for which we request information: |
| + // the timestamp, the target list and the string value of the selection. |
| + // |
| + // The process takes some time since there are several round-trips to the X |
| + // server and the selection-owning app(s). We need to protect against |
| + // restarting the process when we are in the middle of it since we store |
| + // state between the various selectionNotify() callbacks we will get. We set |
| + // a timer to flag when it is happening. Where the selection owner(s) are |
| + // behaving themselves we cancel the timer in FinishGetSelections(). If they |
| + // don't respond in reasonable time then we assume something went wrong and |
| + // will allow a retry after the timer expires. |
| + // |
| + // We start out trying to establish the timestamp(s) on the selections to |
| + // decide which one to retrieve. If a selection and timestamp are passed in |
| + // then we always use that selection and treat it as new. Otherwise we |
| + // request the timestamp(s) from the selection owner. If we get a valid |
| + // timestamp and one of the selections is new, then we use that selection. |
| + // If we get a valid timestamp but the selections are not new, then we just |
| + // stop processing there. |
| + // |
| + // If we don't get a valid timestamp (for example an app like Mozilla which |
| + // sets a zero timestamp, or an app like VMWare which doesn't support the |
| + // timestamp target), then we can't use the timestamp optimisation so we have |
| + // to get the actual value of the selection each time. We use either the |
| + // PRIMARY or CLIPBOARD selection as appropriate. |
| + // |
| + // Before getting the value of the chosen selection, we request the list of |
| + // targets supported for that selection. If UTF8_STRING is supported then we |
| + // use that, otherwise we fall back to using STRING. |
| + // |
| + // As a final minor complication, when one of the selections is actually |
| + // owned by us, we don't request the details for it. |
| + if (!get_selections_time_.is_null() && |
| + (base::TimeTicks::Now() - get_selections_time_) < |
| + base::TimeDelta::FromSeconds(5)) { |
| + return; |
| + } |
| + |
| + if (selection != None) { |
| + if (selection != clipboard_atom_ && selection != XA_PRIMARY) { |
| + return; |
| + } |
| + } else { |
| + selection = XA_PRIMARY; |
| + } |
| + |
| + if (IsSelectionOwner(selection)) { |
| + FinishGetSelections(); |
| + } |
| + |
| + get_selections_time_ = base::TimeTicks::Now(); |
| + |
| + if (timestamp) { |
| + current_selection_time_ = timestamp; |
| + GetSelectionTargets(selection); |
| + } else { |
| + new_selection_ = 0; |
| + had_valid_timestamp_ = false; |
| + GetSelectionTimestamp(selection, CurrentTime); |
| + } |
| +} |
| + |
| +void XServerClipboard::FinishGetSelections() { |
| + getting_initial_selection_ = false; |
| + get_selections_time_ = base::TimeTicks(); |
| +} |
| + |
| +void XServerClipboard::OnPropertyNotify(XEvent* event) { |
| + if (large_selection_property_ != None && |
| + event->xproperty.atom == large_selection_property_ && |
| + event->xproperty.state == PropertyNewValue) { |
| + Atom type; |
| + int format; |
| + unsigned long item_count, after; |
| + unsigned char *data; |
| + XGetWindowProperty(display_, clipboard_window_, large_selection_property_, |
| + 0, ~0L, True, AnyPropertyType, &type, &format, |
| + &item_count, &after, &data); |
| + if (type != None) { |
| + XFree(data); |
| + |
| + // If the property is zero-length then the large transfer is complete. |
| + if (item_count == 0) { |
| + large_selection_property_ = None; |
| + } |
| + } |
| + } |
| +} |
| + |
| +void XServerClipboard::OnSelectionNotify(XEvent* event) { |
| + if (event->xselection.property != None) { |
| + Atom type; |
| + int format; |
| + unsigned long item_count, after; |
| + unsigned char *data; |
| + XGetWindowProperty(display_, clipboard_window_, |
| + event->xselection.property, 0, ~0L, True, |
| + AnyPropertyType, &type, &format, |
| + &item_count, &after, &data); |
| + if (type == large_selection_atom_) { |
| + // Large selection - just read and ignore these for now. |
| + large_selection_property_ = event->xselection.property; |
| + } else { |
| + // Standard selection - call the selection notifier. |
| + large_selection_property_ = None; |
| + if (type != None) { |
| + DoSelectionNotify(&event->xselection, type, format, item_count, data); |
| + XFree(data); |
| + } |
| + } |
| + } |
| + DoSelectionNotify(&event->xselection, 0, 0, 0, 0); |
| +} |
| + |
| +void XServerClipboard::OnSelectionRequest(XEvent* event) { |
| + XSelectionEvent selection_event; |
| + selection_event.type = SelectionNotify; |
| + selection_event.display = event->xselectionrequest.display; |
| + selection_event.requestor = event->xselectionrequest.requestor; |
| + selection_event.selection = event->xselectionrequest.selection; |
| + selection_event.time = event->xselectionrequest.time; |
| + selection_event.target = event->xselectionrequest.target; |
| + if (event->xselectionrequest.property == None) { |
| + event->xselectionrequest.property = event->xselectionrequest.target; |
| + } |
| + if (!IsSelectionOwner(selection_event.selection)) { |
| + selection_event.property = None; |
| + } else { |
| + selection_event.property = event->xselectionrequest.property; |
| + if (selection_event.target == targets_atom_) { |
| + Atom targets[3]; |
| + targets[0] = timestamp_atom_; |
| + targets[1] = utf8_string_atom_; |
| + targets[2] = XA_STRING; |
| + XChangeProperty(display_, selection_event.requestor, |
| + selection_event.property, XA_ATOM, 32, PropModeReplace, |
| + reinterpret_cast<unsigned char*>(targets), 3); |
| + } else if (selection_event.target == timestamp_atom_) { |
| + Time time = selection_own_time_[selection_event.selection]; |
| + XChangeProperty(display_, selection_event.requestor, |
| + selection_event.property, XA_INTEGER, 32, |
| + PropModeReplace, reinterpret_cast<unsigned char*>(&time), |
| + 1); |
| + } else if (selection_event.target == utf8_string_atom_ || |
| + selection_event.target == XA_STRING) { |
| + if (!data_.empty()) { |
| + XChangeProperty(display_, selection_event.requestor, |
| + selection_event.property, selection_event.target, 8, |
| + PropModeReplace, |
| + reinterpret_cast<unsigned char*>( |
| + const_cast<char*>(data_.data())), |
| + data_.size()); |
| + } |
| + } |
| + } |
| + XSendEvent(display_, selection_event.requestor, False, 0, |
| + reinterpret_cast<XEvent*>(&selection_event)); |
| +} |
| + |
| +void XServerClipboard::DoSelectionNotify(XSelectionEvent* event, |
| + Atom type, |
| + int format, |
| + int item_count, |
| + void* data) { |
| + bool finished = false; |
| + |
| + if (event->target == timestamp_atom_) { |
| + finished = GotSelectionTimestamp(event, format, item_count, data); |
| + } else if (event->target == targets_atom_) { |
| + finished = GotSelectionTargets(event, format, item_count, data); |
| + } else if (event->target == utf8_string_atom_ || |
| + event->target == XA_STRING) { |
| + finished = GotSelectionString(event, format, item_count, data); |
| + } |
| + |
| + if (finished) { |
| + FinishGetSelections(); |
| + } |
| +} |
| + |
| +bool XServerClipboard::GotSelectionTimestamp(XSelectionEvent* event, |
| + int format, |
| + int item_count, |
| + void* data) { |
| + if (event->property == timestamp_atom_) { |
| + if (data && format == 32 && item_count == 1) { |
| + Time selection_time = *static_cast<Time*>(data); |
| + if (selection_time != 0) { |
| + had_valid_timestamp_ = true; |
| + } |
| + if (IsTimeLater(selection_time, current_selection_time_)) { |
| + current_selection_time_ = selection_time; |
| + new_selection_ = event->selection; |
| + } |
| + } |
| + } |
| + |
| + if (event->selection == XA_PRIMARY) { |
| + if (!IsSelectionOwner(clipboard_atom_)) { |
| + GetSelectionTimestamp(clipboard_atom_, event->time); |
| + return false; |
| + } |
| + } else if (event->selection != clipboard_atom_) { |
| + return false; |
| + } |
| + |
| + if (getting_initial_selection_ && had_valid_timestamp_) { |
| + return true; |
| + } |
| + |
| + if (new_selection_) { |
| + GetSelectionTargets(new_selection_); |
| + } else if (!had_valid_timestamp_) { |
| + GetSelectionTargets(XA_PRIMARY); |
| + } else { |
| + return true; |
| + } |
| + return false; |
| +} |
| + |
| +bool XServerClipboard::GotSelectionTargets(XSelectionEvent* event, |
| + int format, |
| + int item_count, |
| + void* data) { |
| + if (event->property == targets_atom_) { |
| + if (data && format == 32) { |
| + // The XGetWindowProperty man-page specifies that the returned |
| + // property data will be an array of |long|s in the case where |
| + // |format| == 32. On a 64-bit system, each item gets padded to |
| + // 8 bytes, the size of a |long|. |
| + const long* targets = static_cast<const long*>(data); |
| + for (int i = 0; i < item_count; i++) { |
| + if (targets[i] == static_cast<long>(utf8_string_atom_)) { |
| + GetSelectionString(event->selection, utf8_string_atom_); |
| + return false; |
| + } |
| + } |
| + } |
| + } |
| + GetSelectionString(event->selection, XA_STRING); |
| + return false; |
| +} |
| + |
| +bool XServerClipboard::GotSelectionString(XSelectionEvent* event, |
| + int format, |
| + int item_count, |
| + void* data) { |
| + if (event->property != selection_string_atom_ || !data || format != 8) { |
| + return true; |
| + } |
| + |
| + std::string text(static_cast<char*>(data), item_count); |
| + |
| + if (event->target == XA_STRING || event->target == utf8_string_atom_) { |
| + GotCutTextUtf8(text); |
| + } |
| + return true; |
| +} |
| + |
| +void XServerClipboard::GotCutTextUtf8(const std::string& text) { |
| + data_ = text; |
| + new_cut_text_ = false; |
| + if (!getting_initial_selection_) { |
| + callback_.Run(kMimeTypeTextUtf8, data_); |
| + } |
| +} |
| + |
| +bool XServerClipboard::IsSelectionOwner(Atom selection) { |
| + return selections_owned_.find(selection) != selections_owned_.end(); |
| +} |
| + |
| +void XServerClipboard::GetSelectionTimestamp(Atom selection, Time time) { |
| + XConvertSelection(display_, selection, timestamp_atom_, timestamp_atom_, |
| + clipboard_window_, time); |
| +} |
| + |
| +void XServerClipboard::GetSelectionTargets(Atom selection) { |
| + XConvertSelection(display_, selection, targets_atom_, targets_atom_, |
| + clipboard_window_, CurrentTime); |
| +} |
| + |
| +void XServerClipboard::GetSelectionString(Atom selection, Atom target) { |
| + XConvertSelection(display_, selection, target, selection_string_atom_, |
| + clipboard_window_, CurrentTime); |
| +} |
| + |
| +void XServerClipboard::OwnSelection(Atom selection, Time time) { |
| + XSetSelectionOwner(display_, selection, clipboard_window_, time); |
| + if (XGetSelectionOwner(display_, selection) == clipboard_window_) { |
| + selections_owned_.insert(selection); |
| + selection_own_time_[selection] = time; |
| + } else { |
| + LOG(ERROR) << "XSetSelectionOwner failed for selection " << selection; |
| + } |
| +} |
| + |
| +} // namespace remoting |