1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.editors.layout.refactoring; 18 19 import static org.eclipse.jface.viewers.StyledString.DECORATIONS_STYLER; 20 import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER; 21 22 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 23 import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator; 24 import com.android.resources.ResourceType; 25 import com.android.utils.Pair; 26 27 import org.eclipse.core.resources.IProject; 28 import org.eclipse.jface.viewers.CheckStateChangedEvent; 29 import org.eclipse.jface.viewers.CheckboxTableViewer; 30 import org.eclipse.jface.viewers.ICheckStateListener; 31 import org.eclipse.jface.viewers.IStructuredContentProvider; 32 import org.eclipse.jface.viewers.StyledCellLabelProvider; 33 import org.eclipse.jface.viewers.StyledString; 34 import org.eclipse.jface.viewers.Viewer; 35 import org.eclipse.jface.viewers.ViewerCell; 36 import org.eclipse.swt.SWT; 37 import org.eclipse.swt.events.SelectionAdapter; 38 import org.eclipse.swt.events.SelectionEvent; 39 import org.eclipse.swt.layout.GridData; 40 import org.eclipse.swt.layout.GridLayout; 41 import org.eclipse.swt.layout.RowLayout; 42 import org.eclipse.swt.widgets.Button; 43 import org.eclipse.swt.widgets.Composite; 44 import org.eclipse.swt.widgets.Label; 45 import org.eclipse.swt.widgets.Table; 46 import org.eclipse.swt.widgets.Text; 47 import org.w3c.dom.Attr; 48 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.Comparator; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Set; 57 58 class ExtractStyleWizard extends VisualRefactoringWizard { 59 public ExtractStyleWizard(ExtractStyleRefactoring ref, LayoutEditorDelegate editor) { 60 super(ref, editor); 61 setDefaultPageTitle(ref.getName()); 62 } 63 64 @Override 65 protected void addUserInputPages() { 66 String initialName = "styleName"; 67 addPage(new InputPage(mDelegate.getEditor().getProject(), initialName)); 68 } 69 70 /** 71 * Wizard page which inputs parameters for the {@link ExtractStyleRefactoring} 72 * operation 73 */ 74 private static class InputPage extends VisualRefactoringInputPage { 75 private final IProject mProject; 76 private final String mSuggestedName; 77 private Text mNameText; 78 private Table mTable; 79 private Button mRemoveExtracted; 80 private Button mSetStyle; 81 private Button mRemoveAll; 82 private Button mExtend; 83 private CheckboxTableViewer mCheckedView; 84 85 private String mParentStyle; 86 private Set<Attr> mInSelection; 87 private List<Attr> mAllAttributes; 88 private int mElementCount; 89 private Map<Attr, Integer> mFrequencyCount; 90 private Set<Attr> mShown; 91 private List<Attr> mInitialChecked; 92 private List<Attr> mAllChecked; 93 private List<Map.Entry<String, List<Attr>>> mRoot; 94 private Map<String, List<Attr>> mAvailableAttributes; 95 96 public InputPage(IProject project, String suggestedName) { 97 super("ExtractStyleInputPage"); 98 mProject = project; 99 mSuggestedName = suggestedName; 100 } 101 102 @Override 103 public void createControl(Composite parent) { 104 initialize(); 105 106 Composite composite = new Composite(parent, SWT.NONE); 107 composite.setLayout(new GridLayout(2, false)); 108 109 Label nameLabel = new Label(composite, SWT.NONE); 110 nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); 111 nameLabel.setText("Style Name:"); 112 113 mNameText = new Text(composite, SWT.BORDER); 114 mNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); 115 mNameText.addModifyListener(mModifyValidateListener); 116 117 mRemoveExtracted = new Button(composite, SWT.CHECK); 118 mRemoveExtracted.setSelection(true); 119 mRemoveExtracted.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1)); 120 mRemoveExtracted.setText("Remove extracted attributes"); 121 mRemoveExtracted.addSelectionListener(mSelectionValidateListener); 122 123 mRemoveAll = new Button(composite, SWT.CHECK); 124 mRemoveAll.setSelection(false); 125 mRemoveAll.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1)); 126 mRemoveAll.setText("Remove all extracted attributes regardless of value"); 127 mRemoveAll.addSelectionListener(mSelectionValidateListener); 128 129 boolean defaultSetStyle = false; 130 if (mParentStyle != null) { 131 mExtend = new Button(composite, SWT.CHECK); 132 mExtend.setSelection(true); 133 mExtend.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1)); 134 mExtend.setText(String.format("Extend %1$s", mParentStyle)); 135 mExtend.addSelectionListener(mSelectionValidateListener); 136 defaultSetStyle = true; 137 } 138 139 mSetStyle = new Button(composite, SWT.CHECK); 140 mSetStyle.setSelection(defaultSetStyle); 141 mSetStyle.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1)); 142 mSetStyle.setText("Set style attribute on extracted elements"); 143 mSetStyle.addSelectionListener(mSelectionValidateListener); 144 145 new Label(composite, SWT.NONE); 146 new Label(composite, SWT.NONE); 147 148 Label tableLabel = new Label(composite, SWT.NONE); 149 tableLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); 150 tableLabel.setText("Choose style attributes to extract:"); 151 152 mCheckedView = CheckboxTableViewer.newCheckList(composite, SWT.BORDER 153 | SWT.FULL_SELECTION | SWT.HIDE_SELECTION); 154 mTable = mCheckedView.getTable(); 155 mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 2)); 156 ((GridData) mTable.getLayoutData()).heightHint = 200; 157 158 mCheckedView.setContentProvider(new ArgumentContentProvider()); 159 mCheckedView.setLabelProvider(new ArgumentLabelProvider()); 160 mCheckedView.setInput(mRoot); 161 final Object[] initialSelection = mInitialChecked.toArray(); 162 mCheckedView.setCheckedElements(initialSelection); 163 164 mCheckedView.addCheckStateListener(new ICheckStateListener() { 165 @Override 166 public void checkStateChanged(CheckStateChangedEvent event) { 167 // Try to disable other elements that conflict with this 168 boolean isChecked = event.getChecked(); 169 if (isChecked) { 170 Attr attribute = (Attr) event.getElement(); 171 List<Attr> list = mAvailableAttributes.get(attribute.getLocalName()); 172 for (Attr other : list) { 173 if (other != attribute && mShown.contains(other)) { 174 mCheckedView.setChecked(other, false); 175 } 176 } 177 } 178 179 validatePage(); 180 } 181 }); 182 183 // Select All / Deselect All 184 Composite buttonForm = new Composite(composite, SWT.NONE); 185 buttonForm.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1)); 186 RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL); 187 rowLayout.marginTop = 0; 188 rowLayout.marginLeft = 0; 189 buttonForm.setLayout(rowLayout); 190 Button checkAllButton = new Button(buttonForm, SWT.FLAT); 191 checkAllButton.setText("Select All"); 192 checkAllButton.addSelectionListener(new SelectionAdapter() { 193 @Override 194 public void widgetSelected(SelectionEvent e) { 195 // Select "all" (but not conflicting settings) 196 mCheckedView.setCheckedElements(mAllChecked.toArray()); 197 validatePage(); 198 } 199 }); 200 Button uncheckAllButton = new Button(buttonForm, SWT.FLAT); 201 uncheckAllButton.setText("Deselect All"); 202 uncheckAllButton.addSelectionListener(new SelectionAdapter() { 203 @Override 204 public void widgetSelected(SelectionEvent e) { 205 mCheckedView.setAllChecked(false); 206 validatePage(); 207 } 208 }); 209 210 // Initialize UI: 211 if (mSuggestedName != null) { 212 mNameText.setText(mSuggestedName); 213 } 214 215 setControl(composite); 216 validatePage(); 217 } 218 219 private void initialize() { 220 ExtractStyleRefactoring ref = (ExtractStyleRefactoring) getRefactoring(); 221 222 mElementCount = ref.getElements().size(); 223 224 mParentStyle = ref.getParentStyle(); 225 226 // Set up data structures needed by the wizard -- to compute the actual 227 // attributes to list in the wizard (there could be multiple attributes 228 // of the same name (on different elements) and we only want to show one, etc.) 229 230 Pair<Map<String, List<Attr>>, Set<Attr>> result = ref.getAvailableAttributes(); 231 // List of all available attributes on the selected elements 232 mAvailableAttributes = result.getFirst(); 233 // Set of attributes that overlap the text selection, or all attributes if 234 // wizard is invoked from GUI context 235 mInSelection = result.getSecond(); 236 237 // The root data structure, which we set as the table root. The content provider 238 // will produce children from it. This is the entry set of a map from 239 // attribute name to list of attribute nodes for that attribute name. 240 mRoot = new ArrayList<Map.Entry<String, List<Attr>>>( 241 mAvailableAttributes.entrySet()); 242 243 // Sort the items by attribute name -- the attribute name is the key 244 // in the entry set above. 245 Collections.sort(mRoot, new Comparator<Map.Entry<String, List<Attr>>>() { 246 @Override 247 public int compare(Map.Entry<String, List<Attr>> e1, 248 Map.Entry<String, List<Attr>> e2) { 249 return e1.getKey().compareTo(e2.getKey()); 250 } 251 }); 252 253 // Set of attributes actually included in the list shown to the user. 254 // (There could be many additional "aliasing" nodes on other elements 255 // with the same name.) Note however that we DO show multiple attribute 256 // occurrences of the same attribute name: precisely one for each unique -value- 257 // of that attribute. 258 mShown = new HashSet<Attr>(); 259 260 // The list of initially checked attributes. 261 mInitialChecked = new ArrayList<Attr>(); 262 263 // The list of attributes to be checked if "Select All" is chosen (this is not 264 // the same as *all* attributes, since we need to exclude any conflicts) 265 mAllChecked = new ArrayList<Attr>(); 266 267 // All attributes. 268 mAllAttributes = new ArrayList<Attr>(); 269 270 // Frequency count, from attribute to integer. Attributes that do not 271 // appear in the list have frequency 1, not 0. 272 mFrequencyCount = new HashMap<Attr, Integer>(); 273 274 for (Map.Entry<String, List<Attr>> entry : mRoot) { 275 // Iterate over all attributes of the same name, and sort them 276 // by value. This will make it easy to list each -unique- value in the 277 // wizard. 278 List<Attr> attrList = entry.getValue(); 279 Collections.sort(attrList, new Comparator<Attr>() { 280 @Override 281 public int compare(Attr a1, Attr a2) { 282 return a1.getValue().compareTo(a2.getValue()); 283 } 284 }); 285 286 // We need to compute a couple of things: the frequency for all identical 287 // values (and stash them in the frequency map), and record the first 288 // attribute with a particular value into the list of attributes to 289 // be shown. 290 Attr prevAttr = null; 291 String prev = null; 292 List<Attr> uniqueValueAttrs = new ArrayList<Attr>(); 293 for (Attr attr : attrList) { 294 String value = attr.getValue(); 295 if (value.equals(prev)) { 296 Integer count = mFrequencyCount.get(prevAttr); 297 if (count == null) { 298 count = Integer.valueOf(2); 299 } else { 300 count = Integer.valueOf(count.intValue() + 1); 301 } 302 mFrequencyCount.put(prevAttr, count); 303 } else { 304 uniqueValueAttrs.add(attr); 305 prev = value; 306 prevAttr = attr; 307 } 308 } 309 310 // Sort the values by frequency (and for equal frequencies, alphabetically 311 // by value) 312 Collections.sort(uniqueValueAttrs, new Comparator<Attr>() { 313 @Override 314 public int compare(Attr a1, Attr a2) { 315 Integer f1 = mFrequencyCount.get(a1); 316 Integer f2 = mFrequencyCount.get(a2); 317 if (f1 == null) { 318 f1 = Integer.valueOf(1); 319 } 320 if (f2 == null) { 321 f2 = Integer.valueOf(1); 322 } 323 int delta = f2.intValue() - f1.intValue(); 324 if (delta != 0) { 325 return delta; 326 } else { 327 return a1.getValue().compareTo(a2.getValue()); 328 } 329 } 330 }); 331 332 // Add the items in order, and select those attributes that overlap 333 // the selection 334 mAllAttributes.addAll(uniqueValueAttrs); 335 mShown.addAll(uniqueValueAttrs); 336 Attr first = uniqueValueAttrs.get(0); 337 mAllChecked.add(first); 338 if (mInSelection.contains(first)) { 339 mInitialChecked.add(first); 340 } 341 } 342 } 343 344 @Override 345 protected boolean validatePage() { 346 boolean ok = true; 347 348 String text = mNameText.getText().trim(); 349 350 if (text.length() == 0) { 351 setErrorMessage("Provide a name for the new style"); 352 ok = false; 353 } else { 354 ResourceNameValidator validator = ResourceNameValidator.create(false, mProject, 355 ResourceType.STYLE); 356 String message = validator.isValid(text); 357 if (message != null) { 358 setErrorMessage(message); 359 ok = false; 360 } 361 } 362 363 Object[] checkedElements = mCheckedView.getCheckedElements(); 364 if (checkedElements.length == 0) { 365 setErrorMessage("Choose at least one attribute to extract"); 366 ok = false; 367 } 368 369 if (ok) { 370 setErrorMessage(null); 371 372 // Record state 373 ExtractStyleRefactoring refactoring = (ExtractStyleRefactoring) getRefactoring(); 374 refactoring.setStyleName(text); 375 refactoring.setRemoveExtracted(mRemoveExtracted.getSelection()); 376 refactoring.setRemoveAll(mRemoveAll.getSelection()); 377 refactoring.setApplyStyle(mSetStyle.getSelection()); 378 if (mExtend != null && mExtend.getSelection()) { 379 refactoring.setParent(mParentStyle); 380 } 381 List<Attr> attributes = new ArrayList<Attr>(); 382 for (Object o : checkedElements) { 383 attributes.add((Attr) o); 384 } 385 refactoring.setChosenAttributes(attributes); 386 } 387 388 setPageComplete(ok); 389 return ok; 390 } 391 392 private class ArgumentLabelProvider extends StyledCellLabelProvider { 393 public ArgumentLabelProvider() { 394 } 395 396 @Override 397 public void update(ViewerCell cell) { 398 Object element = cell.getElement(); 399 Attr attribute = (Attr) element; 400 401 StyledString styledString = new StyledString(); 402 styledString.append(attribute.getLocalName()); 403 styledString.append(" = ", QUALIFIER_STYLER); 404 styledString.append(attribute.getValue()); 405 406 if (mElementCount > 1) { 407 Integer f = mFrequencyCount.get(attribute); 408 String s = String.format(" (in %d/%d elements)", 409 f != null ? f.intValue(): 1, mElementCount); 410 styledString.append(s, DECORATIONS_STYLER); 411 } 412 cell.setText(styledString.toString()); 413 cell.setStyleRanges(styledString.getStyleRanges()); 414 super.update(cell); 415 } 416 } 417 418 private class ArgumentContentProvider implements IStructuredContentProvider { 419 public ArgumentContentProvider() { 420 } 421 422 @Override 423 public Object[] getElements(Object inputElement) { 424 if (inputElement == mRoot) { 425 return mAllAttributes.toArray(); 426 } 427 428 return new Object[0]; 429 } 430 431 @Override 432 public void dispose() { 433 } 434 435 @Override 436 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { 437 } 438 } 439 } 440 } 441