I'd like to implement a custom drag on an AdvancedDataGrid tree structure, which only allows the drag on the branches (not leaves).
I'm having much difficultly with this, trying to use the MouseDown event, but not having luck!
Okay, I think I've got the mousedown able to figure out if the item is a branch of leaf. Any help on how to perform the custom drag with an advanceddatagrid??
var grid : AdvancedDataGrid = AdvancedDataGrid(event.currentTarget);
if(grid.selectedItem.hasOwnProperty("categories")
&& grid.selectedItem.categories!=null) {
Alert.show("yes");
}
else { Alert.show("no"); }
I've tried many different ways to do this, and this is the best solution that I have come up with. AdvancedDataGrid already has logic built in to handle the mouseMove/mouseOver/mouseDown events, so there's no need to recreate it. Simply override the DragEvent handlers.
I have duplicated the code I use in several applications that need similar functionality, included your logic for determining if the item can be dragged, as well as some logic to determine if it can be dropped in a certain location. I have also included extensive commenting to help explain why certain things are done. I hope this helps!
CustomADG.as:
package
{
import mx.controls.AdvancedDataGrid;
import mx.controls.listClasses.IListItemRenderer;
import mx.core.DragSource;
import mx.events.DragEvent;
import mx.managers.DragManager;
import mx.utils.ObjectUtil;
public class DragDropADG extends AdvancedDataGrid
{
private var itemRendererUnderPoint:IListItemRenderer
public function DragDropADG()
{
super();
}
override protected function dragStartHandler(event:DragEvent):void
{
/* Create a new Array from the Array of selectedItems, filtering out items
that are not "branches". */
var selectedBranches:Array /* of Object */ =
selectedItems.filter(hasCategories);
function hasCategories(element:*, index:int, array:Array):Boolean
{
/* Returns true if the item is a Branch (has children in the categories
property). */
return (element.hasOwnProperty("categories") &&
element.categories != null);
}
/* Exit if no Branches are selected. This will stop the drag operation from
starting. */
if (selectedBranches.length == 0)
return;
/* Reset the selectedItems Array to include only selected Branches. This
will deselect any "non-Branch" items. */
selectedItems = selectedBranches;
/* Create a copy of the Array of selected indices to be sorted for
display in the drag proxy. */
var sortedSelectedIndices:Array /* of int */ =
ObjectUtil.copy(selectedIndices) as Array /* of int */;
// Sort the selected indices
sortedSelectedIndices.sort(Array.NUMERIC);
/* Create an new Array to store the selected Branch items sorted in the
order that they are displayed in the AdvancedDataGrid. */
var draggedBranches:Array = [];
var itemRendererAtIndex:IListItemRenderer;
for each (var index:int in sortedSelectedIndices)
{
itemRendererAtIndex = indexToItemRenderer(index);
var branchItem:Object = itemRendererAtIndex.data;
draggedBranches.push(branchItem);
}
// Create a new DragSource Object to store data about the Drag operation.
var dragSource:DragSource = new DragSource();
// Add the Array of Branches to be dragged to the DragSource Object.
dragSource.addData(draggedBranches, "draggedBranches");
// Create a new Container to serve as the Drag Proxy.
var dragProxy:DragProxyContainer = new DragProxyContainer();
/* Update the labels in the Drag Proxy using the "label" field of the items
being dragged. */
dragProxy.setLabelText(draggedBranches);
/* Create a point relative to this component from the mouse
cursor location (for the DragEvent). */
var eventPoint:Point = new Point(event.localX, event.localY);
// Initiate the Drag Event
DragManager.doDrag(this, dragSource, event, dragProxy,
-eventPoint.x, -eventPoint.y, 0.8);
}
/* This function runs when ANY item is dragged over any part of this
AdvancedDataGrid (even if the item is from another component). */
override protected function dragEnterHandler(event:DragEvent):void
{
/* If the item(s) being dragged does/do not contain dragged Branches,
it/they are being dragged from another component; exit the function to
prevent a drop from occurring. */
if (!event.dragSource.hasFormat("draggedBranches"))
return;
var dropIndex:int = calculateDropIndex(event);
/* Get the itemRenderer of the current drag target, to determine if the
drag target can accept a drop. */
var dropTargetItemRenderer:IListItemRenderer =
indexToItemRenderer(dropIndex);
/* If the item is being dragged where there is no itemRenderer, exit the
function, to prevent a drop from occurring. */
if (dropTargetItemRenderer == null)
return;
/* If the item is being dragged onto an itemRenderer with no data, exit the
function, to prevent a drop from occurring. */
if (dropTargetItemRenderer.data == null)
return;
/* Store the underlying item for the itemRenderer being dragged over, to
validate that it can be dropped there. */
var dragEnterItem:Object = dropTargetItemRenderer.data
if (!dragEnterItem.hasOwnProperty("categories")
return;
if (dragEnterItem.categories == null)
return;
var eventDragSource:DragSource = event.dragSource;
eventDragSource.addData(dragEnterItem, "dropTargetItem");
/* Add an dragDrop Event Listener to the itemRenderer so that the
necessary will run when it is dropped.*/
dropTargetItemRenderer.addEventListener(DragEvent.DRAG_DROP,
itemRenderer_dragDropHandler);
// Specify that the itemRenderer being dragged over can accept a drop.
DragManager.acceptDragDrop(dropTargetItemRenderer);
}
/* Perform any logic that you want to occur once the user drops the item. */
private function itemRenderer_dragDropHandler(event:DragEvent):void
{
var eventDragSource:DragSource = event.dragSource;
var dropTargetItem:Object =
eventDragSource.dataForFormat("dropTargetItem");
if (dropTargetItem == null)
return;
var draggedBranchesData:Object =
eventDragSource.dataForFormat("draggedBranches");
var draggedBranches:Array /* of Object */ =
draggedBranchesData as Array /* of Object */;
// Call any other functions to update your underlying data, etc.
}
}
}
DragProxyContainer.as:
package
{
import mx.containers.VBox;
import mx.core.UITextField;
[Bindable]
public class DragProxyContainer extends VBox
{
private var textField:UITextField = new UITextField();
public function DragProxyContainer()
{
super();
minWidth = 150;
addChild(textField);
}
public function setLabelText(items:Array, labelField:String = "label"):void
{
var labelText:String;
var numItems:int = items.length;
if (numItems > 1)
{
labelText = numItems.toString() + " items";
}
else
{
var firstItem:Object = items[0];
labelText = firstItem[labelField];
}
textField.text = labelText;
}
}
}
You can try handling the dragEnter() event to stop dragging if the object is a leaf.
Related
Is there any workaround to create submenu in a flex context menu other than stopping right click from javascript.
Regards,
Hi Frank,
Yes, I want to create submenus in a context menu. Can you help me here.
Regards,
Hi Frank,
I need the context menu for the application not for datagrid.
In my initial question the phrase "other than stopping right click from javascript" means
"catch the right click in html, call a javascript function and over js call a as function."
The project that you have specified does the above procedure. I don't want to use this
procedure. Is there any other way for achieving submenus in a flex context menu. Could you
please tell me if so..
Regards,
Arvind
Yes, there is.
I don't know, what you exactly mean with this:
other than stopping right click from
javascript.
But, if you want to create a entry in submenu, do this:
//Instance of my own class
private var myContext:myContextMenu = new myContextMenu();
application.contextMenu = myContext.myContextMenu;
//Here is the Class:
package com.my.components
{
/* ////////////////////////////////////////////
///// My Context MenĂ¼ /////////////////////
///////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
//to use: //
// private var myContext:MyContextMenu = new MyContextMenu(); //
// init() in creationComplete //
// application.contextMenu = myContext.myContextMenu; //
////////////////////////////////////////////////////////////////////////////// */
import flash.display.Sprite;
import flash.events.ContextMenuEvent;
import flash.net.URLRequest;
import flash.net.navigateToURL;
import flash.text.TextField;
import flash.ui.ContextMenu;
import flash.ui.ContextMenuBuiltInItems;
import flash.ui.ContextMenuItem;
public class MyContextMenu extends Sprite
{
public var myContextMenu:ContextMenu;
private var menuLabel:String = String.fromCharCode(169)+" My Company GmbH";
public function MyContextMenu()
{
myContextMenu = new ContextMenu;
removeDefaultItems();
addCustomItems();
myContextMenu.addEventListener(ContextMenuEvent.MENU_SELECT, menuSelectHandler);
super();
}
private function removeDefaultItems():void
{
myContextMenu.hideBuiltInItems();
var defaultItems:ContextMenuBuiltInItems = myContextMenu.builtInItems;
defaultItems.print = true;
}
private function addCustomItems():void
{
var item:ContextMenuItem = new ContextMenuItem(menuLabel);
myContextMenu.customItems.push(item);
item.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT,menuItemSelectHandler);
}
private function menuSelectHandler(event:ContextMenuEvent):void
{
}
private function menuItemSelectHandler(event:ContextMenuEvent):void
{
navigateToURL(new URLRequest('http://www.my-company.de'));
}
private function createLabel():TextField
{
var txtField:TextField = new TextField();
//txtField.text = textLabel;
txtField.text = "RightClickHere";
return txtField;
}
}
}
Have fun
EDIT:
There is an interesting project here. They catch the right click in html, call a javascript function and over js call a as function.
Unfortunately, the limitation of FP or NativeMenu APi allowed just on level contextmenu. Read here
Frank
my mission is to select an item in a DataGrid instance with nothing but the coordinates on screen.
We are implementing right-click functionality in our Flash application, with the goal of being able to right-click a DG row, which would select that row plus show a popup window containing some context commands.
I have managed to get the right click event into my Flex app with the help of this site.
Further progress so far has been to obtain the DataGrid instance via
var objects : Array = this.getObjectsUnderPoint(new Point(this.mouseX, this.mouseY));
and then to investige each of the array's items, for one of those 'parent.parentList' refers to the DataGrid instance.
Now I am stuck -- I couldn't find any point-to-item converter function or anything. Any comments about my approach so far very welcome, too!
Thanks!
PS: Using the standard Flash ContextMenu is, unfortunately, not an option.
/**
* Let mx and my be the mouse coordinates
* (relative to the stage, not relative to the clicked object)
* */
var len:Number = dg.dataProvider.length;
var i:Number;
var p1:Point;
var p2:Point;
var renderer:DisplayObject;
for(i = 0; i < len; i++)
{
renderer = DisplayObject(dg.indexToItemRenderer(i));
if(!renderer)//item is not displayed (scroll to view it)
continue;
p1 = new Point(renderer.x, renderer.y);
p2 = new Point(renderer.width, renderer.height);
p1 = renderer.parent.localToGlobal(p1);
p2 = renderer.localToGlobal(p2);
if(mx >= p1.x && mx <= p2.x && my >= p1.y && my <= p2.y)
{
trace("You clicked on " + dg.dataProvider.getItemAt(i));
break;
}
}
You can attach the ContextMenu to the DataGrid's itemRenderer instead - that way you can get the right-clicked item from the event's currentTarget property. As simple as it can get.
You could use the itemRollOver event (and the related itemRollOut) to keep track of the most recent item that the mouse was over. Just save the item in a variable. When you display the context menu, you can use the saved item directly rather than trying to find it based on the (x,y) coordinates.
Here is the complete AS3 code for the Flash side of things. Note that you also need Javascript in your embedding HTML to make it work.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="horizontal"
minWidth="1024" minHeight="768"
creationComplete="onAppCreationComplete()"
click="onRightClick()"
>
<mx:DataGrid
id="dgTest"
dataProvider="{['aaa','bbbbbbbbbbbbbbb']}"
>
<mx:columns>
<mx:DataGridColumn />
</mx:columns>
</mx:DataGrid>
<mx:Script>
<![CDATA[
import mx.binding.utils.BindingUtils;
import mx.controls.Alert;
import mx.controls.Menu;
import mx.effects.Fade;
import mx.events.MenuEvent;
[Bindable]
public var customContextMenuItem : Object;
public var customContextMenu : Menu;
protected function onAppCreationComplete () : void
{
ExternalInterface.addCallback("rightClick", onRightClick);
this.customContextMenu = this.createCustomContextMenu();
}
protected function onRightClick () : void
{
// find datagrid at mouse click coords
var dg : DataGrid = this.getDataGridFromObjectsUnderPoint(this.mouseX, this.mouseY);
if (dg) {
// if any, find clicked item
this.customContextMenuItem = this.findClickedItem(this.mouseX, this.mouseY, dg);
if (this.customContextMenuItem) {
// right clicking an item with the menu already showing does not show a new menu
// unless the previous one is hidden first
this.customContextMenu.hide();
this.customContextMenu.show(this.mouseX+3, this.mouseY+2);
}
}
}
protected function getDataGridFromObjectsUnderPoint (x:Number, y:Number) : DataGrid
{
var objectsHere : Array = this.getObjectsUnderPoint(new Point(this.mouseX, this.mouseY));
for each (var dispObj:DisplayObject in objectsHere) {
while (dispObj) {
if (dispObj is DataGrid)
return dispObj as DataGrid;
dispObj = dispObj.parent;
}
}
return null;
}
/**
* Returns a dataProvider item that displays at the given coords for the given dataGrid.
* Code provided by Stackoverflow user http://stackoverflow.com/users/165297/amarghosh,
* thanks a lot!
*/
protected function findClickedItem (x:Number, y:Number, dg:DataGrid) : Object
{
var p1 : Point;
var p2 : Point;
var renderer : DisplayObject;
for(var i:int=0; i<dg.dataProvider.length; i++) {
renderer = DisplayObject(dg.indexToItemRenderer(i));
if (!renderer) //item is not displayed (scroll to view it)
continue;
p1 = new Point(renderer.x, renderer.y);
p2 = new Point(renderer.width, renderer.height);
p1 = renderer.parent.localToGlobal(p1);
p2 = renderer.localToGlobal(p2);
if(x >= p1.x && x <= p2.x && y >= p1.y && y <= p2.y)
return dg.dataProvider.getItemAt(i);
}
return null;
}
protected function createCustomContextMenu () : Menu
{
// create a dynamic-object as our first menu item entry, and use data binding
// to dynamically populate the 'title' value whenever our right-clicked item
// has changed
var menuItem : Object = new Object();
menuItem.title = "default";
BindingUtils.bindSetter(function (item:Object) : void {
trace(item);
menuItem.title = "Edit '" + item + "'";
}, this, ["customContextMenuItem"]);
var dataProvider : Array = [ menuItem, {title:"Exit"} ];
// create a nicely styled menu that looks very different to the standard Flash menu
var menu : Menu = Menu.createMenu(this, dataProvider, false);
menu.setStyle("fontWeight", "bold");
menu.setStyle("backgroundColor", 0x000000); // standard back/foreground
menu.setStyle("color", 0xf0f0f0);
menu.setStyle("rollOverColor", 0x444444); // mouse hover back/foreground
menu.setStyle("textRollOverColor", 0xffffff);
menu.setStyle("selectionColor", 0x444444); // mouse click back/foreground
menu.setStyle("textSelectedColor", 0xe18c31);
menu.setStyle("openDuration", 0);
menu.labelField = "title";
// we want to react to clicks in the menu
menu.addEventListener(MenuEvent.ITEM_CLICK, function (event:MenuEvent) : void {
Alert.show("Menu item clicked - clicked item title '" + event.item.title + "'");
});
// done
return menu;
}
]]>
</mx:Script>
</mx:Application>
dg is the dataGrid.
The coordinate of the top of the row in dg's contents system (i.e. from the header's bottom) is:
var topOfRow:int = ( int(dg.mouseY / dg.rowHeight) -1 ) * dg.rowHeight;
You can now adjust to other coordinate systems:
For example to dg's system:
topOfRow += dh.headerHeight;
Or use localToGlobal() or whatever.
I think there is a simple solution to this question, just not simple enough for me to find it.
Question:
How do you constrain a TitleWindow in Flex 3 from being dragged off the screen/stage? Is there a way to restrict the TitleWindow to the viewing area?
Example: Let's say I have an application that take 100% of the screen. Next, I create a TitleWindow via the PopUpManager. I can then proceed to click and hold (drag) that window off the screen, then release the mouse button. That window is now lost off-screen somewhere. Is there a way to keep the window from being dragged beyond the viewing area?
Thanks for the help in advance.
this is a very old post, but here's another way of doing it:
Whether you are extending the component or not, in the TitleWindow definition add the following line: move:"doMove(event)"
Import the Application library (import mx.core.Application;)
and add the doMove function:
private function doMove(event:Event):void
{//keeps TW inside layout
var appW:Number=Application.application.width;
var appH:Number=Application.application.height;
if(this.x+this.width>appW)
{
this.x=appW-this.width;
}
if(this.x<0)
{
this.x=0;
}
if(this.y+this.height>appH)
{
this.y=appH-this.height;
}
if(this.y<0)
{
this.y=0;
}
}
For flex 4 the answer is here: http://blog.flexexamples.com/2010/01/20/constraining-the-movement-on-a-spark-titlewindow-container-in-flex-4/
You can set its isPopUp property to false to prevent it from being dragged in the first place.
var popupWin:TitleWindow = PopUpManager.createPopUp(this, TitleWindow);
PopUpManager.centerPopUp(popupWin);
popupWin.isPopUp = false;
I don't know if the DragManager class in flex supports bounds checking, but if you really want to allow dragging but limit its bounds, you can still set isPopUp to false and implement the dragging code yourself so that the component never goes outside the limits specified by you. Check startDrag() method for an example. Bounds rectangle is the key.
Flex 4
<s:TitleWindow xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
windowMoving="windowMovingHandler(event)">
.
.
.
protected function windowMovingHandler(event:TitleWindowBoundsEvent):void
{
var appBounds:Rectangle = parentApplication.getBounds(DisplayObject(parentApplication));
if(!appBounds.containsRect(event.afterBounds)){
event.preventDefault();
}
}
// for better precision, corect appBounds manualy, or, instead of "parentApplication.getBounds..." create new rectangle of your application size
Subclass the TitleWindow and add a canvas over the title bar as a drag proxy. Then you can explicity call startDrag with a boundary rectangle.
This is pretty skeletal, but should put you on the path...
The reason for the proxy is you may get some weird behavior when you click the titleBar label if you don't have the canvas over it.
public class MyTitleWindow extends TitleWindow
{
public var titleBarOverlay:Canvas;
override protected function createChildren():void
{
super.createChildren();
if(!titleBarOverlay)
{
titleBarOverlay = new Canvas();
titleBarOverlay.width = this.width;
titleBarOverlay.height = this.titleBar.height;
titleBarOverlay.alpha = 0;
titleBarOverlay.setStyle("backgroundColor", 0x000000);
rawChildren.addChild(titleBarOverlay);
}
addListeners();
}
override protected function updateDisplayList(w:Number, h:Number):void
{
super.updateDisplayList(w, h);
titleBarOverlay.width = this.width;
titleBarOverlay.height = this.titleBar.height;
}
private function addListeners():void
{
titleBarOverlay.addEventListener(MouseEvent.MOUSE_DOWN, onTitleBarPress, false, 0, true);
titleBarOverlay.addEventListener(MouseEvent.MOUSE_UP, onTitleBarRelease, false, 0, true);
}
private function onTitleBarPress(event:MouseEvent):void
{
// Here you can set the boundary using owner, parent, parentApplication, etc.
this.startDrag(false, new Rectangle(0, 0, parent.width - this.width, parent.height - this.height));
}
private function onTitleBarRelease(event:Event):void
{
this.stopDrag();
}
}
You could simply override the move function and prevent "illegal" movement (it is called internally by the Panel drag management).
I think that you also should listen on stage resize, because reducing it (e.g. if the user resize the browser window) could send your popup out of stage even without actually moving it.
public class MyTitleWindow extends TitleWindow {
public function MyTitleWindow() {
// use a weak listener, or remember to remove it
stage.addEventListener(Event.RESIZE, onStageResize,
false, EventPriority.DEFAULT, true);
}
private function onStageResize(event:Event):void {
restoreOutOfStage();
}
override public function move(x:Number, y:Number):void {
super.move(x, y);
restoreOutOfStage();
}
private function restoreOutOfStage():void {
// avoid the popup from being positioned completely out of stage
// (use the actual width/height of the popup instead of 50 if you
// want to keep your *entire* popup on stage)
var topContainer:DisplayObjectContainer =
Application.application.parentDocument;
var minX:int = 50 - width;
var maxX:int = topContainer.width - 50;
var minY:int = 0;
var maxY:int = topContainer.height - 50;
if (x > maxX)
x = maxX
else if (x < minX)
x = minX;
if (y > maxY)
y = maxY
else if (y < minY)
y = minY;
}
}
In your TitleWindow's creationComplete handler add the following:
this.moveArea.visible=false;
This will do the job.
On the other hand, if you have a custom skin, you can remove the "moveArea" part. This should work, too.
I have an arrayCollection of objects that extend Sprite, and have bitmaps within them.
I want to display these in a list (or some other component that would allow a user to scroll through them, and see their associated data.)
When I do: myList.dataProvider = myArrayCollection
the list just shows a bunch of lines of [Object, Item] instead of the visual sprites.
Here is a simplified version of my Object:
public class myUIC extends UIComponent
{
public var mySprite:Sprite = new Sprite;
[Embed(source="assets/BGimage.png")]
public var BGimage:Class;
public var myBitmap:Bitmap;
public var wordText:TextField = new TextField;
public function myUIC(myWord:String)
{
this.wordText.text = myWord;
this.myBitmap = new BGimage;
this.mySprite.addChild(this.myBitmap);
this.mySprite.addChild(this.wordText);
this.addChild(this.mySprite);
}
}
Tried many different ways to get it to show up in a List, but can't do it.
See this tutorial: Flex Examples - displaying icons in a flex list control
Sounds like you may want to try writing a simple item renderer (perhaps based off UIComponent) that adds the associated sprite the display list of the render using addChild().
try rawChildren.addChild for adding the Sprite
Here, try using an itemRenderer something like this. It ought to work with any generic DisplayObject. It's grabbing the width and height from the assigned data property, so you might need to set variableRowHeight to true in your actual list for it to work as expected.
package
{
import flash.display.DisplayObject;
import mx.controls.listClasses.IListItemRenderer;
import mx.core.UIComponent;
import mx.events.FlexEvent;
/*
Extending UIComponent means we can add Sprites (or any DisplayObject)
with addChild() directly, instead of going through the rawChildren property.
Plus, in this case, we don't need the extra overhead of Canvas's layout code.
IListItemRenderer lets us use it as a List's itemRenderer. UIComponent already
implements all of IListItemRenderer except for the data property
*/
public class SpriteRenderer extends UIComponent implements IListItemRenderer
{
// Implementing the data property for IListItemRenderer is really easy,
// you can find example code in the LiveDocs for IDataRenderer
private var _data:Object;
[Bindable("dataChange")]
public function get data():Object
{
return _data;
}
public function set data(value:Object):void
{
if (value !== _data) {
// We need to make sure to remove any previous data object from the child list
// since itemRenderers are recycled
if (_data is DisplayObject && contains(_data as DisplayObject)) {
removeChild(_data as DisplayObject);
}
_data = value;
// Now we just make sure that the new data object is something we can add
// and add it
if (_data is DisplayObject) {
this.width = (_data as DisplayObject).width;
this.height = (_data as DisplayObject).height;
addChild(_data as DisplayObject);
}
dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE));
}
}
public function SpriteRenderer()
{
super();
}
}
}
I am using a custom item renderer in a combobox to display a custom drawing instead of the default text label.
This works fine for the dropdown list but the displayed item ( when the list is closed) is still the textual representation of my object.
Is there a way to have the displayed item rendered the same way as the one in the dropdown?
By default you cannot do this. However, if you extend ComboBox you can add this functionality easily. Here is a quick example, it is a rough version and probably needs testing / tweaking but it shows how you could accomplish this.
package
{
import mx.controls.ComboBox;
import mx.core.UIComponent;
public class ComboBox2 extends ComboBox
{
public function ComboBox2()
{
super();
}
protected var textInputReplacement:UIComponent;
override protected function createChildren():void {
super.createChildren();
if ( !textInputReplacement ) {
if ( itemRenderer != null ) {
//remove the default textInput
removeChild(textInput);
//create a new itemRenderer to use in place of the text input
textInputReplacement = itemRenderer.newInstance();
addChild(textInputReplacement);
}
}
}
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
super.updateDisplayList(unscaledWidth, unscaledHeight);
if ( textInputReplacement ) {
textInputReplacement.width = unscaledWidth;
textInputReplacement.height = unscaledHeight;
}
}
}
}
I tried the above solution, but found that the selectedItem did not display when the combobox was closed. A extra line of code was required to bind the itemRenderer data property to the selectedItem:
if ( !textInputReplacement ) {
if ( itemRenderer != null ) {
//remove the default textInput
removeChild(textInput);
//create a new itemRenderer to use in place of the text input
textInputReplacement = itemRenderer.newInstance();
// ADD THIS BINDING:
// Bind the data of the textInputReplacement to the selected item
BindingUtils.bindProperty(textInputReplacement, "data", this, "selectedItem", true);
addChild(textInputReplacement);
}
}
I've extended Dane's code a bit further. In some cases clicking did not open the drop box with my renderer and I noticed that the normal Flex ComboBox skins did not fire. Thus in replaceTextInput() I added some additional event listeners and save a reference to the ComboBox button used to display the skins. Now it behaves just like the normal ComboBox.
Here's the code:
package
{
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import mx.binding.utils.BindingUtils;
import mx.controls.Button;
import mx.controls.ComboBox;
import mx.core.IFactory;
import mx.core.UIComponent;
import mx.events.DropdownEvent;
/**
* Extension of the standard ComboBox that will use the assigned 'itemRenderer'
* for both the list items and the selected item.
*
* Based on code from:
* http://stackoverflow.com/questions/269773/flex-custom-item-renderer-for-the-displayed-item-in-the-combobox
*/
public class ComboBoxFullRenderer extends ComboBox
{
protected var textInputReplacement:UIComponent;
private var _increaseW:Number = 0;
private var _increaseH:Number = 0;
/**
* Keeps track of the current open/close state of the drop down list.
*/
protected var _isOpen:Boolean = false;
/**
* Stores a reference to the 'Button' which overlays the ComboBox. Allows
* us to pass events to it so skins are properly triggered.
*/
protected var _buttonRef:Button = null;
/**
* Constructor.
*/
public function ComboBoxFullRenderer() {
super();
}
/**
* Sets a value to increase the width of our ComboBox to adjust sizing.
*
* #param val Number of pixels to increase the width of the ComboBox.
*/
public function set increaseW(val:Number):void {
_increaseW = val;
}
/**
* Sets a value to increase the height of our ComboBox to adjust sizing.
*
* #param val Number of pixels to increase the height of the ComboBox.
*/
public function set increaseH(val:Number):void {
_increaseH = val;
}
/**
* Override the 'itemRenderer' setter so we can also replace the selected
* item renderer.
*
* #param value The renderer to be used to display the drop down list items
* and the selected item.
*/
override public function set itemRenderer(value:IFactory):void {
super.itemRenderer = value;
replaceTextInput();
}
/**
* Override base 'createChildren()' routine to call our 'replaceTextInput()'
* method to replace the standard selected item renderer.
*
* #see #replaceTextInput();
*/
override protected function createChildren():void {
super.createChildren();
replaceTextInput();
}
/**
* Routine to replace the ComboBox 'textInput' child with our own child
* that will render the selected data element. Will create an instance of
* the 'itemRenderer' set for this ComboBox.
*/
protected function replaceTextInput():void {
if ( !textInputReplacement ) {
if ( this.itemRenderer != null && textInput != null ) {
//remove the default textInput
removeChild(textInput);
//create a new itemRenderer instance to use in place of the text input
textInputReplacement = this.itemRenderer.newInstance();
// Listen for clicks so we can open/close the drop down when
// renderer components are clicked.
textInputReplacement.addEventListener(MouseEvent.CLICK, _onClick);
// Listen to the mouse events on our renderer so we can feed them to
// the ComboBox overlay button. This will make sure the button skins
// are activated. See ComboBox::commitProperties() code.
textInputReplacement.addEventListener(MouseEvent.MOUSE_DOWN, _onMouseEvent);
textInputReplacement.addEventListener(MouseEvent.MOUSE_UP, _onMouseEvent);
textInputReplacement.addEventListener(MouseEvent.ROLL_OVER, _onMouseEvent);
textInputReplacement.addEventListener(MouseEvent.ROLL_OUT, _onMouseEvent);
textInputReplacement.addEventListener(KeyboardEvent.KEY_DOWN, _onMouseEvent);
// Bind the data of the textInputReplacement to the selected item
BindingUtils.bindProperty(textInputReplacement, "data", this, "selectedItem", true);
// Add our renderer as a child.
addChild(textInputReplacement);
// Listen for open close so we can maintain state. The
// 'isShowingDropdown' property is mx_internal so we don't
// have access to it.
this.addEventListener(DropdownEvent.OPEN, _onOpen);
this.addEventListener(DropdownEvent.CLOSE, _onClose);
// Save a reference to the mx_internal button for the combo box.
// We will need this so we can call its dispatchEvent() method.
for (var i:int = 0; i < this.numChildren; i++) {
var temp:Object = this.getChildAt(i);
if (temp is Button) {
_buttonRef = temp as Button;
break;
}
}
}
}
}
/**
* Detect open events on the drop down list to keep track of the current
* drop down state so we can react properly to a click on our selected
* item renderer.
*
* #param event The DropdownEvent.OPEN event for the combo box.
*/
protected function _onOpen(event:DropdownEvent) : void {
_isOpen = true;
}
/**
* Detect close events on the drop down list to keep track of the current
* drop down state so we can react properly to a click on our selected
* item renderer.
*
* #param event The DropdownEvent.CLOSE event for the combo box.
*/
protected function _onClose(event:DropdownEvent) : void {
_isOpen = false;
}
/**
* When we detect a click on our renderer open or close the drop down list
* based on whether the drop down is currently open/closed.
*
* #param event The CLICK event from our selected item renderer.
*/
protected function _onClick(event:MouseEvent) : void {
if (_isOpen) {
this.close(event);
} else {
this.open();
}
}
/**
* React to certain mouse/keyboard events on our selected item renderer and
* pass the events to the ComboBox 'button' so that the skins are properly
* applied.
*
* #param event A mouse or keyboard event to send to the ComboBox button.
*
*/
protected function _onMouseEvent(event:Event) : void {
if (_buttonRef != null) {
_buttonRef.dispatchEvent(event);
}
}
} // end class
} // end package
Thank you maclema and Maurits de Boer. I added a couple more things to this class to make it fit my needs:
I overrode set itemRenderer so that this will work if you set the itemRenderer through AS instead of mxml. I moved the text input replacement code to its own function to avoid duplication.
I added setters for 'increaseW' and 'increaseH' to resize the combobox if necessary because my renderer was too big for the combobox at first.
I subtracted 25 from the textInputReplacement width so it doesn't ever overlap the dropdown button... may be better to use something more proportional to accommodate different skins and such.
Code:
package
{
import mx.binding.utils.BindingUtils;
import mx.controls.ComboBox;
import mx.core.IFactory;
import mx.core.UIComponent;
public class ComboBox2 extends ComboBox
{
public function ComboBox2()
{
super();
}
protected var textInputReplacement:UIComponent;
private var _increaseW:Number = 0;
private var _increaseH:Number = 0;
public function set increaseW(val:Number):void
{
_increaseW = val;
}
public function set increaseH(val:Number):void
{
_increaseH = val;
}
override public function set itemRenderer(value:IFactory):void
{
super.itemRenderer = value;
replaceTextInput();
}
override protected function createChildren():void
{
super.createChildren();
replaceTextInput();
}
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
unscaledWidth += _increaseW;
unscaledHeight += _increaseH;
super.updateDisplayList(unscaledWidth, unscaledHeight);
if ( textInputReplacement ) {
textInputReplacement.width = unscaledWidth - 25;
textInputReplacement.height = unscaledHeight;
}
}
protected function replaceTextInput():void
{
if ( !textInputReplacement ) {
if ( this.itemRenderer != null ) {
//remove the default textInput
removeChild(textInput);
//create a new itemRenderer to use in place of the text input
textInputReplacement = this.itemRenderer.newInstance();
addChild(textInputReplacement);
// ADD THIS BINDING:
// Bind the data of the textInputReplacement to the selected item
BindingUtils.bindProperty(textInputReplacement, "data", this, "selectedItem", true);
addChild(textInputReplacement);
}
}
}
}
}
I was looking for a way to do this using the Spark ComboBox.
This thread was very useful to me but so far there have only been answers on how to do it using an mx:ComboBox. I thought that I should append my answer on how to do it using a spark ComboBox.
Create a new skin of the ComboBox
Hide and disable the textInput
Insert your own component
This is what the skin would look like:
<s:SparkSkin>
<... Lots of other stuff/>
<s:BorderContainer height="25">
<WHATEVER YOU NEED HERE!/>
</s:BorderContainer>
<!-- Disable the textInput and hide it -->
<s:TextInput id="textInput"
left="0" right="18" top="0" bottom="0"
skinClass="spark.skins.spark.ComboBoxTextInputSkin"
visible="false" enabled="false"/>
</s:SparkSkin>
With the Spark ComboBox this process is very easy and does not require you to extend ComboBox.
I found an easier way of changing the renderer for the selected element. This one only works if your element inherits from the TextInput class, in Flex 4.0 or above.
In Flex v4.5, in ComboBase.createChildren at line 1177, you will find that the class definable for the textInput can be passed using the style key textInputClass:
// Mechanism to use MXFTETextInput.
var textInputClass:Class = getStyle("textInputClass");
if (!textInputClass || FlexVersion.compatibilityVersion < FlexVersion.VERSION_4_0)
{
textInput = new TextInput();
}
else
{
textInput = new textInputClass();
}
Just change the value of this key in the constructor of your combo and now you have your own renderer for the selectedItem.
public function ComboAvailableProfessor()
{
super();
itemRenderer = new ClassFactory( ProfessorAvailableListItemRenderer );
setStyle( 'textInputClass', ProfessorAvailableSelectedListItemRenderer );
}
Finally you must bind the data property to the selectedItem property in your combo in order to get data displayed.
override protected function createChildren():void
{
super.createChildren();
BindingUtils.bindProperty( textInput, 'data', this, 'selectedItem', true );
}