By Lon (Alonzo) Hosford
I created this debug console for use in learning and teaching Actionscript programming. This is developed in pure Actionscript without component libraries. For that reason it can be used in Flash, AIR, and Flex. It will work in Flash IDE like Flash CS4 or Flex Builder. It also works creating Flash, Flex or AIR from the Flex SDK command line.
The debug console plays in the published movie. It also outputs the results to the consoles in the respective IDEs: Flex Builder, Flash CS4, etc. You can turn that feature off and make the DebugConsole invisible when you publish if you wish.
This is an ActionScript project created in Flex Builder and updated to Flex Builder 4. Download the example code. You can build this with the free Flex SDK by using the code in the src folder. Same for Flash CS3 and CS4. You need to create a Flash Document in the src folder and set the document class to DebuggerLite
.
If you want to use the DebugConsole in a Flash CS4 or CS3 you do not need to set the document class to DebuggerLite
. Rather in Actionscript you include this code:
import com.lonhosford.util.debug.lite.DebugConsole var debugConsole:DebugConsole = DebugConsole.getInstance(); stage.addChild(debugConsole); debugConsole.width = 200; debugConsole.height = 200; debugConsole.write("Hello World");
For your convenience you can download a Flash CS4 ready to go example.
Testing Application In Flex Builder
This is a testing Actionscript application in Flex Builder. No Flash or Flex components are used so there is not need to import libraries. The DebugConsole
class is instantiated as _debugConsole
on line 23. It is a singleton design pattern so only one exists for the application.
A Timer
is used to generate entries to the DebugConsole
. The Timer
is instantiated on line 24 as _testTimer
.
The _testTimeCount
variable is used to determine how many generated entries to the DebugConsole
. General purpose is to see when vertical scroll bar appears.
The _showWideLineAt
variable on line 25 is used add a very wide line of text to trip showing the horizontal scroll bar. The _showWideLineAt
variable trips this when it equal _testTimer.currentCount
value.
The _testTimeDelay
variable on line 26 is the millisecond value for the _testTimer
. Just depends on how fast you want to see the automated updates to the DebugConsole
.
/** * Purpose: Test Debugger Lite <p>Author: Lon Hosford www.lonhosford.com 908 996 3773</p> <p>Date: March 12, 2010</p> * */ package { import com.lonhosford.util.debug.lite.DebugConsole; import flash.display.Sprite; import flash.events.KeyboardEvent; import flash.events.TimerEvent; import flash.ui.Keyboard; import flash.utils.Timer; [SWF(width=500, height = 300, frameRate = 30)] /** * Testing application for DebugConsole. * @see com.lonhosford.util.debug.lite.DebugConsole * */ public class DebuggerLite extends Sprite { private var _debugConsole:DebugConsole = DebugConsole.getInstance(); private var _testTimeCount:Number = 15; private var _showWideLineAt:Number = 2; private var _testTimeDelay:Number = 250; private var _testTimer:Timer; public function DebuggerLite() { stage.addChild(_debugConsole); _debugConsole.width = stage.stageWidth; _debugConsole.height = stage.stageHeight; _testTimer = new Timer(_testTimeDelay,_testTimeCount); _testTimer.addEventListener(TimerEvent.TIMER,testTimerEventHandler); _testTimer.addEventListener(TimerEvent.TIMER_COMPLETE,testTimerCompleteEventHandler); stage.addEventListener(KeyboardEvent.KEY_UP, stageKeyUpEventHandler); start(); } /** * Starts the automated testing. * */ private function start():void { _debugConsole.write("Debugger Lite Tester"); _debugConsole.write("Hit Space Bar to Clear."); _debugConsole.write("Hit Ctr+ Space Bar to Repeat."); _debugConsole.write("\n"); _testTimer.start(); } /** * Handles the KeyboardEvent.KEY_UP event for the stage. * */ private function stageKeyUpEventHandler(event:KeyboardEvent):void { // Space bar if ( event.keyCode == Keyboard.SPACE ) { // Control key if (event.ctrlKey) { _testTimer.stop(); _testTimer.reset(); _debugConsole.clear(); start(); } // No control key else { _debugConsole.clear(); } } } /** * Handles the TimerEvent.TIMER event. * */ private function testTimerEventHandler(event:TimerEvent):void { _debugConsole.write(new Date().toTimeString()); if (_testTimer.currentCount == _showWideLineAt) { _debugConsole.write("START Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. END"); } } /** * Handles the TimerEvent.TIMER_COMPLETE event. * */ private function testTimerCompleteEventHandler(event:TimerEvent):void { _debugConsole.write("LAST LINE"); } } }
[ad name=”Google Adsense”]
DebugConsole Class
This is the DebugConsole
class. It is a singleton and as such can be instantiated anywhere in your code to use the write()
method on line 291. The DebugConsole write()
method calls the Debugger class write()
method. The Debugger class write()
method dispatches a DebuggerEvent.WRITE
which the DebugConsole
listens in the consoleUpdate()
on line 308. For this reason you can also instantiate the Debugger
class in code and use its write()
method if you prefer. You only need to instantiate the DebugConsole
where you intend to add it to your display.
package com.lonhosford.util.debug.lite { import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.text.TextFormat; import flash.text.TextFormatAlign; /** * UI for debug.lite package * */ public class DebugConsole extends Sprite { private static var _instance:DebugConsole; // Singleton instance private var _debugger:Debugger; private var _content_tf:TextField; // The console content private var _lineCount_tf:TextField; // The line count private var _width:Number = 0; // Width private var _height:Number = 0; // Height private var _scrollVContainer:Sprite; // Vertical scroller container private var _scrollHContainer:Sprite; // Horizontal scroller container private var _scrollLeftButton:DebugConsoleArrowButton; // Scroll left button private var _scrollRightButton:DebugConsoleArrowButton; // Scroll right button private var _scrollUpButton:DebugConsoleArrowButton; // Scroll up button private var _scrollDownButton:DebugConsoleArrowButton; // Scroll down button private var _horizontalScrollIncrement:int = 10; // Character count increment for horizontal scrolling private var _verticalScrollIncrement:int = 1; // Line count increment for vertical scrolling. private var _autoScroll:Boolean = true; // Autoscroll to last line /** * Constructor * * @param pvt Enforces a singleton pattern. * @see #getInstance() * */ public function DebugConsole(pvt:DebugConsolePrivateClass) { _content_tf = new TextField(); _lineCount_tf = new TextField(); var format:TextFormat = new TextFormat(); format.font = "_typewriter"; format.color = 0x000000; format.size = 12; format.indent = 2; _content_tf.defaultTextFormat = format; _content_tf.background= true; _content_tf.backgroundColor = 0xffffff; var format2:TextFormat = new TextFormat(); format2.font = "_typewriter"; format2.color = 0x000000; format2.size = 12; format2.leftMargin = 1 format2.align = TextFormatAlign.RIGHT; //format2.indent = 1; _lineCount_tf.defaultTextFormat = format2; _lineCount_tf.background= true; _lineCount_tf.backgroundColor = 0xcccccc; _content_tf.addEventListener(Event.SCROLL, _console_tfScrollHandler); _scrollHContainer = new Sprite(); _scrollVContainer = new Sprite(); _scrollLeftButton = new DebugConsoleArrowButton(); _scrollLeftButton.addEventListener(MouseEvent.CLICK,scrollLeftButtonMouseClick); _scrollRightButton = new DebugConsoleArrowButton(); _scrollRightButton.addEventListener(MouseEvent.CLICK,scrollRightButtonMouseClick); _scrollUpButton = new DebugConsoleArrowButton(); _scrollUpButton.addEventListener(MouseEvent.CLICK,scrollUpButtonMouseClick); _scrollDownButton = new DebugConsoleArrowButton(); _scrollDownButton.addEventListener(MouseEvent.CLICK,scrollDownButtonMouseClick); addChild(_content_tf); addChild(_lineCount_tf); addChild(_scrollHContainer); addChild(_scrollVContainer); _scrollHContainer.addChild(_scrollLeftButton); _scrollHContainer.addChild(_scrollRightButton); _scrollVContainer.addChild(_scrollUpButton); _scrollVContainer.addChild(_scrollDownButton); _debugger = Debugger.getInstance(); _debugger.addEventListener(DebuggerEvent.WRITE,consoleUpdate); } /** * Singleton instantiation method * */ public static function getInstance():DebugConsole { if (DebugConsole._instance == null) { DebugConsole._instance = new DebugConsole(new DebugConsolePrivateClass()); } return DebugConsole._instance; } /** * Redraws the components * */ private function draw():void { var rightPanelHeight:Number; var bottomPanelWidth:Number; var lineCount_tf_width:Number = _lineCount_tf.textWidth + 12;// Extra for TextField overhead var bottomPanelHeight:Number = 20; var rightPanelWidth:Number = 20; var tfHeight:Number;// = _height; var tfWidth:Number = _width; // Component border graphics.clear(); graphics.beginFill(0xffffff); graphics.lineStyle(2, 0x000000); graphics.drawRect(0, 0, _width, _height); graphics.endFill(); _scrollLeftButton.draw( bottomPanelHeight, bottomPanelHeight, 0x00ffff, 1, 0x000000, 0x666666, 90 ); _scrollRightButton.draw( bottomPanelHeight, bottomPanelHeight, 0x00ffff, 1, 0x000000, 0x666666, -90 ); _scrollUpButton.draw( rightPanelWidth, rightPanelWidth, 0x00ffff, 1, 0x000000, 0x666666, 0 ); _scrollDownButton.draw( rightPanelWidth, rightPanelWidth, 0x00ffff, 1, 0x000000, 0x666666, 180 ); tfHeight = height rightPanelHeight = height-3; bottomPanelWidth = width - 3 - lineCount_tf_width; if (_content_tf.textHeight > _content_tf.height) { bottomPanelWidth -= rightPanelWidth - 1; } if (_content_tf.textWidth > _content_tf.width) { rightPanelHeight -= bottomPanelHeight - 1; } // Right scrollbar panel if (_content_tf.textHeight > _content_tf.height) { _scrollUpButton.x = 0; _scrollUpButton.y = 0; _scrollDownButton.x = 0; _scrollDownButton.y = 0; _scrollVContainer.graphics.clear(); _scrollVContainer.graphics.beginFill(0xcccccc); _scrollVContainer.graphics.lineStyle(1, 0x000000); _scrollVContainer.graphics.drawRect(0, 0, rightPanelWidth - 1, rightPanelHeight); _scrollVContainer.graphics.endFill(); _scrollVContainer.x = _width - _scrollVContainer.width + .5; _scrollVContainer.y = .5; _scrollDownButton.x = (_scrollVContainer.width - _scrollDownButton.width) ; _scrollDownButton.y = (_scrollVContainer.height - _scrollDownButton.height) ; _scrollUpButton.x = _scrollDownButton.x; _scrollUpButton.y = 0; tfWidth -= _scrollVContainer.width - 1; _scrollVContainer.visible = true; } else { _scrollVContainer.visible = false; } // Bottom scrollbar panel if (_content_tf.textWidth > _content_tf.width) { _scrollLeftButton.x = 0; _scrollLeftButton.y = 0; _scrollRightButton.x = 0; _scrollRightButton.y = 0; _scrollHContainer.graphics.clear(); _scrollHContainer.graphics.beginFill(0xcccccc); _scrollHContainer.graphics.lineStyle(1, 0x000000); _scrollHContainer.graphics.drawRect(0, 0, bottomPanelWidth, bottomPanelHeight - 1); _scrollHContainer.graphics.endFill(); _scrollHContainer.y = _height - _scrollHContainer.height + .5; _scrollHContainer.x = lineCount_tf_width; _scrollLeftButton.x = (_scrollHContainer.width - _scrollLeftButton.width) ; _scrollLeftButton.y = (_scrollHContainer.height - _scrollLeftButton.height) ; _scrollRightButton.x = 0; _scrollRightButton.y = _scrollLeftButton.y; _scrollHContainer.visible = true; tfHeight -= _scrollHContainer.height -1; } else { _scrollHContainer.visible = false; } // Left bottom rectangle if horizontal and vertical scroll bars are visible. if (_scrollHContainer.visible && _scrollVContainer.visible) { graphics.beginFill(0xcccccc); graphics.lineStyle(0, 0x000000,0); graphics.drawRect(_scrollHContainer.x + _scrollHContainer.width, _scrollVContainer.height, bottomPanelHeight-2, bottomPanelHeight-2); graphics.endFill(); } // Left bottom rectangle if horizontal scroll bar is visible. if (_scrollHContainer.visible) { graphics.beginFill(0xcccccc); graphics.lineStyle(1, 0x000000,100); graphics.drawRect( 0, _scrollHContainer.y, lineCount_tf_width, bottomPanelHeight-1); graphics.endFill(); } // Position and resize line count text field. _lineCount_tf.width = lineCount_tf_width; _lineCount_tf.height = tfHeight - 4; _lineCount_tf.x = 1; _lineCount_tf.y = 1; _lineCount_tf.scrollV = _content_tf.scrollV; // Position and resize line content text field. _content_tf.width = tfWidth - 2 - lineCount_tf_width; _content_tf.height = _lineCount_tf.height; _content_tf.x = _lineCount_tf.x + _lineCount_tf.width ; _content_tf.y = _lineCount_tf.y; _scrollUpButton.enabled = _content_tf.scrollV != 1; _scrollDownButton.enabled = _content_tf.scrollV != _content_tf.maxScrollV; _scrollLeftButton.enabled = _content_tf.scrollH != _content_tf.maxScrollH; _scrollRightButton.enabled = _content_tf.scrollH != 0; } /** * Change width * */ override public function set width(width:Number):void { _width = width; draw(); } /** * Change height * */ override public function set height(height:Number):void { _height = height; draw(); } /** * Scroll to last line. * @default true * */ public function set autoScroll(autoScrollEnabled:Boolean):void { _autoScroll = autoScrollEnabled; } /** * Adds one line. Multiple lines can be added inserting \n. A blank line can be added with \n. * <p>The writing is delegated to the Debugger class.</p> * @see com.lonhosford.util.debug.lite.Debugger.write() * */ public function write(msg:String):void { _debugger.write(msg); } /** * Clears the display. Line count is not reset to 0 and continues to increment. * */ public function clear():void { _lineCount_tf.text = ""; _content_tf.text = ""; draw(); } /** * Handles the DebuggerEvent.WRITE event. * */ private function consoleUpdate(e:DebuggerEvent):void { var debugMessage:DebugMessage = e.debugMessage; _lineCount_tf.appendText(debugMessage.lineNumber + "." + "\n"); if (debugMessage.text == "\n") { debugMessage.text = ""; } _content_tf.appendText( debugMessage.text + "\n"); if(_autoScroll) { _content_tf.scrollV = _content_tf.maxScrollV; } draw(); } /** * Handler for the scrollUpButton MouseEvent.CLICK event. * */ private function scrollUpButtonMouseClick(e:MouseEvent):void { if (_content_tf.scrollV <= _verticalScrollIncrement) { _content_tf.scrollV = 1; } else { _content_tf.scrollV -= _verticalScrollIncrement; } draw(); } /** * Handler for the scrollRightButton MouseEvent.CLICK event. * */ private function scrollRightButtonMouseClick(e:MouseEvent):void { if (_content_tf.scrollH <= _horizontalScrollIncrement) { _content_tf.scrollH = 0; } else { _content_tf.scrollH -= _horizontalScrollIncrement; } draw(); } /** * Handler for the scrollDownButton MouseEvent.CLICK event. * */ private function scrollDownButtonMouseClick(e:MouseEvent):void { if (_content_tf.scrollV + _verticalScrollIncrement >= _content_tf.maxScrollV) { _content_tf.scrollV = _content_tf.maxScrollV; } else { _content_tf.scrollV += _verticalScrollIncrement; } draw(); } /** * Handler for the scrollLeftButton MouseEvent.CLICK event. * */ private function scrollLeftButtonMouseClick(e:MouseEvent):void { if (_content_tf.scrollH + _horizontalScrollIncrement >= _content_tf.maxScrollH) { _content_tf.scrollH =_content_tf.maxScrollH; } else { _content_tf.scrollH += _horizontalScrollIncrement; } draw(); } /** * Handler for the _content_tf Event.SCROLL event. * */ public function _console_tfScrollHandler(event:Event):void { draw(); } } } /** * Singleton enforcer class * */ class DebugConsolePrivateClass { public function DebugConsolePrivateClass() { } }
Debugger Class
This class receives write messages and dispatches them. It also will write to the IDE debug console on line 60.
package com.lonhosford.util.debug.lite { import flash.events.EventDispatcher; /** * Singleton for receiving and dispatching debugging messages * */ public class Debugger extends EventDispatcher { private static var _instance:Debugger; private var _msg:String; private var _isTracing:Boolean = true; // State of using the trace() function. private var _lineCount:Number = 0; // Line count of tracing messages private var _lang_productName:String = "Actionscript 3 Debugger Lite"; private var _lang_productVersion:String = "Version"; /** * Constructor * * @param pvt Enforces a singleton pattern. * @see #getInstance() * */ public function Debugger(pvt:DebuggerPrivateClass) { } /** * Singleton instantiation method * */ public static function getInstance():Debugger { if (Debugger._instance == null) { Debugger._instance = new Debugger(new DebuggerPrivateClass()); } return Debugger._instance; } /** * Turns on or off tracing to the Flash or Flex IDE console. * @default true * */ public function set isTracing(value:Boolean):void { _isTracing = value; } /** * Adds one line. Multiple lines can be added inserting \n. A blank line can be added with \n. * <p>The writing is delegated to the Debugger class.</p> * */ public function write(msg:String):void { var messageLines:Array = new Array(); if (msg == "\n") { messageLines.push(""); } else { messageLines = msg.split("\n") } if (_isTracing) { trace(msg); } if ( _lineCount == 0 ) { messageLines.splice(0,0,_lang_productName + "\t" + _lang_productVersion + " " + DebugVersion.VERSION); messageLines.splice(1,0, DebugVersion.AUTHOR + "\t" + DebugVersion.AUTHOR_WEB_SITE); messageLines.splice(2,0,"\n"); } for (var msgLinesIndex:uint = 0; msgLinesIndex <= messageLines.length - 1; msgLinesIndex++) { dispatchMessageEvent(messageLines[msgLinesIndex]); } } /** * Dispatches a DebuggerEvent.WRITE * @see DebuggerEvent * @see DebugMessage * */ private function dispatchMessageEvent(msg:String):void { var debugMessage:DebugMessage = new DebugMessage(); debugMessage.text = msg; debugMessage.lineNumber = ++_lineCount; var e:DebuggerEvent = new DebuggerEvent(DebuggerEvent.WRITE, debugMessage); dispatchEvent(e); } } } /** * Singleton enforcer class * */ class DebuggerPrivateClass { public function DebuggerPrivateClass() { } }
[ad name=”Google Adsense”]
DebugConsoleArrowButton Class
UI for the scroll buttons. Simple shapes using a triangle to indicate direction of scroll.
package com.lonhosford.util.debug.lite { import flash.display.Sprite; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.utils.Timer; /** * Arrow button UI and logic * */ public class DebugConsoleArrowButton extends Sprite { private var _container:Sprite = new Sprite(); private var _triangle:DebugTriangleShape = new DebugTriangleShape(); private var _backgroundRect:Sprite = new Sprite(); private var _width:Number; private var _height:Number; private var _color:int; // Color of arrow. private var _borderWidth:Number; // Border width of button. private var _borderColor:int; // Border color of button. private var _backgroundColor:int; // Background color of button. private var _direction:Number; // Rotation of the UI. private var _mouseDownStartTimeDelay:Number = 350; // Initial delay before starting repeating MouseEvent.CLICK events. private var _mouseDownTimeDelay:Number = 100; // Delay between each MouseEvent.CLICK event. private var _mouseDownTimer:Timer; // Timer for repeating MouseEvent.CLICK events. /** * Constructor * */ public function DebugConsoleArrowButton() { _mouseDownTimer = new Timer(_mouseDownTimeDelay,0); _mouseDownTimer.addEventListener(TimerEvent.TIMER,mouseDownTimerEventHandler); _container.addChild(_backgroundRect); _backgroundRect.addChild(_triangle); addChild(_container); } /** * Handler for TimerEvent.TIMER event. Resets the delay interval once the default delay * is reached. * */ private function mouseDownTimerEventHandler(event:TimerEvent):void { if (_mouseDownTimer.delay == _mouseDownStartTimeDelay) { _mouseDownTimer.delay = _mouseDownTimeDelay; } var e:MouseEvent = new MouseEvent(MouseEvent.CLICK); dispatchEvent(e); } /** * Sets enabled state. * */ internal function set enabled(enabledState:Boolean):void { alpha = enabledState ? 1 : .25; if (enabledState) { addEventListener(MouseEvent.MOUSE_UP,mouseUpEventHandler); addEventListener(MouseEvent.MOUSE_DOWN,mouseDownEventHandler); addEventListener(MouseEvent.MOUSE_OUT,mouseOutEventHandler); } } /** * Handler for MouseEvent.MOUSE_OUT event. Stops the mouse down repeat timer. * */ private function mouseOutEventHandler(e:MouseEvent):void { _mouseDownTimer.stop(); } /** * Handler for MouseEvent.MOUSE_UP event. Stops the mouse down repeat timer. * */ private function mouseUpEventHandler(e:MouseEvent):void { _mouseDownTimer.stop(); } /** * Handler for MouseEvent.MOUSE_DOWN event. Starts mouse down timer. * */ private function mouseDownEventHandler(e:MouseEvent):void { _mouseDownTimer.delay = _mouseDownStartTimeDelay; _mouseDownTimer.start(); } /** * Draw the button UI. * */ internal function draw( p_width:Number, p_height:Number, color:int, borderWidth:Number, borderColor:int, backgroundColor:int, direction:Number = 0):void { _width = p_width; _height = p_height; _color = color; _borderWidth = borderWidth; _borderColor = borderColor; _backgroundColor = backgroundColor; _direction = direction; _backgroundRect.graphics.clear(); _backgroundRect.graphics.beginFill(_backgroundColor); _backgroundRect.graphics.lineStyle(_borderWidth, _borderColor); _backgroundRect.graphics.drawRect(0, 0, _width - _borderWidth, _height - _borderWidth); _backgroundRect.graphics.endFill(); _triangle.draw( _width , 0,_color, _color); _triangle.scaleX = _triangle.scaleY = .6; _triangle.x = (_triangle.width / 2) + ((_width - _triangle.width - _borderWidth) / 2) ; _triangle.y = (_triangle.height / 2) + ((_height - _triangle.height - _borderWidth) / 2); _triangle.rotation = _direction; } } }
DebugTriangleShape Class
Utility to draw a rectangle shape with a center registration point.
package com.lonhosford.util.debug.lite { import flash.display.Shape; import flash.display.Sprite; /** * Utility to draw a triangle shape with a center registration point. * */ public class DebugTriangleShape extends Sprite { public function DebugTriangleShape( ) { } internal function draw( size:Number, borderWidth:Number, borderColor:int, backgroundColor:int ):void { graphics.clear(); graphics.beginFill(backgroundColor); graphics.lineStyle(borderWidth, borderColor); size = size / 2; graphics.moveTo(-size, size); graphics.lineTo(-0, -size); graphics.lineTo(size, size ); graphics.endFill(); } } }
DebuggerEvent Class
Events for the Debugger
class.
package com.lonhosford.util.debug.lite {import flash.events.Event; /** * Events for the Debugger * @see DebugMessage * @see Debugger * */ public class DebuggerEvent extends Event { public static const WRITE:String = "debug.DebuggerEvent.Write"; public var debugMessage:DebugMessage; public function DebuggerEvent(type:String, debugMessage:DebugMessage) { super(type, bubbles); this.debugMessage = debugMessage } override public function clone():Event { return new DebuggerEvent(type, debugMessage); } } }
DebugMessage Class
The data values sent with the DebuggerEvent
.
package com.lonhosford.util.debug.lite { /** * Data for a DebuggerEvent. * @see DebuggerEvent * */ public class DebugMessage { public var text:String; public var lineNumber:Number; public function DebugMessage() { } } }
DebugVersion Class
A place to hold the static values for the code.
package com.lonhosford.util.debug.lite { /** * Common version data. * */ public final class DebugVersion { public static const VERSION:String = "1.00.01"; public static const AUTHOR:String = "Lon (Alonzo) Hosford"; public static const AUTHOR_WEB_SITE:String = "https://www.lonhosford.com"; } }