Sunday, October 23, 2011

Creating a Flex AIR text editor: Part 13

In this tutorial we will add close buttons to our tabs in the tabbar, dispatch and catch events when the user closes a tab and determine which tab exactly was closed by reading its unique personal data from the ArrayCollection.

In this tutorial we're going to create a few custom components. In the root tags, add a new namespace, I called it custom.

xmlns:custom="*"

Change the existing TabBar line to this:

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

As you can see, it is now a CustomTabBar with a custom namespace. It has a custom itemRenderer which is a CustomTab class. Also, we listen to the tabClose event (which is custom) and call an onTabClose(event); method.

Let's edit the existing ArrayCollection to add some custom random data that we can read, just to make sure the application works.

<mx:ArrayCollection id="tabHeadings">
<fx:Object label="Tab one" somedata="1" />
<fx:Object label="Tab two" somedata="2"/>
<fx:Object label="Tab three" somedata="3"/>
<fx:Object label="Tab four" somedata="4"/>
</mx:ArrayCollection>

Add a new onTabClose function, make it Alert a message like this:

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

Now create a new CustomTabBar.mxml file, which will be our custom tabbar component. The root tags will be Spark TabBar. Here we will also use Metadata tags to declare an event called tabClose.

<?xml version="1.0" encoding="utf-8"?>
<s:TabBar xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx">
    <fx:Metadata>
        [Event(name="tabClose")]
    </fx:Metadata>
</s:TabBar>

We have set the itemRenderer property to a CustomTab class, so create a new CustomTab.mxml.

Here's the full code for that class:

<?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.label
            }
        }              

        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>

As you can see, we declare the tabClose event in the Metadata tags too.

The labelClose_clickHandler function dispatches this event, that we listen to from the main file. The rest of the code is mainly drawing the tabs. The base for this class code was taken from the Adobe website.

And you're done!

Here's the main file 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"
   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.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;
[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;

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;

preferences.data.firsttime = null;

// 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();
}

private function menuSelect(evt:FlexNativeMenuEvent):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;
}
}
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.somedata);
}
]]>
</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="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="tabHeadings">
<fx:Object label="Tab one" somedata="1" />
<fx:Object label="Tab two" somedata="2"/>
<fx:Object label="Tab three" somedata="3"/>
<fx:Object label="Tab four" somedata="4"/>
</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="{tabHeadings}" width="100%" y="{tabY}" itemRenderer="CustomTab" height="22" tabClose="onTabClose(event);" />
<mx:Box id="toolBar" width="100%" backgroundColor="#dddddd" height="24" visible="{pref_toolbar}">
</mx:Box>
</s:Group>

</s:WindowedApplication>

Now when you try to close a tab, it alerts you the value of the somedata property for this tab object.

Thanks for reading!

No comments:

Post a Comment