/*
This file is part of leafdigital leafChat.
leafChat is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
leafChat 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package com.leafdigital.scripting;
import java.awt.*;
import java.util.HashMap;
import com.leafdigital.ui.api.*;
import com.leafdigital.ui.api.Button;
import com.leafdigital.ui.api.Dialog;
import com.leafdigital.ui.api.Label;
import com.leafdigital.ui.api.Window;
import leafchat.core.api.*;
/**
* Interface for users to edit an individual script.
*/
@UIHandler("scripteditor")
public class ScriptEditor implements Script.StateListener
{
private final static Class<?>[] itemTypes =
{
ItemCommand.class,ItemMenu.class,ItemEvent.class,ItemVariable.class
};
private ScriptingTool owner;
private Script script;
private Window w;
private PluginContext context;
/** UI: Panel containing all items */
public VerticalPanel itemsUI;
/** UI: Save button */
public Button saveUI;
/** UI: Revert button */
public Button revertUI;
/** UI: Add button */
public Button addUI;
/** UI: Script status */
public Label statusUI;
/** UI: Enabled checkbox */
public CheckBox enabledUI;
/** UI: Save/compile progress */
public Progress progressUI;
HashMap<ScriptItem, ItemDetails> detailsMap =
new HashMap<ScriptItem, ItemDetails>();
ScriptEditor(PluginContext context,ScriptingTool owner,Script script) throws GeneralException
{
this.context=context;
this.owner=owner;
this.script=script;
UI ui = (context.getSingle(UI.class));
w = ui.createWindow("scripteditor", this);
w.setRemember("scripting", script.getName());
initWindow();
w.show(false);
script.addStateListener(this);
}
private void initWindow()
{
// Set title
w.setTitle("Editing script - "+script.getName());
// Remove any existing items
ScriptItem[] items=detailsMap.keySet().toArray(new ScriptItem[detailsMap.keySet().size()]);
for(int i=0;i<items.length;i++)
{
deleteItem(items[i]);
}
// Add new items
items=script.getItems();
for(int i=0;i<items.length;i++)
{
addItem(items[i]);
}
enabledUI.setChecked(script.isEnabled());
addUI.setEnabled(true);
scriptStateChanged(script);
progressUI.setVisible(false);
}
/**
* Object that handles UI callbacks from each item.
*/
@UIHandler("scriptitem")
public class ItemCallbacks
{
/** UI: Item summary */
public Label summaryUI;
/** UI: Item title */
public Label titleUI;
/** UI: Item error info (if any) */
public Label errorUI;
/** UI: Item variables list */
public Label variablesUI;
/** UI: Spacer used if there's no error */
public Spacer noErrorUI;
/** UI: Edit area for code */
public EditArea codeUI;
/** UI: Enabled checkbox */
public CheckBox enabledUI;
/** UI: Settings button */
public Button settingsUI;
/** UI: Delete button */
public Button deleteUI;
private ScriptItem item;
private ItemCallbacks(ScriptItem item)
{
this.item=item;
}
private void init()
{
titleUI.setText("<strong>"+item.getTypeName()+"</strong>");
summaryUI.setText(item.getSummaryLabel());
if(item.getVariablesLabel()==null)
variablesUI.setVisible(false);
else
variablesUI.setText(item.getVariablesLabel());
if(item.hasErrors())
{
errorUI.setText("<error>"+item.getErrorLabel()+"</error>");
noErrorUI.setVisible(false);
}
else
{
errorUI.setVisible(false);
noErrorUI.setVisible(true);
}
if(item instanceof UserCodeItem)
{
codeUI.setValue(((UserCodeItem)item).getUserCode());
codeUI.highlightErrorLines(((UserCodeItem)item).getErrorLines());
}
else
{
codeUI.setVisible(false);
}
enabledUI.setChecked(item.isEnabled());
}
private void disable()
{
codeUI.setEnabled(false);
enabledUI.setEnabled(false);
settingsUI.setEnabled(false);
deleteUI.setEnabled(false);
}
/** Action: User changes enabled checkbox */
@UIAction
public void changeEnabled()
{
item.setEnabled(enabledUI.isChecked());
}
/** Action: Code text changes */
@UIAction
public void changeCode()
{
((UserCodeItem)item).setUserCode(codeUI.getValue());
}
/** Action: Code focused */
@UIAction
public void focusCode()
{
if(codeUI.getValue().equals(ItemCommand.DEFAULTCODE))
codeUI.selectAll();
}
/** Action: Click settings button */
@UIAction
public void actionSettings()
{
new SettingsDialog(item);
}
/** Action: Click delete button */
@UIAction
public void actionDelete()
{
int value=context.getSingle(UI.class).showQuestion(w,"Confirm delete",
"Are you sure you want to delete this script item? Deleted items cannot be restored.",
UI.BUTTON_YES|UI.BUTTON_CANCEL,"Delete item",null,null,UI.BUTTON_CANCEL);
if(value==UI.BUTTON_YES)
{
script.deleteItem(item);
deleteItem(item);
}
}
/**
* Paints item background graphics.
* @param g Context
* @param left Left pixels
* @param top Top pixels
* @param width Width in pixels
* @param height Height in pixels
*/
@UIAction
public void paintBackground(Graphics2D g, int left, int top,
int width, int height)
{
Color initial=item.getStripeRGB();
g.setColor(item.hasErrors() ? Color.red : initial);
g.fillRect(left+4,top,4,height);
left+=8;
width-=8;
initial=item.getNormalStripeRGB();
int
rStart=(initial.getRed()+3*255)>>2,
gStart=(initial.getGreen()+3*255)>>2,
bStart=(initial.getBlue()+3*255)>>2;
for(int x=0;x<width;x+=4)
{
int proportion=256-((x<<8)/width);
g.setColor(new Color(
(proportion * 255 + (256-proportion) * rStart)>>8,
(proportion * 255 + (256-proportion) * gStart)>>8,
(proportion * 255 + (256-proportion) * bStart)>>8
));
int w=x+4>width ? width-x : 4;
g.fillRect(x+left,top,w,height);
}
}
}
private static class ItemDetails
{
Page p;
ItemCallbacks callbacks;
public ItemDetails(Page p,ItemCallbacks callbacks)
{
this.p=p;
this.callbacks=callbacks;
}
}
/**
* Adds an item to the display (not the script).
* @param item New item
*/
void addItem(ScriptItem item)
{
ItemCallbacks callbacks=new ItemCallbacks(item);
Page p = context.getSingle(UI.class).createPage(
"scriptitem", callbacks);
callbacks.init();
detailsMap.put(item,new ItemDetails(p,callbacks));
itemsUI.add(p);
}
/**
* Deletes an item from the display (not the script).
* @param item Item to go
*/
void deleteItem(ScriptItem item)
{
ItemDetails details=detailsMap.remove(item);
itemsUI.remove(details.p);
}
void focus()
{
w.activate();
}
/**
* Callback: Window is closed.
*/
@UIAction
public void closed()
{
script.removeStateListener(this);
owner.informClosed(script);
}
/**
* Callback: Window is closing.
* @throws GeneralException Any error
*/
@UIAction
public void closing() throws GeneralException
{
if(script.isChanged())
{
int action=context.getSingle(UI.class).showQuestion(
w,"Confirm close", "This script has unsaved changes.",
UI.BUTTON_YES|UI.BUTTON_NO|UI.BUTTON_CANCEL,
"Save changes","Discard changes",null,UI.BUTTON_YES);
switch(action)
{
case UI.BUTTON_YES: actionSave(); break;
case UI.BUTTON_NO: actionRevert(); break;
case UI.BUTTON_CANCEL: return;
}
}
// Just close straight off if no changes
w.close();
}
/**
* User clicks Save button.
* @throws GeneralException Any error
*/
@UIAction
public void actionSave() throws GeneralException
{
progressUI.setIndeterminate();
progressUI.setVisible(true);
revertUI.setEnabled(false);
saveUI.setEnabled(false);
enabledUI.setEnabled(false);
addUI.setEnabled(false);
for(ItemDetails details : detailsMap.values())
{
details.callbacks.disable();
}
script.save(new Script.SaveContinuation()
{
@Override
public void afterSave(boolean success)
{
if(!success)
{
try
{
int action=UI.BUTTON_YES; // Only ask if it's not already disabled
if(script.isEnabled())
{
action=context.getSingle(UI.class).showQuestion(
w,"Error in script", "This script contains errors and can only be saved if you disable it.",
UI.BUTTON_YES|UI.BUTTON_CANCEL,
"Save and disable",null,null,UI.BUTTON_YES);
}
if(action==UI.BUTTON_YES)
{
script.saveAndDisable();
}
}
catch(GeneralException ge)
{
ErrorMsg.report("Error saving script",ge);
}
}
initWindow();
owner.informChanged(script);
}
@Override
public void afterSave(Throwable t)
{
t.printStackTrace();
initWindow();
}
});
}
/**
* User clicks Revert button.
* @throws GeneralException Any error
*/
@UIAction
public void actionRevert() throws GeneralException
{
script.load();
initWindow();
}
/**
* User clicks Help button.
*/
@UIAction
public void actionHelp()
{
owner.actionHelp();
}
/**
* User clicks Add button.
*/
@UIAction
public void actionAdd()
{
new SettingsDialog(null);
}
/**
* User changes Enabled checkbox.
* @throws GeneralException Any error
*/
@UIAction
public void changeEnabled() throws GeneralException
{
script.setEnabled(enabledUI.isChecked());
}
/**
* Item settings dialog.
*/
@UIHandler("itemsettings")
public class SettingsDialog
{
private Dialog d;
/** UI: Item type */
public Dropdown typeUI;
/** UI: Settings page */
public Page settingsUI;
/** UI: OK button */
public Button okUI;
/** UI: Top part of dialog */
public HorizontalPanel topBitUI;
private ScriptItem startItem;
SettingsDialog(ScriptItem item)
{
this.startItem=item;
d = context.getSingle(UI.class).createDialog("itemsettings",this);
if(startItem!=null)
{
d.setTitle("Item settings");
okUI.setLabel("OK");
topBitUI.setVisible(false);
settingsUI.setContents(
startItem.getPage(okUI));
}
else
{
// Show list of available types
for(int i=0;i<itemTypes.length;i++)
{
ScriptItem newItem;
try
{
newItem=(ScriptItem)itemTypes[i].getConstructor(Script.class,int.class).
newInstance(script, script.getItems().length);
}
catch(Exception e)
{
throw new BugException(e);
}
typeUI.addValue(newItem,newItem.getTypeName());
}
changeType();
}
d.show(w);
}
/** Action: User clicks OK. */
@UIAction
public void actionOK()
{
d.close();
if(startItem==null)
{
ScriptItem newItem=(ScriptItem)typeUI.getSelected();
newItem.saveSettings();
script.addItem(newItem);
addItem(newItem);
}
else
{
ScriptItem existingItem=startItem;
existingItem.saveSettings();
ItemDetails details=detailsMap.get(existingItem);
details.callbacks.init();
}
}
/** Action: User clicks Cancel. */
@UIAction
public void actionCancel()
{
d.close();
}
/** Action: User changes Type dropdown. */
@UIAction
public void changeType()
{
settingsUI.setContents(
((ScriptItem)typeUI.getSelected()).getPage(okUI));
}
}
@Override
public void scriptStateChanged(Script s)
{
saveUI.setEnabled(s.isChanged());
revertUI.setEnabled(s.isChanged());
enabledUI.setEnabled(!s.isChanged() && !s.hasErrors());
enabledUI.setChecked(s.isEnabled());
if(s.isChanged())
{
statusUI.setText(
script.isEnabled()
? "Changes to this script will not take effect until you click Save."
: "This script is disabled. Changes will not take effect until you Save then enable it.");
}
else
{
statusUI.setText("");
}
}
}