diff --git a/src/as/com/threerings/flex/ChatDisplayBox.as b/src/as/com/threerings/flex/ChatDisplayBox.as new file mode 100644 index 00000000..92a1f020 --- /dev/null +++ b/src/as/com/threerings/flex/ChatDisplayBox.as @@ -0,0 +1,82 @@ +package com.threerings.flex { + +import flash.display.DisplayObjectContainer; + +import flash.events.Event; + +import mx.controls.TextArea; + +import com.threerings.crowd.chat.client.ChatDirector; +import com.threerings.crowd.chat.client.ChatDisplay; +import com.threerings.crowd.chat.data.ChatMessage; +import com.threerings.crowd.chat.data.UserMessage; +import com.threerings.crowd.util.CrowdContext; + +/** + * A very simple chat display. + */ +public class ChatDisplayBox extends TextArea + implements ChatDisplay +{ + public function ChatDisplayBox (ctx :CrowdContext) + { + _ctx = ctx; + this.editable = false; + + // TODO + width = 400; + height = 150; + } + + // documentation inherited from interface ChatDisplay + public function clear () :void + { + this.htmlText = ""; + } + + // documentation inherited from interface ChatDisplay + public function displayMessage ( + msg :ChatMessage, alreadyDisplayed :Boolean) :Boolean + { + if (!_scrollBot) { + _scrollBot = (verticalScrollPosition == maxVerticalScrollPosition); + } + + // display the message + if (msg is UserMessage) { + this.htmlText += "<" + + (msg as UserMessage).speaker + "> "; + } + this.htmlText += msg.message; + return true; + } + + override public function parentChanged (p :DisplayObjectContainer) :void + { + super.parentChanged(p); + + var chatdir :ChatDirector = _ctx.getChatDirector(); + if (p != null) { + chatdir.addChatDisplay(this); + } else { + chatdir.removeChatDisplay(this); + } + } + + // documentation inherited + override protected function updateDisplayList (uw :Number, uh :Number) :void + { + super.updateDisplayList(uw, uh); + + if (_scrollBot) { + verticalScrollPosition = maxVerticalScrollPosition; + _scrollBot = false; + } + } + + /** The giver of life. */ + protected var _ctx :CrowdContext; + + protected var _scrollBot :Boolean; +} +} diff --git a/src/as/com/threerings/flex/CommandButton.as b/src/as/com/threerings/flex/CommandButton.as new file mode 100644 index 00000000..373620b4 --- /dev/null +++ b/src/as/com/threerings/flex/CommandButton.as @@ -0,0 +1,54 @@ +package com.threerings.flex { + +import flash.events.MouseEvent; + +import mx.controls.Button; + +/** + * A command button simply dispatches a Controller command (with an optional + * argument) when it is clicked. + */ +public class CommandButton extends Button +{ + /** + * Create a command button. + */ + public function CommandButton (cmd :String = null, arg :Object = null) + { + setCommand(cmd, arg); + } + + /** + * Set the command and argument to be issued when this button is pressed. + */ + public function setCommand (cmd :String, arg :Object = null) :void + { + _cmd = cmd; + _arg = arg; + } + + override protected function clickHandler (event :MouseEvent) :void + { + super.clickHandler(event); + + if (enabled) { + // stop the click event + event.stopImmediatePropagation(); + + // dispatch the command event + // if the arg is null and we're a toggle button, dispatch our state + var arg :Object = _arg; + if (arg == null && toggle) { + arg = selected; + } + CommandEvent.dispatch(this, _cmd, arg); + } + } + + /** The command to submit when clicked. */ + protected var _cmd :String; + + /** The argument that accompanies our command. */ + protected var _arg :Object; +} +} diff --git a/src/as/com/threerings/flex/CommandEvent.as b/src/as/com/threerings/flex/CommandEvent.as new file mode 100644 index 00000000..e00b723a --- /dev/null +++ b/src/as/com/threerings/flex/CommandEvent.as @@ -0,0 +1,88 @@ +package com.threerings.flex { + +import flash.errors.IllegalOperationError; + +import flash.events.Event; +import flash.events.IEventDispatcher; + +public class CommandEvent extends Event +{ + /** The event type for all controller events. */ + public static const TYPE :String = "commandEvt"; + + /** + * Use this method to dispatch CommandEvents. + */ + public static function dispatch ( + disp :IEventDispatcher, cmd :String, arg :Object = null) :void + { + // Create the event + var event :CommandEvent = create(cmd, arg); + + // Dispatch it. A return value of true means that the event was + // never cancelled, so we complain. + if (disp == null || disp.dispatchEvent(event)) { + Log.getLog(CommandEvent).warning("Unhandled controller command " + + "[cmd=" + cmd + ", arg=" + arg + "]."); + } + } + + /** The command. */ + public var command :String; + + /** An optional argument. */ + public var arg :Object; + + /** + * Command events may not be directly constructed, use the dispatch + * method to do your work. + */ + public function CommandEvent (command :String, arg :Object) + { + super(TYPE, true, true); + if (_blockConstructor) { + throw new IllegalOperationError(); + } + this.command = command; + this.arg = arg; + } + + /** + * Mark this command as handled, stopping its propagation up the + * hierarchy. + */ + public function markAsHandled () :void + { + preventDefault(); + stopImmediatePropagation(); + } + + override public function clone () :Event + { + return create(command, arg); + } + + override public function toString () :String + { + return "CommandEvent[" + command + " (" + arg + ")]"; + } + + /** + * A factory method for privately creating command events. + */ + protected static function create (cmd :String, arg :Object) :CommandEvent + { + var event :CommandEvent; + _blockConstructor = false; + try { + event = new CommandEvent(cmd, arg); + } finally { + _blockConstructor = true; + } + return event; + } + + /** Used to prevent unauthorized construction. */ + protected static var _blockConstructor :Boolean = true; +} +} diff --git a/src/as/com/threerings/flex/CommandMenu.as b/src/as/com/threerings/flex/CommandMenu.as new file mode 100644 index 00000000..4aadeb48 --- /dev/null +++ b/src/as/com/threerings/flex/CommandMenu.as @@ -0,0 +1,176 @@ +package com.threerings.flex { + +import flash.display.DisplayObject; +import flash.display.DisplayObjectContainer; + +import flash.geom.Rectangle; + +import mx.controls.Menu; +import mx.core.mx_internal; +import mx.core.ScrollPolicy; +import mx.events.MenuEvent; + +/** + * A pretty standard menu that can submit CommandEvents if menu items + * have "command" and possibly "arg" properties. Commands are submitted to + * controllers for processing. Alternatively, you may specify + * "callback" properties that specify a function closure to call, with the + * "arg" property containing either a single arg or an array of args. + * + * Example dataProvider array: + * [ { label: "Go home", icon: homeIconClass, + * command: Controller.GO_HOME, arg: homeId }, + * { type: "separator"}, + { label: "Crazytown", callback: setCrazy, arg: [ true, false ] }, + * { label: "Other places", children: subMenuArray } + * ]; + * + * See "Defining menu structure and data" in the Flex manual for the + * full list. + */ +public class CommandMenu extends Menu +{ + public function CommandMenu () + { + super(); + + addEventListener(MenuEvent.ITEM_CLICK, itemClicked); +// addEventListener(MenuEvent.MENU_SHOW, handleMenuShown); + } + + /** + * Factory method to create a command menu. + * + * @param parent The parent of this menu. + * @param items an array of menu items. + */ + public static function createMenu ( + parent :DisplayObjectContainer, items :Array) :CommandMenu + { + var menu :CommandMenu = new CommandMenu(); + menu.tabEnabled = false; + menu.showRoot = true; + Menu.popUpMenu(menu, parent, items); + return menu; + } + + /** + * Actually pop up the menu. This can be used instead of show(). + */ + public function popUp ( + trigger :DisplayObject, popUpwards :Boolean = false) :void + { + var r :Rectangle = trigger.getBounds(trigger.stage); + + if (popUpwards) { + show(r.x, 0); + // then, reposition the y once we know our size + y = r.y - getExplicitOrMeasuredHeight(); + + } else { + // simply position it below the trigger + show(r.x, r.y + r.height); + } + } + + /** + * Callback for MenuEvent.ITEM_CLICK. + */ + protected function itemClicked (event :MenuEvent) :void + { + var arg :Object = getItemArgument(event.item); + var cmd :String = getItemCommand(event.item); + var fn :Function; + if (cmd == null) { + fn = getItemCallback(event.item); + } + if (cmd != null || fn != null) { + event.stopImmediatePropagation(); + if (cmd != null) { + CommandEvent.dispatch(mx_internal::parentDisplayObject, cmd, arg); + + } else { + try { + var args :Array = (arg as Array); + if (args == null && arg != null) { + args = [ arg ]; + } + fn.apply(null, args); + + } catch (err :Error) { + Log.getLog(this).warning("Unable to call menu callback: " + + event.item); + } + } + } + } + +// /** +// * Callback for MenuEvent.MENU_SHOW. +// */ +// protected function handleMenuShown (event :MenuEvent) :void +// { +// // just ensure that every menu can scroll +// event.menu.verticalScrollPolicy = ScrollPolicy.ON; +// } + + /** + * Get the command for the specified item, if any. + * Somewhat similar to bits in the DefaultDataDescriptor. + */ + protected function getItemCommand (item :Object) :String + { + try { + if (item is XML) { + return (item.@command as String); + + } else if (item is Object) { + return (item.command as String); + } + } catch (e :Error) { + // fall through + } + + return null; + } + + /** + * Get the callback function for the specified item, if any. + */ + protected function getItemCallback (item :Object) :Function + { + try { + if (item is XML) { + return (item.@callback as Function); + + } else if (item is Object) { + return (item.callback as Function); + } + } catch (e :Error) { + // fall through + } + + return null; + } + + /** + * Get the command for the specified item, if any. + * Somewhat similar to bits in the DefaultDataDescriptor. + */ + protected function getItemArgument (item :Object) :Object + { + try { + if (item is XML) { + return item.@arg; + + } else if (item is Object) { + return item.arg; + } + } catch (e :Error) { + // fall through + } + + return null; + } +} +} diff --git a/src/as/com/threerings/flex/CursorManager.as b/src/as/com/threerings/flex/CursorManager.as new file mode 100644 index 00000000..2d1cfd9d --- /dev/null +++ b/src/as/com/threerings/flex/CursorManager.as @@ -0,0 +1,301 @@ +package com.threerings.flex { + +import flash.display.DisplayObject; +import flash.display.InteractiveObject; +import flash.display.Sprite; +import flash.events.MouseEvent; +import flash.text.TextField; +import flash.text.TextFieldType; +import flash.ui.Mouse; + +import mx.core.Application; +import mx.managers.SystemManager; +import mx.styles.CSSStyleDeclaration; +import mx.styles.StyleManager; + +import com.threerings.util.HashMap; + +/** + * A better CursorManager. + */ +public class CursorManager +{ + public static const SYSTEM_CURSOR :int = 0; + + public static const BUSY_CURSOR :int = 1; + + //-------------------------------------------------------------------------- + // + // Class methods + // + //-------------------------------------------------------------------------- + + /** + * Makes the cursor visible. + * Cursor visibility is not reference-counted. + * A single call to the showCursor() method + * always shows the cursor regardless of how many calls + * to the hideCursor() method were made. + */ + public static function showCursor () :void + { + if (_currentCursorId == SYSTEM_CURSOR) { + Mouse.show(); + } else { + _cursorHolder.visible = true; + } + } + + /** + * Makes the cursor invisible. + * Cursor visibility is not reference-counted. + * A single call to the hideCursor() method + * always hides the cursor regardless of how many calls + * to the showCursor() method were made. + */ + public static function hideCursor () :void + { + _cursorHolder.visible = false; + Mouse.hide(); + } + + public static function getCurrentCursorId () :int + { + return _currentCursorId; + } + + /** + * Creates a new cursor. + * + * @param cursorClass Class of the cursor to display. + * + * @param xOffset Number that specifies the x offset + * of the cursor, in pixels, relative to the mouse pointer. + * The default value is 0. + * + * @param yOffset Number that specifies the y offset + * of the cursor, in pixels, relative to the mouse pointer. + * The default value is 0. + * + * @return The ID of the cursor. + */ + public static function addCursor ( + cursorClass :Class, xOffset :int = 0, yOffset :int = 0) :int + { + var cursorId :int = _nextCursorId++; + + var rec :CursorRecord = new CursorRecord(cursorClass); + rec.x = xOffset; + rec.y = yOffset; + + _cursors.put(cursorId, rec); + + return cursorId; + } + + /** + * Set the current cursor. + */ + public static function setCursor (id :int) :void + { + if (id == _currentCursorId) { + return; + } + + if (!_initialized) { + // oh, let's set it up + _systemManager = Application.application.systemManager; + + // set up the busy cursor + var cursorManagerStyleDeclaration :CSSStyleDeclaration = + StyleManager.getStyleDeclaration("CursorManager"); + var busyCursorClass :Class = + cursorManagerStyleDeclaration.getStyle("busyCursor"); + _busyCursor = new CursorRecord(busyCursorClass); + + // The first time a cursor is requested of the CursorManager, + // create a Sprite to hold the cursor symbol + _cursorHolder = new Sprite(); + _cursorHolder.mouseEnabled = false; + _systemManager.cursorChildren.addChild(_cursorHolder); + + _initialized = true; + } + + // figure out what the new cursor will be like + var rec :CursorRecord; + if (id == BUSY_CURSOR) { + rec = _busyCursor; + + } else if (id != SYSTEM_CURSOR) { + rec = (_cursors.get(id) as CursorRecord); + if (rec == null) { + // a bogus id was specified + // TODO: throw an error? + return; + } + } + + if (rec != null && (rec.cursor is Class)) { + // go ahead and instantiate the class + try { + var disp :DisplayObject = new (rec.cursor as Class); + rec.cursor = disp; + if (disp is InteractiveObject) { + (disp as InteractiveObject).mouseEnabled = false; + } + + } catch (err :Error) { + // this cursor is not usable, bail + return; + } + } + + // always remove any custom cursors from the hierarchy + if (_cursorHolder.numChildren > 0) { + _cursorHolder.removeChildAt(0); + } + + if (id != SYSTEM_CURSOR) { + Mouse.hide(); + var currentCursor :DisplayObject = (rec.cursor as DisplayObject); + _cursorHolder.addChild(currentCursor); + _cursorHolder.x = _systemManager.mouseX + rec.x; + _cursorHolder.y = _systemManager.mouseY + rec.y; + + _currentXOffset = rec.x; + _currentYOffset = rec.y; + _systemManager.stage.addEventListener( + MouseEvent.MOUSE_MOVE, mouseMoveHandler); + + + } else { + _currentXOffset = 0; + _currentYOffset = 0; + _systemManager.stage.removeEventListener( + MouseEvent.MOUSE_MOVE, mouseMoveHandler); + + Mouse.show(); + } + + _currentCursorId = id; + } + + /** + * Removes a cursor from the cursor list. + * If the cursor being removed is the currently displayed cursor, + * the CursorManager displays the next cursor in the list, if one exists. + * If the list becomes empty, the CursorManager displays + * the default system cursor. + * + * @param cursorID ID of cursor to remove. + */ + public static function removeCursor (id :int) :void + { + var rec :CursorRecord = (_cursors.remove(id) as CursorRecord); + + if (rec != null && id == _currentCursorId) { + setCursor(SYSTEM_CURSOR); + } + } + + /** + * Removes all of the cursors from the cursor list + * and restores the system cursor. + */ + public static function removeAllCursors () :void + { + _cursors.clear(); + + if (_currentCursorId != BUSY_CURSOR) { + setCursor(SYSTEM_CURSOR); + } + } + + //-------------------------------------------------------------------------- + // + // Class event handlers + // + //-------------------------------------------------------------------------- + + /** + * @private + */ + private static function mouseMoveHandler (event :MouseEvent) :void + { + _cursorHolder.x = _systemManager.mouseX + _currentXOffset; + _cursorHolder.y = _systemManager.mouseY + _currentYOffset; + + var target :Object = event.target; + + // Do target test. + if (!_overTextField && + target is TextField && target.type == TextFieldType.INPUT) { + _overTextField = true; + _showSystemCursor = true; + + } else if (_overTextField && + !(target is TextField && target.type == TextFieldType.INPUT)) { + _overTextField = false; + _showCustomCursor = true; + } + + // Handle switching between system and custom cursor. + if (_showSystemCursor) { + _showSystemCursor = false; + _cursorHolder.visible = false; + Mouse.show(); + } + if (_showCustomCursor) { + _showCustomCursor = false; + _cursorHolder.visible = true; + Mouse.hide(); + } + } + + /** A mapping of all assigned cursor ids. */ + private static const _cursors :HashMap = new HashMap(); + + private static var _currentCursorId :int = 0; // SYSTEM_CURSOR + + private static var _nextCursorId :int = 2; // skip BUSY + + private static var _initialized :Boolean = false; + + private static var _cursorHolder :Sprite; + + private static var _currentXOffset :int = 0; + private static var _currentYOffset :int = 0; + + /** A record for the busy cursor (where it can't be removed). */ + private static var _busyCursor :CursorRecord; + + private static var _overTextField :Boolean = false; + + private static var _overLink :Boolean = false; + + private static var _showSystemCursor :Boolean = false; + + private static var _showCustomCursor :Boolean = false; + + private static var _systemManager :SystemManager = null; +} +} + + +/** + */ +class CursorRecord extends Object +{ + public function CursorRecord (clazz :Class) + { + cursor = clazz; + } + + /** The class, or instantiated cursor. */ + public var cursor :Object + + /** The x/y offset for the hotspot. */ + public var x :Number; + public var y :Number; +} diff --git a/src/as/com/threerings/flex/FunctionEffect.as b/src/as/com/threerings/flex/FunctionEffect.as new file mode 100644 index 00000000..9e669252 --- /dev/null +++ b/src/as/com/threerings/flex/FunctionEffect.as @@ -0,0 +1,31 @@ +package com.threerings.flex { + +import mx.effects.IEffectInstance; +import mx.effects.Effect; + +public class FunctionEffect extends Effect +{ + /** The function to call. */ + public var func :Function; + + /** The arguments to pass to the function. */ + public var args :Array; + + public function FunctionEffect (target :Object = null) + { + super(target); + + instanceClass = FunctionEffectInstance; + } + + // documentation inherited + override protected function initInstance (instance :IEffectInstance) :void + { + super.initInstance(instance); + + var fe :FunctionEffectInstance = (instance as FunctionEffectInstance); + fe.func = func; + fe.args = args; + } +} +} diff --git a/src/as/com/threerings/flex/FunctionEffectInstance.as b/src/as/com/threerings/flex/FunctionEffectInstance.as new file mode 100644 index 00000000..88bef0df --- /dev/null +++ b/src/as/com/threerings/flex/FunctionEffectInstance.as @@ -0,0 +1,29 @@ +package com.threerings.flex { + +import mx.effects.effectClasses.ActionEffectInstance; + +public class FunctionEffectInstance extends ActionEffectInstance +{ + public function FunctionEffectInstance (target :Object) + { + super(target); + } + + /** The function to call. */ + public var func :Function; + + /** The args to pass. */ + public var args :Array; + + override public function play () :void + { + super.play(); + + // call the function! + func.apply(null, args); + + // and we're done + finishRepeat(); + } +} +} diff --git a/src/as/com/threerings/flex/ScrollBox.as b/src/as/com/threerings/flex/ScrollBox.as new file mode 100644 index 00000000..4a795fce --- /dev/null +++ b/src/as/com/threerings/flex/ScrollBox.as @@ -0,0 +1,147 @@ +package com.threerings.flex { + +import flash.display.DisplayObject; +import flash.display.Graphics; +import flash.display.Sprite; + +import flash.events.Event; +import flash.events.MouseEvent; + +import flash.geom.Matrix; +import flash.geom.Rectangle; + +import mx.containers.Canvas; + +/** + * A control for scrolling some component separately. + */ +public class ScrollBox extends Canvas +{ + public function ScrollBox ( + target :DisplayObject, maxWidth :int, maxHeight :int) + { + _target = target; + _maxWidth = maxWidth; + _maxHeight = maxHeight; + + opaqueBackground = 0xFFFFFF; + + _box = new Sprite(); + rawChildren.addChild(_box); + + recheckBounds(); + + // TODO: only when something changes + addEventListener(Event.ENTER_FRAME, enterFrame); + + _box.addEventListener(MouseEvent.MOUSE_DOWN, spritePressed); + } + + override public function setActualSize (w :Number, h :Number) :void + { + super.setActualSize(w, h); + + recheckBounds(); + + graphics.clear(); + graphics.lineStyle(1, 0); + graphics.drawRect(0, 0, width, height); + } + + protected function enterFrame (vent :Event) :void + { + if (!_dragging) { + recheckBounds(); + } + } + + /** + * Get the bounds of the scrollable area. Broken-out for easy overridding. + */ + protected function getScrollBounds () :Rectangle + { + var bounds :Rectangle = _target.transform.pixelBounds; +// var m :Matrix = _target.transform.concatenatedMatrix; +// bounds.topLeft = m.transformPoint(bounds.topLeft); +// bounds.bottomRight = m.transformPoint(bounds.bottomRight); + return bounds; + } + + protected function recheckBounds () :void + { + var bounds :Rectangle = getScrollBounds(); + + // see what's visible, what's not.. + var scroller :Rectangle = _target.scrollRect; + if (scroller == null) { + scroller = bounds.clone(); + } + + if ((_bounds != null) && _bounds.equals(bounds) && + (_scroller != null) && _scroller.equals(scroller)) { + return; + } + + _bounds = bounds.clone(); + _scroller = scroller; + + _scale = Math.min(1, + Math.min(_maxWidth / _bounds.width, _maxHeight / _bounds.height)); + width = _scale * _bounds.width; + height = _scale * _bounds.height; + + drawBox(_box, _scroller.width * _scale, _scroller.height * _scale); + + _box.x = (_scroller.x - _bounds.x) * _scale; + _box.y = (_scroller.y - _bounds.y) * _scale; + } + + protected function drawBox (box :Sprite, ww :Number, hh :Number) :void + { + var g :Graphics = _box.graphics; + g.clear(); + g.beginFill(0x0000FF); + g.drawRect(0, 0, ww, hh); + g.endFill(); + } + + protected function spritePressed (evt :MouseEvent) :void + { + _box.addEventListener(Event.ENTER_FRAME, boxUpdate); + _box.startDrag(true, + new Rectangle(0, 0, width - _box.width, height - _box.height)); + stage.addEventListener(MouseEvent.MOUSE_UP, spriteReleased); + _dragging = true; + } + + protected function spriteReleased (evt :MouseEvent) :void + { + stage.removeEventListener(MouseEvent.MOUSE_UP, spriteReleased); + _box.stopDrag(); + _box.removeEventListener(Event.ENTER_FRAME, boxUpdate); + _dragging = false; + } + + protected function boxUpdate (evt :Event) :void + { + var r :Rectangle = _target.scrollRect; + r.x = (_box.x / _scale) + _bounds.x; + r.y = (_box.y / _scale) + _bounds.y; + _target.scrollRect = r; + } + + protected var _dragging :Boolean; + + protected var _target :DisplayObject; + + protected var _maxWidth :int; + protected var _maxHeight :int; + + protected var _box :Sprite; + + protected var _scale :Number; + + protected var _bounds :Rectangle; + protected var _scroller :Rectangle; +} +}