Monday, March 9, 2015
How It’s Made Election Info App Part 1
In an earlier blog post, we announced the Election Info sample app. We briefly talked about how we were able to use Apps Script to easily create a comprehensive sample application that provided timely voting information. This post shows just how easy it is to use Apps Script to get information from an external API and integrate with various Google services to create a rich web application and provide a meaningful user experience.
UrlFetch
First, we use UrlFetchApp to get JSON from the Google Civic Information API and use Utilities.jsonParse to convert it to a useful javascript object.
var url =
https://www.googleapis.com/civicinfo/us_v1/voterinfo/2000/lookup;
var address = { "address" : "1263 Pacific Ave. Kansas City KS" };
var options =
{
method : "post",
contentType : "application/json",
payload: Utilities.jsonStringify(address)
};
var responseText = UrlFetchApp.fetch(url, options).getContentText();
var response = Utilities.jsonParse(responseText);
After getting the response object, we can simply drill into it to access various data provided by the API.
Calendar Service
One of the things the response object provides us with is the election date. Using Apps Scripts Calendar service, it is really easy to create an event on voting day in the users calendar with the polling address. First, we create a Date object from the date string. We then create an all-day event on the default calendar on this date, passing along the polling address we get from the response object.
// create a Date object from the response date string
// ("2012-11-6" --> Date object)
var [year, month, day] = response.election.electionDay.split(-);
// javascript months are zero-indexed
var electionDate = new Date(year, month-1, day);
// get the first polling locations address
var pollAddress = response.pollingLocations[0].address;
var cal = CalendarApp.getDefaultCalendar();
cal.createAllDayEvent("Go Vote!", electionDate, {location:pollAddress});
Maps Service
Using the Maps service, we can generate static maps with the users home or polling address as shown in the following code snippet. We display these maps on the web app page, then embed them in the reminder email and bring-along document as we will show in the following sections.
var userAddress = response.normalizedInput;
var normalizedAddress = userAddress.line1 + +
userAddress.city + , +
userAddress.state + +
userAddress.zip;
// normalizedAddress looks like "501 Kildaire Rd Chapel Hill, NC 27516"
var staticMapUrl = Maps.newStaticMap().setSize(600, 300)
.addMarker(normalizedAddress)
.getMapUrl();
Gmail Service
We also provide a simple method for users to email themselves all of this information. Using the Gmail service, we can send an HTML email that embeds the voting information and the static maps we generated above. The Apps Script documentation contains great tutorials such as the Maps tutorial we used to generate the directions below.
var email = Session.getActiveUser().getEmail();
var body = Election Date: + electionDate + <br/>
+ Your polling address: + pollAddress + <br/>
+ Polling Hours: + pollingHours + <br/>
+ <img src=" + directions.mapUrl + "/> <br/>
+ Directions: + dirList;
MailApp.sendEmail(email, Upcoming Election Voting Information,
Voting Info, {htmlBody: body});
Document Service
Using the Document service, we were able to easily generate a bring-along document with polling address, hours, and directions. The follow code excerpt shows how easy it is to add different elements like headers, tables, and paragraphs to a document. Apps Script also provides an extensive list of methods to programmatically control the look and presentation of the various elements.
var title = "Voting Information";
var doc = DocumentApp.create(title + " for " + homeAddress);
var reportTitle = doc.appendParagraph(title);
reportTitle.setFontFamily(DocumentApp.FontFamily.ARIAL)
.setFontSize(22).setForegroundColor(#4A86E8)
.setBold(true)
.setAlignment(DocumentApp.HorizontalAlignment.CENTER);
var header = doc.addHeader();
header.appendParagraph(Generated by the Election Info application +
built on Google Apps Script)
.setAlignment(DocumentApp.HorizontalAlignment.CENTER)
.setAttributes({ITALIC : true});
var tableStyle = {};
tableStyle[DocumentApp.Attribute.PADDING_BOTTOM] = 0;
tableStyle[DocumentApp.Attribute.PADDING_TOP] = 0;
tableStyle[DocumentApp.Attribute.PADDING_LEFT] = 0;
tableStyle[DocumentApp.Attribute.PADDING_RIGHT] = 0;
var addressTable = doc.appendTable([
[Your address: + homeAddress],
[Your Polling Location: + pollAddress],
[]
]).setAttributes(tableStyle);
// add appropriately sized poll location image
addressTable.getCell(1,0).appendImage(pollImg.getBlob())
.setHeight(300).setWidth(600);
// populate last row of the table with polling hours
addressTable.getCell(2,0).clear().appendParagraph("Polling Hours: ");
addressTable.getCell(2,0).appendParagraph(
UserProperties.getProperty(Keys.POLLING_HOURS));
Here is an image which shows the generated bring-along document embedded with static map images from the Maps service.
Apps Script allowed us to easily take information from an external API and tie it into various Google services to provide a great user experience. Stay tuned for an upcoming blog post showing how we created the front end!
![]() | Kalyan Reddy profile | Stack Overflow Kalyan is a Developer Programs Engineer on the Google Apps Script team based in NYC. He is committed to increasing developer productivity by helping them fully utilize the power of Apps Script. In his free time, he enjoys participating in the Maker community and hacking together robots. |
Sunday, March 1, 2015
Teaching Habits That Inspire You Out of Learning Part 1
- Link to ZaidLearns Del.icio.us Teaching Links

"At the University of California at Berkeley, the Distinguished Teaching Award was instituted in 1959 to recognize and reward excellence in teaching. Since the inception of the award, over 150 faculty in forty-eight departments have been honored...although these essays (by the award winners) were prepared independently over a number of years, there are striking similarities about what good teachers say about teaching. On at least ten propositions, the contributors are in near or total agreement (Source):
- The teachers main task is to guide students through the learning process, not to dispense information.
- The goal of teaching is to help students read, speak, write, and think critically—and to expect students to do these things.
- Learning is a "messy" process, and the search for truth and knowledge is open-ended.
- Good teachers love their subject matter.
- Good research and good teaching go hand in hand. Students engagement with the subject is enhanced by knowing about the teachers own research, and the interaction with students often provides new insights into the research.
- The best teachers genuinely respect students and their intellectual capabilities.
- Good teachers are rarely satisfied with their teaching. They constantly evaluate and modify what they do.
- Good teachers usually had good teachers, and they see themselves as passing on their own teachers gifts to a new generation of students.
- Good teachers treasure the small moments of discovery in the classroom and the more enduring effect they have on students lives.
- Good teachers do not see teaching as separate from other activities; rather, they see their lives as remarkably integrated."

- Part 1 - Whiteboard And I Are One!
- Part 2 - I Have Bragging Rights, Because I Am …
- Part 3 - Is PowerPoint Evil?
- Part 4 - No Stupid Questions! I am Serious!
- Part 5 - Show Up to Throw Up! 21st Century Thinking?
Saturday, February 28, 2015
My First Keynote Part 1
- SlideShare Version
- SlideBoom Version
- PDF Version (77 MB)
- IMETC Event
FIRST KEYNOTE
"Rehearse and use a script, please!" Good idea, but I want it to have a natural flow (A learning conversation with the audience and myself), so no script, except for the slides (Though, if I was a President I would use one!). I hope my ideas and reflections will be free flowing guided by the spicy slides. It will be an interesting experiment, which will be revealed in Part 2. Also, Part 2 (next post) will discuss and reflect all the things I would have learned during this 3-day learning adventure. No, I am not yet ready for any live UStream show!
PRESENTATION SLIDES
In the keynote presentation, I will first discuss a bit about how my first ebook entitled 69 Learning Adventures in 6 Galaxies came to life. Then, I will explore some learning or thinking skills we need to nurture in students and ourselves to be successful in todays fast evolving innovation world. I will also look at some of the educational challenges we might face, and explore a case study on how I revamped a critical thinking course. Finally, I will look at some possibilities and conclude the learning overload session. In short, we are all probably going to get dizzy (learning overload!) one way or the other. Anyway, here are the slides to enjoy before the actual event:
I suppose after all my past workshops and 20-minute presentations (taken a 2-year break from that!), I kind of look forward to this great opportunity to talk for one hour (or less) to a bunch of people about the things I have learned over the last few years. And for that, I am truly grateful to God (Allah) and would like to also thank the organizers for giving me this opportunity. Thank you so much!
Until part 2, have fun trying to make sense of my slides. If they dont make sense, perhaps you could invite me to give a talk. Though, I would prefer if I was given 2 hours. Meaning, we will have approximately one hour to discuss, reflect and learn together. Now, that is where the real learning will take place :)
10 Secrets to Great Teaching Part 1
- TeacherTube version
- YouTube
- Google Video
PART 1
In part 1, I explore 4 (of 10) secrets to great teaching, or ingredients to become a great teacher. What are the ingredients of a great teacher? Well, when I complete the 2-part series you will at least know my standpoint on this issue. My findings is basically based on what I have observed, read, experienced and learned over the last few years in Higher Education.
Yes, you are certainly free to agree or disagree with all my ideas and thoughts about great teaching. What really matters, is not being right or wrong, but taking this wonderful opportunity to be part of a messy global conversation (in audio format) about this issue.
Here, enjoy part 1 (not sure about that!), and hopefully part 2 will be out sometime next week:
ZAIDLEARN EXPLORES AUDIO & VIDEO
This is probably the first time most of you hear me speak. I have to admit that I speak much faster normally (I tried hard to slow down, and probably sounded a bit too slow this time around.). Also, I did not use any script, as I wanted it to be as natural as possible.
I used PowerPoint to create the slides, and interestingly (exploring!) used Adobe Presenter to record the audio (Recommendation: For audio recording, use Audacity, which is easy-to-use and has more features). Finally, I used Windows Movie Maker (first time using!) to integrate the PNG converted PowerPoint slides, and the audio (MP3) files from the published Adobe Presenter presentation. Although, Windows Movie Maker is quite limited in terms of features, I actually enjoyed using it.
To explore uploading possibilities, I uploaded my 10-minute presentation to YouTube, TeacherTube and Google Video. The Google Video output is not good, but the YouTube and TeacherTube outputs are alright. I suppose I will explore slidecasting on SlideShare later, after I have completed part 2.
SELF-EVALUATION
My voice is a disaster and the PowerPoint slides need a face lift! Yes, perhaps I should include more music (to spice it up!), pictures, and flower it with a bit more animations. A video showing me babbling would be interesting, too (not really!).
Though, I really enjoyed exploring my voice, and hopefully I will create many more videos (learning nuggets of 10 minutes or less) sharing my ideas, thoughts and reflections about learning in the near future. Hopefully, someone out there will be listening and joining the conversation.
I suppose it is about time that ZaidLearn speaks out, besides rambling this and that with written words only.
I AM STILL LEARNING, and hopefully I will evolve. In the meantime, please be patient as I learn how to talk (on the Tube) :)
Friday, February 27, 2015
TEMS13 Gamified Scoring Algorithm to Decide the e Learning Super Hero of 2013! Part 2


SPONSORS
- International Medical University
- iBerry - The Academic Porthole
- WizIQ
- Listly

AWARDS
Top e-Learning Mover & Shaker of the Year (2013)
- 1st Prize = £100 (British Pounds)
- Top 3 will get free (commercial) Listly accounts for a year.
- The nominee with the most votes, wins this special award with bragging rights for one year.
- 1st Prize = £200 (British Pounds)
- Top 3 will get free (commercial) Listly accounts for a year.
- The nominee with the highest score (based on the gamified points system revealed below) wins this award, which is the most prestigious of the two awards, because it takes into account who is voting, besides the number of votes. Also, it requires the nominee to show pro-active professionalism and appreciation of the other participating nominees.

Besides that, we will announce the Top 10 for both awards based on the big Continents in the world (North-America, Europe, Asia, Africa and South-America).
Please join the fun and vote!
- CLICK HERE to know the reasons for creating this poll (Part 1), and instructions on how to vote (video tutorial).
- CLICK HERE to subscribe to the Twitter List following all the nominees (using Twitter).
- CLICK HERE to view and update the #TEMS13 Padlet Wall, which is where share something positive about the nominees.
ONLINE AWARD CEREMONY
REALITY
Lets face it, polling is really a silly way to decide on who was the Super (e-)Learning Hero of 2013. For example, in a professional learning community, would a vote (of appreciation for someones work) from your Grandmother, or 5-year old son be valued with the same respect as a vote from Stephen Downes? Of course not!GAMIFIED SCORING
Well, I still believe that polling can be used to decide the Super (e-)Learning Hero of 2013 in a fairer way. So, here is a gamified scoring algorithm I put together to filter out the wannabes from the real Super (e-)Learning Heroes. What do you think? You are most welcome to suggest improvements, or other scoring items to be included. Please use the comments section below, or tweet your idea using #TEMS13 Twitter hashtag. Thanks!Here we go:


NETWORK
Here it is:
Have fun discovering some pretty amazing educators in the #TEMS13 list!
- British Pounds
- Bragging rights for one year!
- Surely their networks will grow.
- Admiration from new fans.
- More invitations to give talks and/or facilitate workshops.
- Etc.
Wednesday, February 18, 2015
Arrays in C Part 3
So far I told you about some basic topics of arrays in C like initialization of arrays, accepting elements in array, printing the values from an array and so on. Today I will tell you about one advance use of arrays like passing the values of arrays to a function. It can be done in two ways, call by value and call by reference.
Passing Array Elements to a Function
Call by Value
Lets straight away starts with one program.#include<stdio.h>
void printarr(int);
int main()
{
int x;
int nums[]={43,54,64,56,65};
for(x=0;x<5;x++)
printarr(nums[x]);
return 0;
}
void printarr(int n)
{
printf ("
%d",n) ;
}
Output

- I have declared an integer array nums and inserted 10 elements into it.
- After that I have started one for loop. Inside the loop I have written one statement i.e. printarr(nums[x]). It will call the function printarr().
- At first the value of x is 0. Then the first element of nums array will be passed to the printarr() function.
- That value will be received in the formal argument n of the function. After that it will display the element on the screen with printf() function.
- Now the control again moves to the main() function. The printarr() function is again called and second element of array nums is passed to it. This process will continue until the loop stops executing. And at last the program will stop.
Call by Reference
Lets understand it with one program.#include<stdio.h>
void printarr(int*);
int main()
{
int x;
int nums[]={43,54,64,56,65};
for(x=0;x<5;x++)
printarr(&nums[x]);
return 0;
}
void printarr(int *n)
{
printf("
%d",*n) ;
}
Explanation
- The code of the program is almost similar to the previous one. But in this case we are passing the address of array elements to the printarr() function.
- Note that I have passed the address of array elements with the help of address of operator i.e. &.
- The address of the array element is received by an integer pointer variable n.
- And in last I have printed the value by using printf() function. The output will be same as previous program.
Which one is better?
As you can see both the approaches are giving the same results. However I need to do little modifications in each program. Remember that the second program is better than the first one. Because it is using the pointer. Usage of pointer decreases the execution time of program. This is the reason people use to prefer pointers while using arrays in C.
Tuesday, February 10, 2015
Terrain Correction of SAR Images Part 4
The Range Doppler Algorithm does not simulate a SAR image to coregister this and the original SAR image, but calculates displacement based on orbit parameters and a DEM. The Range Doppler algorithm is much faster in processing scenes. When comparing scenes with both SARSIM and Range Doppler methods, I find no difference in the final product. However, the Range Doppler method does not work for quite a few of my scenes. If I understood ESA correctly, this is due to not accurate enough data in the SAR metadata, such that calculations of the displacement is incorrect. This appears to be special with data from the Arctic regions
I havent therefore used this one that much, but in other areas of the world it may be worth using the Range Doppler Terrain Correction.
Choose Geometry>Terrain Correction>Range Doppler Terrain Correction (in Graph Builder choose "Terrain Correction"). The settings are as follows

Thursday, February 5, 2015
Creating EasyTooltip class Part 4
Before we move on to adding new code, lets go to TooltipListener class and set tip and list variables public instead of private:
package com.kircode.EasyTooltip
{
import flash.display.DisplayObject;
import flash.events.MouseEvent;
/**
* Manages a single visible object that displays a tooltip when rolled over.
* @author Kirill Poletaev
*/
public class TooltipListener
{
public var tip:Tooltip;
public var list:DisplayObject;
public function TooltipListener(listener:DisplayObject, tooltip:Tooltip)
{
list = listener;
tip = tooltip;
listener.addEventListener(MouseEvent.ROLL_OVER, onOver);
listener.addEventListener(MouseEvent.ROLL_OUT, onOut);
}
public function kill():void {
list.removeEventListener(MouseEvent.ROLL_OVER, onOver);
list.removeEventListener(MouseEvent.ROLL_OUT, onOut);
}
private function onOver(evt:MouseEvent):void {
tip.display = true;
}
private function onOut(evt:MouseEvent):void {
tip.display = false;
}
}
}
Now lets create a new class called TooltipCursor. I want to make this the only object thats displayed in Tooltip - the content of it changes depending on what tooltip is currently displayed, but the object itself is the same.
Create TooltipCursor.as class, which extends MvoieClip. Add "txt" variable, which is a TextField, add it to the display list:
package com.kircode.EasyTooltip
{
import flash.display.MovieClip;
import flash.text.TextField;
/**
* The cursor that follows the mouse (if not set otherwise) and displays each tooltip.
* @author Kirill Poletaev
*/
public class TooltipCursor extends MovieClip
{
public var txt:TextField;
public function TooltipCursor()
{
txt = new TextField();
addChild(txt);
txt.y = - 20;
}
}
}
We can now go to EasyTooltip.as and declare an instance of this new class:
private var cursor:TooltipCursor;
In the constructor, set its mouseChildren and mouseEnabled properties to false, add it to the parents display list and also add an ENTER_FRAME event listener for the cursor.
cursor = new TooltipCursor();
cursor.mouseChildren = false;
cursor.mouseEnabled = false;
parent.addChild(cursor);
cursor.addEventListener(Event.ENTER_FRAME, onFrame);
In the event handler, we loop through all listeners to see which ones "display" property is true. If none is displayed, set cursors alpha to 0. If one of the listeners display value is true - set the cursors alpha to 1, and update its text fields text value and width:
private function onFrame(evt:Event):void {
var displayInd:int = -1;
for (var i:int = 0; i < listeners.length; i++) {
if (listeners[i].tip.display) {
displayInd = i;
break;
}
}
if (displayInd == -1) {
cursor.alpha = 0;
}else {
cursor.alpha = 1;
cursor.txt.text = listeners[displayInd].tip.msg;
cursor.txt.width = cursor.txt.textWidth + 10;
}
cursor.x = par.mouseX;
cursor.y = par.mouseY;
}
Full EasyTooltip class so far:
package com.kircode.EasyTooltip
{
import flash.display.DisplayObjectContainer;
import flash.display.DisplayObject;
import flash.events.Event;
/**
* Utility for creation of tooltips.
* @author Kirill Poletaev
*/
public class EasyTooltip
{
private var par:DisplayObject;
public var listeners:Array;
private var cursor:TooltipCursor;
/**
* Create a Tooltip manager. Needed for creating and managing tooltips.
* @paramparent Reference to the parent of tooltips - the container, which will contain the objects that will be rolled over.
*/
public function EasyTooltip(parent:DisplayObjectContainer)
{
par = parent;
listeners = [];
cursor = new TooltipCursor();
cursor.mouseChildren = false;
cursor.mouseEnabled = false;
parent.addChild(cursor);
cursor.addEventListener(Event.ENTER_FRAME, onFrame);
trace("Tooltip manager created!");
}
/**
* Add a Tooltip listener.
* @paramlistener Object, which invokes the tooltip on roll over.
* @paramtooltip Message to be displayed.
* @returnNewly created Tooltip object. Can be used to dynamically change its properties in real-time.
*/
public function addListener(listener:DisplayObject, tooltip:String):Tooltip {
var tip:Tooltip = new Tooltip(tooltip);
var list:TooltipListener = new TooltipListener(listener, tip);
listeners.push(list);
return tip;
}
private function onFrame(evt:Event):void {
var displayInd:int = -1;
for (var i:int = 0; i < listeners.length; i++) {
if (listeners[i].tip.display) {
displayInd = i;
break;
}
}
if (displayInd == -1) {
cursor.alpha = 0;
}else {
cursor.alpha = 1;
cursor.txt.text = listeners[displayInd].tip.msg;
cursor.txt.width = cursor.txt.textWidth + 10;
}
cursor.x = par.mouseX;
cursor.y = par.mouseY;
}
}
}
Now when you roll over a listener, you can see a very simple tooltip appearing, and disappearing as you roll out.
Thanks for reading!
Creating Advanced Alert Window class Part 34
Firstly, go to AdvAlertManager and declare 3 new variables for holding default width, height and minheigh values:
private var defWidth:int;
private var defHeight:int;
private var defMinHeight:int;
Now go to the constructor and add 3 new parameters - defaultWidth, defaultHeight and defaultMinHeight.
Inside the constructor, apply these values to defWidth, defHeight and defMinHeight.
Also, remove the if..statement that checks if tabDisable is true before adding keyboard listener.
/**
* AdvAlertManager constructor.
* @paramdefaultWindowContainer Parent container of alert windows.
* @paramstage Stage reference.
* @paramwindowSkin Default skin for alert windows.
* @parambuttonSkin Default skin for buttons in this window.
* @paramopenSound Default window open sound.
* @paramdisableTab Disable tab focusing on the parent container when an alert window is visible. This also enables tab navigation for the alert windows buttons.
* @paramdefaultWidth Set default width for all window alerts. 300 by default.
* @paramdefaultHeight Set default height for all window alerts. 0 by default. If 0, the window is auto-sized based on the text length.
* @paramdefaultMinHeight Set default minimal height for all window alerts. 0 by default.
*/
public function AdvAlertManager(defaultWindowContainer:DisplayObjectContainer, stage:Stage, windowSkin:AdvAlertSkin = null, buttonSkin:AdvAlertButtonSkin = null, openSound:Sound = null, disableTab:Boolean = true, defaultWidth:int = 300, defaultHeight:int = 0, defaultMinHeight:int = 0)
{
defWidth = defaultWidth;
defHeight = defaultHeight;
defMinHeight = defaultMinHeight;
openingSound = openSound;
skin = windowSkin;
bSkin = buttonSkin;
tabDisable = disableTab;
if (skin == null) skin = new AdvAlertSkin();
if (bSkin == null) bSkin = new AdvAlertButtonSkin();
defaultContainer = defaultWindowContainer;
pWidth = stage.stageWidth;
pHeight = stage.stageHeight;
stg = stage;
windows = [];
stg.addEventListener(KeyboardEvent.KEY_DOWN, kDown);
}
The if...statement was taken away because well add an option to change the value of tabDisable variable. Instead of deciding whether to add a keyboard listener based on the value, well always add the keyboard event, but check if tabDisable is true inside the kDown function before executing the code:
private function kDown(evt:KeyboardEvent):void {
if(tabDisable){
if (evt.keyCode == 9 && topWindow!=null && topWindow.buttons.length>0) {
focusedButton++;
if (focusedButton == topWindow.buttons.length) {
focusedButton = 0;
}
for (var i:int = 0; i < topWindow.buttons.length; i++) {
topWindow.buttons[i].over = false;
topWindow.buttons[i].updateDraw();
}
topWindow.buttons[focusedButton].over = true;
topWindow.buttons[focusedButton].updateDraw();
}
if (evt.keyCode == 13 && topWindow != null && topWindow.buttons.length > 0) {
if (focusedButton == -1) {
focusedButton = 0;
}
var currentIndex:int = windows.length - 1;
var closeHandler:Function = windows[currentIndex].onclose;
closeWindow(currentIndex, closeHandler);
}
}
}
Now go to alert() function and add 3 new lines that check if width, height or minHeight values are 0, and if so - apply default values to them:
if (width == 0) width = defWidth;
if (height == 0) height = defHeight;
if (minHeight == 0) minHeight = defMinHeight;
Full alert() function:
/**
* Create an alert window.
* @paramtext Text value of the alert window.
* @paramtitle Title value of the alert window.
* @parambuttons (Optional) Array of AdvAlertButton objects that represent buttons in the alert window.
* @paramcloseHandler (Optional) Close handling function. Must receive a string value as parameter (which will hold the label of the button that was clicked).
* @paramwidth (Optional) Width of the alert window. If 0, set to default.
* @paramheight (Optional) Height of the alert window. If 0, set to default. If default is 0, window will be auto sized.
* @paramposition (Optional) Coordinates of top-left corner of the alert window. If not specified - the window is centered.
* @paramminHeight (Optional) The minimum height value of the alert window. 0 by default.
*/
public function alert(text:String, title:String = "", buttons:Array = null, closeHandler:Function = null, width:int = 0, height:int = 0, position:Point = null, minHeight:int = 0, openSound:Sound = null):AdvAlertWindow {
if (width == 0) width = defWidth;
if (height == 0) height = defHeight;
if (minHeight == 0) minHeight = defMinHeight;
if (openSound != null) {
openSound.play();
}else
if (openingSound != null) {
openingSound.play();
}
if (position == null) position = new Point((pWidth / 2) - (width / 2), (pHeight / 2) - (height / 2));
if (buttons == null) buttons = [new AdvAlertButton("OK")];
for (var i:int = buttons.length - 1; i >= 0; i--) {
if (!buttons[i] is AdvAlertButton) {
throw new Error("An item in buttons array is not an AdvAlertButton instance. Ignoring...");
buttons.splice(i, 1);
}else {
buttons[i].addEventListener(MouseEvent.CLICK, buttonHandler);
}
}
var w:AdvAlertWindow = new AdvAlertWindow(text, title, width, height, position, skin, buttons, bSkin, pWidth, pHeight, minHeight);
var b:AdvAlertBlur = new AdvAlertBlur(pWidth, pHeight, skin);
var currentIndex:int = windows.length;
windows.push( {window:w, blur:b, onclose:closeHandler} );
defaultContainer.addChild(b);
defaultContainer.addChild(w);
topWindow = w;
focusedButton = -1;
if (tabDisable) {
defaultContainer.tabChildren = false;
defaultContainer.tabEnabled = false;
}
function buttonHandler(evt:MouseEvent):void {
closeWindow(currentIndex, closeHandler);
}
return w;
}
Now create a new public function called reset(), with the same parameters as the constructor. The contents are the same as in the constructor too, with the exception of 2 lines - exclude the ones that set windows array to a new array and the one that adds keyboard listener.
/**
* Set new options for the alert manager object - has the same parameters as the constructor.
* @paramdefaultWindowContainer Parent container of alert windows.
* @paramstage Stage reference.
* @paramwindowSkin Default skin for alert windows.
* @parambuttonSkin Default skin for buttons in this window.
* @paramopenSound Default window open sound.
* @paramdisableTab Disable tab focusing on the parent container when an alert window is visible. This also enables tab navigation for the alert windows buttons.
* @paramdefaultWidth Set default width for all window alerts. 300 by default.
* @paramdefaultHeight Set default height for all window alerts. 0 by default. If 0, the window is auto-sized based on the text length.
* @paramdefaultMinHeight Set default minimal height for all window alerts. 0 by default.
*/
public function reset(defaultWindowContainer:DisplayObjectContainer, stage:Stage, windowSkin:AdvAlertSkin = null, buttonSkin:AdvAlertButtonSkin = null, openSound:Sound = null, disableTab:Boolean = true, defaultWidth:int = 300, defaultHeight:int = 0, defaultMinHeight:int = 0)
{
defWidth = defaultWidth;
defHeight = defaultHeight;
defMinHeight = defaultMinHeight;
openingSound = openSound;
skin = windowSkin;
bSkin = buttonSkin;
tabDisable = disableTab;
if (skin == null) skin = new AdvAlertSkin();
if (bSkin == null) bSkin = new AdvAlertButtonSkin();
defaultContainer = defaultWindowContainer;
pWidth = stage.stageWidth;
pHeight = stage.stageHeight;
stg = stage;
}
Full AdvAlertManager class:
package com.kircode.AdvAlert
{
import flash.display.DisplayObjectContainer;
import flash.display.MovieClip;
import flash.display.Stage;
import flash.events.MouseEvent;
import flash.geom.Point;
import flash.events.KeyboardEvent;
import flash.events.Event;
import flash.media.Sound;
/**
* Advanced Alert window manager.
* @author Kirill Poletaev
*/
public class AdvAlertManager
{
private var windows:Array;
private var defaultContainer:DisplayObjectContainer;
private var pWidth:int;
private var pHeight:int;
private var skin:AdvAlertSkin;
private var bSkin:AdvAlertButtonSkin;
private var tabDisable:Boolean;
private var topWindow:AdvAlertWindow;
private var focusedButton:int;
private var stg:Stage;
private var openingSound:Sound;
private var defWidth:int;
private var defHeight:int;
private var defMinHeight:int;
/**
* AdvAlertManager constructor.
* @paramdefaultWindowContainer Parent container of alert windows.
* @paramstage Stage reference.
* @paramwindowSkin Default skin for alert windows.
* @parambuttonSkin Default skin for buttons in this window.
* @paramopenSound Default window open sound.
* @paramdisableTab Disable tab focusing on the parent container when an alert window is visible. This also enables tab navigation for the alert windows buttons.
* @paramdefaultWidth Set default width for all window alerts. 300 by default.
* @paramdefaultHeight Set default height for all window alerts. 0 by default. If 0, the window is auto-sized based on the text length.
* @paramdefaultMinHeight Set default minimal height for all window alerts. 0 by default.
*/
public function AdvAlertManager(defaultWindowContainer:DisplayObjectContainer, stage:Stage, windowSkin:AdvAlertSkin = null, buttonSkin:AdvAlertButtonSkin = null, openSound:Sound = null, disableTab:Boolean = true, defaultWidth:int = 300, defaultHeight:int = 0, defaultMinHeight:int = 0)
{
defWidth = defaultWidth;
defHeight = defaultHeight;
defMinHeight = defaultMinHeight;
openingSound = openSound;
skin = windowSkin;
bSkin = buttonSkin;
tabDisable = disableTab;
if (skin == null) skin = new AdvAlertSkin();
if (bSkin == null) bSkin = new AdvAlertButtonSkin();
defaultContainer = defaultWindowContainer;
pWidth = stage.stageWidth;
pHeight = stage.stageHeight;
stg = stage;
windows = [];
stg.addEventListener(KeyboardEvent.KEY_DOWN, kDown);
}
private function kDown(evt:KeyboardEvent):void {
if(tabDisable){
if (evt.keyCode == 9 && topWindow!=null && topWindow.buttons.length>0) {
focusedButton++;
if (focusedButton == topWindow.buttons.length) {
focusedButton = 0;
}
for (var i:int = 0; i < topWindow.buttons.length; i++) {
topWindow.buttons[i].over = false;
topWindow.buttons[i].updateDraw();
}
topWindow.buttons[focusedButton].over = true;
topWindow.buttons[focusedButton].updateDraw();
}
if (evt.keyCode == 13 && topWindow != null && topWindow.buttons.length > 0) {
if (focusedButton == -1) {
focusedButton = 0;
}
var currentIndex:int = windows.length - 1;
var closeHandler:Function = windows[currentIndex].onclose;
closeWindow(currentIndex, closeHandler);
}
}
}
/**
* Close the top alert window.
*/
public function closeLast():void {
var currentIndex:int = windows.length - 1;
var closeHandler:Function = windows[currentIndex].onclose;
closeWindow(currentIndex, closeHandler);
}
private function closeWindow(currentIndex:int, closeHandler:Function):void {
if (closeHandler != null) closeHandler.call(topWindow.buttons[focusedButton], topWindow.buttons[focusedButton].txt);
windows[currentIndex].window.parent.removeChild(windows[currentIndex].window);
windows[currentIndex].blur.parent.removeChild(windows[currentIndex].blur);
windows.splice(currentIndex, 1);
focusedButton = -1;
if (windows.length > 0) topWindow = windows[windows.length - 1].window;
if (windows.length == 0) topWindow = null;
if (tabDisable && windows.length==0) {
defaultContainer.tabChildren = true;
defaultContainer.tabEnabled = true;
}
}
/**
* Set skin of all future created alert windows.
* @paramwindowSkin Reference to an AdvAlertSkin object.
*/
public function setSkin(windowSkin:AdvAlertSkin):void {
skin = windowSkin;
}
/**
* Set skin of all buttons of future created alert windows.
* @parambuttonSkin Reference to an AdvAlertButtonSkin object.
*/
public function setButtonSkin(buttonSkin:AdvAlertButtonSkin):void {
bSkin = buttonSkin;
}
/**
* Set new options for the alert manager object - has the same parameters as the constructor.
* @paramdefaultWindowContainer Parent container of alert windows.
* @paramstage Stage reference.
* @paramwindowSkin Default skin for alert windows.
* @parambuttonSkin Default skin for buttons in this window.
* @paramopenSound Default window open sound.
* @paramdisableTab Disable tab focusing on the parent container when an alert window is visible. This also enables tab navigation for the alert windows buttons.
* @paramdefaultWidth Set default width for all window alerts. 300 by default.
* @paramdefaultHeight Set default height for all window alerts. 0 by default. If 0, the window is auto-sized based on the text length.
* @paramdefaultMinHeight Set default minimal height for all window alerts. 0 by default.
*/
public function reset(defaultWindowContainer:DisplayObjectContainer, stage:Stage, windowSkin:AdvAlertSkin = null, buttonSkin:AdvAlertButtonSkin = null, openSound:Sound = null, disableTab:Boolean = true, defaultWidth:int = 300, defaultHeight:int = 0, defaultMinHeight:int = 0)
{
defWidth = defaultWidth;
defHeight = defaultHeight;
defMinHeight = defaultMinHeight;
openingSound = openSound;
skin = windowSkin;
bSkin = buttonSkin;
tabDisable = disableTab;
if (skin == null) skin = new AdvAlertSkin();
if (bSkin == null) bSkin = new AdvAlertButtonSkin();
defaultContainer = defaultWindowContainer;
pWidth = stage.stageWidth;
pHeight = stage.stageHeight;
stg = stage;
}
/**
* Create an alert window.
* @paramtext Text value of the alert window.
* @paramtitle Title value of the alert window.
* @parambuttons (Optional) Array of AdvAlertButton objects that represent buttons in the alert window.
* @paramcloseHandler (Optional) Close handling function. Must receive a string value as parameter (which will hold the label of the button that was clicked).
* @paramwidth (Optional) Width of the alert window. If 0, set to default.
* @paramheight (Optional) Height of the alert window. If 0, set to default. If default is 0, window will be auto sized.
* @paramposition (Optional) Coordinates of top-left corner of the alert window. If not specified - the window is centered.
* @paramminHeight (Optional) The minimum height value of the alert window. 0 by default.
*/
public function alert(text:String, title:String = "", buttons:Array = null, closeHandler:Function = null, width:int = 0, height:int = 0, position:Point = null, minHeight:int = 0, openSound:Sound = null):AdvAlertWindow {
if (width == 0) width = defWidth;
if (height == 0) height = defHeight;
if (minHeight == 0) minHeight = defMinHeight;
if (openSound != null) {
openSound.play();
}else
if (openingSound != null) {
openingSound.play();
}
if (position == null) position = new Point((pWidth / 2) - (width / 2), (pHeight / 2) - (height / 2));
if (buttons == null) buttons = [new AdvAlertButton("OK")];
for (var i:int = buttons.length - 1; i >= 0; i--) {
if (!buttons[i] is AdvAlertButton) {
throw new Error("An item in buttons array is not an AdvAlertButton instance. Ignoring...");
buttons.splice(i, 1);
}else {
buttons[i].addEventListener(MouseEvent.CLICK, buttonHandler);
}
}
var w:AdvAlertWindow = new AdvAlertWindow(text, title, width, height, position, skin, buttons, bSkin, pWidth, pHeight, minHeight);
var b:AdvAlertBlur = new AdvAlertBlur(pWidth, pHeight, skin);
var currentIndex:int = windows.length;
windows.push( {window:w, blur:b, onclose:closeHandler} );
defaultContainer.addChild(b);
defaultContainer.addChild(w);
topWindow = w;
focusedButton = -1;
if (tabDisable) {
defaultContainer.tabChildren = false;
defaultContainer.tabEnabled = false;
}
function buttonHandler(evt:MouseEvent):void {
closeWindow(currentIndex, closeHandler);
}
return w;
}
}
}
Heres an example of using both of the new features in main.as:
var mySkin:AdvAlertSkin = new AdvAlertSkin();
mySkin.addContent(warning_icon, new Point(5, 40));
mySkin.textPadding.left = 140;
mySkin.titleFilters = [new DropShadowFilter(0, 0, 0, 1, 3, 3, 2, 3)];
mySkin.textFilters = [new DropShadowFilter(1, 45, 0, 0.4, 0, 0, 5, 1)];
var myButtonSkin:AdvAlertButtonSkin = new AdvAlertButtonSkin();
myButtonSkin.textFilters = [new DropShadowFilter(1, 45, 0, 0.4, 0, 0, 5, 1)];
myButtonSkin.hover_textFilters = [new DropShadowFilter(1, 45, 0, 0.4, 0, 0, 5, 1), new DropShadowFilter(0, 0, 0, 1, 3, 3, 1, 3)];
AlertManager = new AdvAlertManager(this, stage);
AlertManager.alert("Alert window with default settings.", "Example alert!");
AlertManager.reset(this, stage, mySkin, myButtonSkin, null, true, 400, 0, 160);
AlertManager.alert("Alert window with custom settings.", "Example alert!");
Thanks for reading!
Android beginner tutorial Part 59 Implicit Intents
So far weve only used Explicit Intents and called Activities directly. Those always had a clear target - an Activity class to launch. Implicit Intents dont name a target.
An Explicit Intent is always delivered to its target, while Implicit ones need to pass a filter. Intent filters are elements of an applications manifest, which dictate which intents to receive and which ones to block.
An Intent Filter in the manifest file has fields that describe the action, data and category of the Activity. All these fields can be specified in the Implicit Intent and then compared with available filters. If the Intents action, data and category match with the filters of an Activity, that Activity is launched.
Today we will update our application from the previous tutorial and create a new application, which launches the first one using an implicit intent.
Firstly, go to the AndroidManifest.xml of the first application. View it as plain XML file and youll find this in the code:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
We can add multiple filters for an activity. Lets create a new custom one, which also uses 2 fields - action and category. Set their values as shown:
<activity
android:name="com.kircode.codeforfood_test.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="com.kircode.codeforfood_test.LAUNCH_TEST" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
The "com.kircode.codeforfood_test.LAUNCH_TEST" String is what we will later give to an Implicit Intent to search for a suitable Activity to launch.
The manifest xml looks like this:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kircode.codeforfood_test"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="16" />
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.kircode.codeforfood_test.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="com.kircode.codeforfood_test.LAUNCH_TEST" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name="com.kircode.codeforfood_test.SecondActivity"></activity>
</application>
</manifest>
Now create a new Application (File>New>Android Application Project). I called mine "CodeForFood Test Two". When youve created it, go to its layout xml and add a button:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="32sp"
android:text="Second Application"
/>
<Button android:id="@+id/launchButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Launch CodeForFood"
/>
</LinearLayout>
In the java class of the new Activity, use this code:
package com.example.codeforfoodtest_two;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Button btn = (Button)findViewById(R.id.launchButton);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.kircode.codeforfood_test.LAUNCH_TEST");
startActivity(intent);
}
});
}
}
As you can see, the code to create an Implicit Intent is simple. All we do is specify the action as the parameter of the constructor.
If you also want to filter by data, specify the data in the second parameter, or using the setData() method.
If you want to filter by category too, use addCategory() method of the intent.
Thats all for today!
We now have a second application, which launches the first application using an implicit intent.
Thanks for reading!
Wednesday, February 4, 2015
Creating a Flex AIR text editor Part 40
Go to the doOpen() function. Inside of it we have an internal function called fileLoad which is called once the user selects the file on their hard drive. Here we can check if this file is already open and if it is, throw a warning message instead of executing the code. Add a check with a function fileDuplicateCheck(file.nativePath) as the conditional.
private function doOpen():void {
var file:File = new File();
file.browseForOpen("Open document", [new FileFilter("Text documents", "*.txt"), new FileFilter("All files", "*")]);
file.addEventListener(Event.SELECT, fileLoad);
function fileLoad(evt:Event):void {
if(fileDuplicateCheck(file.nativePath)){
var stream:FileStream = new FileStream();
stream.open(file, FileMode.READ);
var str:String = stream.readUTFBytes(stream.bytesAvailable);
stream.close();
str = str.replace(File.lineEnding, "
");
statusMessage = "[ " + new Date().toLocaleTimeString() + " ] " + file.name + " opened";
updateStatus();
tabData.addItem( { title:file.name, textData: str, saved:true, location:file.nativePath} );
tabSelectedIndex = tabData.length - 1;
tabChange();
updateTextSize();
}else {
Alert.show("File " + file.name + " is already open","Error");
}
}
}
This fileDuplicateCheck obviously returns a boolean value - true if this file has no duplicates, false of this file is already open.
The check is done using a simple loop:
private function fileDuplicateCheck(loc:String):Boolean {
var toReturn:Boolean = true;
for (var i:int = 0; i < tabData.length; i++) {
if (tabData[i].location == loc) {
toReturn = false;
tabSelectedIndex = i;
tabChange();
break;
}
}
return toReturn;
}
As you can see, we also select the tab of that file if a match was found.
Now lets add the save all functionality.
First go to the doSave() function and add a conditional which checks if the current tab has its saved property false. Only execute the code if the saved property is false (theres no need in saving a file with no changes made!).
private function doSave(ind:int, updateIndexNeeded:Boolean = true):void {
if(!tabData[ind].saved){
if (tabData[ind].location != "") {
saveFile(textArea.text, tabData[ind].location, updateIndexNeeded);
statusMessage = "[ " + new Date().toLocaleTimeString() + " ] " + tabData[ind].title + " saved";
updateStatus();
tabData[ind].saved = true;
}else{
doSaveAs(textArea.text, ind, updateIndexNeeded);
}
}
}
Now create a function called doSaveAll(), which calls doSave() on each index using a loop.
private function doSaveAll():void {
for (var i:int = 0; i < tabData.length; i++) {
doSave(i);
}
}
Now go to the tool bar section and find your save all button, set its click property to the doSaveAll function:
<custom:IconButton icon="@Embed(../lib/disk_multiple.png)" toolTip="Save all" click="doSaveAll();" />
Add a Save All menu item to the XML:
<fx:XML id="windowMenu">
<root>
<menuitem label="File">
<menuitem label="New" key="n" controlKey="true" />
<menuitem label="Open" key="o" controlKey="true" />
<menuitem label="Save" key="s" controlKey="true" />
<menuitem label="Save As" key="s" controlKey="true" shiftKey="true" />
<menuitem label="Save All" key="s" controlKey="true" altKey="true" />
<menuitem type="separator"/>
<menuitem label="Print" key="p" controlKey="true" />
</menuitem>
<menuitem label="Edit">
<menuitem label="Undo" key="z" controlKey="true" enabled="{canUndo}" />
<menuitem label="Redo" key="y" controlKey="true" enabled="{canRedo}" />
<menuitem type="separator"/>
<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 label="Line count" type="check" toggled="{pref_linecount}" />
<menuitem label="Side pane" type="check" toggled="{pref_sidepane}" />
</menuitem>
</root>
</fx:XML>
And update the menuSelect function so that it calls doSaveAll() if the selected menu item was Save All.
private function menuSelect(evt:FlexNativeMenuEvent):void {
(evt.item.@label == "New")?(doNew()):(void);
(evt.item.@label == "Open")?(doOpen()):(void);
(evt.item.@label == "Save")?(doSave(tabSelectedIndex)):(void);
(evt.item.@label == "Save As")?(doSaveAs(textArea.text, tabSelectedIndex)):(void);
(evt.item.@label == "Save All")?(doSaveAll()):(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 == "Side pane")?(pref_sidepane = !pref_sidepane):(void);
(evt.item.@label == "Line count")?(pref_linecount = !pref_linecount):(void);
(evt.item.@label == "Font...")?(doFont()):(void);
(evt.item.@label == "Print")?(doPrint()):(void);
(evt.item.@label == "Undo")?(doUndo()):(void);
(evt.item.@label == "Redo")?(doRedo()):(void);
savePreferences();
updateStatus();
if (pref_wrap) {
pref_linecount = false;
}
updateTextSize();
countLines();
}
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}"
minWidth="400" minHeight="200" height="700" width="900">
<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.MouseEvent;
import flash.events.NativeWindowBoundsEvent;
import flash.filesystem.File;
import flash.filesystem.FileStream;
import flash.net.SharedObject;
import flashx.textLayout.accessibility.TextAccImpl;
import flashx.textLayout.edit.EditManager;
import flashx.textLayout.edit.TextScrap;
import mx.collections.ArrayCollection;
import mx.controls.Alert;
import mx.controls.TextArea;
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.CloseEvent;
import flash.ui.ContextMenu;
import flash.ui.ContextMenuItem;
import flash.events.ContextMenuEvent;
import mx.events.ResizeEvent;
import mx.core.FlexGlobals;
import mx.printing.FlexPrintJob;
import mx.printing.FlexPrintJobScaleType;
import flashx.undo.UndoManager;
import flashx.textLayout.operations.UndoOperation;
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]
private var pref_sidepane:Boolean = true;
[Bindable]
private var pref_linecount: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 textWidth:Number;
[Bindable]
private var textY:Number;
[Bindable]
private var textX:Number;
[Bindable]
private var tabY:Number;
[Bindable]
private var sidePaneY:Number;
[Bindable]
private var sidePaneX:Number;
[Bindable]
private var sidePaneHeight:Number;
[Bindable]
private var sidePaneWidth:Number = 180;
[Bindable]
private var sideContentWidth:Number = 170;
[Bindable]
private var tabWidth:Number;
[Bindable]
private var lineCountWidth:Number = 40;
[Bindable]
private var lineNumbers:String = "1";
[Bindable]
private var lineDisplayedNum:int = 1;
[Bindable]
private var tabSelectedIndex:int = 0;
[Bindable]
private var canUndo:Boolean = false;
[Bindable]
private var canRedo:Boolean = false;
private var previousTextInOperation:String = "";
private var currentTextInOperation:String = "";
private var previousIndex:int = 0;
private var rightclickTabIndex:int = 0;
private var untitledNum:int = 0;
private var tabsToClose:int = 0;
private var closeAfterConfirm:Boolean = false;
public var fontWindow:FontWindow = new FontWindow();
private var undoManager:UndoManager;
private var editManager:EditManager;
private var saveWait:Boolean = false;
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 = false;
preferences.data.status = true;
preferences.data.toolbar = true;
preferences.data.sidepane = true;
preferences.data.linecount = 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_sidepane = preferences.data.sidepane;
pref_fontsettings = preferences.data.fontsettings;
pref_linecount = preferences.data.linecount;
// 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);
// Context menu declaration for the tabbar control
var cm_close:ContextMenuItem = new ContextMenuItem("Close tab");
cm_close.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, tabContextClose);
var cm_closeother:ContextMenuItem = new ContextMenuItem("Close other tabs");
cm_closeother.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, tabContextCloseOther);
var cm:ContextMenu = new ContextMenu();
cm.items = [cm_close, cm_closeother];
cm.hideBuiltInItems();
tabBar.contextMenu = cm;
tabBar.addEventListener(MouseEvent.RIGHT_MOUSE_DOWN, tabRightClick);
// Context menu declaration for the tab management list control
sideList.contextMenu = cm;
sideList.addEventListener(MouseEvent.RIGHT_MOUSE_DOWN, listRightClick);
// Listen to keyboard
addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
// Undo management
undoManager = new UndoManager();
editManager = new EditManager(undoManager);
textArea.textFlow.interactionManager = editManager;
}
private function menuSelect(evt:FlexNativeMenuEvent):void {
(evt.item.@label == "New")?(doNew()):(void);
(evt.item.@label == "Open")?(doOpen()):(void);
(evt.item.@label == "Save")?(doSave(tabSelectedIndex)):(void);
(evt.item.@label == "Save As")?(doSaveAs(textArea.text, tabSelectedIndex)):(void);
(evt.item.@label == "Save All")?(doSaveAll()):(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 == "Side pane")?(pref_sidepane = !pref_sidepane):(void);
(evt.item.@label == "Line count")?(pref_linecount = !pref_linecount):(void);
(evt.item.@label == "Font...")?(doFont()):(void);
(evt.item.@label == "Print")?(doPrint()):(void);
(evt.item.@label == "Undo")?(doUndo()):(void);
(evt.item.@label == "Redo")?(doRedo()):(void);
savePreferences();
updateStatus();
if (pref_wrap) {
pref_linecount = false;
}
updateTextSize();
countLines();
}
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.data.sidepane = pref_sidepane;
preferences.data.linecount = pref_linecount;
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 {
editManager.insertText(str);
}
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();
}
}
updateLineScroll();
if (sideList.selectedIndices.length==0) {
sideList.selectedIndex = tabSelectedIndex;
}
}
private function onResize(evt:ResizeEvent):void {
updateTextSize();
}
private function updateTextSize():void {
tabY = (toolBar.visible)?(toolBar.height):(0);
textX = (pref_linecount)?(lineCountWidth):(0);
var statusHeight:Number = (pref_status)?(statusBar.height):(0);
textWidth = (pref_sidepane)?(width - sidePaneWidth - textX):(width - textX);
tabWidth = textWidth + textX;
var tabbarScrollHeight:Number = (tabData.length * 170 > tabWidth)?(15):(0);
textY = tabBar.height + tabY + tabbarScrollHeight;
textHeight = height - textY - statusHeight;
focusManager.setFocus(textArea);
sidePaneHeight = textHeight + tabBar.height + tabbarScrollHeight;
sidePaneY = textY - tabBar.height - tabbarScrollHeight;
sidePaneX = width - sidePaneWidth;
}
private function updateStatus():void {
var str:String = new String();
str = (pref_wrap)?("Word wrapping on"):(caretPosition());
status = str + " " + statusMessage;
}
private function caretPosition():String {
var pos:int = textArea.selectionActivePosition;
var str:String = textArea.text.substring(0, pos);
var lines:Array = str.split("
");
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 {
if(!closeAfterConfirm){
evt.preventDefault();
var allWindows:Array = NativeApplication.nativeApplication.openedWindows;
for (var i:int = 1; i < allWindows.length; i++)
{
allWindows[i].close();
}
// Check if there are any unsaved tabs
var needSaving:Boolean = false;
tabsToClose = 0;
for (var u:int = 0; u < tabData.length; u++) {
if (tabData[u].saved == false) {
needSaving = true;
tabsToClose++;
}
}
// If there are unsaved tabs, dont close window yet, set closeAfterConfirm to true and close all tabs
if (needSaving) {
closeAfterConfirm = true;
for (var t:int = 0; t < tabData.length; t++) {
closeTab(t);
}
}
if (!needSaving) {
FlexGlobals.topLevelApplication.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);
lineCount.setStyle("fontFamily", pref_fontsettings.fontfamily);
lineCount.setStyle("fontSize", pref_fontsettings.fontsize);
lineCount.setStyle("fontStyle", pref_fontsettings.fontstyle);
lineCount.setStyle("fontWeight", pref_fontsettings.fontweight);
lineCount.setStyle("color", pref_fontsettings.fontcolor);
lineCount.setStyle("contentBackgroundColor", pref_fontsettings.bgcolor);
}
private function onTabClose(evt:Event):void {
var tabWidth:Number = tabBar.width / tabData.length;
var cIndex:int = Math.floor(tabBar.mouseX / tabWidth);
tabSelectedIndex = cIndex;
tabChange();
closeTab(tabSelectedIndex);
}
private function onListClose(evt:Event):void {
tabSelectedIndex = sideList.selectedIndex;
tabChange();
closeTab(tabSelectedIndex);
}
private function closeTab(index:int):void {
if (tabData[index].saved) {
removeTab(index);
} else
if (!tabData[index].saved) {
Alert.show("Save " + tabData[index].title + " before closing?", "Confirmation", Alert.YES | Alert.NO, null, confirmClose);
}
function confirmClose(evt:CloseEvent):void {
tabsToClose--;
if (evt.detail == Alert.YES) {
tabSelectedIndex = index;
tabChange();
removeTab(index, true);
doSave(index, false);
}else {
removeTab(index);
}
}
}
private function removeTab(index:int, waitForSave:Boolean = false):void {
if(!closeAfterConfirm){
// if this is the last tab, create a new empty tab
if (tabData.length == 1) {
tabData.addItem( { title:"Untitled", textData:"", saved:false, location:""} );
}
statusMessage = "[ " + new Date().toLocaleTimeString() + " ] Tab closed: " + tabData[index].title;
updateStatus();
tabData.removeItemAt(index);
tabSelectedIndex = tabBar.selectedIndex;
previousIndex = tabSelectedIndex;
textArea.text = tabData[tabSelectedIndex].textData;
textArea.selectRange(tabData[tabSelectedIndex].selectedAnchor, tabData[tabSelectedIndex].selectedActive);
}
if (closeAfterConfirm && tabsToClose == 0 && waitForSave == false) {
FlexGlobals.topLevelApplication.close();
}
if (waitForSave) {
saveWait = true;
}
countLines();
updateTextSize();
undoManager.clearAll();
textChange();
}
private function doNew():void {
statusMessage = "[ " + new Date().toLocaleTimeString() + " ] New tab created";
updateStatus();
untitledNum++;
tabData.addItem( { title:"Untitled("+untitledNum+")", textData:"", saved:false, location:""} );
tabSelectedIndex = tabData.length - 1;
tabChange();
updateTextSize();
}
public function tabChange(from:String = "none", ind:int = 0, createOperation:Boolean = true):void {
if (from == "tabbar") {
tabSelectedIndex = tabBar.selectedIndex;
}
if (from == "sidelist") {
tabSelectedIndex = sideList.selectedIndex;
}
if (from == "operation") {
tabSelectedIndex = ind;
}
tabData[previousIndex].textData = textArea.text;
tabData[previousIndex].selectedActive = textArea.selectionActivePosition;
tabData[previousIndex].selectedAnchor = textArea.selectionAnchorPosition;
if (createOperation) {
var operation:TabOperation = new TabOperation(previousIndex, tabSelectedIndex, undoManager);
undoManager.pushUndo(operation);
}
previousIndex = tabSelectedIndex;
textArea.text = tabData[tabSelectedIndex].textData;
textArea.selectRange(tabData[tabSelectedIndex].selectedAnchor, tabData[tabSelectedIndex].selectedActive);
updateStatus();
countLines();
textChange();
}
private function tabContextClose(evt:ContextMenuEvent):void{
closeTab(rightclickTabIndex);
}
private function tabContextCloseOther(evt:ContextMenuEvent):void {
var len:int = tabData.length;
for (var i:int = 0; i < len; i++) {
if (i != rightclickTabIndex) {
closeTab(i);
}
}
}
private function tabRightClick(evt:MouseEvent):void {
var tabWidth:Number = tabBar.width / tabData.length;
var rcIndex:int = Math.floor(tabBar.mouseX / tabWidth);
rightclickTabIndex = rcIndex;
}
private function listRightClick(evt:MouseEvent):void {
var tabHeight:Number = 20;
var rcIndex:int = Math.floor((sideList.mouseY + sideList.scroller.verticalScrollBar.value) / tabHeight);
rightclickTabIndex = rcIndex;
}
private function onKeyDown(evt:KeyboardEvent):void{
if (evt.ctrlKey) {
// Ctrl+TAB - next tab
if (evt.keyCode == 9 && !evt.shiftKey) {
if (tabData.length - tabSelectedIndex > 1) {
tabSelectedIndex++;
tabChange();
}
}
// Ctrl+Shift+TAB - previous tab
if (evt.keyCode == 9 && evt.shiftKey) {
if (tabSelectedIndex > 0) {
tabSelectedIndex--;
tabChange();
}
}
// Ctrl+number (1-8) - go to numbered tab
if (evt.keyCode >= 49 && evt.keyCode <= 56) {
var num:int = evt.keyCode - 48;
if (tabData.length > num - 1) {
tabSelectedIndex = num - 1;
tabChange();
}
}
// Ctrl+9 - go to last tab
if (evt.keyCode == 57) {
tabSelectedIndex = tabData.length - 1;
tabChange();
}
}
}
private function closeSidePane():void{
pref_sidepane = !pref_sidepane
savePreferences();
updateTextSize();
}
private function countLines():void {
if (pref_linecount && !pref_wrap) {
var totalLines:int = textArea.text.split("
").length;
if (totalLines != lineDisplayedNum) {
updateTextSize();
updateLineCount(totalLines, totalLines-lineDisplayedNum, lineDisplayedNum);
lineDisplayedNum = totalLines;
}
}
}
private function updateLineCount(total:int, difference:int, current:int):void {
if (difference > 0) {
for (var i:int = current + 1; i < (total+1); i++) {
lineNumbers += "
" + (i);
}
}
if (difference < 0) {
var charsInTheEnd:int = 0;
for (var u:int = 0; u < -difference; u++) {
charsInTheEnd += ((current - u).toString().length + 1);
}
lineNumbers = lineCount.text.substring(0, lineCount.text.length - charsInTheEnd);
}
}
private function updateLineScroll():void{
lineCount.scroller.verticalScrollBar.value = textArea.scroller.verticalScrollBar.value;
}
private function doPrint():void {
var printJob:FlexPrintJob = new FlexPrintJob();
if (!printJob.start()) return;
tempText.visible = true;
tempText.setStyle("lineBreak", "toFit");
tempText.text = textArea.text;
tempText.width = printJob.pageWidth;
tempText.heightInLines = NaN;
tempText.setStyle("horizontalScrollPolicy", "off");
tempText.setStyle("verticalScrollPolicy", "off");
printJob.printAsBitmap = false;
printJob.addObject(tempText, "matchWidth");
printJob.send();
tempText.visible = false;
}
private function textChange():void{
canUndo = undoManager.canUndo();
canRedo = undoManager.canRedo();
focusManager.setFocus(textArea);
}
private function doUndo():void {
undoManager.undo();
textChange();
}
private function doRedo():void {
undoManager.redo();
textChange();
}
private function doOpen():void {
var file:File = new File();
file.browseForOpen("Open document", [new FileFilter("Text documents", "*.txt"), new FileFilter("All files", "*")]);
file.addEventListener(Event.SELECT, fileLoad);
function fileLoad(evt:Event):void {
if(fileDuplicateCheck(file.nativePath)){
var stream:FileStream = new FileStream();
stream.open(file, FileMode.READ);
var str:String = stream.readUTFBytes(stream.bytesAvailable);
stream.close();
str = str.replace(File.lineEnding, "
");
statusMessage = "[ " + new Date().toLocaleTimeString() + " ] " + file.name + " opened";
updateStatus();
tabData.addItem( { title:file.name, textData: str, saved:true, location:file.nativePath} );
tabSelectedIndex = tabData.length - 1;
tabChange();
updateTextSize();
}else {
Alert.show("File " + file.name + " is already open","Error");
}
}
}
private function fileDuplicateCheck(loc:String):Boolean {
var toReturn:Boolean = true;
for (var i:int = 0; i < tabData.length; i++) {
if (tabData[i].location == loc) {
toReturn = false;
tabSelectedIndex = i;
tabChange();
break;
}
}
return toReturn;
}
private function saveUpdate():void {
if (tabData[tabSelectedIndex].saved) {
tabData[tabSelectedIndex].saved = false;
tabBar.dataProvider = new ArrayCollection([]);
tabBar.dataProvider = tabData;
tabBar.selectedIndex = tabSelectedIndex;
sideList.dataProvider = tabData;
}
}
private function doSave(ind:int, updateIndexNeeded:Boolean = true):void {
if(!tabData[ind].saved){
if (tabData[ind].location != "") {
saveFile(textArea.text, tabData[ind].location, updateIndexNeeded);
statusMessage = "[ " + new Date().toLocaleTimeString() + " ] " + tabData[ind].title + " saved";
updateStatus();
tabData[ind].saved = true;
}else{
doSaveAs(textArea.text, ind, updateIndexNeeded);
}
}
}
private function doSaveAll():void {
for (var i:int = 0; i < tabData.length; i++) {
doSave(i);
}
}
private function refreshData():void {
tabBar.dataProvider = new ArrayCollection([]);
tabBar.dataProvider = tabData;
tabBar.selectedIndex = tabSelectedIndex;
sideList.dataProvider = tabData;
}
private function doSaveAs(text:String, ind:int, updateIndexNeeded:Boolean = true):void {
var file:File = new File();
file.browseForSave("Save file");
file.addEventListener(Event.SELECT, fileSave);
function fileSave(evt:Event):void {
if (file.name.length > 0) {
// See if user entered extension for the file (for example .txt)
// If not, add .txt by default
var extReg:RegExp = /.([a-z0-9]{2,})/i;
if (extReg.test(file.name)) {
saveFile(text, file.nativePath, updateIndexNeeded);
tabData[ind].location = file.nativePath;
tabData[ind].title = file.name;
tabData[ind].saved = true;
}else{
saveFile(text, file.nativePath + ".txt", updateIndexNeeded);
tabData[ind].location = file.nativePath + ".txt";
tabData[ind].title = file.name + ".txt";
tabData[ind].saved = true;
}
}else{
Alert.show("You need to enter a name for your file.", "Error");
}
}
}
private function saveFile(text:String, location:String, updateIndexNeeded:Boolean = true):void {
var file:File = new File(location);
var stream:FileStream = new FileStream();
stream.open(file, FileMode.WRITE);
var str:String = text;
str = str.replace(/
/g, File.lineEnding);
stream.writeUTFBytes(str);
stream.close();
if (closeAfterConfirm && tabsToClose == 0 && saveWait) {
FlexGlobals.topLevelApplication.close();
}
saveWait = false;
if (updateIndexNeeded) {
refreshData();
}
}
]]>
</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 label="Save" key="s" controlKey="true" />
<menuitem label="Save As" key="s" controlKey="true" shiftKey="true" />
<menuitem label="Save All" key="s" controlKey="true" altKey="true" />
<menuitem type="separator"/>
<menuitem label="Print" key="p" controlKey="true" />
</menuitem>
<menuitem label="Edit">
<menuitem label="Undo" key="z" controlKey="true" enabled="{canUndo}" />
<menuitem label="Redo" key="y" controlKey="true" enabled="{canRedo}" />
<menuitem type="separator"/>
<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 label="Line count" type="check" toggled="{pref_linecount}" />
<menuitem label="Side pane" type="check" toggled="{pref_sidepane}" />
</menuitem>
</root>
</fx:XML>
<mx:ArrayCollection id="tabData">
<fx:Object title="Untitled" textData="" saved="false" seletedActive="0" selectedAnchor="0" location="" />
</mx:ArrayCollection>
<mx:ArrayCollection id="sidePaneData">
<fx:Object icon="@Embed(../lib/page.png)" tip="Tab management" />
<fx:Object icon="@Embed(../lib/folder_magnify.png)" tip="File browsing" />
<fx:Object icon="@Embed(../lib/book.png)" tip="Snippets" />
</mx:ArrayCollection>
<mx:ArrayCollection id="sidePaneTabHeadings">
<fx:String>Tab management</fx:String>
<fx:String>File browsing</fx:String>
<fx:String>Snippets</fx:String>
</mx:ArrayCollection>
</fx:Declarations>
<s:Group width="100%" height="100%">
<s:TextArea id="textArea" width="{textWidth}" height="{textHeight}" y="{textY}" x="{textX}" lineBreak="{(pref_wrap)?(toFit):(explicit)}" click="cursorFix(); updateStatus();" change="updateStatus(); countLines(); textChange(); saveUpdate();" keyDown="updateStatus();" borderVisible="false" focusThickness="0" />
<s:Scroller horizontalScrollPolicy="auto" verticalScrollPolicy="off" width="{tabWidth}" y="{tabY}">
<s:Group>
<custom:CustomTabBar id="tabBar" dataProvider="{tabData}" itemRenderer="CustomTab" height="22" tabClose="onTabClose(event);" change="tabChange(tabbar);" selectedIndex="{tabSelectedIndex}" labelField="saved">
<custom:layout>
<s:HorizontalLayout gap="-1" columnWidth="170" variableColumnWidth="false"/>
</custom:layout>
</custom:CustomTabBar>
</s:Group>
</s:Scroller>
<s:TextArea id="lineCount" width="{lineCountWidth}" text="{lineNumbers}" visible="{pref_linecount}" height="{textHeight}" y="{textY}" editable="false" selectable="false" mouseEnabled="false" textAlign="right" verticalScrollPolicy="off" horizontalScrollPolicy="off" />
<mx:HBox id="toolBar" width="100%" backgroundColor="#dddddd" height="30" visible="{pref_toolbar}" paddingTop="2" paddingLeft="3">
<custom:IconButton icon="@Embed(../lib/page.png)" toolTip="New document" click="doNew();" />
<custom:IconButton icon="@Embed(../lib/folder_page.png)" toolTip="Open" click="doOpen();" />
<custom:IconButton icon="@Embed(../lib/disk.png)" toolTip="Save" click="doSave(tabSelectedIndex);" />
<custom:IconButton icon="@Embed(../lib/disk_multiple.png)" toolTip="Save all" click="doSaveAll();" />
<custom:IconButton icon="@Embed(../lib/printer.png)" toolTip="Print" click="doPrint();" />
<s:Label text="|" fontSize="18" color="#bbbbbb" paddingTop="4" />
<custom:IconButton icon="@Embed(../lib/arrow_undo.png)" toolTip="Undo" enabled="{canUndo}" click="doUndo();" />
<custom:IconButton icon="@Embed(../lib/arrow_redo.png)" toolTip="Redo" enabled="{canRedo}" click="doRedo();" />
<s:Label text="|" fontSize="18" color="#bbbbbb" paddingTop="4" />
<custom:IconButton icon="@Embed(../lib/cut.png)" toolTip="Cut" click="doCut();" />
<custom:IconButton icon="@Embed(../lib/page_white_copy.png)" toolTip="Copy" click="doCopy();" />
<custom:IconButton icon="@Embed(../lib/paste_plain.png)" toolTip="Paste" click="doPaste();" />
</mx:HBox>
<mx:Box id="sidePane" width="{sidePaneWidth}" y="{sidePaneY}" x="{sidePaneX}" height="{sidePaneHeight}" backgroundColor="#dddddd" visible="{pref_sidepane}" paddingTop="5" paddingLeft="5" horizontalScrollPolicy="off">
<s:Group>
<s:Label text="{sidePaneTabHeadings.getItemAt(sidePaneButtons.selectedIndex)}" width="{sidePaneWidth}" />
<mx:Image source="@Embed(../lib/bullet_go.png)" top="-4" right="15" click="closeSidePane();" useHandCursor="true" buttonMode="true"/>
</s:Group>
<mx:ToggleButtonBar id="sidePaneButtons" dataProvider="{sidePaneData}" iconField="icon" width="{sidePaneWidth-10}" toolTipField="tip" />
<mx:ViewStack id="sidePaneStack" height="100%" selectedIndex="{sidePaneButtons.selectedIndex}">
<s:NavigatorContent id="tabs">
<custom:CustomList id="sideList" dataProvider="{tabData}" width="{sideContentWidth}" height="100%" itemRenderer="CustomListItem" selectedIndex="{tabSelectedIndex}" change="tabChange(sidelist);" tabClose="onListClose(event);" />
</s:NavigatorContent>
<s:NavigatorContent id="files">
<mx:FileSystemTree height="100%" width="100%" />
</s:NavigatorContent>
<s:NavigatorContent id="snippets">
<s:VGroup height="100%" width="100%">
<mx:Tree height="100%" width="100%" />
<s:Button width="100%" label="New snippet" />
<s:Button width="100%" label="Manage snippets" />
</s:VGroup>
</s:NavigatorContent>
</mx:ViewStack>
</mx:Box>
</s:Group>
<s:TextArea id="tempText" borderVisible="false" visible="false"/>
</s:WindowedApplication>
Everything works OK, except for the fact that if there are multiple unsaved tabs that have no location yet (Undefined ones) and you use Save All, the code throws an error. This is due to the fact that multiple browse dialog windows are called out at the same time.
We will work on this in the next part.
Thanks for reading!