5 Replies Latest reply on Dec 17, 2003 1:26 AM by 807587

    DND from a JList with a single gesture

    807587
      I am writing an application that allows users to drag an item from a JList in one JInternalFrame to a JList in another. The users are complaining that "sometimes it works and sometimes it doesn't". I'm using JDK 1.4.2_02.

      As an example of the required behavior, imagine using the Windows Explorer to move a file from one directory to another. You do not have to click once on a file name to select it and click again to start the drag operation; you just click once and start dragging, whether the file was "selected" or not.

      However, when you try to drag from a JList, the gesture works only if the item you click on is already selected. Otherwise you have to click once on the item to select it and click again to start dragging. For most applications (for instance a word processor) you would expect this behavior, but for this application it is confusing and unacceptable. You feel like you're using a vaccuum cleaner that doesn't always pick up the dirt.

      The reason for the behavior can be found in the platform-dependent Swing code, in either com/sun/java/swing/plaf/gtk/SynthListUI.java or javax/swing/plaf/basic/BasicTextUI.java:
          private static final ListDragGestureRecognizer defaultDragRecognizer = new ListDragGestureRecognizer();
      
          /**
           * Drag gesture recognizer for JList components
           */
          static class ListDragGestureRecognizer extends BasicDragGestureRecognizer {
      
           /**
            * Determines if the following are true:
            * <ul>
            * <li>the press event is located over a selection
            * <li>the dragEnabled property is true
            * <li>A TranferHandler is installed
            * </ul>
            * <p>
            * This is implemented to perform the superclass behavior
            * followed by a check if the dragEnabled 
            * property is set and if the location picked is selected.
            */
              protected boolean isDragPossible(MouseEvent e) {
               if (super.isDragPossible(e)) {
                JList list = (JList) this.getComponent(e);
                if (list.getDragEnabled()) {
                    ListUI ui = list.getUI();
                    int row = ui.locationToIndex(list, new Point(e.getX(),e.getY()));
                    if ((row != -1) && list.isSelectedIndex(row)) {
                     return true;
                    }
                }
               }
               return false;
           }
          }
      Note that the method returns true only if the row is already selected.

      How can I fix this? The obvious method is to substitute my own version of ListDragGestureRecognizer, but that's an unsafe hack. BasicDragGestureRecognizer is not available to the application programmer and is potentially different for every platform and for different versions of the JVM. It's not "documented" that I know of, so there's no way to guarantee my own version is compatible.

      Mark Lutton
        • 1. Re: DND from a JList with a single gesture
          807587
          First, see http://developer.java.sun.com/developer/bugParade/bugs/4521075.html

          There are two approaches there that worked for me: simon's and scmorr's. I prefer Simon's, but here is the code for both:

          1) based on scmorr's comments:
             private void fixListMouseListeners()
             {
                MouseListener[] allMouseListeners = getMouseListeners();
                String listenerName = "javax.swing.plaf.basic.BasicListUI$ListDragGestureRecognizer";
                MouseListener listDragGestureRecognizer = null;
                
                // find the drag gesture listener we are looking for and remove it from
                // mouse listeners
                for (int i = 0; i < allMouseListeners.length; ++i)
                {
                   MouseListener currentListener = allMouseListeners[ i ];
                   String currentListenerClassName = currentListener.getClass().getName();
                   
                   if (currentListenerClassName.equals(listenerName))
                   {
                      listDragGestureRecognizer = currentListener;
                      removeMouseListener(listDragGestureRecognizer);
                      break;
                   }
                }
          
                // add drag gesture listener back in at the end -- this allows the
                // mouse input handler (which does selection) to run first
                if (listDragGestureRecognizer != null)
                   addMouseListener(listDragGestureRecognizer);
             }  // fixListMouseListeners()
          2) simon's approach:
             // the cached selection event
             private MouseEvent myCachedEvent;
          
             
             /**
              * overriding this so we can select and drag at same time
              * @param firstIndex is first interval index
              * @param lastIndex is last interval index
              * @param isAdjusting is whether event is an adjusting event
              */
             protected void fireSelectionValueChanged(int firstIndex,
                                                      int lastIndex, 
                                                      boolean isAdjusting) 
             {
                // now continue with update of whoever is listening to list for selection changes
                super.fireSelectionValueChanged(firstIndex, lastIndex, isAdjusting);
          
                // if the selection occurred and we have cached the event,
                // launch a copy of the cached event so dragging can occur right
                // away, if necessary
                if (myCachedEvent != null) 
                {
                   super.processMouseEvent(new MouseEvent(               
                         (Component) myCachedEvent.getSource(),
                         myCachedEvent.getID(),     
                         myCachedEvent.getWhen(),     
                         myCachedEvent.getModifiersEx(),     
                         myCachedEvent.getX(),     
                         myCachedEvent.getY(),     
                         myCachedEvent.getClickCount(),     
                         myCachedEvent.isPopupTrigger()));
                   myCachedEvent= null;
                }
             }  // fireSelectionValueChanged()
          
          
             /**
              * overriding so we can cache the mouse event
              * @param e is the invoking event
              */
             protected void processMouseEvent(MouseEvent e)
             {
                int modifiers = e.getModifiersEx();
                
                // if clicked with left button, cache the event
                if ((modifiers & e.BUTTON1_DOWN_MASK) != 0)
                   myCachedEvent = e;
                   
                // go ahead and do normal processing on event
                super.processMouseEvent(e);
             }  // processMouseEvent()
          This does the job, but I had further requirements that required a bit more intervention. I am working with dragging an item between 2 JLists (single selection mode), and I am also trying to enforce that only one of my 2 lists can have a selection at any given time, using a ListSelectionListener, paying attention to events only if getValueIsAdjusting() is true.

          The problem comes if you abort a drag and then select an item in the other list that you weren't dragging from. When you drag, the list you drag from doesn't get a setValueIsAdjusting(false) since you don't get the mouseReleased() from MouseInputHandler, and leaving the list in "isAdjusting == true"-mode when a drag is aborted makes my selection stuff not work.

          My solution was to explictly call comp.setValueIsAdjusting(false) in my exportDone() function of my custom TransferHandler. Now everything is cool.
          • 2. Re: DND from a JList with a single gesture
            807587
            Thanks for the reply! (When faced with a problem like this, I always have to decide what to spend the next several hours doing: reading all the documentation to find out what it's supposed to do, reading all the Swing source code to find out why it does what it does, searching through all the forums, or reading all the bug lists. I usually don't read the bug lists because I assume that if there's a bug, it's in my code, not Sun's.)

            I tried Simon's approach of overriding fireSelectionValueChanged(). At first this did not work because fireSelectionValueChanged() is never called if you have no ListSelectionListener. I added this to the constructor of my JList class:
                  addListSelectionListener(
                    new ListSelectionListener() {
                      public void valueChanged(ListSelectionEvent e) {
                      }
                    }
                  );
                 
            After I added this, fireSelectionValueChanged() was called. (This is JDK 1.4.2_02. Does it behave differently in other versions? Fortunately for this internal application we can require a specific version of the JDK for everybody.)

            I suppose I could have put the code in my fireSelectionValueChanged() directly into the valueChanged() that I added; as an internal anonymous class it has access to JList's protected methods.

            Can you think of any other problems that might be caused by this (besides the one you mentioned)? I note that if you click the mouse on the list item that is already selected and then press the down-arrow to select the next one, processMouseEvent() is called with the cached mouse event (because this is the first changed selection since you clicked the mouse). I would have predicted that this would re-select the item the mouse pointed to, but it doesn't. Even though processMouseEvent() is called (and I verified this), the selection works as you expect: the down-arrow selects the next item. So this appears to solve my problem without causing any new problems.
            • 3. Re: DND from a JList with a single gesture
              807587
              I am using 1.4.2_02 as well. I don't know how it works with other versions.

              Oh man! My brain must have been totally shut off from other methods of input than mouse. I was so busy with dragging I forgot all about arrows and tabbing.

              Even if it doesn't cause you problems, you still might want to add something like:
                 /**
                  * overriding to get rid of cached mouse event -- if you click on something
                  * with mouse that is already selected, a mouse event will be cached, but
                  * it will not be cleared because fireSelectionValueChanged() won't be called,
                  * so if an up or down arrow causes a selection change, we don't want it to
                  * fire that old mouse event
                  * @param e is trigger event
                  */
                 protected void processComponentKeyEvent(KeyEvent e)
                 {
                    if (myCachedEvent != null)
                       myCachedEvent = null;
                    
                    super.processComponentKeyEvent(e);
                 }  // processComponentKeyEvent()
              I think it is preferrable to override processComponentKeyEvent() insteadof processKeyEvent().

              I can't think of any other general problems with this, but I am no expert.

              However, this whole discussion shows me I have major problems for my particular situation. This changes everything for me. I need to pay attention to non-isAdjusting events, not the other way around. This is going to take some re-thinking...
              • 4. Re: DND from a JList with a single gesture
                807587
                Here's another problem: Try Ctrl-click on an unselected list item. (This is a single-selection list.)

                When you press the mouse button, the unselected item will be selected, but when you release it it will be unselected. (If you quickly press and release, the selection will appear to blink.)

                To solve this, first I added code to display mouse events and SelectionValueChanged events. Here is what happens when I point the mouse at an unselected item, click, wait, and click again:
                MOUSE_PRESSED,(337,28),button=1,modifiers=Button1,extModifiers=Button1,clickCount=1
                Selection value changed.  first=0, last=1, isAdjusting=true
                MOUSE_RELEASED,(337,28),button=1,modifiers=Button1,clickCount=1
                Selection value changed.  first=0, last=1, isAdjusting=false
                MOUSE_CLICKED,(337,28),button=1,modifiers=Button1,clickCount=1
                MOUSE_PRESSED,(334,25),button=1,modifiers=Button1,extModifiers=Button1,clickCount=1
                MOUSE_RELEASED,(334,25),button=1,modifiers=Button1,clickCount=1
                MOUSE_CLICKED,(334.25),button=1,modifiers=Button1,clickCount=1
                If Ctrl, Shift, or Alt is pressed, you'll see them in the modifiers.

                Note that the ONLY gesture we are interested in cacheing is the MOUSE_PRESSED, and only if no keys are pressed. Also note that the SelectionValueChanged event is fired the first time immediately after the MOUSE_PRESSED, before processing any other gesture. So we can clear the cached mouse event after any other mouse event, or mouse motion event for that matter. The only time we will process the mouse event a second time is when MOUSE_PRESSED is immediately followed by SelectionValueChanged. In the case where we click on the already-selected item we won't process the event again, but that is OK because dragging works anyway in that case. So here is the new code:
                    protected void processMouseEvent(MouseEvent e) 
                    {
                      int modifiers = e.getModifiersEx();
                      
                      // Cache event only if mouse pressed and no keys.
                      if ((e.getID() == MouseEvent.MOUSE_PRESSED) &&
                         ((modifiers & MouseEvent.BUTTON1_DOWN_MASK) != 0) &&
                         ((modifiers & 
                            (MouseEvent.ALT_DOWN_MASK |
                             MouseEvent.CTRL_DOWN_MASK |
                             MouseEvent.SHIFT_DOWN_MASK)) == 0) 
                         ) {
                          myCachedEvent = e;
                      } else
                         myCachedEvent = null;
                      // Go ahead and do normal processing on events.
                      super.processMouseEvent(e);
                    }
                
                    protected void processMouseMotionEvent(MouseEvent e) {
                      myCachedEvent = null;
                      super.processMouseMotionEvent(e);
                    }
                    
                    protected void processComponentKeyEvent(KeyEvent e)
                    {
                      myCachedEvent = null;
                      super.processComponentKeyEvent(e);
                    }
                • 5. Re: DND from a JList with a single gesture
                  807587
                  I don't see your Ctrl-click behavior. For me Ctrl-click toggles the selection state of the clicked-on item.

                  I agree with you that we only care about MOUSE_PRESSED events, but I am not sure I accept your axiom that we never want any keys pressed when select/dragging. It is possible that a combination of Shift and/or ALT might be desirable for certain behavior, like copying instead of moving. Ctrl when dragging is blocked by the MouseInputHandler.

                  Personally, for my application, I don't care if modifier keys are pressed, except that I screen out Ctrl-clicks (as in your code) since I didn't want the user to be able to deselect.