001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.ruby.rubyproject.ui.customizer;
043:
044: import java.awt.Component;
045: import java.awt.GridBagConstraints;
046: import java.awt.GridBagLayout;
047: import java.awt.Insets;
048: import java.awt.event.ActionEvent;
049: import java.awt.event.ActionListener;
050: import java.io.File;
051: import java.net.URI;
052: import java.net.URL;
053: import java.util.Iterator;
054: import java.util.HashSet;
055: import java.util.Set;
056: import java.util.Vector;
057: import java.text.MessageFormat;
058: import javax.swing.DefaultCellEditor;
059: import javax.swing.DefaultListCellRenderer;
060: import javax.swing.JButton;
061: import javax.swing.JComponent;
062: import javax.swing.JFileChooser;
063: import javax.swing.JLabel;
064: import javax.swing.JList;
065: import javax.swing.JOptionPane;
066: import javax.swing.JPanel;
067: import javax.swing.JScrollPane;
068: import javax.swing.JTable;
069: import javax.swing.JTextField;
070: import javax.swing.ListSelectionModel;
071: import javax.swing.SwingUtilities;
072: import javax.swing.event.ListSelectionEvent;
073: import javax.swing.event.ListSelectionListener;
074: import javax.swing.event.CellEditorListener;
075: import javax.swing.event.ChangeEvent;
076: import javax.swing.table.DefaultTableCellRenderer;
077: import javax.swing.table.DefaultTableModel;
078: import org.netbeans.api.project.ProjectUtils;
079: import org.netbeans.api.project.SourceGroup;
080: import org.netbeans.api.project.Sources;
081: import org.netbeans.modules.ruby.rubyproject.RubyProject;
082: import org.netbeans.api.project.FileOwnerQuery;
083: import org.netbeans.api.project.Project;
084: import org.netbeans.api.project.ProjectInformation;
085: import org.netbeans.modules.ruby.rubyproject.SourceRoots;
086: import org.openide.DialogDisplayer;
087: import org.openide.DialogDescriptor;
088: import org.openide.awt.Mnemonics;
089: import org.openide.filesystems.FileObject;
090: import org.openide.filesystems.FileUtil;
091: import org.openide.util.NbBundle;
092:
093: /** Handles adding, removing, reordering of source roots.
094: *
095: * @author Tomas Zezula
096: */
097: public final class RubySourceRootsUi {
098:
099: public static DefaultTableModel createModel(SourceRoots roots) {
100:
101: String[] rootLabels = roots.getRootNames();
102: String[] rootProps = roots.getRootProperties();
103: URL[] rootURLs = roots.getRootURLs();
104: Object[][] data = new Object[rootURLs.length][2];
105: for (int i = 0; i < rootURLs.length; i++) {
106: data[i][0] = new File(URI.create(rootURLs[i]
107: .toExternalForm()));
108: data[i][1] = roots.getRootDisplayName(rootLabels[i],
109: rootProps[i]);
110: }
111: return new SourceRootsModel(data);
112:
113: }
114:
115: public static EditMediator registerEditMediator(RubyProject master,
116: SourceRoots sourceRoots, JTable rootsList,
117: JButton addFolderButton, JButton removeButton,
118: JButton upButton, JButton downButton) {
119:
120: EditMediator em = new EditMediator(master, sourceRoots,
121: rootsList, addFolderButton, removeButton, upButton,
122: downButton);
123:
124: // Register the listeners
125: // On all buttons
126: addFolderButton.addActionListener(em);
127: removeButton.addActionListener(em);
128: upButton.addActionListener(em);
129: downButton.addActionListener(em);
130: // On list selection
131: rootsList.getSelectionModel().addListSelectionListener(em);
132: DefaultCellEditor editor = new DefaultCellEditor(
133: new JTextField());
134: editor.addCellEditorListener(em);
135: rootsList.setDefaultRenderer(File.class, new FileRenderer(
136: FileUtil.toFile(master.getProjectDirectory())));
137: rootsList.setDefaultEditor(String.class, editor);
138: // Set the initial state of the buttons
139: em.valueChanged(null);
140:
141: DefaultTableModel model = (DefaultTableModel) rootsList
142: .getModel();
143: String[] columnNames = new String[2];
144: columnNames[0] = NbBundle.getMessage(RubySourceRootsUi.class,
145: "CTL_PackageFolders");
146: columnNames[1] = NbBundle.getMessage(RubySourceRootsUi.class,
147: "CTL_PackageLabels");
148: model.setColumnIdentifiers(columnNames);
149: rootsList
150: .setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
151:
152: return em;
153: }
154:
155: /**
156: * Opens the standard dialog for warning an user about illegal source roots.
157: * @param roots the set of illegal source/test roots
158: */
159: public static void showIllegalRootsDialog(Set/*<File>*/roots) {
160: JButton closeOption = new JButton(NbBundle.getMessage(
161: RubySourceRootsUi.class, "CTL_RubySourceRootsUi_Close"));
162: closeOption.getAccessibleContext().setAccessibleDescription(
163: NbBundle.getMessage(RubySourceRootsUi.class,
164: "AD_RubySourceRootsUi_Close"));
165: JPanel warning = new WarningDlg(roots);
166: String message = NbBundle.getMessage(RubySourceRootsUi.class,
167: "MSG_InvalidRoot");
168: JOptionPane optionPane = new JOptionPane(new Object[] {
169: message, warning }, JOptionPane.WARNING_MESSAGE, 0,
170: null, new Object[0], null);
171: optionPane.getAccessibleContext().setAccessibleDescription(
172: NbBundle.getMessage(RubySourceRootsUi.class,
173: "AD_InvalidRootDlg"));
174: DialogDescriptor dd = new DialogDescriptor(optionPane, NbBundle
175: .getMessage(RubySourceRootsUi.class,
176: "TITLE_InvalidRoot"), true,
177: new Object[] { closeOption, }, closeOption,
178: DialogDescriptor.DEFAULT_ALIGN, null, null);
179: DialogDisplayer.getDefault().notify(dd);
180: }
181:
182: // Private innerclasses ----------------------------------------------------------------
183:
184: public static class EditMediator implements ActionListener,
185: ListSelectionListener, CellEditorListener {
186:
187: final JTable rootsList;
188: final JButton addFolderButton;
189: final JButton removeButton;
190: final JButton upButton;
191: final JButton downButton;
192: private final Project project;
193: private final SourceRoots sourceRoots;
194: private final Set<File> ownedFolders;
195: private final DefaultTableModel rootsModel;
196: private EditMediator relatedEditMediator;
197: private File lastUsedDir; //Last used current folder in JFileChooser
198:
199: public EditMediator(RubyProject master,
200: SourceRoots sourceRoots, JTable rootsList,
201: JButton addFolderButton, JButton removeButton,
202: JButton upButton, JButton downButton) {
203:
204: if (!(rootsList.getModel() instanceof DefaultTableModel)) {
205: throw new IllegalArgumentException(
206: "Jtable's model has to be of class DefaultTableModel"); // NOI18N
207: }
208:
209: this .rootsList = rootsList;
210: this .addFolderButton = addFolderButton;
211: this .removeButton = removeButton;
212: this .upButton = upButton;
213: this .downButton = downButton;
214: this .ownedFolders = new HashSet<File>();
215:
216: this .project = master;
217: this .sourceRoots = sourceRoots;
218:
219: this .ownedFolders.clear();
220: this .rootsModel = (DefaultTableModel) rootsList.getModel();
221: Vector data = rootsModel.getDataVector();
222: for (Iterator it = data.iterator(); it.hasNext();) {
223: Vector row = (Vector) it.next();
224: File f = (File) row.elementAt(0);
225: this .ownedFolders.add(f);
226: }
227: }
228:
229: public void setRelatedEditMediator(EditMediator rem) {
230: this .relatedEditMediator = rem;
231: }
232:
233: // Implementation of ActionListener ------------------------------------
234:
235: /** Handles button events
236: */
237: public void actionPerformed(ActionEvent e) {
238:
239: Object source = e.getSource();
240:
241: if (source == addFolderButton) {
242:
243: // Let user search for the Jar file
244: JFileChooser chooser = new JFileChooser();
245: FileUtil.preventFileChooserSymlinkTraversal(chooser,
246: null);
247: chooser
248: .setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
249: chooser.setMultiSelectionEnabled(true);
250: if (this .sourceRoots.isTest()) {
251: chooser.setDialogTitle(NbBundle.getMessage(
252: RubySourceRootsUi.class,
253: "LBL_TestFolder_DialogTitle")); // NOI18N
254: } else {
255: chooser.setDialogTitle(NbBundle.getMessage(
256: RubySourceRootsUi.class,
257: "LBL_SourceFolder_DialogTitle")); // NOI18N
258: }
259: File curDir = this .lastUsedDir;
260: if (curDir == null) {
261: curDir = FileUtil.toFile(this .project
262: .getProjectDirectory());
263: }
264: if (curDir != null) {
265: chooser.setCurrentDirectory(curDir);
266: }
267: int option = chooser.showOpenDialog(SwingUtilities
268: .getWindowAncestor(addFolderButton)); // Sow the chooser
269:
270: if (option == JFileChooser.APPROVE_OPTION) {
271: curDir = chooser.getCurrentDirectory();
272: if (curDir != null) {
273: this .lastUsedDir = curDir;
274: if (this .relatedEditMediator != null) {
275: this .relatedEditMediator.lastUsedDir = curDir;
276: }
277: }
278: File files[] = chooser.getSelectedFiles();
279: addFolders(files);
280: }
281:
282: } else if (source == removeButton) {
283: removeElements();
284: } else if (source == upButton) {
285: moveUp();
286: } else if (source == downButton) {
287: moveDown();
288: }
289: }
290:
291: // Selection listener implementation ----------------------------------
292:
293: /** Handles changes in the selection */
294: public void valueChanged(ListSelectionEvent e) {
295: int[] si = rootsList.getSelectedRows();
296:
297: // addJar allways enabled
298:
299: // addLibrary allways enabled
300:
301: // addArtifact allways enabled
302:
303: // remove enabled only if selection is not empty
304: boolean remove = si != null && si.length > 0;
305: // and when the selection does not contain unremovable item
306:
307: // up button enabled if selection is not empty
308: // and the first selected index is not the first row
309: boolean up = si != null && si.length > 0 && si[0] != 0;
310:
311: // up button enabled if selection is not empty
312: // and the laset selected index is not the last row
313: boolean down = si != null && si.length > 0
314: && si[si.length - 1] != rootsList.getRowCount() - 1;
315:
316: removeButton.setEnabled(remove);
317: upButton.setEnabled(up);
318: downButton.setEnabled(down);
319: }
320:
321: public void editingCanceled(ChangeEvent e) {
322:
323: }
324:
325: public void editingStopped(ChangeEvent e) {
326: // fireActionPerformed();
327: }
328:
329: private void addFolders(File files[]) {
330: int[] si = rootsList.getSelectedRows();
331: int lastIndex = si == null || si.length == 0 ? -1
332: : si[si.length - 1];
333: ListSelectionModel selectionModel = this .rootsList
334: .getSelectionModel();
335: selectionModel.clearSelection();
336: Set<File> rootsFromOtherProjects = new HashSet<File>();
337: Set<File> rootsFromRelatedSourceRoots = new HashSet<File>();
338: out: for (int i = 0; i < files.length; i++) {
339: File normalizedFile = FileUtil.normalizeFile(files[i]);
340: Project p;
341: if (ownedFolders.contains(normalizedFile)) {
342: Vector dataVector = rootsModel.getDataVector();
343: for (int j = 0; j < dataVector.size(); j++) {
344: //Sequential search in this minor case is faster than update of positions during each modification
345: File f = (File) ((Vector) dataVector
346: .elementAt(j)).elementAt(0);
347: if (f.equals(normalizedFile)) {
348: selectionModel.addSelectionInterval(j, j);
349: }
350: }
351: } else if (this .relatedEditMediator != null
352: && this .relatedEditMediator.ownedFolders
353: .contains(normalizedFile)) {
354: rootsFromRelatedSourceRoots.add(normalizedFile);
355: continue;
356: }
357: if ((p = FileOwnerQuery
358: .getOwner(normalizedFile.toURI())) != null
359: && !p.getProjectDirectory().equals(
360: project.getProjectDirectory())) {
361: final Sources sources = p.getLookup().lookup(
362: Sources.class);
363: if (sources == null) {
364: rootsFromOtherProjects.add(normalizedFile);
365: continue;
366: }
367: final SourceGroup[] sourceGroups = sources
368: .getSourceGroups(Sources.TYPE_GENERIC);
369: final SourceGroup[] javaGroups = sources
370: .getSourceGroups(RubyProject.SOURCES_TYPE_RUBY);
371: final SourceGroup[] groups = new SourceGroup[sourceGroups.length
372: + javaGroups.length];
373: System.arraycopy(sourceGroups, 0, groups, 0,
374: sourceGroups.length);
375: System.arraycopy(javaGroups, 0, groups,
376: sourceGroups.length, javaGroups.length);
377: final FileObject projectDirectory = p
378: .getProjectDirectory();
379: final FileObject fileObject = FileUtil
380: .toFileObject(normalizedFile);
381: if (projectDirectory == null || fileObject == null) {
382: rootsFromOtherProjects.add(normalizedFile);
383: continue;
384: }
385: for (int j = 0; j < groups.length; j++) {
386: final FileObject sgRoot = groups[j]
387: .getRootFolder();
388: if (fileObject.equals(sgRoot)) {
389: rootsFromOtherProjects.add(normalizedFile);
390: continue out;
391: }
392: if (!projectDirectory.equals(sgRoot)
393: && FileUtil.isParentOf(sgRoot,
394: fileObject)) {
395: rootsFromOtherProjects.add(normalizedFile);
396: continue out;
397: }
398: }
399: }
400: int current = lastIndex + 1 + i;
401: rootsModel
402: .insertRow(
403: current,
404: new Object[] {
405: normalizedFile,
406: sourceRoots
407: .createInitialDisplayName(normalizedFile) }); //NOI18N
408: selectionModel.addSelectionInterval(current, current);
409: this .ownedFolders.add(normalizedFile);
410: }
411: if (!rootsFromOtherProjects.isEmpty()
412: || !rootsFromRelatedSourceRoots.isEmpty()) {
413: rootsFromOtherProjects
414: .addAll(rootsFromRelatedSourceRoots);
415: showIllegalRootsDialog(rootsFromOtherProjects);
416: }
417: // fireActionPerformed();
418: }
419:
420: private void removeElements() {
421:
422: int[] si = rootsList.getSelectedRows();
423:
424: if (si == null || si.length == 0) {
425: assert false : "Remove button should be disabled"; // NOI18N
426: }
427:
428: // Remove the items
429: for (int i = si.length - 1; i >= 0; i--) {
430: this .ownedFolders
431: .remove(((Vector) rootsModel.getDataVector()
432: .elementAt(si[i])).elementAt(0));
433: rootsModel.removeRow(si[i]);
434: }
435:
436: if (rootsModel.getRowCount() != 0) {
437: // Select reasonable item
438: int selectedIndex = si[si.length - 1] - si.length + 1;
439: if (selectedIndex > rootsModel.getRowCount() - 1) {
440: selectedIndex = rootsModel.getRowCount() - 1;
441: }
442: rootsList.setRowSelectionInterval(selectedIndex,
443: selectedIndex);
444: }
445:
446: // fireActionPerformed();
447:
448: }
449:
450: private void moveUp() {
451:
452: int[] si = rootsList.getSelectedRows();
453:
454: if (si == null || si.length == 0) {
455: assert false : "MoveUp button should be disabled"; // NOI18N
456: }
457:
458: // Move the items up
459: ListSelectionModel selectionModel = this .rootsList
460: .getSelectionModel();
461: selectionModel.clearSelection();
462: for (int i = 0; i < si.length; i++) {
463: Vector item = (Vector) rootsModel.getDataVector()
464: .elementAt(si[i]);
465: int newIndex = si[i] - 1;
466: rootsModel.removeRow(si[i]);
467: rootsModel.insertRow(newIndex, item);
468: selectionModel.addSelectionInterval(newIndex, newIndex);
469: }
470: // fireActionPerformed();
471: }
472:
473: private void moveDown() {
474:
475: int[] si = rootsList.getSelectedRows();
476:
477: if (si == null || si.length == 0) {
478: assert false : "MoveDown button should be disabled"; // NOI18N
479: }
480:
481: // Move the items up
482: ListSelectionModel selectionModel = this .rootsList
483: .getSelectionModel();
484: selectionModel.clearSelection();
485: for (int i = si.length - 1; i >= 0; i--) {
486: Vector item = (Vector) rootsModel.getDataVector()
487: .elementAt(si[i]);
488: int newIndex = si[i] + 1;
489: rootsModel.removeRow(si[i]);
490: rootsModel.insertRow(newIndex, item);
491: selectionModel.addSelectionInterval(newIndex, newIndex);
492: }
493: // fireActionPerformed();
494: }
495:
496: }
497:
498: private static class SourceRootsModel extends DefaultTableModel {
499:
500: public SourceRootsModel(Object[][] data) {
501: super (data, new Object[] { "location", "label" });//NOI18N
502: }
503:
504: public @Override
505: boolean isCellEditable(int row, int column) {
506: return column == 1;
507: }
508:
509: public @Override
510: Class getColumnClass(int columnIndex) {
511: switch (columnIndex) {
512: case 0:
513: return File.class;
514: case 1:
515: return String.class;
516: default:
517: return super .getColumnClass(columnIndex);
518: }
519: }
520: }
521:
522: private static class FileRenderer extends DefaultTableCellRenderer {
523:
524: private final File projectFolder;
525:
526: public FileRenderer(File projectFolder) {
527: this .projectFolder = projectFolder;
528: }
529:
530: public @Override
531: Component getTableCellRendererComponent(JTable table,
532: Object value, boolean isSelected, boolean hasFocus,
533: int row, int column) {
534: String displayName;
535: if (value instanceof File) {
536: File root = (File) value;
537: String pfPath = projectFolder.getAbsolutePath()
538: + File.separatorChar;
539: String srPath = root.getAbsolutePath();
540: if (srPath.startsWith(pfPath)) {
541: displayName = srPath.substring(pfPath.length());
542: } else {
543: displayName = srPath;
544: }
545: } else {
546: displayName = null;
547: }
548: Component c = super .getTableCellRendererComponent(table,
549: displayName, isSelected, hasFocus, row, column);
550: if (c instanceof JComponent) {
551: ((JComponent) c).setToolTipText(displayName);
552: }
553: return c;
554: }
555:
556: }
557:
558: private static class WarningDlg extends JPanel {
559:
560: public WarningDlg(Set invalidRoots) {
561: this .initGui(invalidRoots);
562: }
563:
564: private void initGui(Set invalidRoots) {
565: setLayout(new GridBagLayout());
566: JLabel label = new JLabel();
567: Mnemonics.setLocalizedText(label, NbBundle.getMessage(
568: RubySourceRootsUi.class, "LBL_InvalidRoot"));
569: GridBagConstraints c = new GridBagConstraints();
570: c.gridx = GridBagConstraints.RELATIVE;
571: c.gridy = GridBagConstraints.RELATIVE;
572: c.gridwidth = GridBagConstraints.REMAINDER;
573: c.fill = GridBagConstraints.HORIZONTAL;
574: c.anchor = GridBagConstraints.NORTHWEST;
575: c.weightx = 1.0;
576: c.insets = new Insets(12, 0, 6, 0);
577: ((GridBagLayout) this .getLayout()).setConstraints(label, c);
578: this .add(label);
579: JList roots = new JList(invalidRoots.toArray());
580: roots.setCellRenderer(new InvalidRootRenderer(true));
581: JScrollPane p = new JScrollPane(roots);
582: c = new GridBagConstraints();
583: c.gridx = GridBagConstraints.RELATIVE;
584: c.gridy = GridBagConstraints.RELATIVE;
585: c.gridwidth = GridBagConstraints.REMAINDER;
586: c.fill = GridBagConstraints.BOTH;
587: c.anchor = GridBagConstraints.NORTHWEST;
588: c.weightx = c.weighty = 1.0;
589: c.insets = new Insets(0, 0, 12, 0);
590: ((GridBagLayout) this .getLayout()).setConstraints(p, c);
591: this .add(p);
592: label.setLabelFor(roots);
593: roots.getAccessibleContext().setAccessibleDescription(
594: NbBundle.getMessage(RubySourceRootsUi.class,
595: "AD_InvalidRoot"));
596: JLabel label2 = new JLabel();
597: label2.setText(NbBundle.getMessage(RubySourceRootsUi.class,
598: "MSG_InvalidRoot2"));
599: c = new GridBagConstraints();
600: c.gridx = GridBagConstraints.RELATIVE;
601: c.gridy = GridBagConstraints.RELATIVE;
602: c.gridwidth = GridBagConstraints.REMAINDER;
603: c.fill = GridBagConstraints.HORIZONTAL;
604: c.anchor = GridBagConstraints.NORTHWEST;
605: c.weightx = 1.0;
606: c.insets = new Insets(0, 0, 0, 0);
607: ((GridBagLayout) this .getLayout())
608: .setConstraints(label2, c);
609: this .add(label2);
610: }
611:
612: private static class InvalidRootRenderer extends
613: DefaultListCellRenderer {
614:
615: private final boolean projectConflict;
616:
617: public InvalidRootRenderer(boolean projectConflict) {
618: this .projectConflict = projectConflict;
619: }
620:
621: public @Override
622: Component getListCellRendererComponent(JList list,
623: Object value, int index, boolean isSelected,
624: boolean cellHasFocus) {
625: File f = (File) value;
626: String message = f.getAbsolutePath();
627: if (projectConflict) {
628: Project p = FileOwnerQuery.getOwner(f.toURI());
629: if (p != null) {
630: ProjectInformation pi = ProjectUtils
631: .getInformation(p);
632: String projectName = pi.getDisplayName();
633: message = MessageFormat.format(NbBundle
634: .getMessage(RubySourceRootsUi.class,
635: "TXT_RootOwnedByProject"),
636: new Object[] { message, projectName });
637: }
638: }
639: return super.getListCellRendererComponent(list,
640: message, index, isSelected, cellHasFocus);
641: }
642: }
643: }
644:
645: }
|