Thursday, October 13, 2011

Creating a Flex AIR text editor: Part 3

In this tutorial we will add 3 things to the text editor - implement tab insertion, add Copy and Paste features and add a fix for the cursor.

If you try running the existing text editor we have, you will see that you are not able to insert tabs using the tab key. This can be fixed by creating a TextFlow object for the text area's textFlow property, creating a Configuration object for the TextFlow's configuration property, and setting that object's manageTabKey property to true.

This is done in the init function:

// Allow insertion of tabs
var textFlow:TextFlow = textArea.textFlow;
var config:Configuration = Configuration(textFlow.configuration);
config.manageTabKey = true;

Next, we are going to add the Copy and Paste features. You can already use Ctrl+C and Ctrl+V and using the Copy and Paste buttons from your right click context menu, but every text editor should have these features stored under the Edit menu item. This means we will have to manually do the Copy and Paste functionality, which will work when the user uses the items in the menu AND if he uses Ctrl+C or Ctrl+V.

First add these menu items to the XML:

<fx:Declarations>
<fx:XML id="windowMenu">
<root>
<menuitem label="File">
<menuitem label="Open" key="o" controlKey="true" />
</menuitem>
<menuitem label="Edit">
<menuitem label="Copy" key="c" controlKey="true" />
<menuitem label="Paste" key="v" controlKey="true" />
</menuitem>
<menuitem label="Settings">
<menuitem label="Word wrap" type="check" toggled="{pref_wrap}" />
</menuitem>
</root>
</fx:XML>
</fx:Declarations>

In the menuSelect function, add checks for the Copy and Paste menu items and direct them to doCopy and doPaste functions:

private function menuSelect(evt:FlexNativeMenuEvent):void {
(evt.item.@label == "Word wrap")?(pref_wrap = !pref_wrap):(void);
(evt.item.@label == "Copy")?(doCopy()):(void);
(evt.item.@label == "Paste")?(doPaste()):(void);
savePreferences();
}

We will use the selectionActivePosition and selectionAnchorPosition properties of the textArea object to find out what's selected, then pass it to the clipboard:

private function doCopy():void {
var selectedText:String = textArea.text.substring(textArea.selectionActivePosition, textArea.selectionAnchorPosition);
System.setClipboard(selectedText);
}

When pasting, we take the data from the clipboard and insert it using insertText method:

private function doPaste():void{
var myClip:Clipboard = Clipboard.generalClipboard;
var pastedText:String = myClip.getData(ClipboardFormats.TEXT_FORMAT) as String;
insertText(pastedText);
}

I created the insertText method because there are going to be more situations when we will need to insert stuff into text.

Here's the function which returns the result, it handle all the selection related things too:

private function insertText(str:String):void{
var oldSel1:int = textArea.selectionActivePosition;
var oldSel2:int = textArea.selectionAnchorPosition;
var preText:String = textArea.text.substring(0, oldSel1);
var postText:String = textArea.text.substring(oldSel2);
var newSelectRange:int = preText.length + str.length;
textArea.text = preText + str + postText;
textArea.selectRange(newSelectRange, newSelectRange);
}

I noticed a bug/glitch which sometimes happens when you paste using keyboard and then click on the text area - the mouse cursor turns from the ibeam cursor to the arrow one, and stays like that until you click on the text or move out and back on to the text area.

A fix for this would be forcing the mouse to change its cursor to ibeam when the user clicks the text area:

<s:TextArea id="textArea" width="100%" height="100%" borderVisible="false" lineBreak="{(pref_wrap)?('toFit'):('explicit')}" click="cursorFix();" />

private function cursorFix():void{
Mouse.cursor = "ibeam";
}

Here is the full code:

<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
                       xmlns:s="library://ns.adobe.com/flex/spark"
                       xmlns:mx="library://ns.adobe.com/flex/mx"
   creationComplete="init();" title="Kirpad" showStatusBar="false">
   
<s:menu>
<mx:FlexNativeMenu dataProvider="{windowMenu}" showRoot="false" labelField="@label" keyEquivalentField="@key" itemClick="menuSelect(event);" />
</s:menu>

<fx:Script>
<![CDATA[
import flash.events.KeyboardEvent;
import flash.net.SharedObject;
import mx.collections.ArrayCollection;
import mx.controls.Alert;
import mx.events.FlexNativeMenuEvent;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.elements.Configuration;
import flash.system.System;
import flash.desktop.Clipboard;
import flash.desktop.ClipboardFormats;
import flash.ui.Mouse;

private var preferences:SharedObject = SharedObject.getLocal("kirpadPreferences");
[Bindable]
private var pref_wrap:Boolean = true;

private function init():void {
// Set preferences if loaded for the first time
if (preferences.data.firsttime == null) {
preferences.data.firsttime = true;
preferences.data.wrap = true;
preferences.flush();
}

// Set preferences loaded from local storage
pref_wrap = preferences.data.wrap;

// Allow insertion of tabs
var textFlow:TextFlow = textArea.textFlow;
var config:Configuration = Configuration(textFlow.configuration);
config.manageTabKey = true;
}

private function menuSelect(evt:FlexNativeMenuEvent):void {
(evt.item.@label == "Word wrap")?(pref_wrap = !pref_wrap):(void);
(evt.item.@label == "Copy")?(doCopy()):(void);
(evt.item.@label == "Paste")?(doPaste()):(void);
savePreferences();
}

private function savePreferences():void {
preferences.data.wrap = pref_wrap;
preferences.flush();
}

private function doCopy():void {
var selectedText:String = textArea.text.substring(textArea.selectionActivePosition, textArea.selectionAnchorPosition);
System.setClipboard(selectedText);
}

private function doPaste():void{
var myClip:Clipboard = Clipboard.generalClipboard;
var pastedText:String = myClip.getData(ClipboardFormats.TEXT_FORMAT) as String;
insertText(pastedText);
}

private function insertText(str:String):void{
var oldSel1:int = textArea.selectionActivePosition;
var oldSel2:int = textArea.selectionAnchorPosition;
var preText:String = textArea.text.substring(0, oldSel1);
var postText:String = textArea.text.substring(oldSel2);
var newSelectRange:int = preText.length + str.length;
textArea.text = preText + str + postText;
textArea.selectRange(newSelectRange, newSelectRange);
}

private function cursorFix():void{
Mouse.cursor = "ibeam";
}
]]>
</fx:Script>

<fx:Declarations>
<fx:XML id="windowMenu">
<root>
<menuitem label="File">
<menuitem label="Open" key="o" controlKey="true" />
</menuitem>
<menuitem label="Edit">
<menuitem label="Copy" key="c" controlKey="true" />
<menuitem label="Paste" key="v" controlKey="true" />
</menuitem>
<menuitem label="Settings">
<menuitem label="Word wrap" type="check" toggled="{pref_wrap}" />
</menuitem>
</root>
</fx:XML>
</fx:Declarations>

<s:TextArea id="textArea" width="100%" height="100%" borderVisible="false" lineBreak="{(pref_wrap)?('toFit'):('explicit')}" click="cursorFix();" />
</s:WindowedApplication>

We will continue next time.

Thanks for reading!

No comments:

Post a Comment