Monday, October 24, 2011

Creating a Flex AIR text editor: Part 14

In this tutorial we will make our application run faster by optimizing the existing code a little, and also add the ability to create new tabs and change/save text and selection data among the tabs.

Firstly, we will optimize what we already have.

Go to your everyFrame function and take out the code that's responsible for updating the text size, it will look like this:

private function everyFrame(evt:Event):void {
if (!heightFixed && height==initHeight) {
height = initHeight - 20;
if (height != initHeight) {
heightFixed = true;
}
}
}

Turn that piece of code into a separate function:

private function updateTextSize():void {
tabY = (toolBar.visible)?(toolBar.height):(0);
textY = tabBar.height + tabY;
var statusHeight:Number = (pref_status)?(statusBar.height):(0);
textHeight = height - textY - statusHeight;
focusManager.setFocus(textArea);
}

Create a resizing listener in the init function:

// Create a listener for resizing
addEventListener(NativeWindowBoundsEvent.RESIZE, onResize);

In the onResize function, call the updateTextSize method we just created:

private function onResize(evt:ResizeEvent):void {
updateTextSize();
}

Update the everyFrame function to call updateTextSize() too when the height is fixed:

private function everyFrame(evt:Event):void {
if (!heightFixed && height==initHeight) {
height = initHeight - 20;
if (height != initHeight) {
heightFixed = true;
updateTextSize();
}
}
}

Now all the lagging that there might've been is fixed.

Next, let's work on the tabs. Rename the tabHeadings array collection to tabData, because it isn't going to be used just for headings.

Change the collection to this:

<mx:ArrayCollection id="tabData">
<fx:Object title="Untitled" textData="" saved="false" seletedActive="0" selectedAnchor="0" />
</mx:ArrayCollection>

This is what each tab will look like on the code-side - it is going to have a title property, a textData property to store the text, a saved boolean to determine if this text was saved after last edit, and two properties to hold values of the selection.

Change the dataProvider for your CustomTabBar to tabData instead of tabHeadings, and add a handler for the change event - direct it to a tabChange function:

<custom:CustomTabBar id="tabBar" dataProvider="{tabData}" width="100%" y="{tabY}" itemRenderer="CustomTab" height="22" tabClose="onTabClose(event);" change="tabChange();" />

Declare a new variable in the beginning of the code, call it previousIndex. This will hold the index of the previously selected tab. Make it 0 by default:

private var previousIndex:Number = 0;

In the tabChange function, set the previously selected tab's data to whatever it is in the textArea (the data and the selection point positions), then update the previousIndex property and then feed the textArea the new data from the newly selected tab:

private function tabChange():void {
tabData[previousIndex].textData = textArea.text;
tabData[previousIndex].selectedActive = textArea.selectionActivePosition;
tabData[previousIndex].selectedAnchor = textArea.selectionAnchorPosition;
previousIndex = tabBar.selectedIndex;
textArea.text = tabData[tabBar.selectedIndex].textData;
textArea.selectRange(tabData[tabBar.selectedIndex].selectedAnchor, tabData[tabBar.selectedIndex].selectedActive);
}

Now we will create the ability to add a new tab.

Create a new menu item first under the File menu:

<menuitem label="New" key="n" controlKey="true" />

In the menuSelect function, add a line for this menu item, which will call a doNew() function when this button is clicked:

(evt.item.@label == "New")?(doNew()):(void);

The doNew() function adds a new item to the array collection and calls the tabChange() function:

private function doNew():void{
tabData.addItem( { title:"Untitled", textData:"", saved:false } );
tabBar.selectedIndex = tabData.length - 1;
tabChange();
}

Also, now that we don't have the somedata property in our tabData objects, you need to update the onTabClose() function to display something else in order for the code to work:

private function onTabClose(evt:Event):void{
Alert.show("Tab being closed: " + evt.target.data.label);
}

Finally, go to the CustomTab.mxml file. Find the updateDisplayList function, update it so that it first checks whether the object's saved property is true or false. If it is true, display title, if it is false, display title with a "*" symbol in the end.

override protected function updateDisplayList(w:Number, h:Number):void
        {
            super.updateDisplayList(w,h);
            if (labelDisplay)
            {
                labelDisplay.text = (data.saved)?(data.title):(data.title + "*");
            }
        }   

Full code for CustomTab.mxml:

<?xml version="1.0" encoding="utf-8"?>
<s:ItemRenderer
        xmlns:fx="http://ns.adobe.com/mxml/2009"
        xmlns:s="library://ns.adobe.com/flex/spark"
        xmlns:mx="library://ns.adobe.com/flex/mx"
        width="100%"
        height="100"
        autoDrawBackground="false"
>   

    <fx:Metadata>
        [Event(name="tabClose")]
    </fx:Metadata>

    <fx:Script>
    <![CDATA[
    
import flash.events.Event;
        import mx.controls.Alert;
        import mx.events.CloseEvent;

        private var tab:*;
        override public function set data(value:Object):void
        {
            super.data = value;
            tab = value;
        }

        override protected function updateDisplayList(w:Number, h:Number):void
        {
            super.updateDisplayList(w,h);
            if (labelDisplay)
            {
                labelDisplay.text = (data.saved)?(data.title):(data.title + "*");
            }
        }              

        protected function labelClose_clickHandler(event:MouseEvent):void
        {
            // prevent tab change
            event.stopImmediatePropagation();
dispatchEvent(new Event("tabClose", true));
        }

    ]]>
    </fx:Script>
   
    <s:states>
        <s:State name="normal" basedOn="{data.state}"/>
        <s:State name="selected" basedOn="{data.state}"/>
        <s:State name="hovered" basedOn="{data.state}"/>
    </s:states>

    <!-- background -->
    <s:Rect left="1" right="1" top="1" bottom="0">
        <s:fill>
            <s:LinearGradient rotation="90">
                <s:GradientEntry color="0xffffff" />
                <s:GradientEntry
                    color="0xd8d8d8"
                    alpha="0.85"
                    color.selected="0xffffff"
                    alpha.selected="1.0"
                    color.hovered="0x929496"
                    alpha.hovered="0.85"
                />
            </s:LinearGradient>
        </s:fill>
    </s:Rect>

    <!-- border rectangle -->
    <s:Line left="0" right="0" top="1">
        <s:stroke>
            <s:SolidColorStroke
                weight="1"
                alpha="1.0"
                color="0x999999"
            />
        </s:stroke>
    </s:Line>
    <s:Line left="0" bottom="0" top="1">
        <s:stroke>
            <s:SolidColorStroke
                weight="1"
                alpha="1.0"
                color="0x999999"
            />
        </s:stroke>
    </s:Line>
    <s:Line right="0" bottom="0" top="1">
        <s:stroke>
            <s:SolidColorStroke
                weight="1"
                alpha="1.0"
                color="0x999999"
            />
        </s:stroke>
    </s:Line>
    <s:Line left="0" right="0" bottom="0">
        <s:stroke>
            <s:SolidColorStroke
                weight="1"
                alpha="1.0"
                color="0x999999"
                alpha.selected="0.0"
                color.selected="0xffffff"
            />
        </s:stroke>
    </s:Line>
   
    <s:Label
        id="labelDisplay"
        textAlign="center"
        verticalAlign="middle"
        maxDisplayedLines="1"
        horizontalCenter="0"
        verticalCenter="1"
        left="10"
        right="20"
        top="2"
        bottom="2"
    />

    <s:Label
        id="labelClose"
        text="x"
        fontWeight="bold"
        right="4"
        top="2"
        fontSize="20"
        alpha=".5" 
color="#444444"
        click="labelClose_clickHandler(event)"
        useHandCursor="true"
        buttonMode="true"
    />
</s:ItemRenderer>

Full code for main mxml:

<?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"
   xmlns:custom="*"
   creationComplete="init();" title="Kirpad" showStatusBar="{pref_status}">
   
<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.events.Event;
import flash.events.NativeWindowBoundsEvent;
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;
import mx.events.ResizeEvent;

private var preferences:SharedObject = SharedObject.getLocal("kirpadPreferences");
[Bindable]
private var pref_wrap:Boolean = true;
[Bindable]
private var pref_status:Boolean = true;
[Bindable]
private var pref_toolbar:Boolean = true;
[Bindable]
public var pref_fontsettings:Object = new Object();

private var initHeight:Number;
private var heightFixed:Boolean = false;

private var statusMessage:String;
[Bindable]
private var textHeight:Number;
[Bindable]
private var textY:Number;
[Bindable]
private var tabY:Number;

private var previousIndex:Number = 0;

public var fontWindow:FontWindow = new FontWindow();

private function init():void {

// Create a listener for every frame
addEventListener(Event.ENTER_FRAME, everyFrame);

// Set initHeight to the initial height value on start
initHeight = height;

// Set preferences if loaded for the first time
if (preferences.data.firsttime == null) {
preferences.data.firsttime = true;
preferences.data.wrap = true;
preferences.data.status = true;
preferences.data.toolbar = true;
preferences.data.fontsettings = {fontfamily:"Lucida Console", fontsize:14, fontstyle:"normal", fontweight:"normal", fontcolor:0x000000, bgcolor:0xffffff};
preferences.flush();
}

// Set preferences loaded from local storage
pref_wrap = preferences.data.wrap;
pref_status = preferences.data.status;
pref_toolbar = preferences.data.toolbar;
pref_fontsettings = preferences.data.fontsettings;

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

// Set status message
statusMessage = "[ " + new Date().toLocaleTimeString() + " ] Kirpad initialized";
updateStatus();

// Close all sub-windows if main window is closed
addEventListener(Event.CLOSING, onClose);

// Add listener for the event that is dispatched when new font settings are applied
fontWindow.addEventListener(Event.CHANGE, fontChange);

// Update real fonts with the data from the settings values
updateFonts();

// Create a listener for resizing
addEventListener(NativeWindowBoundsEvent.RESIZE, onResize);
}

private function menuSelect(evt:FlexNativeMenuEvent):void {
(evt.item.@label == "New")?(doNew()):(void);
(evt.item.@label == "Word wrap")?(pref_wrap = !pref_wrap):(void);
(evt.item.@label == "Cut")?(doCut()):(void);
(evt.item.@label == "Copy")?(doCopy()):(void);
(evt.item.@label == "Paste")?(doPaste()):(void);
(evt.item.@label == "Select all")?(doSelectall()):(void);
(evt.item.@label == "Status bar")?(pref_status = !pref_status):(void);
(evt.item.@label == "Tool bar")?(pref_toolbar = !pref_toolbar):(void);
(evt.item.@label == "Font...")?(doFont()):(void);
savePreferences();
updateStatus();
}

private function savePreferences():void {
preferences.data.wrap = pref_wrap;
preferences.data.status = pref_status;
preferences.data.toolbar = pref_toolbar;
preferences.data.fontsettings = pref_fontsettings;
preferences.flush();
}

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

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 doSelectall():void {
textArea.selectAll();
}

private function insertText(str:String):void {
var substrPositions:int = textArea.selectionActivePosition - textArea.selectionAnchorPosition;
var oldSel1:int = (substrPositions>0)?(textArea.selectionAnchorPosition):(textArea.selectionActivePosition);
var oldSel2:int = (substrPositions<0)?(textArea.selectionAnchorPosition):(textArea.selectionActivePosition);
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";
}

private function everyFrame(evt:Event):void {
if (!heightFixed && height==initHeight) {
height = initHeight - 20;
if (height != initHeight) {
heightFixed = true;
updateTextSize();
}
}
}

private function onResize(evt:ResizeEvent):void {
updateTextSize();
}

private function updateTextSize():void {
tabY = (toolBar.visible)?(toolBar.height):(0);
textY = tabBar.height + tabY;
var statusHeight:Number = (pref_status)?(statusBar.height):(0);
textHeight = height - textY - statusHeight;
focusManager.setFocus(textArea);
}

private function updateStatus():void {
var str:String = new String();
str = (pref_wrap)?("Word wrapping on"):(caretPosition());
status = str + "\t" + statusMessage;
}

private function caretPosition():String {
var pos:int = textArea.selectionActivePosition;
var str:String = textArea.text.substring(0, pos);
var lines:Array = str.split("\n");
var line:int = lines.length;
var col:int = lines[lines.length - 1].length + 1;

return "Ln " + line + ", Col " + col;
}

private function doFont():void{
fontWindow.open();
fontWindow.activate();
fontWindow.visible = true;
fontWindow.setValues(pref_fontsettings.fontsize, pref_fontsettings.fontfamily, pref_fontsettings.fontstyle, pref_fontsettings.fontweight, pref_fontsettings.fontcolor, pref_fontsettings.bgcolor);
}

private function onClose(evt:Event):void{
var allWindows:Array = NativeApplication.nativeApplication.openedWindows;
for (var i:int = 0; i < allWindows.length; i++)
{
allWindows[i].close();
}
}

private function fontChange(evt:Event):void{
pref_fontsettings.fontfamily = fontWindow.fontCombo.selectedItem.fontName;
pref_fontsettings.fontsize = fontWindow.sizeStepper.value;

if (fontWindow.styleCombo.selectedIndex == 0) {
pref_fontsettings.fontstyle = "normal";
pref_fontsettings.fontweight = "normal";
}
if (fontWindow.styleCombo.selectedIndex == 1) {
pref_fontsettings.fontstyle = "italic";
pref_fontsettings.fontweight = "normal";
}
if (fontWindow.styleCombo.selectedIndex == 2) {
pref_fontsettings.fontstyle = "normal";
pref_fontsettings.fontweight = "bold";
}
if (fontWindow.styleCombo.selectedIndex == 3) {
pref_fontsettings.fontstyle = "italic";
pref_fontsettings.fontweight = "bold";
}

pref_fontsettings.fontcolor = fontWindow.colorPicker.selectedColor;
pref_fontsettings.bgcolor = fontWindow.bgColorPicker.selectedColor;

savePreferences();
updateFonts();
}

private function updateFonts():void{
textArea.setStyle("fontFamily", pref_fontsettings.fontfamily);
textArea.setStyle("fontSize", pref_fontsettings.fontsize);
textArea.setStyle("fontStyle", pref_fontsettings.fontstyle);
textArea.setStyle("fontWeight", pref_fontsettings.fontweight);
textArea.setStyle("color", pref_fontsettings.fontcolor);
textArea.setStyle("contentBackgroundColor", pref_fontsettings.bgcolor);
}

private function onTabClose(evt:Event):void{
Alert.show("Tab being closed: " + evt.target.data.label);
}

private function doNew():void{
tabData.addItem( { title:"Untitled", textData:"", saved:false } );
tabBar.selectedIndex = tabData.length - 1;
tabChange();
}

private function tabChange():void {
tabData[previousIndex].textData = textArea.text;
tabData[previousIndex].selectedActive = textArea.selectionActivePosition;
tabData[previousIndex].selectedAnchor = textArea.selectionAnchorPosition;
previousIndex = tabBar.selectedIndex;
textArea.text = tabData[tabBar.selectedIndex].textData;
textArea.selectRange(tabData[tabBar.selectedIndex].selectedAnchor, tabData[tabBar.selectedIndex].selectedActive);
}
]]>
</fx:Script>

<fx:Declarations>
<fx:XML id="windowMenu">
<root>
<menuitem label="File">
<menuitem label="New" key="n" controlKey="true" />
<menuitem label="Open" key="o" controlKey="true" />
</menuitem>
<menuitem label="Edit">
<menuitem label="Cut" key="x" controlKey="true" />
<menuitem label="Copy" key="c" controlKey="true" />
<menuitem label="Paste" key="v" controlKey="true" />
<menuitem type="separator"/>
<menuitem label="Select all" key="a" controlKey="true" />
</menuitem>
<menuitem label="Settings">
<menuitem label="Word wrap" type="check" toggled="{pref_wrap}" />
<menuitem label="Font..."/>
</menuitem>
<menuitem label="View">
<menuitem label="Tool bar" type="check" toggled="{pref_toolbar}" />
<menuitem label="Status bar" type="check" toggled="{pref_status}" />
</menuitem>
</root>
</fx:XML>
<mx:ArrayCollection id="tabData">
<fx:Object title="Untitled" textData="" saved="false" seletedActive="0" selectedAnchor="0" />
</mx:ArrayCollection>
</fx:Declarations>

<s:Group width="100%" height="100%">
<s:TextArea id="textArea" width="100%" height="{textHeight}" y="{textY}" borderVisible="false" lineBreak="{(pref_wrap)?('toFit'):('explicit')}"  click="cursorFix(); updateStatus();" change="updateStatus();" keyDown="updateStatus();"/>
<custom:CustomTabBar id="tabBar" dataProvider="{tabData}" width="100%" y="{tabY}" itemRenderer="CustomTab" height="22" tabClose="onTabClose(event);" change="tabChange();" />
<mx:Box id="toolBar" width="100%" backgroundColor="#dddddd" height="24" visible="{pref_toolbar}">
</mx:Box>
</s:Group>

</s:WindowedApplication>

Thanks for reading!

No comments:

Post a Comment