How can I "unaccept" a drag in Flex? - actionscript-3

Once I've called DragManager.acceptDrag is there any way to "unaccept" the drag? Say that I have a view which can accept drag and drop, but only in certain areas. Once the user drags over one of these areas I call DragManager.acceptDrag(this) (from a DragEvent.DRAG_OVER handler), but if the user then moves out of this area I'd like to change the status of the drag to not accepted and show the DragManager.NONE feedback. However, neither calling DragManager.acceptDrag(null) nor DragManager.showFeedback(DragManager.NONE) seems to have any effect. Once I've accepted the drag an set the feedback type I can't seem to change it.
Just to make it clear: the areas where the user should be able to drop are not components or even display objects, in fact they are just ranges in the text of a text field (like the selection). Had they been components of their own I could have solved it by making each of them accept drag events individually. I guess I could create proxy components that float over the text to emulate it, but I'd rather not if it isn't necessary.
I've managed to get it working in both AIR and the browser now, but only by putting proxy components on top of the ranges of text where you should be able to drop things. That way I get the right feedback and drops are automatically unaccepted on drag exit.
This is the oddest thing about D&D in AIR:
DragManager.doDrag(initiator, source, event, dragImage, offsetX, offsetY);
In browser-based Flex, offsetX and offsetY should be negative (so says the documentation, and it works fine). However, when running exactly the same code in AIR you have to make the offsets positive. The same numbers, but positive. That is very, very weird.
I've tested some more and what #maclema works, but not if you run in AIR. It seems like drag and drop in AIR is different. It's really, really weird because not only is the feedback not showing correctly, and it's not possible to unaccept, but the coordinates are also completely off. I just tried my application in a browser instead of AIR and dragging and dropping is completely broken.
Also, skipping the dragEnter handler works fine in AIR, but breaks everything when running in a browser.

Are you using only the dragEnter method? If you are trying to reject the drag while still dragging over the same component you need to use both the dragEnter and dragOver methods.
Check out this example:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script>
<![CDATA[
import mx.core.DragSource;
import mx.managers.DragManager;
import mx.events.DragEvent;
private function onDragEnter(e:DragEvent):void {
if ( e.target == lbl ) {
if ( e.localX < lbl.width/2 ) {
trace("accept");
DragManager.acceptDragDrop(this);
}
else {
DragManager.acceptDragDrop(null);
}
}
}
private function doStartDrag(e:MouseEvent):void {
if ( e.buttonDown ) {
var ds:DragSource = new DragSource();
ds.addData("test", "text");
DragManager.doDrag(btn, ds, e);
}
}
]]>
</mx:Script>
<mx:Label id="lbl" text="hello world!" left="10" top="10" dragEnter="onDragEnter(event)" dragOver="onDragEnter(event)" />
<mx:Button id="btn" x="47" y="255" label="Button" mouseMove="doStartDrag(event)"/>
</mx:Application>

If you don't need native drag and drop in AIR, you can get the Flex drag and drop behavior by subclassing WindowedApplication and setting the DragManager. See this post on the Adobe Jira for more info: https://bugs.adobe.com/jira/browse/SDK-13983

You are misunderstanding the concept. Your "unaccept" is achieved by implementing the dragOverHandler and signaling that the data is not wanted.
Here is the basic concept:
register the dragEnterHandler or override the already registered method.
function dragEnterHandler(event: DragEvent):void {
if (data suites at least one location in this component)
DragManager.acceptDragDrop(this);
}
This enables your container to receive further messages (dragOver/dragExit). But this is NOT the location to decide which kind of mouse cursor should be displayed.
Without DragManager.acceptDragDrop(this); the other handlers aren't called.
register the dragOverHandler or override the already registered method.
function dragOverHandler(event: DragEvent):void {
if (data suites at least no location in this component) {
DragManager.showFeedback(DragManager.NONE);
return;
}
... // handle other cases and show the cursor / icon you want
}
Calling DragManager.showFeedback(DragManager.NONE); does the trick to display the "unaccept".
register the dragExitHandler or override the already registered method.
function dragOverHandler(event: DragEvent):void {
// handle the recieved data as you like.
}

ok, I see the problem now. Rather than null, try setting it to the dragInitiator.
Check this out.
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script>
<![CDATA[
import mx.controls.Alert;
import mx.events.DragEvent;
import mx.managers.DragManager;
import mx.core.DragSource;
private function doStartDrag(e:MouseEvent):void {
if ( e.buttonDown && !DragManager.isDragging ) {
var ds:DragSource = new DragSource();
ds.addData("test", "test");
DragManager.doDrag(btn, ds, e);
}
}
private function handleDragOver(e:DragEvent):void {
if ( e.localX < cvs.width/2 ) {
//since null does nothing, lets just set to accept the drag
//operation, but accept it to the dragInitiator
DragManager.acceptDragDrop(e.dragInitiator);
}
else {
//accept drag
DragManager.acceptDragDrop(cvs);
DragManager.showFeedback( DragManager.COPY );
}
}
private function handleDragDrop(e:DragEvent):void {
if ( e.dragSource.hasFormat("test") ) {
Alert.show("Got a drag drop!");
}
}
]]>
</mx:Script>
<mx:Canvas x="265" y="66" width="321" height="245" backgroundColor="#FF0000" id="cvs" dragOver="handleDragOver(event)" dragDrop="handleDragDrop(event)">
</mx:Canvas>
<mx:Button id="btn" x="82" y="140" label="Drag Me" mouseDown="doStartDrag(event)"/>
</mx:WindowedApplication>

Yes, drag and drop is different in AIR. I HATE that! It takes a lot of playing around to figure out how to get things to work the same as custom dnd that was built in flex.
As for the coordinates, maybe play around with localToContent, and localToGlobal methods. They may help in translating the coordinates to something useful.
Good luck. I will let you know if I think of anything else.

Related

How can I make Flex Busy Indicator load early?

I'm building a mobile app with Flash Builder 4.6 and AIR.
My Main.mxml class -- a ViewNavigatorApplication -- references a splash screen set to a minimum time of 6 seconds. After the splashscreen disappears, there's a period of a couple of seconds during which the content area of the screen is just blank white -- and that's when I need the Busy Indicator to appear.
EDIT: Additional Code, as requested:
In the Main.mxml class:
protected function viewnavigatorAppInitializeHandler(event:FlexEvent):void
{
//checks various criteria to determine which data to load, ie which service call
// to make. When that is determined, code calls setUpModel().
}
private function setUpModel():void {
Model.Instance.initialize(); //adds listeners in the model,
//makes service call. Data is returned and parsed in the event handler.
//APPLICATION_MODEL_LOADED event is then dispatched, and handled here
Model.Instance.addEventListener( Model.APPLICATION_MODEL_LOADED, modelReadyHandler );
}
private function modelReadyHandler(e:Event):void
{
//Busy Indicator only appears *after* the HomeListView is loaded.
//I need it to appear *while* these calls are being made.
navigator.pushView(HomeListView); //this is my First View
}
So I think the question is: what is it that is being displayed before HomeListView? If it's Main.mxml -- the ViewNavigatorApplication -- then how do I avoid the 'not assignable' error?
In HomeListView:
[Bindable] private var categoryList:ArrayCollection = new ArrayCollection();
private function viewActivateHandler():void
{
//the service call has been made, and data is ready to be loaded into the control
categories = Model.Instance.Categories;
I've tried this inside HomeListView:
<s:Group width="100%" height="100%">
<s:BusyIndicator
id="busy"
visible="true" symbolColor="blue"
horizontalCenter="0" verticalCenter="0"
rotationInterval="100" />
</s:Group>
It works. But the problem is it only appears once HomeListView has loaded. I thought that the blank white was HomeListView, just before the list appeared. But it isn't.
So then I tried putting the above code into the Main.mxml file. But I got the following error:
'spark.components.Group' is not assignable to the default property, 'navigationStack', of type 'spark.components.supportClasses.NavigationStack'.
So where can I put this BusyIndicator so that it covers the blank space before HomeListView appears?
I've figured out a way to do this.
From the documentation:
"Unlike Application, ViewNavigatorApplication is not meant to accept UIComponent objects as children. Instead, all visual components should be children of the views managed by the application."
That is why I was getting the error. So for my first View, I created a simple View called HolderView which contains the BusyIndicator:
<?xml version="1.0" encoding="utf-8"?>
<s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark" title="HolderView">
<s:Group width="100%" height="100%">
<s:BusyIndicator
id="busy"
visible="true" symbolColor="blue"
horizontalCenter="0" verticalCenter="0"
rotationInterval="100" />
</s:Group>
Then, from my Main.mxml file, I first load that HolderView.
private function setUpModel():void {
Model.Instance.initialize();
navigator.pushView(HolderView);
Model.Instance.addEventListener(Model.APPLICATION_MODEL_LOADED, modelReadyHandler );
}
Once the data has come back -- on the Model.APPLICATION_MODEL_LOADED event listener -- that is when I will load HomeListView:
private function modelReadyHandler(e:Event):void
{
navigator.pushView(HomeListView);
}
This works. Hope it helps someone with same problem!

Drag area in Flex HGroup is smaller than the group itself

I have an HGroup that I want to drag elements onto. In order to style the HGroup, it's really a Group with a Rect and an HGroup within it.
The drag area in this code is only as large as the elements that are currently in the group (that's why I put a button in there to test it). As you can see, I've manually set the width of the Group and HGroup. Everything reports itself to be 300 wide, as it should be, but the drag area remains only as large as the number of elements in the HGroup. This makes me think the HGroup isn't actually what it's reporting to be (it reports 300 from accessing the .width property after its been set). The Rect is properly 300 wide, so I can see what the drop zone should be.
I feel like I must be doing something very obviously wrong, but I can't see it. Any ideas would be very appreciated!
EDIT: I wonder if there is some sort of masking issue. Adding a drag enter event on the rectangle (which I know is the right size, as I can see it) also doesn't result in the enter event firing. How could I find this out?
package components
{
import mx.core.IUIComponent;
import mx.events.DragEvent;
import mx.graphics.SolidColor;
import mx.managers.DragManager;
import spark.components.Group;
import spark.components.HGroup;
import spark.components.VGroup;
import spark.primitives.Rect;
public class TestVerticalConstraintExpression extends Group
{
private var expressions:VGroup;
private var topExpression:Group;
private var locked:Boolean = false;
public function TestVerticalConstraintExpression()
{
super();
this.width = 300;
expressions = new VGroup();
expressions.width = 300;
this.addElement(getBackground());
topExpression = new HGroup();
expressions.addElement(topExpression);
this.addElement(getBackground());
this.addElement(expressions);
topExpression.width = 300;
topExpression.height = 50;
topExpression.addElement(new ConstraintButton());
// Setup listeners for when things are dropped onto the expression group
topExpression.addEventListener(DragEvent.DRAG_ENTER,
dragEnterHandler);
}
protected function getBackground():Rect
{
var bg:Rect = new Rect();
bg.fill = new SolidColor(Math.round(Math.random()*0xFFFFFF));
bg.left = 0;
bg.right = 0;
bg.top = 0;
bg.bottom = 0;
bg.percentWidth = 100;
bg.percentHeight = 100;
return bg;
}
protected function dragEnterHandler(event:DragEvent):void {
DragManager.acceptDragDrop(event.currentTarget as IUIComponent);
}
}
}
A Group is essentially just a layout container. It has no graphic elements whatsoever, hence it is completely transparent except where you've put your ConstraintButton, which is why you can only drop onto that ConstraintButton and nowhere else in the HGroup.
So you must create a "hitzone" with a graphic element that has the same size as your HGroup. It may have its alpha property set to 0 so that you don't see it, but still can interact with it. The two simplest ways I can think of:
convert your HGroup to a BorderContainer with an Horizontallayout
put the HGroup in a Group together with a background Rect; something like this:
.
<s:Group id="topExpression">
<s:Rect left="0" right="0" top="0" bottom="0">
<s:fill>
<s:SolidColor alpha="0" />
</s:fill>
</s:Rect>
<s:HGroup id="yourOldTopExpression" left="0" right="0" top="0" bottom="0" />
</s:Group>
That said, why do you want to write a "view" in ActionScript? It makes the code so much more verbose (i.e. more work) and harder to read. Not to mention you make mistakes, like adding the background Rect twice to the main Group, which you would have spotted immediatley if your code were written in MXML.
And perhaps more importantly: because you put the subcomponent creation code in the constructor of your main class, your code will actually result in performance loss. You have to know and understand the Flex component lifecycle if you want to write components in pure ActionScript. MXML handles all this nitty-gritty for you.
If you're worried about mixing up MXML and ActionScript, you should read up on the Spark skinning architecture. (For an example of that, you could check out this answer which uses Spark skinning: Custom Composite Control not rendering correctly for only 0.5-1 sec after being added back into a VGROUP).

Flex: preventDefault on spark:ListBase not working

The proposed UX I am trying to achieve is this:
user clicks menu item (via a listBase subclass: e.g. ButtonBar or TabBar)
prevent initial selection
validate if user needs to address issues (e.g. unsaved data on a form, etc.)
if valid, take selection and set the listBase to that selectedIndex, otherwise present warnings to user and cancel out the selection process altogether
This does not work as you'd expect. Utilizing the IndexChangeEvent.CHANGING type and the preventDefault works to kill the selection, but at step 4, when I am programmatically setting the selectedIndex of the listBase, it then tries to redispatch the CHANGING event (this despite what the API docs claim).
Here is a sample application src code if you'd like to try this for yourself. I look forward to your comments & solutions.
Thanks.
J
http://snipt.org/vUji3#expand
<?xml version="1.0" encoding="utf-8"?>
<s:Application minWidth="955" minHeight="600"
xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:mx="library://ns.adobe.com/flex/mx"
xmlns:s="library://ns.adobe.com/flex/spark">
<fx:Script>
<![CDATA[
import flash.utils.setTimeout;
import mx.core.mx_internal;
import spark.events.IndexChangeEvent;
use namespace mx_internal;
[Bindable]
private var doPreventDefault:Boolean;
[Bindable]
private var delayMS:uint = 500;
private function buttonbar_changingHandler( event:IndexChangeEvent ):void
{
// TODO Auto-generated method stub
if ( doPreventDefault )
{
event.preventDefault();
setTimeout( delayedLogic, delayMS, event.newIndex );
}
}
private function delayedLogic( index:int ):void
{
//disabling this results in an endless loop of trying to set the selected index
// doPreventDefault = false;
//this should NOT be hitting the changing handler since we're supposed to be dispatching a value commit event instead.
bb.setSelectedIndex( index, false );
}
]]>
</fx:Script>
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
<s:layout>
<s:VerticalLayout horizontalAlign="center"/>
</s:layout>
<s:ButtonBar id="bb"
changing="buttonbar_changingHandler(event)">
<s:dataProvider>
<s:ArrayList>
<fx:String>btn 0</fx:String>
<fx:String>btn 1</fx:String>
<fx:String>btn 2</fx:String>
</s:ArrayList>
</s:dataProvider>
</s:ButtonBar>
<s:CheckBox label="preventDefault?"
selected="#{ doPreventDefault }"/>
<s:NumericStepper maximum="5000" minimum="500"
stepSize="500" value="#{ delayMS }"/>
</s:Application>
Looking at the SDK, the IndexChangeEvent.CHANGING event is actually preventable - despite the documentation here says that cancelable is false, so my bad on that (although ASDoc went a little sideways), however things get a little interesting from here.
In ListBase #1296 this is only ever dispatched from the commitSelection(dispatchEvents:Boolean = true) method. In ButtonBarBase:dataProvider_changeHandler() is the only place that specifically calls to not dispatch the event, but in ListBase, it's called in commitProperties #939 when there is a proposedSelectionIndex.
So from your code above, if you are trying to set the selection - this is going to call the commitSelection, which I believe is causing the call stack issue. The Timer delay is just going to exacerbate the issue, since at 500ms the UI will have gone through its invalidation cycle at least once, meaning the commitSelection will be executed again because of an invalidateProperties flag is being set from the proprosedSelectionIndex eventually stemming from setSelectedIndex #729
So how to fix this.
I would look at only doing the prevent if the validation fails, otherwise allow it to proceed as normal. If it does fail, call the prevent, set an errorString or equivalent, but don't attempt to change the selection.
[edit] Read RiaStar's comment, and I just concurred with the same 'solution'.

Event listener to keyboard event not listening in a module

I am doing this inside a module containing viewstacks and their childs.
Calling onInit() on creationComplete of module.
When I am inside one of the childs of a viewstack of this module and press Enter, it doesnt not invoke the listener function at all (bp inside this does not get hit).
private function onInit():void{
this.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
}
private function keyPressed(evt:KeyboardEvent):void
{//this breakpoint never gets hit on pressing a key in screen
if (evt.keyCode == Keyboard.ENTER)
{
//do this
}
}
You should add key listeners to stage objects:
private function onInit():void{
this.stage.addEventListener(KeyboardEvent.KEY_DOWN, keyPressed);
}
This can be very frustrating as there are a few different things that can affect this.
1) Add your event listeners at the appropriate place. The code that you have there is fine for capturing, just ensure it is in the parent or above of where the events are being fired.
2) You need to ensure you have focus. This is usually the issue people run into and it's in the docs but not immediately clear. If you look at the live doc link here and do a search for setFocus() - you'll notice that it is in every one of their examples (except top, which is broken!) - yet, they don't ever mention it in the actual documentation on the page.
http://livedocs.adobe.com/flex/3/html/help.html?content=events_11.html
So, even in their first example, if you click into the app (and not the textbox), it wont work!
<?xml version="1.0"?>
<!-- events/TrapAllKeys.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="initApp();">
<mx:Script><![CDATA[
private function initApp():void {
application.addEventListener(KeyboardEvent.KEY_UP, keyHandler);
}
private function keyHandler(event:KeyboardEvent):void {
t1.text = event.keyCode + "/" + event.charCode;
}
]]></mx:Script>
<mx:TextInput id="myTextInput"/>
<mx:Text id="t1"/>
</mx:Application>
If however, you set focus yourself by changing the init function like this, it will!
private function initApp():void {
application.addEventListener(KeyboardEvent.KEY_UP, keyHandler);
myTextInput.setFocus();
}
Another trick for testing if this is your problem is to add a textbox as a child of the container that has the capturing, if they magically work after you click in that text box - its a focus problem indeed!
=)

How to create dynamic variable vbox in actionscript

I have to create several vbox-es in a for each loop.
Now I want to do something like this.
formsArray["vb"+counter] = new VBox;
formsArray["vb"+counter].visible = true;
add labels etc.
I can't get this thing to work. Anybody any idea how to create dynamic variable names for my vbox-es?
Thanks
First off, to use an associative array, you need to use an Object and not an Array (perhaps you already are, then never mind).
You can achieve what you want to do the following way:
var vbox:VBox;
var formsArray:Object = new Object();
var counter:int = 0;
for each(<statement>)
{
vbox = new VBox();
formsArray[("vb" + counter.toString())] = vbox;
counter++;
}
The VBox's visible property is true by default, so no need to explicitly set it.
Answer to additional question in comments:
You don't really need to make use of dynamic references to do what you want to do. You'd be best of creating a custom component for this, extending the VBox class, by creating a new MXML class with VBox as the root tag. Something along these lines:
<mx:VBox ... >
<mx:Button ... click="btnClickHandler()"/>
<mx:Script>
<![CDATA[
// Toggles visibility of the VBox
private function btnClickHandler():void
{
visible = !visible;
}
]]>
</mx:Script>
</mx:VBox>
Then you can just instantiate as many of these custom VBox:es as you need. However, making the VBox invisible will make the contained button invisible as well, making it difficult to click it again. :) You probably want to address that. Anyways, I hope this will point you in the right direction.