package com.buildml.eclipse.packages.properties.filegroup; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ITreeSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreePath; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.layout.RowLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import com.buildml.eclipse.bobj.UIFileGroup; import com.buildml.eclipse.bobj.UIInteger; import com.buildml.eclipse.utils.BmlPropertyPage; import com.buildml.eclipse.utils.GraphitiUtils; import com.buildml.eclipse.utils.UndoOpAdapter; import com.buildml.eclipse.utils.dialogs.VFSTreeSelectionDialog; import com.buildml.model.IFileGroupMgr; import com.buildml.model.undo.FileGroupUndoOp; /** * An Eclipse "property" page that allows viewing/editing of file group's content. * Objects of this class are referenced in the plugin.xml file and are dynamically * created when the properties dialog is opened for a UIFileGroup object. * * @author Peter Smith <psmith@arapiki.com> */ public class FileGroupContentPropertyPage extends BmlPropertyPage { /*=====================================================================================* * FIELDS/TYPES *=====================================================================================*/ /** The ID of the underlying file group */ private int fileGroupId; /** The type of this file group (source, generated, etc) */ private int fileGroupType; /** The TreeViewer control that contains the list of files */ private TreeViewer filesList; /** The current content of the file group */ private ArrayList<Integer> currentMembers; /** The initial content of the file group, before any editing took place */ private ArrayList<Integer> initialMembers; /** * Set to true if we're programmatically changing the selection and should therefore * include the listener events. */ private boolean programmaticallySelecting = false; /** the JFace provider for fetching children of elements in the tree viewer */ private FileGroupContentProvider contentProvider; /*=====================================================================================* * CONSTRUCTORS *=====================================================================================*/ /** * Create a new ActionShellCommandPage object. */ public FileGroupContentPropertyPage() { /* nothing */ } /*-------------------------------------------------------------------------------------*/ /** * The OK button was pressed. Proceed to change the file group in the underlying database */ @Override public boolean performOk() { /* create an undo/redo operation that will invoke the underlying database changes */ FileGroupUndoOp op = new FileGroupUndoOp(buildStore, fileGroupId); op.recordMembershipChange(initialMembers, currentMembers); new UndoOpAdapter("Modify File Group", op).invoke(); return super.performOk(); } /*=====================================================================================* * PROTECTED METHODS *=====================================================================================*/ /** * Create the widgets that appear within the properties dialog box. */ @Override protected Control createContents(Composite parent) { /* determine the numeric ID of the file group */ UIFileGroup fileGroup = (UIFileGroup) GraphitiUtils.getBusinessObjectFromElement(getElement(), UIFileGroup.class); if (fileGroup == null) { return null; } fileGroupId = fileGroup.getId(); fileGroupType = fileGroupMgr.getGroupType(fileGroupId); if (!fetchMembers(fileGroupId)) { return null; } setTitle("File Group Content:"); /* * Create a panel in which all sub-widgets are added. The first (of 2) * columns will content the "list" of files in the file group. The * second (of 2) columns contain buttons for performing actions * on those files. */ Composite panel = new Composite(parent, SWT.NONE); GridLayout layout = new GridLayout(); layout.marginHeight = 0; layout.marginWidth = 0; layout.numColumns = 2; panel.setLayout(layout); /* * The first column - the list of files in the file group. */ filesList = new TreeViewer(panel, SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); filesList.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); contentProvider = new FileGroupContentProvider(buildStore, fileGroupType); filesList.setContentProvider(contentProvider); filesList.setLabelProvider(new FileGroupLabelProvider(buildStore, fileGroupType)); /* * The second column - buttons that we can press to modify the file group content */ Composite buttonPanel = new Composite(panel, SWT.NONE); buttonPanel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); RowLayout buttonPanelLayout = new RowLayout(SWT.VERTICAL); buttonPanelLayout.fill = true; buttonPanelLayout.marginLeft = buttonPanelLayout.marginRight = 10; buttonPanelLayout.spacing = 10; buttonPanel.setLayout(buttonPanelLayout); /* add button - adds a new file to the source file group */ if (fileGroupType == IFileGroupMgr.SOURCE_GROUP) { final Button newButton = new Button(buttonPanel, SWT.NONE); newButton.setText("Add File"); newButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { performAddOperation(); } }); } /* delete button - deletes the selected files */ final Button deleteButton = new Button(buttonPanel, SWT.NONE); deleteButton.setText("Delete"); deleteButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { performDeleteOperation(); } }); /* move up button - move the selected files upwards in the list */ final Button moveUpButton = new Button(buttonPanel, SWT.NONE); moveUpButton.setText("Move Up"); moveUpButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { performMoveOperation(-1); } }); /* move down button - move the selected files down the list */ final Button moveDownButton = new Button(buttonPanel, SWT.NONE); moveDownButton.setText("Move Down"); moveDownButton.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event event) { performMoveOperation(1); } }); /* * When items in the list box are selected/deselected, we need to enable/disable * the buttons accordingly. By default, all buttons are disabled. */ deleteButton.setEnabled(false); moveUpButton.setEnabled(false); moveDownButton.setEnabled(false); filesList.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { if (programmaticallySelecting) { return; } int selectedFilesCount = handleSelection(); deleteButton.setEnabled(selectedFilesCount >= 1); enableMoveButtons(moveUpButton, moveDownButton); } }); /* populate the TreeViewer control with all the file members */ populateList(filesList); return panel; } /*-------------------------------------------------------------------------------------*/ /** * Restore the list of files to its initial value. */ @SuppressWarnings("unchecked") @Override protected void performDefaults() { currentMembers = (ArrayList<Integer>)initialMembers.clone(); populateList(filesList); } /*-------------------------------------------------------------------------------------*/ /** * Compute the state of the "Move Up" and "Move Down" buttons. If the selection has the * last member selected, we can't move down. If it has the first member selected, we can't * move up. If no members are selected, neither is an option. * * @param moveUpButton The "Move Up" button control. * @param moveDownButton The "Move Down" button control. */ protected void enableMoveButtons(Button moveUpButton, Button moveDownButton) { /* we must have at least one member selected */ ITreeSelection selection = (ITreeSelection) filesList.getSelection(); int selectedFilesCount = selection.size(); boolean moveUpEnabled = selectedFilesCount >= 1; boolean moveDownEnabled = selectedFilesCount >= 1; /* check whether the first/last members are highlighted */ Iterator<TreeMember> iter = selection.iterator(); while (iter.hasNext()) { TreeMember member = iter.next(); if (member.seq == 0) { moveUpEnabled = false; } else if (member.seq == currentMembers.size() - 1) { moveDownEnabled = false; } } /* enable/disable the button state, as appropriate */ moveUpButton.setEnabled(moveUpEnabled); moveDownButton.setEnabled(moveDownEnabled); } /*=====================================================================================* * PRIVATE METHODS *=====================================================================================*/ /** * Fetch the initial set of members of this file group. * * @param groupId The ID of the group we're editing. * @return True if the group is valid, else false. */ private boolean fetchMembers(int groupId) { int groupSize = fileGroupMgr.getGroupSize(groupId); if (groupSize < 0) { return false; } Integer members[]; if (fileGroupType == IFileGroupMgr.SOURCE_GROUP) { members = fileGroupMgr.getPathIds(groupId); } else { members = fileGroupMgr.getSubGroups(groupId); } /* * We store the members in two separate arrays - one to record the initial * set of members, and one to store the "to-be" set of members that the user * is permitted to modify. */ initialMembers = new ArrayList<Integer>(members.length); currentMembers = new ArrayList<Integer>(members.length); for (int i = 0; i < members.length; i++) { initialMembers.add(members[i]); currentMembers.add(members[i]); } return true; } /*-------------------------------------------------------------------------------------*/ /** * Populate the control containing the list of files. * * @param filesList The List control. */ private void populateList(TreeViewer filesList) { TreeMember membersArray[] = new TreeMember[currentMembers.size()]; int i = 0; for (int id : currentMembers) { membersArray[i] = new TreeMember(0, i, id, null); i++; } filesList.setInput(membersArray); filesList.expandAll(); } /*-------------------------------------------------------------------------------------*/ /** * Add a new file (or files) to the file group (the new item will be added to the bottom). */ private void performAddOperation() { VFSTreeSelectionDialog dialog = new VFSTreeSelectionDialog(getShell(), buildStore, "Select files or directories to add to file group.", true, true); dialog.setAllowMultiple(true); /* invoke the dialog, allowing the user to select a directory/file */ if (dialog.open() == VFSTreeSelectionDialog.OK) { Object[] result = dialog.getResult(); if (result.length >= 1) { /* add the new members to the bottom of the members list */ for (int i = 0; i != result.length; i++) { UIInteger selection = (UIInteger)result[i]; int selectionId = selection.getId(); currentMembers.add(selectionId); } populateList(filesList); } } } /*-------------------------------------------------------------------------------------*/ /** * Delete the selected file(s). */ private void performDeleteOperation() { /* fetch the indicies of the selected items */ ITreeSelection selection = (ITreeSelection) filesList.getSelection(); Iterator<Object> iter = selection.iterator(); while (iter.hasNext()) { Object element = iter.next(); /* we can only remove top-level items (for source file groups and merge file groups). */ if (element instanceof TreeMember) { TreeMember member = (TreeMember)element; if (member.level == 0) { currentMembers.remove(member.seq); } } } populateList(filesList); } /*-------------------------------------------------------------------------------------*/ /** * Move the selected list items further up or down the list. * * @param direction -1 to move the selected items upwards, or 1 to move downwards. */ private void performMoveOperation(int direction) { if ((direction != 1) && (direction != -1)) { return; } /* if nothing's selected, there's nothing to do */ ITreeSelection selection = (ITreeSelection) filesList.getSelection(); if (selection.size() == 0) { return; } /* * Fetch the indicies (into the "currentMembers" member) of the selected items. * For example, if the user selects the 2nd and 4th items in the list, * our array should be [2, 4]. To do this, we need to convert the ITreeSelection * that Eclipse gives us into a sorted array of indicies. */ ArrayList<Integer> selectedTopLevelIndicies = new ArrayList<Integer>(); ArrayList<TreeMember> selectedMembers = new ArrayList<TreeMember>(); Iterator<Object> iter = selection.iterator(); int i = 0; while (iter.hasNext()) { Object element = iter.next(); if (element instanceof TreeMember) { TreeMember member = (TreeMember)element; if (member.level == 0) { selectedTopLevelIndicies.add(member.seq); i++; } selectedMembers.add(member); } } /* convert from ArrayList<Integer> to sorted Integer[] */ Integer selectedIndicies[] = new Integer[i]; selectedTopLevelIndicies.toArray(selectedIndicies); Arrays.sort(selectedIndicies); /* * if our first item is at 0, and we're moving up, or our last item is at listSize -1, * and we're moving down, there's nothing to do. We can't move beyond the bounds of * the list. */ int currentMemberSize = currentMembers.size(); if (((direction == -1) && (selectedIndicies[0] == 0)) || ((direction == 1) && (selectedIndicies[selectedIndicies.length - 1] == (currentMemberSize - 1)))) { return; } /* * The direction in which we're moving the files will dictate the order in which * we must traverse the list of files (if we go the wrong direction, list items * will "leapfrog" their neighbours, even if their neighbours are also moving. */ int firstIndex, lastIndex; if (direction == 1) { firstIndex = selectedIndicies.length - 1; lastIndex = -1; } else { firstIndex = 0; lastIndex = selectedIndicies.length; } /* * Now we actually modify the content of "currentMembers". Starting at position * 'firstIndex', and decrementing by 'direction' until we reach 'lastIndex'. */ int pos = firstIndex; while (pos != lastIndex) { int index = selectedIndicies[pos]; int id = currentMembers.get(index); /* shuffle the item along */ currentMembers.remove(index); currentMembers.add(index + direction, id); /* move to next selected file */ pos -= direction; } /* redraw the tree with the modified content */ populateList(filesList); /* * Reset the selection so that the same elements are selected in the new tree. Naturaly * their sequence numbers have now changed. */ for (TreeMember member: selectedMembers) { member.seq += direction; } StructuredSelection newSelection = new StructuredSelection(selectedMembers); filesList.setSelection(newSelection, true); } /*-------------------------------------------------------------------------------------*/ /** * Handle selection behaviour, which depends on the type of file group selected. For * source groups, any number of items can be selected. For merge groups, if the user clicks * on a single path name, we instead auto-select the entire sub-group that the path * belongs to. * * @return The number of items selected. Note that a merge file group will show an entire * subgroup as being selected, but will return "1". */ private int handleSelection() { ITreeSelection selection = (ITreeSelection)filesList.getSelection(); /* for source groups, any number of items can be selected */ if (fileGroupType == IFileGroupMgr.SOURCE_GROUP) { return selection.size(); } /* for merge file groups, selecting a single item will select all of them in that group */ else if (fileGroupType == IFileGroupMgr.MERGE_GROUP) { /* for each selected element, select its entire subtree */ TreePath paths[] = selection.getPaths(); List<TreeMember> elementsToSelect = new ArrayList<TreeMember>(); for (int i = 0; i < paths.length; i++) { /* determine the parent, select it, then select all it children */ TreeMember parentOfSubTree = (TreeMember) paths[i].getFirstSegment(); elementsToSelect.add(parentOfSubTree); Object children[] = contentProvider.getChildren(parentOfSubTree); if (children != null) { for (int j = 0; j < children.length; j++) { elementsToSelect.add((TreeMember)children[j]); } } } /* * Proceed to modify the current selection, taking care to ignore selection events * (since this method is called from within an event listener). */ StructuredSelection newSelection = new StructuredSelection(elementsToSelect); programmaticallySelecting = true; filesList.setSelection(newSelection, true); programmaticallySelecting = false; /* yes, there's a selection */ return elementsToSelect.size(); } /* other situations not yet handled */ return 0; } /*-------------------------------------------------------------------------------------*/ }