/*******************************************************************************
* 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.ui;
import java.util.EventObject;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import edu.cmu.cs.hcii.cogtool.CogTool;
import edu.cmu.cs.hcii.cogtool.CogToolClipboard;
import edu.cmu.cs.hcii.cogtool.CogToolLID;
import edu.cmu.cs.hcii.cogtool.controller.Controller;
import edu.cmu.cs.hcii.cogtool.controller.ControllerRegistry;
import edu.cmu.cs.hcii.cogtool.controller.DesignEditorController;
import edu.cmu.cs.hcii.cogtool.controller.ProjectController;
import edu.cmu.cs.hcii.cogtool.model.Design;
import edu.cmu.cs.hcii.cogtool.model.Project;
import edu.cmu.cs.hcii.cogtool.util.AlertHandler;
import edu.cmu.cs.hcii.cogtool.util.IUndoableEdit;
import edu.cmu.cs.hcii.cogtool.util.L10N;
import edu.cmu.cs.hcii.cogtool.util.ListenerIdentifier;
import edu.cmu.cs.hcii.cogtool.util.ListenerIdentifierMap;
import edu.cmu.cs.hcii.cogtool.util.ManagedText;
import edu.cmu.cs.hcii.cogtool.util.MenuUtil;
import edu.cmu.cs.hcii.cogtool.util.NameChangeAlert;
import edu.cmu.cs.hcii.cogtool.util.UndoManager;
import edu.cmu.cs.hcii.cogtool.util.WindowUtil;
import edu.cmu.cs.hcii.cogtool.util.MenuUtil.MenuItemDefinition;
import edu.cmu.cs.hcii.cogtool.util.MenuUtil.SimpleMenuItemDefinition;
import edu.cmu.cs.hcii.cogtool.view.MenuFactory;
import edu.cmu.cs.hcii.cogtool.view.UndoManagerView;
/**
* Default implementation of UI functionality shared by the views
* for all CogTool model objects.
*
* @author mlh
*/
public abstract class DefaultUI extends UI
implements MenuFactory.ILeadItemUpdater
{
/**
* Specialized listener ID indicating that the window for the
* given project or design should be restored (that is, given the focus
* if it still exists, or re-created otherwise).
* <p>
* Actual restoration occurs in <code>getParameters</code>. The restored
* window is then returned by <code>getParameters</code> in case
* any further semantic action might wish to operate on the window.
* Generally, restoration of the window is all that is expected,
* so controllers do not assign any actions to instances of
* this listener ID subclass.
*
* @author mlh
*/
public static class RestoreParentControllerLID extends ListenerIdentifier
{
public final Project project;
public final Design design; // can be null
/**
* Initialize the ID; if the given design is not <code>null</code>,
* then a <code>DesignEditorController</code> will be restored.
* Otherwise, a <code>ProjectController</code> will be restored.
*
* @param p the project either to restore or as the parent of the
* given design
* @param d the design either to restore or <code>null</code> if
* restoring the project's window
* @author mlh
*/
public RestoreParentControllerLID(Project p, Design d)
{
project = p;
design = d;
}
/**
* Used by <code>getParameters</code> to restore the desired
* window. Returned by <code>getParameters</code> in case
* any further semantic action might wish to operate on the window.
* <p>
* Generally, restoration of the window is all that is expected,
* so controllers do not assign any actions to instances of
* this listener ID subclass.
*
* @return the controller restored
* @author mlh
*/
public Controller restoreWindow()
{
Controller window = null;
// Project controller if design is null
if (design == null) {
boolean notModified;
try {
notModified =
UndoManager.isAtSavePoint(project);
}
catch (IllegalStateException ex) {
System.err.println("Ignoring that isAtSavePoint failed.");
notModified = false;
}
// Presume registered -- registering twice is problematic!
window =
ProjectController.openController(project,
false,
notModified);
}
else {
// Design controller
window =
DesignEditorController.openController(design,
project);
}
return window;
}
}
/**
* All UI subclasses require access to the project that contains
* the model object being edited.
*/
protected Project project;
/**
* Marker string reflecting whether the associated project
* is modified since its last save; suitable for use in constructing
* window titles.
*/
protected String modificationFlag;
/**
* The undo manager itself. Created in the controller, held onto here.
* Mostly used for the connection to the undoManagerview.
*/
protected UndoManager undoManager; // needed in dispose
protected UndoManagerView.UndoAlertHandler undoMgrViewHandler;
/**
* Delegates closing a window to performAction in the corresponding
* Controller object.
*/
protected ShellAdapter closeListener = new ShellAdapter()
{
/**
* Handler for shellClosed -- delegates to performAction
*/
@Override
public void shellClosed(ShellEvent e)
{
CogTool.controllerNexus.saveWindowLocation(project,
getModelObject(),
getShell().getBounds());
/**
* Don't let other listeners progress if save canceled.
*/
e.doit = performAction(CogToolLID.CloseWindow, null, false);
}
};;
/**
* Definition of the "Window" menu for this window.
*/
protected MenuFactory.IWindowMenuData<Project> menuData = null;
/**
* How to react to alerts raised when save points change in any undo
* manager. Updates the window titles, reflecting modification status.
* TODO: Should we reflect modification status in "Window" menu entries?
* If so, do here!
*/
protected AlertHandler undoMgrTitleHandler =
new AlertHandler() {
public void handleAlert(EventObject evt)
{
UndoManager.SavePointChange chg =
(UndoManager.SavePointChange) evt;
updateModificationFlag(chg.nowAtSavePoint);
updateTitle();
}
};
/**
* How to react to alerts that a model object has been renamed.
* Updates window titles and "Window" menu entries.
*/
protected AlertHandler renameHandler =
new AlertHandler() {
public void handleAlert(EventObject evt)
{
updateTitle();
updateWindowMenus();
}
};
protected static final String OPEN_PROJECT_LABEL =
L10N.get("MI.OpenProject", "Display Project");
protected static final String OPEN_DESIGN_LABEL =
L10N.get("MI.OpenDesign", "Display Design");
/**
* Support for creating the lead items for the Window menu.
*
* @param p the project either to restore or as the parent of the
* given design
* @param d the design either to restore or <code>null</code> if
* restoring the project's window
* @author mlh
*/
protected static MenuItemDefinition[] buildLeadItems(Project p, Design d)
{
ListenerIdentifier openProjectLID = null;
ListenerIdentifier openDesignLID = null;
boolean openProjectEnabled = MenuUtil.DISABLED;
boolean openDesignEnabled = MenuUtil.DISABLED;
String openProjectLabel = OPEN_PROJECT_LABEL;
String openDesignLabel = OPEN_DESIGN_LABEL;
// Check to create standard LID's for opening design & project
if (p != null) {
openProjectLID = new RestoreParentControllerLID(p, null);
openProjectEnabled = MenuUtil.ENABLED;
openProjectLabel = openProjectLabel + ": " + p.getName();
if (d != null) {
openDesignLID = new RestoreParentControllerLID(p, d);
openDesignEnabled = MenuUtil.ENABLED;
openDesignLabel = openDesignLabel + ": " + d.getName();
}
}
return new MenuItemDefinition[]
{ new SimpleMenuItemDefinition(openProjectLabel,
openProjectLID,
openProjectEnabled),
new SimpleMenuItemDefinition(openDesignLabel,
openDesignLID,
openDesignEnabled),
MenuUtil.SEPARATOR };
}
// Suitable for a Design window
protected static MenuItemDefinition[] buildLeadItems(Project p)
{
return buildLeadItems(p, null);
}
// Suitable for a Project window
protected static MenuItemDefinition[] buildLeadItems()
{
return buildLeadItems(null, null);
}
/**
* All CogTool model editing views keep track of the <code>Project</code>
* instance that contains (or is) the model object being edited.
*
* @param proj the Project instance that is or contains the model
* object being edited
* @param windowMenuLabel the label for this window's entry in the
* window menu
* @param leadItems the lead menu items for the window menu of this window
* @author mlh
*/
public DefaultUI(Project proj,
String windowMenuLabel,
MenuItemDefinition[] leadItems,
UndoManager undoMgr)
{
super();
project = proj;
// Manage current project modification state and future changes
// to that state and model object names.
if (project != null) {
try {
updateModificationFlag(UndoManager.isAtSavePoint(project));
}
catch (IllegalStateException ex) {
System.err.println("Ignoring that isAtSavePoint failed.");
updateModificationFlag(false);
}
project.addHandler(this,
NameChangeAlert.class,
renameHandler);
try {
UndoManager.addSavePointChangeHandler(project,
undoMgrTitleHandler);
}
catch (IllegalStateException ex) {
System.err.println("Ignoring fact that addSavePointChangeHandler failed.");
}
}
undoManager = undoMgr;
// Create the undo manager view handler,
// resetting the undo view handler to represent any values currently in
// the undo manager. IE: remember undo.
if (undoManager != null) {
undoMgrViewHandler =
new UndoManagerView.UndoAlertHandler(undoManager,
lIDMap,
CogToolLID.Undo,
CogToolLID.Redo);
undoManager.addHandler(this,
UndoManager.UndoRedoEvent.class,
undoMgrViewHandler);
}
menuData = new DefaultWindowMenuData(this,
windowMenuLabel,
leadItems);
}
/**
* Return the UI's Project instance
*/
public Project getProject()
{
return project;
}
@Override
protected int canIDCauseSelection(ListenerIdentifier lid)
{
if (undoManager != null) {
IUndoableEdit edit = null;
if (lid == CogToolLID.Undo) {
edit = undoManager.editToBeUndone();
}
else if (lid == CogToolLID.Redo) {
edit = undoManager.editToBeRedone();
}
if (edit != null) {
lid = edit.getLID();
}
// else not undo/redo; query lid directly
}
return super.canIDCauseSelection(lid);
}
@Override
protected boolean doesIDCommitChanges(ListenerIdentifier lid)
{
if (undoManager != null) {
if (lid == CogToolLID.Undo) {
return idCommitsChanges(undoManager.editToBeUndone().getLID());
}
if (lid == CogToolLID.Redo) {
return idCommitsChanges(undoManager.editToBeRedone().getLID());
}
}
if (lid == CogToolLID.Paste) {
return CogToolClipboard.hasNonTextPasteContent();
}
// Not undo/redo; query lid directly
return super.doesIDCommitChanges(lid);
}
protected static final String operationCanceled =
L10N.get("DEFINT.OperationCanceled",
"The requested operation was canceled due to the following error:");
@Override
public boolean performAction(ListenerIdentifier id)
{
if ((undoManager != null) && doesIDCommitChanges(id)) {
Text textWithFocus = WindowUtil.getFocusedText();
if (textWithFocus != null) {
if (textWithFocus instanceof ManagedText) {
ManagedText performText = (ManagedText) textWithFocus;
if (! performText.confirm(ManagedText.LOSE_FOCUS)) {
getStandardInteraction().setStatusMessage(operationCanceled);
return false;
}
}
}
}
return super.performAction(id);
}
/**
* Return the primary model object this view/ui is responsible for
* editing.
*
* @return the primary model object this view/ui is responsible for
* editing
* @author mlh
*/
protected abstract Object getModelObject();
/**
* Based on the given modification state, recompute the modification
* marker string used in window titles.
*
* @param atSavePoint true if modification state means "saved";
* false if the associated project or any of its
* components were modified since the last save
* @author mlh
*/
protected void updateModificationFlag(boolean atSavePoint)
{
modificationFlag = atSavePoint ? "" : "* ";
}
/**
* Update the window title for this view/ui; we expect subclasses
* to override.
*
* @author mlh
*/
protected void updateTitle()
{
// subclasses may override
}
/**
* Subclasses should override to construct the string for the window
* menu's item label corresponding to this window.
*/
protected abstract String buildWindowMenuLabel();
/**
* Update the given lead item if necessary.
*
* @param leadItem the lead menu item to update
* @param position the 0-based index of the item in the Window menu
*/
public void updateLeadItem(MenuItem leadItem, int position)
{
Object itemData = leadItem.getData();
if ((itemData != null) &&
(itemData instanceof RestoreParentControllerLID))
{
RestoreParentControllerLID lid =
(RestoreParentControllerLID) itemData;
if (lid.design != null) {
leadItem.setText(OPEN_DESIGN_LABEL + ": "
+ lid.design.getName());
}
else if (lid.project != null) {
leadItem.setText(OPEN_PROJECT_LABEL + ": "
+ lid.project.getName());
}
}
}
/**
* Updates all "Window" menus based on the current information
* for the associated model object.
* <p>
* Subclasses should override to fix their IWindowMenuData,
* then they should invoke super.updateWindowMenus()
*
* @author mlh
*/
protected void updateWindowMenus()
{
menuData.setEntryLabel(buildWindowMenuLabel());
// The subclass has updated the associated "Window" definition;
// reflect those changes in all "Window" menus.
MenuFactory.updateMenuLabels(menuData, this);
}
/**
* Sets the "always-enabled" widgets;
* call this at the end of the subclass constructor!
* <p>
* All model editing windows support:
* Paste, SaveProject, SaveProjectAs, and CloseWindow
* <p>
* Take this opportunity to interpose a listener when the
* associated SWT window is closed by the user without using
* a CogTool menu item. This listener will also save the window's
* location in case a new window is restored for the associated
* model object.
*
* @author mlh
*/
@Override
protected void setInitiallyEnabled(boolean forConstruction)
{
super.setInitiallyEnabled(forConstruction);
if (undoMgrViewHandler != null) {
undoMgrViewHandler.resetView(undoManager);
}
setEnabled(CogToolLID.Paste,
ListenerIdentifierMap.ALL,
MenuUtil.ENABLED);
setEnabled(CogToolLID.SaveProject,
ListenerIdentifierMap.ALL,
MenuUtil.ENABLED);
setEnabled(CogToolLID.SaveProjectAs,
ListenerIdentifierMap.ALL,
MenuUtil.ENABLED);
setEnabled(CogToolLID.CloseWindow,
ListenerIdentifierMap.ALL,
MenuUtil.ENABLED);
setEnabled(CogToolLID.CloseProject,
ListenerIdentifierMap.ALL,
MenuUtil.ENABLED);
setEnabled(CogToolLID.Properties,
ListenerIdentifierMap.ALL,
MenuUtil.ENABLED);
Shell window = getShell();
if (forConstruction && (window != null)) {
window.removeShellListener(closeListener);
window.addShellListener(closeListener);
}
}
/**
* Fetches the parameters needed by any <code>performAction</code>
* invoked for the "specialized" <code>ListenerIdentifier</code>.
* In some cases, the determination of the parameters requires
* information from the original "general" LID subclass instance
* (see, for example, SEDemoLID).
* <p>
* If the given "general" LID is actually an instance of
* <code>RestoreParentControllerLID</code>, then the request is actually
* to restore the window of either the containing project or design.
*
* May return UNSET if no value computed; it is then the responsibility
* of the subclass to return something valid (i.e., not UNSET).
*
* @param originalLID the general LID value returned from a menu command
* @param transmutedLID the specific value representing an actual,
* concrete application function returned by
* a call to <code>specialize()</code>
* @param isContextSelection true if we should parameterize based on
* the current contextual selection;
* false to use the standard selection
* @return the parameters the <code>IListenerAction</code> may require
* to perform the requested semantic action
* @author mlh
*/
@Override
public Object getParameters(ListenerIdentifier originalLID,
ListenerIdentifier transmutedLID,
boolean isContextSelection)
{
Object parameters = super.getParameters(originalLID,
transmutedLID,
isContextSelection);
if (parameters != UNSET) {
return parameters;
}
if (originalLID instanceof RestoreParentControllerLID) {
return ((RestoreParentControllerLID) originalLID).restoreWindow();
}
return UNSET;
}
/**
* Recover any system resources being used to support the view being
* used by this UI instance.
* <p>
* At this point, we must remove alert handlers and the interposed
* listener on the associated SWT Shell object.
*
* @author mlh
*/
@Override
public void dispose()
{
if (undoManager != null) {
undoManager.removeAllHandlers(this);
}
project.removeAllHandlers(this);
try {
UndoManager.removeSavePointChangeHandler(project,
undoMgrTitleHandler);
}
catch (IllegalStateException ex) {
System.err.println("Ignoring fact that removeSavePointChangeHandler failed.");
}
Shell window = getShell();
CogTool.controllerNexus.saveWindowLocation(project,
getModelObject(),
window.getBounds());
if (window != null) {
if (closeListener != null) {
window.removeShellListener(closeListener);
}
}
super.dispose();
}
/**
* Retrieves a saved window location for the associated model object.
* <p>
* Convenience function for subclasses that passes request through to
* the ControllerNexus.
*
* @param model the model object being edited in the window
* @return the location Rectangle previously saved for this model
*/
public Rectangle getWindowLocation()
{
return CogTool.controllerNexus.getWindowLocation(project,
getModelObject());
}
/**
* Returns a saved window zoom level for a previously-edited model object.
* Recall that the map stores the zoom factor in a <code>Double</code>
* instance.
* <p>
* Convenience function for subclasses that passes request through to
* the ControllerNexus.
*
* @param model the model object being edited in the window
* @return the zoom level previously saved for this model
*/
public double getWindowZoom(Object model)
{
return CogTool.controllerNexus.getWindowZoom(project, model);
}
/**
* Saves a window zoom level associated with a particular model object.
* <p>
* Convenience function for subclasses that passes request through to
* the ControllerNexus.
*
* @param model the model object being edited in the window
* @param loc the zoom level for the window
*/
public void saveWindowZoom(Object model, double zoom)
{
CogTool.controllerNexus.saveWindowZoom(project, model, zoom);
}
/**
* Close the registered open controller for the associated model object.
* Used when the object or an ancestor has been removed from the model.
*
* @author mlh
*/
protected void closeOpenController()
{
// MLH TODO: Be nice if we didn't have to import ControllerRegistry
// because of its interface use of DefaultController!
Controller c =
ControllerRegistry.ONLY.findOpenController(getModelObject());
if (c != null) {
c.dispose();
}
}
}