1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package com.oracle.javafx.scenebuilder.kit.editor.job.gridpane;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.editor.EditorController;
  35 import com.oracle.javafx.scenebuilder.kit.editor.job.BatchSelectionJob;
  36 import com.oracle.javafx.scenebuilder.kit.editor.job.Job;
  37 import com.oracle.javafx.scenebuilder.kit.editor.job.gridpane.GridPaneJobUtils.Position;
  38 import com.oracle.javafx.scenebuilder.kit.editor.selection.AbstractSelectionGroup;
  39 import com.oracle.javafx.scenebuilder.kit.editor.selection.GridSelectionGroup;
  40 import com.oracle.javafx.scenebuilder.kit.editor.selection.GridSelectionGroup.Type;
  41 import com.oracle.javafx.scenebuilder.kit.editor.selection.ObjectSelectionGroup;
  42 import com.oracle.javafx.scenebuilder.kit.editor.selection.Selection;
  43 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance;
  44 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  45 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask;
  46 import java.util.ArrayList;
  47 import java.util.HashMap;
  48 import java.util.Iterator;
  49 import java.util.LinkedHashSet;
  50 import java.util.List;
  51 import java.util.Map;
  52 import java.util.Set;
  53 
  54 /**
  55  * Job invoked when adding columns.
  56  *
  57  * This job handles multi-selection as follows :
  58  * - if multiple GridPanes are selected, no column can be selected.
  59  * We add the new column to each GP, either at first position (add before)
  60  * or last position (add after).
  61  * - if multiple columns are selected, a single GridPane is selected.
  62  * We add new columns for each selected column, either before or after.
  63  *
  64  */
  65 public class AddColumnJob extends BatchSelectionJob {
  66 
  67     // Key = target GridPane instance
  68     // Value = set of target column indexes for this GridPane
  69     private final Map<FXOMObject, Set<Integer>> targetGridPanes = new HashMap<>();
  70     private final Position position;
  71 
  72     public AddColumnJob(EditorController editorController, Position position) {
  73         super(editorController);
  74         assert position == Position.BEFORE || position == Position.AFTER;
  75         this.position = position;
  76     }
  77 
  78     @Override
  79     protected List<Job> makeSubJobs() {
  80         final List<Job> result = new ArrayList<>();
  81 
  82         if (GridPaneJobUtils.canPerformAdd(getEditorController())) {
  83 
  84             // Populate the target GridPane map
  85             assert targetGridPanes.isEmpty() == true;
  86             final List<FXOMObject> objectList
  87                     = GridPaneJobUtils.getTargetGridPanes(getEditorController());
  88             for (FXOMObject object : objectList) {
  89                 final Set<Integer> indexList
  90                         = getTargetColumnIndexes(getEditorController(), object);
  91                 targetGridPanes.put(object, indexList);
  92             }
  93 
  94             // Add sub jobs
  95             // First add the new column constraints
  96             final Job addConstraints = new AddColumnConstraintsJob(
  97                     getEditorController(), position, targetGridPanes);
  98             result.add(addConstraints);
  99             // Then move the column content
 100             result.addAll(moveColumnContent());
 101         }
 102         return result;
 103     }
 104 
 105     @Override
 106     protected String makeDescription() {
 107         return "Add Column " + position.name(); //NOI18N
 108     }
 109 
 110     @Override
 111     protected AbstractSelectionGroup getNewSelectionGroup() {
 112         final AbstractSelectionGroup asg;
 113         // Update new selection :
 114         // - if there is more than 1 GridPane, we select the GridPane instances
 115         // - if there is a single GridPane, we select the added columns
 116         if (targetGridPanes.size() > 1) {
 117             Set<FXOMObject> objects = targetGridPanes.keySet();
 118             asg = new ObjectSelectionGroup(objects, objects.iterator().next(), null);
 119         } else {
 120             assert targetGridPanes.size() == 1;
 121             final FXOMInstance targetGridPane
 122                     = (FXOMInstance) targetGridPanes.keySet().iterator().next();
 123             final Set<Integer> targetIndexes = targetGridPanes.get(targetGridPane);
 124             assert targetIndexes.size() >= 1;
 125             final Set<Integer> addedIndexes
 126                     = GridPaneJobUtils.getAddedIndexes(targetIndexes, position);
 127 
 128             asg = new GridSelectionGroup(targetGridPane, Type.COLUMN, addedIndexes);
 129         }
 130         return asg;
 131     }
 132 
 133     private List<Job> moveColumnContent() {
 134 
 135         final List<Job> result = new ArrayList<>();
 136 
 137         for (FXOMObject targetGridPane : targetGridPanes.keySet()) {
 138 
 139             final Set<Integer> targetIndexes = targetGridPanes.get(targetGridPane);
 140 
 141             final DesignHierarchyMask mask = new DesignHierarchyMask(targetGridPane);
 142             final int columnsSize = mask.getColumnsSize();
 143             final Iterator<Integer> iterator = targetIndexes.iterator();
 144 
 145             int shiftIndex = 0;
 146             int targetIndex = iterator.next();
 147             while (targetIndex != -1) {
 148                 // Move the columns content :
 149                 // - from the target index
 150                 // - to the next target index if any or the last column index otherwise
 151                 int fromIndex, toIndex;
 152 
 153                 switch (position) {
 154                     case BEFORE:
 155                         // fromIndex included
 156                         // toIndex excluded
 157                         fromIndex = targetIndex;
 158                         if (iterator.hasNext()) {
 159                             targetIndex = iterator.next();
 160                             toIndex = targetIndex - 1;
 161                         } else {
 162                             targetIndex = -1;
 163                             toIndex = columnsSize - 1;
 164                         }
 165                         break;
 166                     case AFTER:
 167                         // fromIndex excluded
 168                         // toIndex included
 169                         fromIndex = targetIndex + 1;
 170                         if (iterator.hasNext()) {
 171                             targetIndex = iterator.next();
 172                             toIndex = targetIndex;
 173                         } else {
 174                             targetIndex = -1;
 175                             toIndex = columnsSize - 1;
 176                         }
 177                         break;
 178                     default:
 179                         assert false;
 180                         return result;
 181                 }
 182 
 183                 // If fromIndex >= columnsSize, we are below the last existing column
 184                 // => no column content to move
 185                 if (fromIndex < columnsSize) {
 186                     final int offset = 1 + shiftIndex;
 187                     final List<Integer> indexes
 188                             = GridPaneJobUtils.getIndexes(fromIndex, toIndex);
 189                     final ReIndexColumnContentJob reIndexJob = new ReIndexColumnContentJob(
 190                             getEditorController(), offset, targetGridPane, indexes);
 191                     result.add(reIndexJob);
 192                 }
 193 
 194                 shiftIndex++;
 195             }
 196         }
 197         return result;
 198     }
 199 
 200     /**
 201      * Returns the list of target column indexes for the specified GridPane
 202      * instance.
 203      *
 204      * @return the list of target indexes
 205      */
 206     private Set<Integer> getTargetColumnIndexes(
 207             final EditorController editorController,
 208             final FXOMObject targetGridPane) {
 209 
 210         final Selection selection = editorController.getSelection();
 211         final AbstractSelectionGroup asg = selection.getGroup();
 212 
 213         final Set<Integer> result = new LinkedHashSet<>();
 214 
 215         // Selection == GridPane columns
 216         // => return the list of selected columns
 217         if (asg instanceof GridSelectionGroup
 218                 && ((GridSelectionGroup) asg).getType() == Type.COLUMN) {
 219             final GridSelectionGroup gsg = (GridSelectionGroup) asg;
 220             result.addAll(gsg.getIndexes());
 221         } //
 222         // Selection == GridPanes or Selection == GridPane rows
 223         // => return either the first (BEFORE) or the last (AFTER) column index
 224         else {
 225             switch (position) {
 226                 case BEFORE:
 227                     result.add(0);
 228                     break;
 229                 case AFTER:
 230                     final DesignHierarchyMask mask
 231                             = new DesignHierarchyMask(targetGridPane);
 232                     final int size = mask.getColumnsSize();
 233                     result.add(size - 1);
 234                     break;
 235                 default:
 236                     assert false;
 237                     break;
 238             }
 239         }
 240 
 241         return result;
 242     }
 243 }