Sunday, March 28, 2010

Integrate with Symbian clipboard

Introduction
One day you can face a trivial task: to introduce clipboard support into your Symbian application. At first moment, you’re confused a bit but the solution is nearby. Your SDK (at least from 3rd edition MR) provides an example:


SDK root\Examples\SysLibs\Clipboard\Basics


This sample shows how to pass custom objects via clipboard. It looks great but some kind of overkill if to pass text only. Also, usage of custom stream hides your clipboard content from other apps.

Next stage of googling leads to built-in CPlainText class. It contains two methods to read text from or write to clipboard correspondingly. Nokia provides some kind of “Clipboard for dummies” article as well:
http://wiki.forum.nokia.com/index.php/Clipboard_Copy/Cut/Paste
Copy-paste the code into your sources, compile and run. End of story?


CPlainText could be very nice solution. But in Symbian reality, let read the manual. There is one small but important phrase which can figure out CPlainText as non-acceptable solution in your case:

“…all line feeds are converted into paragraph delimiters…”

What does it mean? Actually, you can play safe as long as your text is paragraph-delimited, and each piece is finished with “carriage return” and “line feed” symbols (code 13 and code 10 correspondingly). And if CPlainText receives, for example, Unix-style text, it modifies the text replacing single “line feed” symbol with pair of “carriage return” and “line feed”.

  • New text is longer (do not forget to re-allocate, buffer overrun attempt otherwise!)
  • Some parsers may fail if expect exact string format (with no “carriage return” symbols, for example)
  • New text and old one are not equal, finally.



Let’s read it. Naïve.
So, we have to repeat what CPlainText does but without text corruption. This turns us back to SDK sample. Naïve implementation may look like following:


TDesC * ReadFromClipboardL(RFs & aFs)
{
TDesC *result = NULL;
CClipboard *cb = CClipboard::NewForReadingLC(aFs);

TStreamId stid = cb->StreamDictionary().At(KClipboardUidTypePlainText);
if (KNullStreamId != stid)
{
RStoreReadStream stream;
stream.OpenLC(cb->Store(), stid);

TBuf<512> *buf = new (ELeave) TBuf<512>;
buf->SetLength(512);
stream >> (*buf)[0];

stream.Close();
CleanupStack::Pop(); // stream.OpenLC
result = buf;
}
CleanupStack::PopAndDestroy(cb);

return result;
}

We try to open plain-text stream of the clipboard and if so, read from the stream. Actually, we have no idea how long is the text so let hard-code some value as 512 symbols (yes, hard-coding is bad style but this time we’re naïve, right?).

Start any text-handling Symbian app (for example, Notes application or SMS Editor) and copy some text to clipboard. Let copy something short and trivial, “Ping” word for example. After that, switch to our test application and run ReadFromClipboardL() code. Regardless dumping to screen or evaluating in debugger, our buffer contains complete trash with no idea about clipboard text. If you try to paste the clipboard content into another text-handling Symbian app, everything is OK. So, error is on our side.


Let’s read it. Reverse-engineering.
If go back to revise our code, first suspicion could fall to hard-coded buffer length. Symbian applications get a clue how long the text is. It is reasonable to expect that plain text is not “so plain”. There should be some header with useful info. Time to reverse-engineering!
If reading clipboard stream byte-by-byte, it’s easy to obtain following picture:

  • First 4 bytes – length of clipboard text.
  • Next bytes – viola, clipboard content!
  • Zero byte
  • Byte with value of 2 (0x02 code) – have no idea about. Number of TInt32 below?
  • 4 bytes with KClipboardUidTypePlainText value
  • 4 bytes with KMaxFieldBufferSize value

We cool! We’ve hacked it! Let’s update the code:

TDesC * ReadFromClipboardL(RFs & aFs)
{
TDesC *result = NULL;
CClipboard *cb = CClipboard::NewForReadingLC(aFs);

TStreamId stid = cb->StreamDictionary().At(KClipboardUidTypePlainText);
if (KNullStreamId != stid)
{
RStoreReadStream stream;
stream.OpenLC(cb->Store(), stid);
const TInt32 size = stream.ReadInt32L();
HBufC *buf = HBufC::NewLC(size);
buf->Des().SetLength(size);

for(TInt i = 0; i <>> buf->Des()[i];

CleanupStack::Pop(buf);
stream.Close();
CleanupStack::Pop(); // stream.OpenLC

result = buf;
}
CleanupStack::PopAndDestroy(cb);

return result;
}


 
Compile, run… Bang! Number of chars is correct but stupid rectangles instead of the text are on the screen. What’s wrong?
By default, modern Symbian S60 treats all chars as Unicode (16 bit wide) but as we “reversed” right now, the clipboard operates with 8 bit wide chars. C’mon, let replace TDesC with TDesC8 and HBufC with HBufC8. Compile, run… Great! We see correct clipboard text (“Ping” word, remember?). You can submit your code into production and go develop other tasks. Or go eat, whatever. Enjoy your coffee until QA engineer or even overseas customer does complain about broken clipboard functionality for non-English languages.



Let’s read it. Unicode.
Quickly repeating our reverse-engineering, we can figure out that non-English chars are present in clipboard stream but they’re encoded somehow. Looks like, 7-bit chars (with zero high bit) are chained “as is” but non-English chars (with high bit set) are prefixed with some extra byte (but it is not counted in string length). Coding this staff manually is going to be boring but other apps do it somehow, right? CPlainText does it as well. So, built-in decoder class should be nearby. And its name is TUnicodeExpander:



TDesC * ReadFromClipboardL(RFs & aFs)
{
TDesC *result = NULL;
CClipboard *cb = CClipboard::NewForReadingLC(aFs);

TStreamId stid = cb->StreamDictionary().At(KClipboardUidTypePlainText);
if (KNullStreamId != stid)
{
RStoreReadStream stream;
stream.OpenLC(cb->Store(), stid);
const TInt32 size = stream.ReadInt32L();
HBufC *buf = HBufC::NewLC(size);
buf->Des().SetLength(size);

TUnicodeExpander e;
TMemoryUnicodeSink sink(&buf->Des()[0]);
e.ExpandL(sink, stream, size);

CleanupStack::Pop(buf);
stream.Close();
CleanupStack::Pop(); // stream.OpenLC
result = buf;
}
CleanupStack::PopAndDestroy(cb);

return result;
}



Happy end. Music. Credits. Lights on.

Write it.
Taking into account our latest knowledge, it could be piece of cake to compose a code with opposite functionality.

  • Create KClipboardUidTypePlainText stream
  • Put 4 bytes with string length
  • Put encoded text
  • Close the stream
Let give a try:


void WriteToClipboardL(RFs &aFs, const TDesC & aText)
{
CClipboard *cb = CClipboard::NewForWritingLC(aFs);

RStoreWriteStream stream;
TStreamId stid = stream.CreateLC(cb->Store());
stream.WriteInt32L(aText.Length());

TUnicodeCompressor c;
TMemoryUnicodeSource source(aText.Ptr());
TInt bytes(0);
TInt words(0);
c.CompressL(stream, source, KMaxTInt, aText.Length(), &bytes, &words);

stream.CommitL();
cb->StreamDictionary().AssignL(KClipboardUidTypePlainText, stid);
cb->CommitL();

stream.Close();
CleanupStack::PopAndDestroy(); // stream.CreateLC
CleanupStack::PopAndDestroy(cb);
}


 

From first glance, this code runs OK, Symbian clipboard does receive the text. You’re in trouble next step, while pasting into any text-handling Symbian app. Your text appears but accompanied with “System error (-25)” message. What’s wrong?
If you step back to our reverse engineering results, you could see the answer. The most funny thing here that any Windows-experienced programmer would point this answer in few seconds. Same time, you can’t imagine this is really the answer if you’re experienced with Symbian text descriptors. It is a real mind-trap. You have to put zero byte at the end of encoded text!



void WriteToClipboardL(RFs &aFs, const TDesC & aText)
{
CClipboard *cb = CClipboard::NewForWritingLC(aFs);

RStoreWriteStream stream;
TStreamId stid = stream.CreateLC(cb->Store());
stream.WriteInt32L(aText.Length());

TUnicodeCompressor c;
TMemoryUnicodeSource source(aText.Ptr());
TInt bytes(0);
TInt words(0);
c.CompressL(stream, source, KMaxTInt, aText.Length(), &bytes, &words);

stream.WriteInt8L(0); // magic command! :)

stream.CommitL();
cb->StreamDictionary().AssignL(KClipboardUidTypePlainText, stid);
cb->CommitL();

stream.Close();
CleanupStack::PopAndDestroy(); // stream.CreateLC
CleanupStack::PopAndDestroy(cb);
}


 
P.S.
I had no chance to verify it vs. hieroglyphs. Anybody?