Kapitola 1. Drag & Drop, Cut & Paste

Obsah

1.1. Úvod
1.1.1. Základní informace
1.1.2. Základní scénář DnD a CCP
1.1.3. DnD podpora v JFC/Swing
1.1.4. TransferHandler objekt
1.1.5. Cut, Copy a Paste
1.1.6. DataFlavor
1.1.7. Doporučeno k přečtení:

1.1. Úvod

1.1.1. Základní informace

1.1.2. Základní scénář DnD a CCP

  • Následující obrázek ukazuje co se děje při přetáhnutí položky z JList do JTextField.

    • šipky ukazují data-flow

    • co přesně se během DnD děje:

      1. uživatel stiskne tlačítko myši nad položkou v JList (source) a pohne myší -- tím iniciuje táhnutí (drag gesture)

      2. na začátku drag gesture JList připraví data (táhnutou pložku) na export a deklaruje jaké source actions JList (coby původce dat) podporuje -- například COPY, MOVE či LINK

      3. během drag gesture uživatel může mačkat Shift a/nebo Control klávesu a tím měnit user action, která je také součástí drag gesture; typicky je sámantika táhnutí MOVE a při stisku Control klávesy se mění na COPY

      4. jak je táhnuto přes komponenty, komponenty (target) jsou neustále dotazovány, zda přijímají táhnutá data nebo ne a zobrazují odpovídající vizuální zpětnou vazbu -- například zobrazují kurzor na místě kam by data mohla být vložena

      5. po uvolnění myši cílová komponenta zjistí jaké byly deklarovány source actions a user actions a vybere si z nabízených možností

      6. nakonec cílová komponenta importuje data a případně se provede akce nad source komponentou

    • to všechno za nás udělá Swing, programátoři mohou tu a tam něco pozměnit, pokud chtějí, ale DnD jako celek prostě funguje "sám od sebe"

  • Přenášení dat přes clipboard (česky: schránku)

1.1.3. DnD podpora v JFC/Swing

  • řada komponent podporuje DnD přímo

  • metody pro přenos dat jsou deklarovány v JComponent

  • následující komponenty mají defaultní podporu pro drag gesture a není třeba nic doprogramovat; stačí podporu aktivovat zavoláním metody setDragEnabled(true)

    • JColorChooser, JEditorPane, JFileChooser, JFormattedTextField, JList, JTable, JTextArea, JTextField, JTextPane, JTree

  • následující komponenty mají defaultní podporu pro drop a není třeba nic doprogramovat

    • JEditorPane, JFormattedTextField, JPasswordField, JTextArea, JTextField, JTextPane, JColorChooser

  • u třech komponent se musí dopsat něco málo kódu: JList, JTable, JTree

1.1.4. TransferHandler objekt

  • přenos dat zprostředkovává třída TransferHandler a její pomocné třídy viz http://java.sun.com/docs/books/tutorial/uiswing/dnd/transferhandler.html

  • výše uvedené komponety mají již implementovaný TransferHandler

  • na přiřazení/aktivaci TransferHandleru se užívaji tři metody

    • setDragEnabled(boolean)— viz výše

    • setDropMode(DropMode) — viz níže

    • setTransferHandler(TransferHandler)— pro nastavení vlastní implementace exportu a importu

1.1.4.1. Podprora exportu dat

  • trojice metod

    • getSourceActions(JComponent) — vrací akce které komponenta podporuje -- nějakou kombinaci z COPY, MOVE a LINK

    • createTransferable(JComponent) — vytvoří objekt s daty typovaný na Transferable rozhraní

    • exportDone(JComponent, Transferable, int) — definuje co se má udělat po skončení přesunu -- např. mazání položky JListu při MOVE

  • příklad implementace:

    int getSourceActions(JComponent c) {
        return COPY_OR_MOVE;
    }
    
    Transferable createTransferable(JComponent c) {
        return new StringSelection(c.getSelection());
    }
    
    void exportDone(JComponent c, Transferable t, int action) {
        if (action == MOVE) {
            c.removeSelection();
        }
    }
    

1.1.4.2. Vnitřní třída TransferHandler.TransferSupport

1.1.4.3. Podprora importu dat

  • dvě metody

    • canImport(TransferHandler.TransferSupport)

      • Swing cyklicky volá tuto metodu komponenty nad kterou je právě prováděněno táhnutí

      • vrací true/false podle toho, zda komponenta pod kurzorem je schopna přijmout Transferable data

      • pokud vraci true, komponenta poskutuje vizuální zpětnou vazbu — jaká ta zpětná vazba je zavisí na komponentě a na táhnutých datech — například ukazuje kurzor v textu tam kam se vkopíruje táhnutý text

      • příklad pro tabulku: http://java.sun.com/docs/books/tutorial/uiswing/dnd/locsensitivedrop.html

        public boolean canImport(TransferHandler.TransferSupport info) {
            // ...
            JTable.DropLocation dl = (JTable.DropLocation)info.getDropLocation();
            int column = dl.getColumn();
            // we do not support invalid columns or the first column of table
            if (column == -1 || column == 0) {
                return false;
            }
            return true;
        }
        

        a demo s kódem: http://java.sun.com/docs/books/tutorial/uiswing/dnd/locsensitivedemo.html

    • importData(TransferHandler.TransferSupport) — voláno po uvolnění myši, zahájí přenos dat

  • příklad

    public boolean canImport(TransferSupport supp) {
        // Check for String flavor
        if (!supp.isDataFlavorSupported(stringFlavor)) {
            return false;
        }
        // Fetch the drop location
        DropLocation loc = supp.getDropLocation();
        // Return whether we accept the location
        return shouldAcceptDropLocation(loc);
    }
    
    public boolean importData(TransferSupport supp) {
        if (!canImport(sup)) {
            return false;
        }
        // Fetch the Transferable and its data
        Transferable t = supp.getTransferable();
        String data = t.getTransferData(stringFlavor);
        // Fetch the drop location
        DropLocation loc = supp.getDropLocation();
        // Insert the data at this location
        insertAt(loc, data);
        return true;
    }
    

1.1.4.4. Nastavení Drop Mode

1.1.4.5. DropLocation

  • určuje kam přesně se mají vložit/nakopírovat data, což závisí na cílové komponentě

Tabulka 1.1. DropLocation vnitřní třídy pro JList, JTree, JTable a JTextComponent třídy

JListJTreeJTableJTextComponent
isInsertgetChildIndexisInsertRowgetIndex
getIndexgetPathisInsertColumngetBias
  getRow 
  getColumn 

1.1.5. Cut, Copy a Paste

  • přidání CCP je triviální

  • je potřeba

    1. ujistit se, že komponenta má aktivní TransferHandler (opakuji, že řada komponent má nějaký TransferHandler defaultně)

    2. rozhodnout se jak se CPP bude ovládat -- většinou pomocí klávesových zkratek

    3. (volitelně) lze připravit menu a/nebo tlačítka pro CCP

    4. rozhoudnout se kam se mají data vkopírovat (paste) -- popíšeme to kódem v importData()

1.1.5.1. CCP v textových komponentách

  • textové komponenty používají DefaultEditorKit s vestavěnou podporou CCP

    • takže pro textové komponenty není třeba dělat vůbec nic

    • pokud chcete tlačítka (viz bod 3 výše), pak takhle:

          /**
           * Create an Edit menu to support cut/copy/paste.
           */
          public JMenuBar createMenuBar () {
              JMenuItem menuItem = null;
              JMenuBar menuBar = new JMenuBar();
              JMenu mainMenu = new JMenu("Edit");
              mainMenu.setMnemonic(KeyEvent.VK_E);
      
              menuItem = new JMenuItem(new DefaultEditorKit.CutAction());
              menuItem.setText("Cut");
              menuItem.setMnemonic(KeyEvent.VK_T);
              mainMenu.add(menuItem);
      
              menuItem = new JMenuItem(new DefaultEditorKit.CopyAction());
              menuItem.setText("Copy");
              menuItem.setMnemonic(KeyEvent.VK_C);
              mainMenu.add(menuItem);
      
              menuItem = new JMenuItem(new DefaultEditorKit.PasteAction());
              menuItem.setText("Paste");
              menuItem.setMnemonic(KeyEvent.VK_P);
              mainMenu.add(menuItem);
      
              menuBar.add(mainMenu);
              return menuBar;
          }

1.1.5.2. CCP v netextových komponentách

  • netextové komponenty nemají DefaultEditorKit s vestavěnou podporou CCP a proto je s nimi víc práce

    1. je třeba přidat CCP akce do action map

      private void setMappings(JList list) { 
        ActionMap map = list.getActionMap();
        map.put(TransferHandler.getCutAction().getValue(Action.NAME),
                TransferHandler.getCutAction());
        map.put(TransferHandler.getCopyAction().getValue(Action.NAME),
                TransferHandler.getCopyAction());
        map.put(TransferHandler.getPasteAction().getValue(Action.NAME),
                TransferHandler.getPasteAction());
      }
    2. pokud implementujeme Edit menu, nezapomeneme na klávesvé zkratky (nepovinně)

      menuItem = new JMenuItem("Cut");
      menuItem.setActionCommand((String)TransferHandler.getCutAction().
        getValue(Action.NAME));
      menuItem.addActionListener(actionListener);
      menuItem.setAccelerator(
        KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK));
      menuItem.setMnemonic(KeyEvent.VK_T);
      mainMenu.add(menuItem);
      
    3. je třeba zaregistrovat klavesové zkratky (tento krok přeskočíte, pokud jste registrovali menu akcelerátor v nepovinném kroku 2)

          InputMap imap = this.getInputMap();
          imap.put(KeyStroke.getKeyStroke("ctrl X"),
              TransferHandler.getCutAction().getValue(Action.NAME));
          imap.put(KeyStroke.getKeyStroke("ctrl C"),
              TransferHandler.getCopyAction().getValue(Action.NAME));
          imap.put(KeyStroke.getKeyStroke("ctrl V"),
              TransferHandler.getPasteAction().getValue(Action.NAME));
      
    4. je třeba definovat, která komponenta by měla reagovat na CCP, když uživatel stiskne Ctrl+něco

      • Pro textové komponenty si DefaultEditorKit pamatuje, která komponenta měla naposledy aktivní (měla focus) a tato komponentu zareaguje

      • Pro netextové komponenty lze použít následující třídu, která dělé to samé:

      public class TransferActionListener implements 
              ActionListener, PropertyChangeListener {
          private JComponent focusOwner = null;
          public TransferActionListener() {
              KeyboardFocusManager manager = KeyboardFocusManager.
                 getCurrentKeyboardFocusManager();
              manager.addPropertyChangeListener("permanentFocusOwner", this);
          }
          public void propertyChange(PropertyChangeEvent e) {
              Object o = e.getNewValue();
              if (o instanceof JComponent) 
                  focusOwner = (JComponent)o;
               else 
                  focusOwner = null;
          }
          public void actionPerformed(ActionEvent e) {
              if (focusOwner == null)
                  return;
              String action = (String)e.getActionCommand();
              Action a = focusOwner.getActionMap().get(action);
              if (a != null) {
                  a.actionPerformed(new ActionEvent(focusOwner,
                           ActionEvent.ACTION_PERFORMED, null));
              }
          }
      }
    5. konečně, je třeba definovat jak se má zpracovat vložení (paste) operace

      • v případě drop operace se místo vložení odvozovalo od pozice myši

      • v případě paste tato infomace chybí a musíme vymyslet něco co dává smysl v kontextu konkrétní aplikace -- viz else větev v následujícím kódu importData

      • zde například vložená položka vsune za vybraou položku JListu nebo nakonec, pokud žádná položka není vybrána:

            public boolean importData(TransferHandler.TransferSupport info) {
                String data = null;
                ... 
                if (info.isDrop()) { //This is a drop
                    JList.DropLocation dl = 
                                  (JList.DropLocation)info.getDropLocation();
                    int index = dl.getIndex();
                    if (dl.isInsert()) {
                        model.add(index, data);
                        return true;
                    } else {
                        model.set(index, data);
                        return true;
                    }
                } else { // This is a paste
                    int index = list.getSelectedIndex();
                    // if there is a valid selection,
                    // insert data after the selection
                    if (index >= 0) {
                        model.add(list.getSelectedIndex()+1, data);
                    // else append to the end of the list
                    } else {
                        model.addElement(data);
                    }
                    return true;
                }
            }
      • toto bylo jediné místo v programu, kde je třeba napsat if-else kód pro rozlišení DnD a CCP (a i to je v princupu nutné dělat jen tehdy, když program podporuje jak DnD tak CCP)

1.1.6. DataFlavor

  • třída která určuje jakého typu jsou data

  • používá se např. v importData()

  • tři nejčastěji používané jsou již předdefinované

    • imageFlavor -- reprezentuje data v java.awt.Image formátu.

    • stringFlavor -- reprezentuje textová data java.lang.String

    • javaFileListFlavor representuje java.io.File objekty v java.util.List formátu; užitečné v aplikacích kam chcete nechat přetáhnout soubory s nějakého souborového mamžeru nebo např. z pracovní plochy

  • výše uvedené většinou stačí a tak se numusíte o DataFlavor více starat

  • když nestačí, protože například používáte nějakou speciální třídu, použijete vlastní DataFlavor

    • konstrutor je DataFlavor(Class, String)

    • použití pro Vaši třídu Osoba: new DataFlavor(Osoba.class, "Osoba");

    • pro pole celých čísel: new DataFlavor(int[].class, "Interger Array");

    • pro přenos dat se vnitřně používá serializace a proto přenášená data (např. Osoba) musí implementovat rozhraní http://java.sun.com/javase/6/docs/api/java/io/Serializable.html