Ensuring the clipboard has the same text regardless of flavour

Answered

Hi,

I am working on a plugin which adjusts the text being copied out and pasted into an editor.  I have succeeded for the most part: plain text is adjusted and copied to the clipboard as intended.  But, the rich-text flavors of text (html, rtf, etc) are copied unadjusted.

I have created a CopyPastePreProcessor extension point.  This is where the text being copied out and pasted in is adjusted.  This was pretty straightforward.  This works flawlessly when pasting to other applications that use the plain text flavor.

But, when pasting to an application such as Word, or TextEdit, the rich-text flavors are used and the unadjusted text gets transferred.  This is problematic.

I have looked into using the CopyPastePostProcessor extension point, but it does not actually allow me to add or modify the content of flavors.  In fact, I believe the actual transfer to the system clipboard occurs after the extension point is invoked.  I gather this, because using 

Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(text), null);

does not change the contents of the system clipboard.  I.e., the paste operation still pastes the unadjusted text.

I have a hack for now, and I will be the first to say that I am not fond of it.  

The hack: I run a TimerTask on a pooled thread half a second after the PreProcessor extension point is invoked and it sets the text to the system clipboard using the above code.  I use a a bit of code to avoid the obvious race conditions, but I feel that this is “not the right way” of accomplishing my goal.

Any suggestions would be most welcome.  Thanks!

ttyl
Alex

0
7 comments

CopyPastePostProcessor is used to provide custom data flavors. It is not really designed to replace the platform-generated flavors, but it looks it might work, if a proper value is provided by com.intellij.codeInsight.editorActions.TextBlockTransferableData#getPriority. TextBlockTransferable will report RTF/HTML flavors two times in getTransferDataFlavors, but the overridden value will be returned when RTF/HTML flavor is queried. It's worth testing though that a Transferable constructed in such a way will be processed by AWT clipboard as expected.

0

Hi Yann,

Thanks for this.  I am not sure how to determine “the right value”.   Should the priority be > 0 or < 0?  I have created a class that extends InputStream and implements TextBlockTransferableData, with  flavors text/rtf and text/html.  The InputStream contains some hard coded data ("Hello World").   However, this data does not override platform generated text/html and text/rtf data.  (As far as I can tell.)

Am I understanding the architecture correctly?

Thanks for your help in this matter.

 

 

0

Update: The TextBlockTransferableData is only retrieved by PSICopyPasteManager during a copy (as far as I can tell).  The getData() method ignores all TextBlockTransferableData that are not MyData (last line of the getData() method).  So, it looks like anything CopPastePostProcess may not help.  :(

0

Could you try specifying order="first" attribute in the CopyPastePostProcessor registration tag in plugin.xml? At least it seems to work for me on macOS locally. Worth checking Windows/Linux separately, just in case, as JDK code is also involved here.

0

Hello Yann, 

Thank you for that helpful advice.  That “half” worked.  The RTF got transferred, but the HTML did not.  I add two flavors in the post processor, text/html and text/rtf.  The rtf gets transferred.  I can tell because the “mark(), transferTo() and ”reset()" methods are invoked on the transferable and the corresponding string is copied into the clipboard with the specified flavour.  The text/html is not transferred, the InputStream methods are called.  Note: I have tried reordering them in the list, but only the RTF is transferred.  Here is the code.  

package ca.dal.cs.abrodsky.demo1;

import com.intellij.codeInsight.editorActions.CopyPastePostProcessor;
import com.intellij.codeInsight.editorActions.TextBlockTransferableData;
import com.intellij.openapi.editor.Editor;
import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.datatransfer.DataFlavor;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class CopyPastePostProcessorExtension<T extends TextBlockTransferableData> extends CopyPastePostProcessor<T> {
    @Override
    public @NotNull List<T> collectTransferableData(@NotNull PsiFile file, @NotNull Editor editor, int @NotNull [] startOffsets, int @NotNull [] endOffsets) {
        T rtf = (T) new InputStreamTransferableData(new DataFlavor("text/rtf;representationclass=java.io.InputStream", "Rich Text Format"));
        T html = (T) new InputStreamTransferableData(new DataFlavor("text/html;representationclass=java.io.InputStream", "HTML Format"));
        
        List<T> data = new ArrayList<>();
        data.add(html);
        data.add(rtf);
        return data;
    }

    private static class InputStreamTransferableData extends ByteArrayInputStream implements TextBlockTransferableData {
        private final DataFlavor flavor;

        public InputStreamTransferableData(DataFlavor dataFlavor) {
            super("Hello, world".getBytes());
            flavor = dataFlavor;
        }

        /**
         * @return
         */
        @Override
        public @Nullable DataFlavor getFlavor() {
            return flavor;
        }

        @Override
        public int getPriority() {
            return 0;
        }

        @Override
        public byte[] readAllBytes() {
            System.out.println("read All bytes from " + flavor);
            return super.readAllBytes();
        }

        @Override
        public int read() {
            System.out.println("read a byte from " + flavor);
            return super.read();
        }

        @Override
        public int readNBytes(byte [] data, int off, int length) {
            System.out.println("read N " + length + " bytes from " + flavor);
            return super.read(data, off, length);
        }

        @Override
        public int read(byte [] data, int off, int length) {
            System.out.println("read  " + length + " bytes from " + flavor);
            return super.read(data, off, length);
        }

        @Override
        public void mark(int readLimit) {
            System.out.println("mark " + readLimit + " bytes from " + flavor);
            super.mark(readLimit);
        }

        @Override
        public void reset() {
            System.out.println("reset from " + flavor);
            super.reset();
        }

        @Override
        public long transferTo(OutputStream outStream) throws IOException {
            System.out.println("transferTo " + flavor);
            return super.transferTo(outStream);
        }
    }
}


 

0

For the override to work plugin should use the exact flavours that the platform uses. For HTML that's text/html; class=java.io.Reader; charset=UTF-8 (reference com.intellij.openapi.editor.richcopy.view.HtmlTransferableData#FLAVOR directly, it's public). 

Please also change the implementation of the HTML data to extend from Reader, not InputStream.

0

Hi Yann,

Thank you!  That did it.  I realized yesterday that I needed to use a Reader for text/html, and that got me half-way. But, I did not realize that the Flavor had to be an exact match for all three parts (mime/class/charset).  Once I set that, it worked as expected.

I'll test this under Windows when I have a chance, but please consider this a solved issue. :)

Thanks again!

Sincerely,
Alex

0

Please sign in to leave a comment.