diff --git a/src/Scripting/ClipboardX11.cxx b/src/Scripting/ClipboardX11.cxx index 6108c5242..5e005011c 100644 --- a/src/Scripting/ClipboardX11.cxx +++ b/src/Scripting/ClipboardX11.cxx @@ -16,6 +16,14 @@ // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +/* + * See the following links for more information on X11 clipboard: + * + * http://www.jwz.org/doc/x-cut-and-paste.html + * http://michael.toren.net/mirrors/doc/X-copy+paste.txt + * https://github.com/kfish/xsel/blob/master/xsel.c + */ + #include "NasalClipboard.hxx" #include @@ -37,13 +45,34 @@ class ClipboardX11: 0, 0 ) ), _atom_targets( XInternAtom(_display, "TARGETS", False) ), + _atom_text( XInternAtom(_display, "TEXT", False) ), + _atom_utf8( XInternAtom(_display, "UTF8_STRING", False) ), _atom_primary( XInternAtom(_display, "PRIMARY", False) ), _atom_clipboard( XInternAtom(_display, "CLIPBOARD", False) ) { assert(_display); - assert(_atom_targets != None); - assert(_atom_primary != None); - assert(_atom_clipboard != None); + } + + virtual ~ClipboardX11() + { + // Ensure we get rid of any selection ownership + if( XGetSelectionOwner(_display, _atom_primary) ) + XSetSelectionOwner(_display, _atom_primary, None, CurrentTime); + if( XGetSelectionOwner(_display, _atom_clipboard) ) + XSetSelectionOwner(_display, _atom_clipboard, None, CurrentTime); + } + + /** + * We need to run an event queue to check for selection request + */ + virtual void update() + { + while( XPending(_display) ) + { + XEvent event; + XNextEvent(_display, &event); + handleEvent(event); + } } /** @@ -51,12 +80,11 @@ class ClipboardX11: */ virtual std::string getText(Type type) { - Atom atom_type = (type == CLIPBOARD ? _atom_clipboard : _atom_primary); + Atom atom_type = typeToAtom(type); //Request a list of possible conversions XConvertSelection( _display, atom_type, _atom_targets, atom_type, _window, CurrentTime ); - XFlush(_display); Atom requested_type = None; bool sent_request = false; @@ -66,67 +94,58 @@ class ClipboardX11: XEvent event; XNextEvent(_display, &event); - if( event.type == SelectionNotify ) + if( event.type != SelectionNotify ) { - Atom target = event.xselection.target; - if(event.xselection.property == None) - { - if( target == _atom_targets ) - // If TARGETS can not be converted no selection is available - break; + handleEvent(event); + continue; + } + Atom target = event.xselection.target; + if(event.xselection.property == None) + { + if( target == _atom_targets ) + // If TARGETS can not be converted no selection is available + break; + + SG_LOG + ( + SG_NASAL, + SG_WARN, + "ClipboardX11::getText: Conversion failed: " + "target=" << getAtomName(target) + ); + break; + } + + //If we're being given a list of targets (possible conversions) + if(target == _atom_targets && !sent_request) + { + sent_request = true; + requested_type = XA_STRING; // TODO select other type + XConvertSelection( _display, atom_type, requested_type, atom_type, + _window, CurrentTime ); + } + else if(target == requested_type) + { + Property prop = readProperty(_window, atom_type); + if( prop.format != 8 ) + { SG_LOG ( SG_NASAL, SG_WARN, - "ClipboardX11::getText: Conversion failed: " - "target=" << getAtomName(target) + "ClipboardX11::getText: can only handle 8-bit data (is " + << prop.format << "-bit) -> retry " + << cnt++ ); - break; + XFree(prop.data); + continue; } - else - { - //If we're being given a list of targets (possible conversions) - if(target == _atom_targets && !sent_request) - { - sent_request = true; - requested_type = XA_STRING; // TODO select other type - XConvertSelection( _display, atom_type, requested_type, atom_type, - _window, CurrentTime ); - } - else if(target == requested_type) - { - Property prop = readProperty(_window, atom_type); - if( prop.format != 8 ) - { - SG_LOG - ( - SG_NASAL, - SG_WARN, - "ClipboardX11::getText: can only handle 8-bit data (is " - << prop.format << "-bit) -> retry " - << cnt++ - ); - XFree(prop.data); - continue; - } - std::string result((const char*)prop.data, prop.num_items); - XFree(prop.data); + std::string result((const char*)prop.data, prop.num_items); + XFree(prop.data); - return result; - } - else - { - SG_LOG - ( - SG_NASAL, - SG_WARN, - "ClipboardX11::getText: wrong target: " << getAtomName(target) - ); - break; - } - } + return result; } else { @@ -134,7 +153,7 @@ class ClipboardX11: ( SG_NASAL, SG_WARN, - "ClipboardX11::getText: unexpected XEvent: " << event.type + "ClipboardX11::getText: wrong target: " << getAtomName(target) ); break; } @@ -148,13 +167,27 @@ class ClipboardX11: */ virtual bool setText(const std::string& text, Type type) { - SG_LOG - ( - SG_NASAL, - SG_ALERT, - "ClipboardX11::setText: not yet implemented!" - ); - return false; + Atom atom_type = typeToAtom(type); + XSetSelectionOwner(_display, atom_type, _window, CurrentTime); + if( XGetSelectionOwner(_display, atom_type) != _window ) + { + SG_LOG + ( + SG_NASAL, + SG_ALERT, + "ClipboardX11::setText: failed to get selection owner!" + ); + return false; + } + + // We need to store the text for sending it to another application upon + // request + if( type == CLIPBOARD ) + _clipboard = text; + else + _selection = text; + + return true; } protected: @@ -162,9 +195,116 @@ class ClipboardX11: Display *_display; Window _window; Atom _atom_targets, + _atom_text, + _atom_utf8, _atom_primary, _atom_clipboard; + std::string _clipboard, + _selection; + + void handleEvent(const XEvent& event) + { + switch( event.type ) + { + case SelectionRequest: + handleSelectionRequest(event.xselectionrequest); + break; + case SelectionClear: + if( event.xselectionclear.selection == _atom_clipboard ) + _clipboard.clear(); + else + _selection.clear(); + break; + default: + SG_LOG + ( + SG_NASAL, + SG_WARN, + "ClipboardX11: unexpected XEvent: " << event.type + ); + break; + } + } + + void handleSelectionRequest(const XSelectionRequestEvent& sel_req) + { + SG_LOG + ( + SG_NASAL, + SG_DEBUG, + "ClipboardX11: handle selection request: " + "selection=" << getAtomName(sel_req.selection) << ", " + "target=" << getAtomName(sel_req.target) + ); + + const std::string& buf = sel_req.selection == _atom_clipboard + ? _clipboard + : _selection; + + // Prepare response to notify whether we have written to the property or + // are unable to do the conversion + XSelectionEvent response; + response.type = SelectionNotify; + response.display = sel_req.display; + response.requestor = sel_req.requestor; + response.selection = sel_req.selection; + response.target = sel_req.target; + response.property = sel_req.property; + response.time = sel_req.time; + + if( sel_req.target == _atom_targets ) + { + static Atom supported[] = { + XA_STRING, + _atom_text, + _atom_utf8 + }; + + changeProperty + ( + sel_req.requestor, + sel_req.property, + sel_req.target, + &supported, + sizeof(supported) + ); + } + else if( sel_req.target == XA_STRING + || sel_req.target == _atom_text + || sel_req.target == _atom_utf8 ) + { + changeProperty + ( + sel_req.requestor, + sel_req.property, + sel_req.target, + buf.data(), + buf.size() + ); + } + else + { + // Notify requestor that we are unable to perform requested conversion + response.property = None; + } + + XSendEvent(_display, sel_req.requestor, False, 0, (XEvent*)&response); + } + + void changeProperty( Window w, + Atom prop, + Atom type, + const void *data, + size_t size ) + { + XChangeProperty + ( + _display, w, prop, type, 8, PropModeReplace, + static_cast(data), size + ); + } + struct Property { unsigned char *data; @@ -179,7 +319,7 @@ class ClipboardX11: int actual_format; unsigned long nitems; unsigned long bytes_after; - unsigned char *ret=0; + unsigned char *ret = 0; int read_bytes = 1024; @@ -204,11 +344,16 @@ class ClipboardX11: return p; } - std::string getAtomName(Atom atom) + std::string getAtomName(Atom atom) const { return atom == None ? "None" : XGetAtomName(_display, atom); } + Atom typeToAtom(Type type) const + { + return type == CLIPBOARD ? _atom_clipboard : _atom_primary; + } + }; //------------------------------------------------------------------------------ diff --git a/src/Scripting/NasalClipboard.cxx b/src/Scripting/NasalClipboard.cxx index db148dce0..6ac146263 100644 --- a/src/Scripting/NasalClipboard.cxx +++ b/src/Scripting/NasalClipboard.cxx @@ -34,18 +34,20 @@ static NasalClipboard::Type parseType(naContext c, int argc, naRef* args, int i) { if( argc > i ) { - if( !naIsString(args[i]) ) - naRuntimeError(c, "clipboard: invalid arg (not a string)"); + if( naIsNum(args[i]) ) + { + if( static_cast(args[i].num) == NasalClipboard::CLIPBOARD ) + return NasalClipboard::CLIPBOARD; + if( static_cast(args[i].num) == NasalClipboard::PRIMARY ) + return NasalClipboard::PRIMARY; + } - std::string type_str( naStr_data(args[i]) ); - boost::to_upper(type_str); - - if( type_str == "CLIPBOARD" ) - return NasalClipboard::CLIPBOARD; - else if( type_str == "PRIMARY" || type_str == "SELECTION" ) - return NasalClipboard::PRIMARY; - else - naRuntimeError(c, "clipboard: unknown clipboard type"); + naRuntimeError + ( + c, + "clipboard: invalid arg " + "(expected clipboard.CLIPBOARD or clipboard.SELECTION)" + ); } return NasalClipboard::CLIPBOARD; @@ -56,7 +58,7 @@ static naRef f_setClipboardText(naContext c, naRef me, int argc, naRef* args) { if( argc < 1 || argc > 2 ) naRuntimeError( c, "clipboard.setText() expects 1 or 2 arguments: " - "text, [, type = \"CLIPBOARD\"]" ); + "text, [, type = clipboard.CLIPBOARD]" ); if( !naIsString(args[0]) ) naRuntimeError(c, "clipboard.setText() invalid arg (arg 0 not a string)"); @@ -74,7 +76,7 @@ static naRef f_getClipboardText(naContext c, naRef me, int argc, naRef* args) { if( argc > 1 ) naRuntimeError(c, "clipboard.getText() accepts max 1 arg: " - "[type = \"CLIPBOARD\"]" ); + "[type = clipboard.CLIPBOARD]" ); const std::string& text = NasalClipboard::getInstance()->getText(parseType(c, argc, args, 0)); @@ -86,11 +88,16 @@ static naRef f_getClipboardText(naContext c, naRef me, int argc, naRef* args) } //------------------------------------------------------------------------------ -// Table of extension functions, terminate with 0,0 +// Table of extension functions static struct {const char* name; naCFunction func; } funcs[] = { { "setText", f_setClipboardText }, - { "getText", f_getClipboardText }, - { 0,0 } // TERMINATION + { "getText", f_getClipboardText } +}; + +// Table of extension symbols +static struct {const char* name; naRef val; } symbols[] = { + { "CLIPBOARD", naNum(NasalClipboard::CLIPBOARD) }, + { "SELECTION", naNum(NasalClipboard::PRIMARY) } }; //------------------------------------------------------------------------------ @@ -111,7 +118,7 @@ void NasalClipboard::init(FGNasalSys *nasal) nasal->globalsSet("clipboard", _clipboard_hash); - for(size_t i=0;funcs[i].name;i++) + for( size_t i = 0; i < sizeof(funcs)/sizeof(funcs[0]); ++i ) { nasal->hashset ( @@ -120,7 +127,14 @@ void NasalClipboard::init(FGNasalSys *nasal) naNewFunc(nasal->context(), naNewCCode(nasal->context(), funcs[i].func)) ); - SG_LOG(SG_NASAL, SG_DEBUG, "Adding clipboard function: " << funcs[i].name ); + SG_LOG(SG_NASAL, SG_DEBUG, "Adding clipboard function: " << funcs[i].name); + } + + for( size_t i = 0; i < sizeof(symbols)/sizeof(symbols[0]); ++i ) + { + nasal->hashset(_clipboard_hash, symbols[i].name, symbols[i].val); + + SG_LOG(SG_NASAL, SG_DEBUG, "Adding clipboard symbol: " << symbols[i].name); } } diff --git a/src/Scripting/NasalClipboard.hxx b/src/Scripting/NasalClipboard.hxx index fca3979c6..c99b1d54a 100644 --- a/src/Scripting/NasalClipboard.hxx +++ b/src/Scripting/NasalClipboard.hxx @@ -41,6 +41,7 @@ class NasalClipboard typedef boost::shared_ptr Ptr; + virtual void update() {} virtual std::string getText(Type type = CLIPBOARD) = 0; virtual bool setText( const std::string& text, Type type = CLIPBOARD ) = 0; diff --git a/src/Scripting/NasalSys.cxx b/src/Scripting/NasalSys.cxx index 5dd28ed1b..a3983015d 100644 --- a/src/Scripting/NasalSys.cxx +++ b/src/Scripting/NasalSys.cxx @@ -588,6 +588,9 @@ void FGNasalSys::init() void FGNasalSys::update(double) { + if( NasalClipboard::getInstance() ) + NasalClipboard::getInstance()->update(); + if(!_dead_listener.empty()) { vector::iterator it, end = _dead_listener.end(); for(it = _dead_listener.begin(); it != end; ++it) delete *it;