/******************************************************************************* * CogTool Copyright Notice and Distribution Terms * CogTool 1.3, Copyright (c) 2005-2013 Carnegie Mellon University * This software is distributed under the terms of the FSF Lesser * Gnu Public License (see LGPL.txt). * * CogTool is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * CogTool is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with CogTool; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * CogTool makes use of several third-party components, with the * following notices: * * Eclipse SWT version 3.448 * Eclipse GEF Draw2D version 3.2.1 * * Unless otherwise indicated, all Content made available by the Eclipse * Foundation is provided to you under the terms and conditions of the Eclipse * Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this * Content and is also available at http://www.eclipse.org/legal/epl-v10.html. * * CLISP version 2.38 * * Copyright (c) Sam Steingold, Bruno Haible 2001-2006 * This software is distributed under the terms of the FSF Gnu Public License. * See COPYRIGHT file in clisp installation folder for more information. * * ACT-R 6.0 * * Copyright (c) 1998-2007 Dan Bothell, Mike Byrne, Christian Lebiere & * John R Anderson. * This software is distributed under the terms of the FSF Lesser * Gnu Public License (see LGPL.txt). * * Apache Jakarta Commons-Lang 2.1 * * This product contains software developed by the Apache Software Foundation * (http://www.apache.org/) * * jopt-simple version 1.0 * * Copyright (c) 2004-2013 Paul R. Holser, Jr. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * Mozilla XULRunner 1.9.0.5 * * The contents of this file are subject to the Mozilla Public License * Version 1.1 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/. * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * The J2SE(TM) Java Runtime Environment version 5.0 * * Copyright 2009 Sun Microsystems, Inc., 4150 * Network Circle, Santa Clara, California 95054, U.S.A. All * rights reserved. U.S. * See the LICENSE file in the jre folder for more information. ******************************************************************************/ package edu.cmu.cs.hcii.cogtool.uimodel; import java.util.Arrays; import java.util.EventObject; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.PaletteData; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import org.eclipse.swt.widgets.TreeItem; import edu.cmu.cs.hcii.cogtool.CogToolPref; import edu.cmu.cs.hcii.cogtool.ResultDisplayPolicy; import edu.cmu.cs.hcii.cogtool.model.AUndertaking; import edu.cmu.cs.hcii.cogtool.model.Demonstration; import edu.cmu.cs.hcii.cogtool.model.Design; import edu.cmu.cs.hcii.cogtool.model.Project; import edu.cmu.cs.hcii.cogtool.model.TaskApplication; import edu.cmu.cs.hcii.cogtool.model.TaskGroup; import edu.cmu.cs.hcii.cogtool.util.AlertHandler; import edu.cmu.cs.hcii.cogtool.util.L10N; import edu.cmu.cs.hcii.cogtool.util.NameChangeAlert; import edu.cmu.cs.hcii.cogtool.util.SWTStringUtil; import edu.cmu.cs.hcii.cogtool.util.StringUtil; import edu.cmu.cs.hcii.cogtool.util.WindowUtil; public class ProjectUIModel extends DefaultUIModel { private static final String selectDesignHelp = L10N.get("PUIM.SelectDesignHelp", "Click to select"); private static final String editDesignHelp = L10N.get("PUIM.EditDesignHelp", "Double-click to edit"); private static final String selectAllHelp = L10N.get("PUIM.SelectAllHelp", "Click to select all tasks"); private static final int COL_WIDTH_NO_RANGE = 120; private static final int COL_WIDTH_WITH_RANGE = 180; public interface TreeRowHook { public void onRowCreation(TreeItem row); public void onRowDeletion(TreeItem row); } public interface TreeColumnHook { public void onColumnCreation(TreeColumn column); public void onColumnDeletion(TreeColumn column); } public static class DesignReordering extends EventObject { public TreeColumn movedDesign; public Design[] newDesignOrdering; public DesignReordering(ProjectUIModel projectUIModel) { super(projectUIModel); } } protected static Color selectedTextColor = WindowUtil.GLOBAL_DISPLAY.getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT); protected static Color unselectedTextColor = WindowUtil.GLOBAL_DISPLAY.getSystemColor(SWT.COLOR_LIST_FOREGROUND); protected static Color selectedBackgroundColor = WindowUtil.GLOBAL_DISPLAY.getSystemColor(SWT.COLOR_LIST_SELECTION); public static Color unselectedTaskBackgroundColor = WindowUtil.GLOBAL_DISPLAY.getSystemColor(SWT.COLOR_LIST_BACKGROUND); protected static Color unselectedGroupBackgroundColor = new Color(WindowUtil.GLOBAL_DISPLAY, 255, 255, 204); protected Tree tree; protected TreeRowHook rowHook = null; protected TreeColumnHook columnHook = null; protected Map<AUndertaking, TreeItem> taskTreeItems = new HashMap<AUndertaking, TreeItem>(); protected AlertHandler taskApplicationResultHandler; protected AlertHandler updateStateHandler = new AlertHandler() { public void handleAlert(EventObject alert) { // TODO: at some point, optimize this to update only // those results for the design's column // Design design = (Design) alert.getSource(); redisplayAllResults(); } }; protected final ProjectUIModel.DesignReordering columnReordered = new ProjectUIModel.DesignReordering(this); // Need to avoid the column reorder event during undo/redo // The index of the currentOrdering array is one plus the index // of a Design in its Project; the corresponding value is the // index of the Design's TreeColumn in the Project's Tree. protected boolean reorderByManipulation = true; protected int[] currentOrdering = null; protected final Listener onColumnReorder = new Listener() { public void handleEvent(Event evt) { if (reorderByManipulation) { int[] newOrder = tree.getColumnOrder(); // Setting the doit doesn't actually work, but it may // eventually; thus, the call to set the column order. if (newOrder[0] != 0) { evt.doit = false; tree.setColumnOrder(currentOrdering); } else if (! Arrays.equals(newOrder, currentOrdering)) { TreeColumn[] columns = tree.getColumns(); columnReordered.movedDesign = (TreeColumn) evt.widget; columnReordered.newDesignOrdering = new Design[columns.length - 1]; for (int i = 1; i < newOrder.length; i++) { columnReordered.newDesignOrdering[i - 1] = (Design) columns[newOrder[i]].getData(); } currentOrdering = newOrder; raiseAlert(columnReordered); } } } }; protected final AlertHandler reorderColumnHeaders = new AlertHandler() { public void handleAlert(EventObject alert) { recreateCurrentOrdering(); // This call is reacting to a change in the model (the project) // so any event reflecting the change in column ordering // cannot be a result of direct manipulation by the user. boolean oldReorderValue = reorderByManipulation; reorderByManipulation = false; tree.setColumnOrder(currentOrdering); reorderByManipulation = oldReorderValue; } }; protected final Listener onDisposeColumn = new Listener() { public void handleEvent(Event evt) { TreeColumn colToDelete = (TreeColumn) evt.widget; CogToolPref.ALERTER.removeAllHandlers(colToDelete); Design design = (Design) colToDelete.getData(); design.removeAllHandlers(ProjectUIModel.this); } }; protected final Listener onDisposeRow = new Listener() { public void handleEvent(Event evt) { TreeItem rowToDelete = (TreeItem) evt.widget; AUndertaking task = (AUndertaking) rowToDelete.getData(); task.removeAllHandlers(ProjectUIModel.this); } }; public static final Color SELECTED_CELL_COLOR = new Color(WindowUtil.GLOBAL_DISPLAY, 135, 180, 235); public static final RGB SELECTED_CELL_RGB = new RGB(20, 120, 200); public static final ImageData SELECTED_CELL_IMAGEDATA = new ImageData(1, 1, 1, new PaletteData(new RGB[] { SELECTED_CELL_RGB })); public ProjectUIModel(Project proj, Tree projectTree, TreeRowHook rh, TreeColumnHook ch) { super(proj); tree = projectTree; tree.setBackground(unselectedTaskBackgroundColor); rowHook = rh; columnHook = ch; // Set up the Handler for changes in the model createTaskApplicationResultChangeHandler(); tree.setData(proj); installDesigns(); installUndertakings(); project.addHandler(this, Project.DesignsReordered.class, reorderColumnHeaders); AlertHandler designChangeHandler = new AlertHandler() { public void handleAlert(EventObject alert) { // Inserts might cause a reorder event; don't react! boolean oldReorderValue = reorderByManipulation; reorderByManipulation = false; Project.DesignChange chg = (Project.DesignChange) alert; if (chg != null) { if (chg.isAdd) { if (chg.atIndex == Project.DesignChange.AT_END) { addTreeColumn((Design) chg.element); } else { addTreeColumn((Design) chg.element, chg.atIndex + 1); } recreateCurrentOrdering(); redisplayAllResults(); tree.redraw(); } else { TreeColumn colToDelete = tree.getColumn(currentOrdering[chg.atIndex + 1]); recoverTreeColumn(colToDelete); recreateCurrentOrdering(); } } reorderByManipulation = oldReorderValue; } }; project.addHandler(this, Project.DesignChange.class, designChangeHandler); AlertHandler taskChangeHandler = new AlertHandler() { public void handleAlert(EventObject alert) { Project.TaskChange chg = (Project.TaskChange) alert; if (chg != null) { if (chg.isAdd) { TreeItem newRow; if (chg.atIndex == Project.TaskChange.AT_END) { newRow = new TreeItem(tree, SWT.NONE); } else { newRow = new TreeItem(tree, SWT.NONE, chg.atIndex); } populateRow((AUndertaking) chg.element, newRow); } else { TreeItem rowToDelete = findChildRow((AUndertaking) chg.element, tree.getItems()); recoverTreeItem(rowToDelete); } } } }; project.addHandler(this, Project.TaskChange.class, taskChangeHandler); } // ProjectUIModel (ctor) protected void recreateCurrentOrdering() { List<Design> designs = project.getDesigns(); TreeColumn[] columns = tree.getColumns(); if ((currentOrdering == null) || (currentOrdering.length != columns.length)) { currentOrdering = new int[columns.length]; } currentOrdering[0] = 0; // We want an ordering such that each ordering value is an index into // the current TreeColumn array so that the designs are displayed in // the Project's ordering. Given the list of designs and the array of // TreeColumns, the (relatively) efficient indexOf call only exists on // the list of designs. Thus, we iterate over the TreeColumns and find // the index in the ordering array to which it belongs. (If we had an // indexOf call that found the TreeColumn whose getData call was a // given Design efficiently, we would instead find the value in the // ordering array for each column index.) for (int ordering = 1; ordering < columns.length; ordering++) { int index = designs.indexOf(columns[ordering].getData()); currentOrdering[index + 1] = ordering; } } public int[] getCurrentDesignOrdering() { if (currentOrdering == null) { recreateCurrentOrdering(); } return currentOrdering; } public Tree getTree() { return tree; } public TreeItem getTaskTreeItem(AUndertaking undertaking) { return taskTreeItems.get(undertaking); } public TreeColumn getDesignTreeColumn(Design design) { int designIndex = tree.getColumnCount() - 1; while (designIndex > 0) { TreeColumn column = tree.getColumn(designIndex--); if (design == column.getData()) { return column; } } return null; } @Override public void dispose() { // Superclass invokes removeAllHandlers on the project! super.dispose(); CogToolPref.ALERTER.removeAllHandlers(this); } protected void installTreeColumn(final TreeColumn designColumn, Design design) { designColumn.addListener(SWT.Dispose, onDisposeColumn); designColumn.setText(design.getName()); designColumn.setResizable(true); designColumn.setWidth(CogToolPref.KLM_RESULT_RANGE.getBoolean() ? COL_WIDTH_WITH_RANGE : COL_WIDTH_NO_RANGE); designColumn.setMoveable(true); designColumn.addListener(SWT.Move, onColumnReorder); CogToolPref.ALERTER.addHandler( this, CogToolPref.PreferencesChange.class, new AlertHandler() { public void handleAlert(EventObject evt) { if (evt == null) { return; } Set<CogToolPref> changed = ((CogToolPref.PreferencesChange)evt).getPrefs(); if (changed.contains(CogToolPref.KLM_RESULT_RANGE)) { if (CogToolPref.KLM_RESULT_RANGE.getBoolean()) { if (designColumn.getWidth() < COL_WIDTH_WITH_RANGE) { designColumn.setWidth(COL_WIDTH_WITH_RANGE); } } else { if (designColumn.getWidth() > COL_WIDTH_NO_RANGE) { designColumn.setWidth(COL_WIDTH_NO_RANGE); } } redisplayAllResults(); } else if (changed.contains(CogToolPref.DISPLAY_DIGITS)) { redisplayAllResults(); } } }); AlertHandler handler = new AlertHandler() { public void handleAlert(EventObject alert) { Design design = (Design) designColumn.getData(); designColumn.setText(design.getName()); designColumn.setToolTipText(design.getName() + "\n" + selectDesignHelp + "\n" + editDesignHelp); } }; designColumn.setData(design); designColumn.setToolTipText(design.getName() + "\n" + selectDesignHelp + "\n" + editDesignHelp); design.addHandler(this, NameChangeAlert.class, handler); design.addHandler(this, Demonstration.StatusChange.class, updateStateHandler); // Update data cells when a change happens design.addHandler(this, TaskApplication.TaskApplicationResultChange.class, taskApplicationResultHandler); if (columnHook != null) { columnHook.onColumnCreation(designColumn); } } protected String[] getTaskRowStrings(AUndertaking task) { return ResultDisplayPolicy.getTaskRowStrings(project, task, ResultDisplayPolicy.WITH_SECS, currentOrdering); } // Redisplay all results protected void redisplayAllResults(TreeItem[] rows) { for (TreeItem row : rows) { AUndertaking task = (AUndertaking) row.getData(); if (task.isTaskGroup()) { redisplayAllResults(row.getItems()); } row.setText(getTaskRowStrings(task)); setRowBackground(task, row); } } public void redisplayAllResults() { redisplayAllResults(tree.getItems()); } // For task represented by row and all parent task groups! protected void redisplayResults(TreeItem row) { // Propagate changes to parent items while (row != null) { AUndertaking task = (AUndertaking) row.getData(); row.setText(getTaskRowStrings(task)); setRowBackground(task, row); row = row.getParentItem(); } } /** * Runs through lists of designs and adds a handler to be notified of * changes which resulted in a recompute of results. * */ protected void createTaskApplicationResultChangeHandler() { taskApplicationResultHandler = new AlertHandler() { public void handleAlert(EventObject alert) { TaskApplication.TaskApplicationResultChange event = (TaskApplication.TaskApplicationResultChange) alert; // System.out.println("Result Change!"); if (event != null) { // TODO: change to only make change on specified // TaskApplication, or just on the column and for each // row. (getSource on event is Design) if (event.task != null) { TreeItem row = taskTreeItems.get(event.task); // System.out.println("Redisplaying!"); redisplayResults(row); } } } }; } protected TreeColumn addTreeColumn(Design design) { TreeColumn newColumn = new TreeColumn(tree, SWT.RIGHT); installTreeColumn(newColumn, design); return newColumn; } protected TreeColumn addTreeColumn(Design design, int atIndex) { TreeColumn newColumn = new TreeColumn(tree, SWT.RIGHT, atIndex); installTreeColumn(newColumn, design); return newColumn; } protected void installDesigns() { TreeColumn designColumn = new TreeColumn(tree, SWT.LEFT); designColumn.setToolTipText(selectAllHelp); // TODO: Bonnie wants some indicator for which columns are designs // Such as changing "Tasks" here to "Tasks \\ Designs" designColumn.setText(L10N.get("PM.Tasks", "Tasks")); designColumn.setResizable(true); designColumn.setWidth(400); if (columnHook != null) { columnHook.onColumnCreation(designColumn); } Iterator<Design> designs = project.getDesigns().iterator(); while (designs.hasNext()) { Design nextDesign = designs.next(); addTreeColumn(nextDesign); } } // Recursive step protected void addTasks(Iterator<AUndertaking> children, TreeItem parentRow) { while (children.hasNext()) { AUndertaking undertaking = children.next(); TreeItem row = new TreeItem(parentRow, SWT.NONE); parentRow.setExpanded(true); populateRow(undertaking, row); } } protected Color setRowBackground(AUndertaking undertaking, TreeItem row) { row.setForeground(unselectedTextColor); if (undertaking.isTaskGroup()) { // instanceof TaskGroup row.setBackground(unselectedGroupBackgroundColor); return unselectedGroupBackgroundColor; } row.setBackground(unselectedTaskBackgroundColor); return unselectedTaskBackgroundColor; } protected void populateRow(AUndertaking undertaking, final TreeItem row) { taskTreeItems.put(undertaking, row); row.addListener(SWT.Dispose, onDisposeRow); // TODO: This is creating a new handler instance for each row // TODO: reuse a single instance by looking up TreeItem in taskTreeItems? AlertHandler handler = new AlertHandler() { public void handleAlert(EventObject alert) { AUndertaking task = (AUndertaking) row.getData(); row.setText(0, SWTStringUtil.insertEllipsis(task.getName(), 300, StringUtil.EQUAL, tree.getFont())); setRowBackground(task, row); } }; row.setData(undertaking); undertaking.addHandler(this, NameChangeAlert.class, handler); row.setText(getTaskRowStrings(undertaking)); Color bkg = setRowBackground(undertaking, row); int numCols = tree.getColumnCount(); for (int i = 0; i < numCols; i++) { row.setBackground(i, bkg); } if (undertaking.isTaskGroup()) { TaskGroup group = (TaskGroup) undertaking; setGroupAlertHandlers(group, row); addTasks(group.getUndertakings().iterator(), row); } row.setExpanded(true); if (rowHook != null) { rowHook.onRowCreation(row); } } // populateRow protected void setGroupAlertHandlers(TaskGroup group, final TreeItem row) { AlertHandler handler = new AlertHandler() { public void handleAlert(EventObject alert) { TaskGroup.TaskChange chg = (TaskGroup.TaskChange) alert; if (chg != null) { if (chg.isAdd) { TreeItem newRow; if (chg.atIndex == TaskGroup.TaskChange.AT_END) { newRow = new TreeItem(row, SWT.NONE); } else { newRow = new TreeItem(row, SWT.NONE, chg.atIndex); } row.setExpanded(true); populateRow((AUndertaking) chg.element, newRow); redisplayResults(newRow); } else { TreeItem rowToDelete = findChildRow((AUndertaking) chg.element, row.getItems()); recoverTreeItem(rowToDelete); } } } }; group.addHandler(this, TaskGroup.TaskChange.class, handler); handler = new AlertHandler() { public void handleAlert(EventObject alert) { // System.out.println("NatureChange!"); // walk up the group tree and recalculate redisplayResults(row); } }; group.addHandler(this, TaskGroup.NatureChange.class, handler); } // setGroupAlertHandlers protected void installUndertakings() { Iterator<AUndertaking> undertakings = project.getUndertakings().iterator(); while (undertakings.hasNext()) { AUndertaking undertaking = undertakings.next(); TreeItem row = new TreeItem(tree, SWT.NONE); populateRow(undertaking, row); } } protected TreeItem findChildRow(AUndertaking task, TreeItem[] children) { for (TreeItem element : children) { if (element.getData() == task) { return element; } } return null; } protected void recoverTreeColumn(TreeColumn colToDelete) { if (columnHook != null) { columnHook.onColumnDeletion(colToDelete); } colToDelete.dispose(); } protected void recoverTreeItem(TreeItem rowToDelete) { if (rowHook != null) { rowHook.onRowDeletion(rowToDelete); } // Need to call getData BEFORE calling dispose, otherwise a // widgetIsDisposed error is created. taskTreeItems.remove(rowToDelete.getData()); TreeItem deletedRowParent = rowToDelete.getParentItem(); rowToDelete.dispose(); if (deletedRowParent != null) { redisplayResults(deletedRowParent); } } public void recolorTree(int designColumn, TreeItem cellTask, boolean nowSelected) { // Set or remove selection marker in name TreeColumn col = tree.getColumn(designColumn); if (nowSelected && (cellTask == null)) { col.setText(L10N.get("PM.DesignMark", "\u2022 ") + col.getText()); } else { col.setText(((Design) col.getData()).getName()); } recolorItems(tree.getItems(), designColumn, nowSelected && (cellTask == null)); if (cellTask != null) { cellTask.setBackground(designColumn, ProjectUIModel.SELECTED_CELL_COLOR); } } protected void recolorItems(TreeItem[] items, int columnIndex, boolean nowSelected) { for (TreeItem item : items) { AUndertaking undertaking = (AUndertaking) item.getData(); if (nowSelected) { item.setBackground(columnIndex, selectedBackgroundColor); item.setForeground(columnIndex, selectedTextColor); } else { item.setForeground(columnIndex, unselectedTextColor); if (undertaking.isTaskGroup()) { item.setBackground(columnIndex, unselectedGroupBackgroundColor); } else { item.setBackground(columnIndex, unselectedTaskBackgroundColor); } } recolorItems(item.getItems(), columnIndex, nowSelected); } } // recolorItems }